From 13c84cd9973458750305c72a919cf921d9b22b04 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 10:29:00 +0200 Subject: main.go: Read config from a new format TOML file Construct a new config format, written in TOML. Read that format in when starting the program. This new format has the benefit of using project name aliases as keys. The goal will be to allow users to send one of those aliases as a command line argument to the program, and thus to have the program post a TimeTask entry for that project. Here's an idea of what the new format looks like: [auth] username = "example" password_cmd = "" [projects.myprojectalias] client = ... project = ... module = ... task = ... work_type = ... time = 7 billable = true [projects.project2] client = ... project = ... module = ... task = ... work_type = ... time = 7 billable = true Eventually, we'll need to remove the `interface{}` from the `Projects` map value and replace it with a real type, but this was just to test that it was possible to get us a nice map from the TOML. --- main.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 5a9f0d1..bfb56b8 100644 --- a/main.go +++ b/main.go @@ -8,22 +8,33 @@ import ( "github.com/teddywing/timetasker/timetask" + "github.com/BurntSushi/toml" "gopkg.in/yaml.v2" ) +// type Config struct { +// Auth struct { +// Username string +// PasswordCmd string `yaml:"password_cmd"` +// } +// Fields timetask.Fields +// Defaults timetask.TimeEntry +// } + type Config struct { Auth struct { Username string - PasswordCmd string `yaml:"password_cmd"` + PasswordCmd string //`toml:"password_cmd"` } - Fields timetask.Fields - Defaults timetask.TimeEntry + Projects map[string]interface{} } var config Config func main() { loadConfig() + log.Printf("%+v", config) + return if len(os.Args) == 1 { fmt.Println("Not enough arguments") @@ -46,13 +57,13 @@ func main() { // timetask.SubmitTimeEntries(config.Fields, time_entries) - timetask.GenerateWeeklyTimesheet(os.Stdout, config.Defaults) + // timetask.GenerateWeeklyTimesheet(os.Stdout, config.Defaults) } func loadConfig() { - config_str, err := ioutil.ReadFile("config.yml") + // config_str, err := ioutil.ReadFile("config2.toml") config = Config{} - err = yaml.Unmarshal(config_str, &config) + _, err := toml.DecodeFile("config2.toml", &config) if err != nil { log.Println(err) } -- cgit v1.2.3 From 1a1d9c3c671d33f7013c94564007e2c5b33cea47 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 10:42:54 +0200 Subject: main.go: Cleanup from 13c84cd9973458750305c72a919cf921d9b22b04 * Get rid of the 'yaml' import since we're now using 'toml' instead. * Get rid of commented code that's no longer relevant. * Get rid of test code that checked that the config loaded correctly. --- main.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/main.go b/main.go index bfb56b8..61cdce6 100644 --- a/main.go +++ b/main.go @@ -9,22 +9,12 @@ import ( "github.com/teddywing/timetasker/timetask" "github.com/BurntSushi/toml" - "gopkg.in/yaml.v2" ) -// type Config struct { -// Auth struct { -// Username string -// PasswordCmd string `yaml:"password_cmd"` -// } -// Fields timetask.Fields -// Defaults timetask.TimeEntry -// } - type Config struct { Auth struct { Username string - PasswordCmd string //`toml:"password_cmd"` + PasswordCmd string } Projects map[string]interface{} } @@ -33,8 +23,6 @@ var config Config func main() { loadConfig() - log.Printf("%+v", config) - return if len(os.Args) == 1 { fmt.Println("Not enough arguments") @@ -61,7 +49,6 @@ func main() { } func loadConfig() { - // config_str, err := ioutil.ReadFile("config2.toml") config = Config{} _, err := toml.DecodeFile("config2.toml", &config) if err != nil { -- cgit v1.2.3 From e99c7b69e0981d4a7b87310cd811b90a2ffbe12f Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 11:22:08 +0200 Subject: Comment out all the things! Half get rid of a lot of code. I don't like and don't want to use our old field types. Get rid of them and the code in 'http.go' that depends on them. Also get rid of the time entry submission code in 'main.go' as that's going to be redone. --- main.go | 44 +++++------ timetask/fields.go | 160 +++++++++++++++++++-------------------- timetask/http.go | 214 ++++++++++++++++++++++++++--------------------------- 3 files changed, 209 insertions(+), 209 deletions(-) diff --git a/main.go b/main.go index 61cdce6..b31a4f0 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,12 @@ package main import ( - "fmt" - "io/ioutil" + // "fmt" + // "io/ioutil" "log" - "os" + // "os" - "github.com/teddywing/timetasker/timetask" + // "github.com/teddywing/timetasker/timetask" "github.com/BurntSushi/toml" ) @@ -24,24 +24,24 @@ var config Config func main() { loadConfig() - if len(os.Args) == 1 { - fmt.Println("Not enough arguments") - os.Exit(1) - } - - file_path := os.Args[len(os.Args)-1] - file, err := ioutil.ReadFile(file_path) - if err != nil { - log.Println(err) - } - - time_entries := []timetask.TimeEntry{} - err = yaml.Unmarshal(file, &time_entries) - if err != nil { - log.Println(err) - } - - log.Printf("%+v", time_entries) + // if len(os.Args) == 1 { + // fmt.Println("Not enough arguments") + // os.Exit(1) + // } + // + // file_path := os.Args[len(os.Args)-1] + // file, err := ioutil.ReadFile(file_path) + // if err != nil { + // log.Println(err) + // } + + // time_entries := []timetask.TimeEntry{} + // err = yaml.Unmarshal(file, &time_entries) + // if err != nil { + // log.Println(err) + // } + // + // log.Printf("%+v", time_entries) // timetask.SubmitTimeEntries(config.Fields, time_entries) diff --git a/timetask/fields.go b/timetask/fields.go index fb3a026..f3bb27d 100644 --- a/timetask/fields.go +++ b/timetask/fields.go @@ -1,85 +1,85 @@ package timetask -import "fmt" +// import "fmt" -type Client struct { - ID int - Name string - Projects []Project -} +// type Client struct { +// ID int +// Name string +// Projects []Project +// } -type Project struct { - ID int - Name string - Modules []Module - Tasks []Task - WorkTypes []WorkType `yaml:"work_types"` -} +// type Project struct { +// ID int +// Name string +// Modules []Module +// Tasks []Task +// WorkTypes []WorkType `yaml:"work_types"` +// } -type Module struct { - ID int - Name string -} -type Task struct { - ID int - Name string -} -type WorkType struct { - ID int - Name string -} - -type Fields struct { - PersonID int `yaml:"person_id"` - Clients []Client -} - -func (f *Fields) ClientByName(client_name string) (*Client, error) { - for _, client := range f.Clients { - if client.Name == client_name { - return &client, nil - } - } - - return nil, fmt.Errorf("Client %s not found", client_name) -} - -func (c *Client) ProjectByName(project_name string) (*Project, error) { - for _, project := range c.Projects { - if project.Name == project_name { - return &project, nil - } - } - - return nil, fmt.Errorf("Project %s not found", project_name) -} - -func (p *Project) ModuleByName(module_name string) (*Module, error) { - for _, module := range p.Modules { - if module.Name == module_name { - return &module, nil - } - } - - return nil, fmt.Errorf("Module %s not found", module_name) -} - -func (p *Project) TaskByName(task_name string) (*Task, error) { - for _, task := range p.Tasks { - if task.Name == task_name { - return &task, nil - } - } - - return nil, fmt.Errorf("Task %s not found", task_name) -} - -func (p *Project) WorkTypeByName(work_type_name string) (*WorkType, error) { - for _, work_type := range p.WorkTypes { - if work_type.Name == work_type_name { - return &work_type, nil - } - } - - return nil, fmt.Errorf("Work type %s not found", work_type_name) -} +// type Module struct { +// ID int +// Name string +// } +// type Task struct { +// ID int +// Name string +// } +// type WorkType struct { +// ID int +// Name string +// } +// +// type Fields struct { +// PersonID int `yaml:"person_id"` +// Clients []Client +// } +// +// func (f *Fields) ClientByName(client_name string) (*Client, error) { +// for _, client := range f.Clients { +// if client.Name == client_name { +// return &client, nil +// } +// } +// +// return nil, fmt.Errorf("Client %s not found", client_name) +// } +// +// func (c *Client) ProjectByName(project_name string) (*Project, error) { +// for _, project := range c.Projects { +// if project.Name == project_name { +// return &project, nil +// } +// } +// +// return nil, fmt.Errorf("Project %s not found", project_name) +// } +// +// func (p *Project) ModuleByName(module_name string) (*Module, error) { +// for _, module := range p.Modules { +// if module.Name == module_name { +// return &module, nil +// } +// } +// +// return nil, fmt.Errorf("Module %s not found", module_name) +// } +// +// func (p *Project) TaskByName(task_name string) (*Task, error) { +// for _, task := range p.Tasks { +// if task.Name == task_name { +// return &task, nil +// } +// } +// +// return nil, fmt.Errorf("Task %s not found", task_name) +// } +// +// func (p *Project) WorkTypeByName(work_type_name string) (*WorkType, error) { +// for _, work_type := range p.WorkTypes { +// if work_type.Name == work_type_name { +// return &work_type, nil +// } +// } +// +// return nil, fmt.Errorf("Work type %s not found", work_type_name) +// } diff --git a/timetask/http.go b/timetask/http.go index 83946ad..94a1597 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -1,13 +1,13 @@ package timetask import ( - "fmt" - "log" + // "fmt" + // "log" "net/http" "net/http/cookiejar" "net/url" - "strconv" - "strings" + // "strconv" + // "strings" "golang.org/x/net/publicsuffix" ) @@ -36,106 +36,106 @@ func Login(username, password string) (resp *http.Response, err error) { return resp, err } -func SubmitTimeEntries(fields Fields, time_entries []TimeEntry) (resp *http.Response, err error) { - v := buildSubmissionParams(fields, time_entries) - - v.Set("module", "time") - v.Set("action", "submitmultipletime") - - return nil, nil -} - -func buildSubmissionParams(fields Fields, time_entries []TimeEntry) url.Values { - v := url.Values{} - entry_indexes := []string{} - - for i, entry := range time_entries { - entry_indexes = append(entry_indexes, strconv.Itoa(i)) - - client, err := fields.ClientByName(entry.Client) - if err != nil { - log.Panic(err) - } - - project, err := client.ProjectByName(entry.Project) - if err != nil { - log.Panic(err) - } - - module, err := project.ModuleByName(entry.Module) - if err != nil { - log.Panic(err) - } - - task, err := project.TaskByName(entry.Task) - if err != nil { - log.Panic(err) - } - - work_type, err := project.WorkTypeByName(entry.WorkType) - if err != nil { - log.Panic(err) - } - - var billable string - if entry.Billable { - billable = "t" - } else { - billable = "f" - } - - v.Set( - fmt.Sprintf("f_personID%d", i), - strconv.Itoa(fields.PersonID), - ) - - v.Set( - fmt.Sprintf("f_clientID%d", i), - strconv.Itoa(client.ID), - ) - - v.Set( - fmt.Sprintf("f_projectID%d", i), - strconv.Itoa(project.ID), - ) - - v.Set( - fmt.Sprintf("f_moduleID%d", i), - strconv.Itoa(module.ID), - ) - - v.Set( - fmt.Sprintf("f_taskID%d", i), - strconv.Itoa(task.ID), - ) - - v.Set( - fmt.Sprintf("f_worktypeID%d", i), - strconv.Itoa(work_type.ID), - ) - - v.Set( - fmt.Sprintf("f_date%d", i), - entry.Date.Format("02/01/06"), // day/month/year - ) - - v.Set( - fmt.Sprintf("f_time%d", i), - strconv.Itoa(entry.Time), - ) - - v.Set( - fmt.Sprintf("f_billable%d", i), - billable, - ) - - v.Set( - fmt.Sprintf("f_description%d", i), - entry.Description, - ) - } - - v.Set("f_entryIndexes", strings.Join(entry_indexes, ",")) - - return v -} +// func SubmitTimeEntries(fields Fields, time_entries []TimeEntry) (resp *http.Response, err error) { +// v := buildSubmissionParams(fields, time_entries) +// +// v.Set("module", "time") +// v.Set("action", "submitmultipletime") +// +// return nil, nil +// } +// +// func buildSubmissionParams(fields Fields, time_entries []TimeEntry) url.Values { +// v := url.Values{} +// entry_indexes := []string{} +// +// for i, entry := range time_entries { +// entry_indexes = append(entry_indexes, strconv.Itoa(i)) +// +// client, err := fields.ClientByName(entry.Client) +// if err != nil { +// log.Panic(err) +// } +// +// project, err := client.ProjectByName(entry.Project) +// if err != nil { +// log.Panic(err) +// } +// +// module, err := project.ModuleByName(entry.Module) +// if err != nil { +// log.Panic(err) +// } +// +// task, err := project.TaskByName(entry.Task) +// if err != nil { +// log.Panic(err) +// } +// +// work_type, err := project.WorkTypeByName(entry.WorkType) +// if err != nil { +// log.Panic(err) +// } +// +// var billable string +// if entry.Billable { +// billable = "t" +// } else { +// billable = "f" +// } +// +// v.Set( +// fmt.Sprintf("f_personID%d", i), +// strconv.Itoa(fields.PersonID), +// ) +// +// v.Set( +// fmt.Sprintf("f_clientID%d", i), +// strconv.Itoa(client.ID), +// ) +// +// v.Set( +// fmt.Sprintf("f_projectID%d", i), +// strconv.Itoa(project.ID), +// ) +// +// v.Set( +// fmt.Sprintf("f_moduleID%d", i), +// strconv.Itoa(module.ID), +// ) +// +// v.Set( +// fmt.Sprintf("f_taskID%d", i), +// strconv.Itoa(task.ID), +// ) +// +// v.Set( +// fmt.Sprintf("f_worktypeID%d", i), +// strconv.Itoa(work_type.ID), +// ) +// +// v.Set( +// fmt.Sprintf("f_date%d", i), +// entry.Date.Format("02/01/06"), // day/month/year +// ) +// +// v.Set( +// fmt.Sprintf("f_time%d", i), +// strconv.Itoa(entry.Time), +// ) +// +// v.Set( +// fmt.Sprintf("f_billable%d", i), +// billable, +// ) +// +// v.Set( +// fmt.Sprintf("f_description%d", i), +// entry.Description, +// ) +// } +// +// v.Set("f_entryIndexes", strings.Join(entry_indexes, ",")) +// +// return v +// } -- cgit v1.2.3 From d998a82d4019b1fc5a15734f091852b1b0f086d4 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 11:24:55 +0200 Subject: Create a new Project type This corresponds to a "project" entry in the new config2.toml file. (See 13c84cd9973458750305c72a919cf921d9b22b04). Instead of decoding generic `interface{}`s as projects from the TOML, make them a real type. The reason why we're using `int`s where we used to use strings is that the new TOML format will have users write IDs directly in the config file, instead of having the program automatically search for those IDs and use them as we had previously designed. Maybe we'll bring that functionality back at some point, but for now it's too bothersome to implement and I don't consider it worth the trouble. --- main.go | 4 ++-- timetask/fields.go | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index b31a4f0..ccdcaed 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,7 @@ import ( "log" // "os" - // "github.com/teddywing/timetasker/timetask" + "github.com/teddywing/timetasker/timetask" "github.com/BurntSushi/toml" ) @@ -16,7 +16,7 @@ type Config struct { Username string PasswordCmd string } - Projects map[string]interface{} + Projects map[string]timetask.Project } var config Config diff --git a/timetask/fields.go b/timetask/fields.go index f3bb27d..da2b37d 100644 --- a/timetask/fields.go +++ b/timetask/fields.go @@ -16,6 +16,16 @@ package timetask // WorkTypes []WorkType `yaml:"work_types"` // } +type Project struct { + Client int + Project int + Module int + Task int + WorkType int + Time int + Billable bool +} + // type Module struct { // ID int // Name string -- cgit v1.2.3 From 302fbc3a9db675c24e3ca2cb0c99d1ae5b88180e Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 11:47:26 +0200 Subject: time_entry.go: Rewrite `TimeEntry` type for the new era Change [Client, Project, Module, Task, WorkType] fields to `int`s instead of strings. In the new era, with config2.toml, these fields will be populated directly with the proper int IDs from TimeTask. Thus these fields need to be `int`s. Get rid of the `UnmarshalYAML` function as in the new era we won't be submitting time entries by YAML file. Instead they will be submitted directly via command line arguments (plus the IDs coming from the config file). --- timetask/time_entry.go | 47 +++++------------------------------------------ 1 file changed, 5 insertions(+), 42 deletions(-) diff --git a/timetask/time_entry.go b/timetask/time_entry.go index 17a8e0c..1cc4203 100644 --- a/timetask/time_entry.go +++ b/timetask/time_entry.go @@ -3,50 +3,13 @@ package timetask import "time" type TimeEntry struct { - Client string - Project string - Module string - Task string - WorkType string `yaml:"work_type"` + Client int + Project int + Module int + Task int + WorkType int Date time.Time Time int Billable bool Description string } - -// Parse date string into a real date -func (te *TimeEntry) UnmarshalYAML(unmarshal func(interface{}) error) error { - var auxiliary struct { - Client string - Project string - Module string - Task string - WorkType string `yaml:"work_type"` - Date string - Time int - Billable bool - Description string - } - - err := unmarshal(&auxiliary) - if err != nil { - return err - } - - date, err := time.Parse("2006-01-02", auxiliary.Date) - if auxiliary.Date != "" && err != nil { - return err - } - - te.Client = auxiliary.Client - te.Project = auxiliary.Project - te.Module = auxiliary.Module - te.Task = auxiliary.Task - te.WorkType = auxiliary.WorkType - te.Date = date - te.Time = auxiliary.Time - te.Billable = auxiliary.Billable - te.Description = auxiliary.Description - - return nil -} -- cgit v1.2.3 From bd1a9e85c2ee87017208772b88190b75319874d2 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 12:00:17 +0200 Subject: http.go: Restore `SubmitTimeEntries` & `buildSubmissionParams` Uncomment these functions. Looks like they'll still be useful after all. Just need a little munging to fit them into the usage of submitting a single time entry and using our new `Project` and `TimeEntry` types. --- timetask/http.go | 212 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 109 insertions(+), 103 deletions(-) diff --git a/timetask/http.go b/timetask/http.go index 94a1597..4832d00 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -36,106 +36,112 @@ func Login(username, password string) (resp *http.Response, err error) { return resp, err } -// func SubmitTimeEntries(fields Fields, time_entries []TimeEntry) (resp *http.Response, err error) { -// v := buildSubmissionParams(fields, time_entries) -// -// v.Set("module", "time") -// v.Set("action", "submitmultipletime") -// -// return nil, nil -// } -// -// func buildSubmissionParams(fields Fields, time_entries []TimeEntry) url.Values { -// v := url.Values{} -// entry_indexes := []string{} -// -// for i, entry := range time_entries { -// entry_indexes = append(entry_indexes, strconv.Itoa(i)) -// -// client, err := fields.ClientByName(entry.Client) -// if err != nil { -// log.Panic(err) -// } -// -// project, err := client.ProjectByName(entry.Project) -// if err != nil { -// log.Panic(err) -// } -// -// module, err := project.ModuleByName(entry.Module) -// if err != nil { -// log.Panic(err) -// } -// -// task, err := project.TaskByName(entry.Task) -// if err != nil { -// log.Panic(err) -// } -// -// work_type, err := project.WorkTypeByName(entry.WorkType) -// if err != nil { -// log.Panic(err) -// } -// -// var billable string -// if entry.Billable { -// billable = "t" -// } else { -// billable = "f" -// } -// -// v.Set( -// fmt.Sprintf("f_personID%d", i), -// strconv.Itoa(fields.PersonID), -// ) -// -// v.Set( -// fmt.Sprintf("f_clientID%d", i), -// strconv.Itoa(client.ID), -// ) -// -// v.Set( -// fmt.Sprintf("f_projectID%d", i), -// strconv.Itoa(project.ID), -// ) -// -// v.Set( -// fmt.Sprintf("f_moduleID%d", i), -// strconv.Itoa(module.ID), -// ) -// -// v.Set( -// fmt.Sprintf("f_taskID%d", i), -// strconv.Itoa(task.ID), -// ) -// -// v.Set( -// fmt.Sprintf("f_worktypeID%d", i), -// strconv.Itoa(work_type.ID), -// ) -// -// v.Set( -// fmt.Sprintf("f_date%d", i), -// entry.Date.Format("02/01/06"), // day/month/year -// ) -// -// v.Set( -// fmt.Sprintf("f_time%d", i), -// strconv.Itoa(entry.Time), -// ) -// -// v.Set( -// fmt.Sprintf("f_billable%d", i), -// billable, -// ) -// -// v.Set( -// fmt.Sprintf("f_description%d", i), -// entry.Description, -// ) -// } -// -// v.Set("f_entryIndexes", strings.Join(entry_indexes, ",")) -// -// return v -// } +func SubmitTimeEntry( + project Project, + time_entry TimeEntry, +) (resp *http.Response, err error) { +} + +func SubmitTimeEntries(fields Fields, time_entries []TimeEntry) (resp *http.Response, err error) { + v := buildSubmissionParams(fields, time_entries) + + v.Set("module", "time") + v.Set("action", "submitmultipletime") + + return nil, nil +} + +func buildSubmissionParams(fields Fields, time_entries []TimeEntry) url.Values { + v := url.Values{} + entry_indexes := []string{} + + for i, entry := range time_entries { + entry_indexes = append(entry_indexes, strconv.Itoa(i)) + + client, err := fields.ClientByName(entry.Client) + if err != nil { + log.Panic(err) + } + + project, err := client.ProjectByName(entry.Project) + if err != nil { + log.Panic(err) + } + + module, err := project.ModuleByName(entry.Module) + if err != nil { + log.Panic(err) + } + + task, err := project.TaskByName(entry.Task) + if err != nil { + log.Panic(err) + } + + work_type, err := project.WorkTypeByName(entry.WorkType) + if err != nil { + log.Panic(err) + } + + var billable string + if entry.Billable { + billable = "t" + } else { + billable = "f" + } + + v.Set( + fmt.Sprintf("f_personID%d", i), + strconv.Itoa(fields.PersonID), + ) + + v.Set( + fmt.Sprintf("f_clientID%d", i), + strconv.Itoa(client.ID), + ) + + v.Set( + fmt.Sprintf("f_projectID%d", i), + strconv.Itoa(project.ID), + ) + + v.Set( + fmt.Sprintf("f_moduleID%d", i), + strconv.Itoa(module.ID), + ) + + v.Set( + fmt.Sprintf("f_taskID%d", i), + strconv.Itoa(task.ID), + ) + + v.Set( + fmt.Sprintf("f_worktypeID%d", i), + strconv.Itoa(work_type.ID), + ) + + v.Set( + fmt.Sprintf("f_date%d", i), + entry.Date.Format("02/01/06"), // day/month/year + ) + + v.Set( + fmt.Sprintf("f_time%d", i), + strconv.Itoa(entry.Time), + ) + + v.Set( + fmt.Sprintf("f_billable%d", i), + billable, + ) + + v.Set( + fmt.Sprintf("f_description%d", i), + entry.Description, + ) + } + + v.Set("f_entryIndexes", strings.Join(entry_indexes, ",")) + + return v +} -- cgit v1.2.3 From 810b140b4a29b1159e76b51b90b9be7d22df1c3e Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 12:08:07 +0200 Subject: buildSubmissionParams(): Support new `TimeEntry`; Submit single entry Change the function to use our new `TimeEntry` type, which doesn't demand asking for IDs as it already has them built in. Additionally, remove the loop here as we only want to submit a single time entry at a time. Add a new `Profile` type that holds onto the user's person_id. Forgot that existed. We'll have users fill that into their config.toml file. --- timetask/http.go | 146 ++++++++++++++++++++-------------------------------- timetask/profile.go | 5 ++ 2 files changed, 62 insertions(+), 89 deletions(-) create mode 100644 timetask/profile.go diff --git a/timetask/http.go b/timetask/http.go index 4832d00..8b90230 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -6,7 +6,7 @@ import ( "net/http" "net/http/cookiejar" "net/url" - // "strconv" + "strconv" // "strings" "golang.org/x/net/publicsuffix" @@ -51,97 +51,65 @@ func SubmitTimeEntries(fields Fields, time_entries []TimeEntry) (resp *http.Resp return nil, nil } -func buildSubmissionParams(fields Fields, time_entries []TimeEntry) url.Values { +func buildSubmissionParams(profile Profile, time_entry TimeEntry) url.Values { v := url.Values{} - entry_indexes := []string{} - - for i, entry := range time_entries { - entry_indexes = append(entry_indexes, strconv.Itoa(i)) - - client, err := fields.ClientByName(entry.Client) - if err != nil { - log.Panic(err) - } - - project, err := client.ProjectByName(entry.Project) - if err != nil { - log.Panic(err) - } - - module, err := project.ModuleByName(entry.Module) - if err != nil { - log.Panic(err) - } - - task, err := project.TaskByName(entry.Task) - if err != nil { - log.Panic(err) - } - - work_type, err := project.WorkTypeByName(entry.WorkType) - if err != nil { - log.Panic(err) - } - - var billable string - if entry.Billable { - billable = "t" - } else { - billable = "f" - } - - v.Set( - fmt.Sprintf("f_personID%d", i), - strconv.Itoa(fields.PersonID), - ) - - v.Set( - fmt.Sprintf("f_clientID%d", i), - strconv.Itoa(client.ID), - ) - - v.Set( - fmt.Sprintf("f_projectID%d", i), - strconv.Itoa(project.ID), - ) - - v.Set( - fmt.Sprintf("f_moduleID%d", i), - strconv.Itoa(module.ID), - ) - - v.Set( - fmt.Sprintf("f_taskID%d", i), - strconv.Itoa(task.ID), - ) - - v.Set( - fmt.Sprintf("f_worktypeID%d", i), - strconv.Itoa(work_type.ID), - ) - - v.Set( - fmt.Sprintf("f_date%d", i), - entry.Date.Format("02/01/06"), // day/month/year - ) - - v.Set( - fmt.Sprintf("f_time%d", i), - strconv.Itoa(entry.Time), - ) - - v.Set( - fmt.Sprintf("f_billable%d", i), - billable, - ) - - v.Set( - fmt.Sprintf("f_description%d", i), - entry.Description, - ) + + v.Set( + "f_personID0", + strconv.Itoa(profile.PersonID), + ) + + v.Set( + "f_clientID0", + strconv.Itoa(time_entry.Client), + ) + + v.Set( + "f_projectID0", + strconv.Itoa(time_entry.Project), + ) + + v.Set( + "f_moduleID0", + strconv.Itoa(time_entry.Module), + ) + + v.Set( + "f_taskID0", + strconv.Itoa(time_entry.Task), + ) + + v.Set( + "f_worktypeID0", + strconv.Itoa(time_entry.WorkType), + ) + + v.Set( + "f_date0", + time_entry.Date.Format("02/01/06"), // day/month/year + ) + + v.Set( + "f_time0", + strconv.Itoa(time_entry.Time), + ) + + var billable string + if time_entry.Billable { + billable = "t" + } else { + billable = "f" } - v.Set("f_entryIndexes", strings.Join(entry_indexes, ",")) + v.Set( + "f_billable0", + billable, + ) + + v.Set( + "f_description0", + time_entry.Description, + ) return v } diff --git a/timetask/profile.go b/timetask/profile.go new file mode 100644 index 0000000..66e9197 --- /dev/null +++ b/timetask/profile.go @@ -0,0 +1,5 @@ +package timetask + +type Profile struct { + PersonID int +} -- cgit v1.2.3 From 44cb0e11566c7416c8859dac18baf24a0337ecce Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 12:14:44 +0200 Subject: Project: Add a TOML tag for the WorkType field Otherwise the TOML decoder didn't know how to parse it. --- timetask/fields.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timetask/fields.go b/timetask/fields.go index da2b37d..f535d92 100644 --- a/timetask/fields.go +++ b/timetask/fields.go @@ -21,7 +21,7 @@ type Project struct { Project int Module int Task int - WorkType int + WorkType int `toml:"work_type"` Time int Billable bool } -- cgit v1.2.3 From 1576d4111d412a1c501b6a8630c4d559302b9b21 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 12:16:33 +0200 Subject: main.go: Add `Profile` to `Config` Require the config.toml file to come with a `[profile]` hash. This allows us to get the "person_id" for correct submission to TimeTask. Additionally, add a TOML tag to `PersonID` in `Profile` to allow it to be decoded. --- main.go | 1 + timetask/profile.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index ccdcaed..aea2112 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ type Config struct { Username string PasswordCmd string } + Profile timetask.Profile Projects map[string]timetask.Project } diff --git a/timetask/profile.go b/timetask/profile.go index 66e9197..9267ee6 100644 --- a/timetask/profile.go +++ b/timetask/profile.go @@ -1,5 +1,5 @@ package timetask type Profile struct { - PersonID int + PersonID int `toml:"person_id"` } -- cgit v1.2.3 From 758cf0bf7e899acee89ee4b8a39c5565622aee31 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 12:42:35 +0200 Subject: http.go: Replace `SubmitTimeEntries` with `SubmitTimeEntry` A slightly altered function that only submits a single time entry and uses our new `Profile`, `Project`, and `TimeEntry` types. --- timetask/http.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/timetask/http.go b/timetask/http.go index 8b90230..76bfdc3 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -37,13 +37,11 @@ func Login(username, password string) (resp *http.Response, err error) { } func SubmitTimeEntry( + profile Profile, project Project, time_entry TimeEntry, ) (resp *http.Response, err error) { -} - -func SubmitTimeEntries(fields Fields, time_entries []TimeEntry) (resp *http.Response, err error) { - v := buildSubmissionParams(fields, time_entries) + v := buildSubmissionParams(profile, time_entry) v.Set("module", "time") v.Set("action", "submitmultipletime") @@ -51,6 +49,7 @@ func SubmitTimeEntries(fields Fields, time_entries []TimeEntry) (resp *http.Resp return nil, nil } + func buildSubmissionParams(profile Profile, time_entry TimeEntry) url.Values { v := url.Values{} -- cgit v1.2.3 From f017c462593496efbc0810ec2603da49a2e3d9d8 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 13:02:03 +0200 Subject: Add `NewTimeEntry()` A new function that creates a `TimeEntry` build from a `Profile`, `Project`, and a few other parameters. Makes it a bit easier to create `TimeEntry`ies. Additionally, add a `PersonID` field to `TimeEntry` so we can pass it around as a self-contained thing without worrying about having to pass a `Profile` along with it. --- timetask/time_entry.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/timetask/time_entry.go b/timetask/time_entry.go index 1cc4203..161de3e 100644 --- a/timetask/time_entry.go +++ b/timetask/time_entry.go @@ -3,6 +3,7 @@ package timetask import "time" type TimeEntry struct { + PersonID int Client int Project int Module int @@ -13,3 +14,24 @@ type TimeEntry struct { Billable bool Description string } + +func NewTimeEntry( + profile Profile, + project Project, + date time.Time, + time int, + description string, +) TimeEntry { + return TimeEntry{ + PersonID: profile.PersonID, + Client: project.Client, + Project: project.Project, + Module: project.Module, + Task: project.Task, + WorkType: project.WorkType, + Date: date, + Time: time, + Billable: project.Billable, + Description: description, + } +} -- cgit v1.2.3 From 2bd0cb518e1eee9be503aea5198889253aa435f7 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 13:03:52 +0200 Subject: Project: Remove `Time` field Time spent on a project can be variable from day to day. The time shouldn't be stored on the project, and should no longer be passed in config.toml. Instead it should be a part of `TimeEntry`, which it now is. --- timetask/fields.go | 1 - 1 file changed, 1 deletion(-) diff --git a/timetask/fields.go b/timetask/fields.go index f535d92..9f7aa99 100644 --- a/timetask/fields.go +++ b/timetask/fields.go @@ -22,7 +22,6 @@ type Project struct { Module int Task int WorkType int `toml:"work_type"` - Time int Billable bool } -- cgit v1.2.3 From 84b921283c4106de55ca7908a58e8f1621b3841f Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 13:35:41 +0200 Subject: SubmitTimeEntry(): Remove `Profile` & `Project` arguments Now that `Profile` and `Project` have been subsumed into `TimeEntry` (f017c462593496efbc0810ec2603da49a2e3d9d8), these arguments should no longer be here and their data should come from `TimeEntry` instead. --- timetask/http.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/timetask/http.go b/timetask/http.go index 76bfdc3..4f52565 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -37,11 +37,9 @@ func Login(username, password string) (resp *http.Response, err error) { } func SubmitTimeEntry( - profile Profile, - project Project, time_entry TimeEntry, ) (resp *http.Response, err error) { - v := buildSubmissionParams(profile, time_entry) + v := buildSubmissionParams(time_entry) v.Set("module", "time") v.Set("action", "submitmultipletime") @@ -50,12 +48,12 @@ func SubmitTimeEntry( } -func buildSubmissionParams(profile Profile, time_entry TimeEntry) url.Values { +func buildSubmissionParams(time_entry TimeEntry) url.Values { v := url.Values{} v.Set( "f_personID0", - strconv.Itoa(profile.PersonID), + strconv.Itoa(time_entry.PersonID), ) v.Set( -- cgit v1.2.3 From 288da883a3a4a91fec67029b7695ec03099cfc46 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 14:16:29 +0200 Subject: Initial stab at submitting time entries for real main.go: * Login as the configured user (haven't yet handled making `PasswordCmd` an actual shell command) * Create a test time entry * Submit that time entry using `SubmitTimeEntry()` http.go: * Create a `baseURL` global that stores the base TimeTask URL to make requests to * Return an `http.Client` from `Login()` that can then be passed to `SubmitTimeEntry()` to reuse the login session. Needed to return a pointer to allow us to return `nil` from the first error handler in the function. Don't like that at all, but we're just trying to get it to work at this point. * Actually make an HTTP POST request in `SubmitTimeEntry()` using the given HTTP Client and existing time entry submission params * Take an `http.Client` argument in `SubmitTimeEntry()` to allow us to use a logged-in session to POST. Otherwise we'd be locked out. * Change `v` variable to `values` in `SubmitTimeEntry()` for better readability --- main.go | 23 +++++++++++++++++++++++ timetask/http.go | 35 +++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index aea2112..1cc56c7 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( // "io/ioutil" "log" // "os" + "time" "github.com/teddywing/timetasker/timetask" @@ -25,6 +26,28 @@ var config Config func main() { loadConfig() + resp, client, err := timetask.Login( + config.Auth.Username, + config.Auth.PasswordCmd, + ) + if err != nil { + log.Fatalln(err) + } + log.Printf("%+v\n", resp) + + time_entry := timetask.NewTimeEntry( + config.Profile, + config.Projects["example"], + time.Now(), + 7, + "timetasker test", + ) + resp, err = timetask.SubmitTimeEntry(*client, time_entry) + if err != nil { + log.Fatalln(err) + } + log.Printf("%+v\n", resp) + // if len(os.Args) == 1 { // fmt.Println("Not enough arguments") // os.Exit(1) diff --git a/timetask/http.go b/timetask/http.go index 4f52565..cb91257 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -12,15 +12,21 @@ import ( "golang.org/x/net/publicsuffix" ) -func Login(username, password string) (resp *http.Response, err error) { +var baseURL string = "https://af83.timetask.com/index.php" + +func Login(username, password string) ( + resp *http.Response, + client *http.Client, + err error, +) { cookies, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) if err != nil { - return nil, err + return nil, nil, err } - client := http.Client{Jar: cookies} + client = &http.Client{Jar: cookies} resp, err = client.PostForm( - "https://af83.timetask.com/index.php", + baseURL, url.Values{ "module": {"people"}, "action": {"loginsubmit"}, @@ -30,21 +36,30 @@ func Login(username, password string) (resp *http.Response, err error) { }, ) if err != nil { - return resp, err + return resp, client, err } - return resp, err + return resp, client, err } func SubmitTimeEntry( + client http.Client, time_entry TimeEntry, ) (resp *http.Response, err error) { - v := buildSubmissionParams(time_entry) + values := buildSubmissionParams(time_entry) - v.Set("module", "time") - v.Set("action", "submitmultipletime") + values.Set("module", "time") + values.Set("action", "submitmultipletime") + + resp, err = client.PostForm( + baseURL, + values, + ) + if err != nil { + return resp, err + } - return nil, nil + return resp, nil } -- cgit v1.2.3 From bd2d890a9ad1128bf25b560fc4d796249c4ed495 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 14:34:13 +0200 Subject: main.go: Print HTTP response bodies for debugging Previously we were just printing the headers. Now print the bodies to allow us to inspect the result. Will probably want to add some handling that messages the user about authentication problems. --- main.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 1cc56c7..2f82643 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,7 @@ package main import ( // "fmt" - // "io/ioutil" + "io/ioutil" "log" // "os" "time" @@ -35,6 +35,10 @@ func main() { } log.Printf("%+v\n", resp) + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + log.Println(string(body)) + time_entry := timetask.NewTimeEntry( config.Profile, config.Projects["example"], @@ -48,6 +52,10 @@ func main() { } log.Printf("%+v\n", resp) + defer resp.Body.Close() + body, err = ioutil.ReadAll(resp.Body) + log.Println(string(body)) + // if len(os.Args) == 1 { // fmt.Println("Not enough arguments") // os.Exit(1) -- cgit v1.2.3 From 460ffe9ca5a7ad9ae5fd5a7d501b8cbd64e7646e Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 14:41:31 +0200 Subject: main.go: Add TOML tag to Config.PasswordCmd This field wasn't parsing correctly because of it's non-automatically-parseable format. --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 2f82643..3a343b5 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ import ( type Config struct { Auth struct { Username string - PasswordCmd string + PasswordCmd string `toml:"password_cmd"` } Profile timetask.Profile Projects map[string]timetask.Project -- cgit v1.2.3 From 24a3a2d9dbe6b2cd48d126f3f36a6d79497b4cc4 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 15:08:23 +0200 Subject: buildSubmissionParams(): Add `f_entryIndexes` param The param needs to be present in the request in order for it to be considered. Add it back in (we had it before 810b140b4a29b1159e76b51b90b9be7d22df1c3e) with only the `0` index being sent in the request. This is because we're only submitting a single time entry, the 0th one. (Other parameters end with a '0'.) --- timetask/http.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/timetask/http.go b/timetask/http.go index cb91257..736fcfb 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -123,5 +123,7 @@ func buildSubmissionParams(time_entry TimeEntry) url.Values { time_entry.Description, ) + v.Set("f_entryIndexes", "0") + return v } -- cgit v1.2.3 From 194f180cbc2cdf8f739b83296e26ea8805d2c122 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 15:21:55 +0200 Subject: http_test.go: Skip Login() test Because it makes a network request. I originally wrote this to test the Login function while developing so it wasn't a big deal, but we really don't want to run this type of test in normal situations. --- timetask/http_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/timetask/http_test.go b/timetask/http_test.go index c54617f..604c3c3 100644 --- a/timetask/http_test.go +++ b/timetask/http_test.go @@ -17,6 +17,8 @@ func init() { } func TestLogin(t *testing.T) { + t.Skip("No requests") + response, err := Login(username, password) if err != nil { t.Fatal(err) -- cgit v1.2.3 From b9c75fc559a5d79042e419cba8a9b5a4546c9b6d Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 15:26:16 +0200 Subject: Remove generator.go, templates/ directory These are not needed in the new world where we only submit a single time entry at a time and entry parameters are filled in on the command line. My guess is we'll likely have some sort of generator in the future to create the initial `config.toml` file, but that won't contain any logic-based data. My guess is we'll probably be able to just stick it in a template string right inside a *.go file. --- templates/timesheet.yml.tmpl | 11 ---------- templates/weekly_timesheet.yml.tmpl | 3 --- timetask/generator.go | 44 ------------------------------------- 3 files changed, 58 deletions(-) delete mode 100644 templates/timesheet.yml.tmpl delete mode 100644 templates/weekly_timesheet.yml.tmpl delete mode 100644 timetask/generator.go diff --git a/templates/timesheet.yml.tmpl b/templates/timesheet.yml.tmpl deleted file mode 100644 index 1930b09..0000000 --- a/templates/timesheet.yml.tmpl +++ /dev/null @@ -1,11 +0,0 @@ -{{- define "timesheet" -}} -- client: {{.Client}} - project: {{.Project}} - module: {{.Module}} - task: {{.Task}} - work_type: {{.WorkType}} - date: {{.Date.Format "02/01/06"}} - time: {{.Time}} - billable: {{.Billable}} - description: {{.Description}} -{{ end -}} diff --git a/templates/weekly_timesheet.yml.tmpl b/templates/weekly_timesheet.yml.tmpl deleted file mode 100644 index 0ea6582..0000000 --- a/templates/weekly_timesheet.yml.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -{{- range . -}} - {{- template "timesheet" . -}} -{{- end -}} diff --git a/timetask/generator.go b/timetask/generator.go deleted file mode 100644 index 5d0fa7f..0000000 --- a/timetask/generator.go +++ /dev/null @@ -1,44 +0,0 @@ -package timetask - -import ( - "io" - "log" - "text/template" - "time" - - "github.com/olebedev/when" - "github.com/olebedev/when/rules/common" - "github.com/olebedev/when/rules/en" -) - -func GenerateWeeklyTimesheet(wr io.Writer, defaults TimeEntry) { - w := when.New(nil) - w.Add(en.All...) - w.Add(common.All...) - - monday, err := w.Parse("last monday", time.Now()) - if err != nil { - log.Panic(err) - } - - time_entries := []TimeEntry{} - day := monday.Time - for i := 1; i <= 5; i++ { - time_entries = append(time_entries, defaults) - time_entries[len(time_entries) - 1].Date = day - day = day.AddDate(0, 0, 1) // Add 1 day - } - - t, err := template.ParseFiles( - "templates/weekly_timesheet.yml.tmpl", - "templates/timesheet.yml.tmpl", - ) - if err != nil { - log.Panic(err) - } - - err = t.Execute(wr, time_entries) - if err != nil { - log.Panic(err) - } -} -- cgit v1.2.3 From 72cfd2966b0de3ae96a37005848087055c4b944a Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 15:30:00 +0200 Subject: Move 'fields.go' to 'project.go' Get rid of all the old commented Fields code. It's no longer used and is no longer relevant. Furthermore, since the only thing left in this file is the `Project` struct, rename the file to 'project.go'. --- timetask/fields.go | 94 ----------------------------------------------------- timetask/project.go | 10 ++++++ 2 files changed, 10 insertions(+), 94 deletions(-) delete mode 100644 timetask/fields.go create mode 100644 timetask/project.go diff --git a/timetask/fields.go b/timetask/fields.go deleted file mode 100644 index 9f7aa99..0000000 --- a/timetask/fields.go +++ /dev/null @@ -1,94 +0,0 @@ -package timetask - -// import "fmt" - -// type Client struct { -// ID int -// Name string -// Projects []Project -// } - -// type Project struct { -// ID int -// Name string -// Modules []Module -// Tasks []Task -// WorkTypes []WorkType `yaml:"work_types"` -// } - -type Project struct { - Client int - Project int - Module int - Task int - WorkType int `toml:"work_type"` - Billable bool -} - -// type Module struct { -// ID int -// Name string -// } -// type Task struct { -// ID int -// Name string -// } -// type WorkType struct { -// ID int -// Name string -// } -// -// type Fields struct { -// PersonID int `yaml:"person_id"` -// Clients []Client -// } -// -// func (f *Fields) ClientByName(client_name string) (*Client, error) { -// for _, client := range f.Clients { -// if client.Name == client_name { -// return &client, nil -// } -// } -// -// return nil, fmt.Errorf("Client %s not found", client_name) -// } -// -// func (c *Client) ProjectByName(project_name string) (*Project, error) { -// for _, project := range c.Projects { -// if project.Name == project_name { -// return &project, nil -// } -// } -// -// return nil, fmt.Errorf("Project %s not found", project_name) -// } -// -// func (p *Project) ModuleByName(module_name string) (*Module, error) { -// for _, module := range p.Modules { -// if module.Name == module_name { -// return &module, nil -// } -// } -// -// return nil, fmt.Errorf("Module %s not found", module_name) -// } -// -// func (p *Project) TaskByName(task_name string) (*Task, error) { -// for _, task := range p.Tasks { -// if task.Name == task_name { -// return &task, nil -// } -// } -// -// return nil, fmt.Errorf("Task %s not found", task_name) -// } -// -// func (p *Project) WorkTypeByName(work_type_name string) (*WorkType, error) { -// for _, work_type := range p.WorkTypes { -// if work_type.Name == work_type_name { -// return &work_type, nil -// } -// } -// -// return nil, fmt.Errorf("Work type %s not found", work_type_name) -// } diff --git a/timetask/project.go b/timetask/project.go new file mode 100644 index 0000000..b8fe5c7 --- /dev/null +++ b/timetask/project.go @@ -0,0 +1,10 @@ +package timetask + +type Project struct { + Client int + Project int + Module int + Task int + WorkType int `toml:"work_type"` + Billable bool +} -- cgit v1.2.3 From 10b741fb0c8e9c61647ea8ff977d6242fc86656e Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 15:35:15 +0200 Subject: Run gofmt on all project files --- main.go | 2 +- timetask/http.go | 1 - timetask/time_entry.go | 18 +++++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index 3a343b5..3111dfe 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,7 @@ type Config struct { Username string PasswordCmd string `toml:"password_cmd"` } - Profile timetask.Profile + Profile timetask.Profile Projects map[string]timetask.Project } diff --git a/timetask/http.go b/timetask/http.go index 736fcfb..8e7de70 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -62,7 +62,6 @@ func SubmitTimeEntry( return resp, nil } - func buildSubmissionParams(time_entry TimeEntry) url.Values { v := url.Values{} diff --git a/timetask/time_entry.go b/timetask/time_entry.go index 161de3e..ff0ad1f 100644 --- a/timetask/time_entry.go +++ b/timetask/time_entry.go @@ -23,15 +23,15 @@ func NewTimeEntry( description string, ) TimeEntry { return TimeEntry{ - PersonID: profile.PersonID, - Client: project.Client, - Project: project.Project, - Module: project.Module, - Task: project.Task, - WorkType: project.WorkType, - Date: date, - Time: time, - Billable: project.Billable, + PersonID: profile.PersonID, + Client: project.Client, + Project: project.Project, + Module: project.Module, + Task: project.Task, + WorkType: project.WorkType, + Date: date, + Time: time, + Billable: project.Billable, Description: description, } } -- cgit v1.2.3 From e439f382fd4bfed4cb547ca59fc0d679658457d4 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 15:36:27 +0200 Subject: main.go;http.go: Remove commented unused imports --- main.go | 2 -- timetask/http.go | 3 --- 2 files changed, 5 deletions(-) diff --git a/main.go b/main.go index 3111dfe..2974434 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,8 @@ package main import ( - // "fmt" "io/ioutil" "log" - // "os" "time" "github.com/teddywing/timetasker/timetask" diff --git a/timetask/http.go b/timetask/http.go index 8e7de70..6e73276 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -1,13 +1,10 @@ package timetask import ( - // "fmt" - // "log" "net/http" "net/http/cookiejar" "net/url" "strconv" - // "strings" "golang.org/x/net/publicsuffix" ) -- cgit v1.2.3 From 573df442661c9d0794688e1da98f1ef966ef6265 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 15:37:23 +0200 Subject: main.go: Remove old commented code This code was used for the old multiple time entry submission version of this program. Remove it now that the program only submits single time entries. We'll have similar argument handling code for user data, and the time entry submission code is already present here. --- main.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/main.go b/main.go index 2974434..41245db 100644 --- a/main.go +++ b/main.go @@ -53,29 +53,6 @@ func main() { defer resp.Body.Close() body, err = ioutil.ReadAll(resp.Body) log.Println(string(body)) - - // if len(os.Args) == 1 { - // fmt.Println("Not enough arguments") - // os.Exit(1) - // } - // - // file_path := os.Args[len(os.Args)-1] - // file, err := ioutil.ReadFile(file_path) - // if err != nil { - // log.Println(err) - // } - - // time_entries := []timetask.TimeEntry{} - // err = yaml.Unmarshal(file, &time_entries) - // if err != nil { - // log.Println(err) - // } - // - // log.Printf("%+v", time_entries) - - // timetask.SubmitTimeEntries(config.Fields, time_entries) - - // timetask.GenerateWeeklyTimesheet(os.Stdout, config.Defaults) } func loadConfig() { -- cgit v1.2.3 From 47ec5ffeea72fff2019636be2c97ee36fcd176a6 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 15:45:28 +0200 Subject: Add TODO Make a note of the things I have in my head right now that I think need to be done to get this project from a "works" state into a "usable" state. --- TODO | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000..ca62220 --- /dev/null +++ b/TODO @@ -0,0 +1,16 @@ +TODO + +2017.06.03: +- Command line arguments: + - Project alias (required) + - Time (required) + - Date (optional, format: 2017-01-31) + - Description (optional) + +- Handle failing responses from the server (show errors to the user) +- Config + - A `--write-config` or similar option that generates and write a bare + config for users to use + - Load the config from XDG + +- Make `PasswordCmd` work -- cgit v1.2.3 From 8d802bff08826523371ab5e951d85d0c0396ccc8 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 16:26:17 +0200 Subject: Add command line argument parsing * Use the Kingpin library to give us POSIX command line argument parsing with a nice interface * Add arguments for the project alias (specified in config.toml), time spent, date, and description * Add a version, required by Kingpin --- main.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/main.go b/main.go index 41245db..fedd7b5 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "github.com/teddywing/timetasker/timetask" "github.com/BurntSushi/toml" + "gopkg.in/alecthomas/kingpin.v2" ) type Config struct { @@ -24,6 +25,27 @@ var config Config func main() { loadConfig() + // Parse command line arguments + project_alias := kingpin.Flag( + "project", + "Project alias defined in config.toml.", + ). + Short('p'). + Required(). + String() + time_spent := kingpin.Flag("time", "Time spent working on project."). + Short('t'). + Default("7"). + Int() + date := kingpin.Flag("date", "Date when work was done (e.g. 2017-01-31)"). + String() + description := kingpin.Flag("description", "Description of work."). + Short('m'). + String() + kingpin.Version("0.1.0") + kingpin.Parse() + + // Submit time entry resp, client, err := timetask.Login( config.Auth.Username, config.Auth.PasswordCmd, -- cgit v1.2.3 From 703fb697dd64a5f79e319b3ef0e323d6eb1c30a0 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 16:29:45 +0200 Subject: main.go: Create time entry before making HTTP requests Make it easier to comment out the HTTP requests when testing command line argument parsing. To test, we're just printing the time entry with: log.Printf("%+v\n", time_entry) Fill in the values from our new command line argument variables. These need to be dereferenced as they come out of Kingpin as pointers. Haven't completely worked out how to deal with the 'date' argument yet so leaving it for later. Need to fail early if `project_alias` isn't recorgnised. --- main.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index fedd7b5..b9a91cf 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,14 @@ func main() { kingpin.Parse() // Submit time entry + time_entry := timetask.NewTimeEntry( + config.Profile, + config.Projects[*project_alias], + time.Now(), + *time_spent, + *description, + ) + resp, client, err := timetask.Login( config.Auth.Username, config.Auth.PasswordCmd, @@ -59,13 +67,6 @@ func main() { body, err := ioutil.ReadAll(resp.Body) log.Println(string(body)) - time_entry := timetask.NewTimeEntry( - config.Profile, - config.Projects["example"], - time.Now(), - 7, - "timetasker test", - ) resp, err = timetask.SubmitTimeEntry(*client, time_entry) if err != nil { log.Fatalln(err) -- cgit v1.2.3 From 73d5fa6c6233331c54d9b7a74407f2d17294e233 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 16:36:34 +0200 Subject: main.go: Fail if an unrecognised project alias is passed If the user specifies a project alias that doesn't exist in their config.toml file, we should fail early and message the user about it. --- main.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index b9a91cf..625f075 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,9 @@ package main import ( "io/ioutil" + "fmt" "log" + "os" "time" "github.com/teddywing/timetasker/timetask" @@ -46,9 +48,15 @@ func main() { kingpin.Parse() // Submit time entry + project, ok := config.Projects[*project_alias] + if !ok { + fmt.Printf("Project '%s' not found\n", *project_alias) + os.Exit(1) + } + time_entry := timetask.NewTimeEntry( config.Profile, - config.Projects[*project_alias], + project, time.Now(), *time_spent, *description, -- cgit v1.2.3 From 4a4b8e00ae62ec3898b7e4f590bd4a3660a0f535 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 16:50:12 +0200 Subject: main.go: Add a short version of the --date argument Forgot to do this in 8d802bff08826523371ab5e951d85d0c0396ccc8. Also, the reason why I made --description's short form `-m` is because I wanted `-d` for date. The `-m` is supposed to be like "message", like in git-commit. --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index 625f075..e655967 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,7 @@ func main() { Default("7"). Int() date := kingpin.Flag("date", "Date when work was done (e.g. 2017-01-31)"). + Short('d'). String() description := kingpin.Flag("description", "Description of work."). Short('m'). -- cgit v1.2.3 From 341e22ef6b8290bba8ba43ecddb9f1175b3cafb9 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 16:56:42 +0200 Subject: main.go: Handle the `--date` argument If no date is passed in, default to the current date. Otherwise, parse the date into a time that can be used to create the `TimeEntry`. * Rename `date` to `date_str` to allow us to use `date` for the `time.Time` that gets sent to `NewTimeEntry()`. * If parsing fails, print an error message and exit. * In order to use the `err` variable without redefining `date` on line 66, define it at the top of the function. --- main.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index e655967..8b3db02 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,8 @@ type Config struct { var config Config func main() { + var err error + loadConfig() // Parse command line arguments @@ -39,7 +41,7 @@ func main() { Short('t'). Default("7"). Int() - date := kingpin.Flag("date", "Date when work was done (e.g. 2017-01-31)"). + date_str := kingpin.Flag("date", "Date when work was done (e.g. 2017-01-31)"). Short('d'). String() description := kingpin.Flag("description", "Description of work."). @@ -55,10 +57,23 @@ func main() { os.Exit(1) } + var date time.Time + + // If the date argument isn't sent, default to today + if *date_str == "" { + date = time.Now() + } else { + date, err = time.Parse("2006-01-02", *date_str) + if err != nil { + fmt.Printf("Date '%s' could not be parsed. Example: -d 2017-01-31\n", *date_str) + os.Exit(1) + } + } + time_entry := timetask.NewTimeEntry( config.Profile, project, - time.Now(), + date, *time_spent, *description, ) -- cgit v1.2.3 From e8bee60ed342ab4df93bf2fb582706ad7fd42546 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 17:25:26 +0200 Subject: Change `TimeEntry.Time` to a `float64` I had forgotten that time spent can be a decimal. Make this field a float and change related code accordingly: * --time flag * `NewTimeEntry()` `time` argument * `buildSubmissionParams()` can't use `strconv.Itoa` as that's an integer function. Instead we use `FormatFloat`. Truncate time parsing to two decimal places because to me that seems like enough. Who's going to say they spent `0.324` hours on something? Also, in order to be able to properly submit values to TimeTask (at least on the edition I use), the times must be written in European/French format, with commas (`,`) as decimal separators. Do a string replace to get this, as the float formatter will give us a period (`.`) separator. --- main.go | 2 +- timetask/http.go | 5 ++++- timetask/time_entry.go | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 8b3db02..e685d25 100644 --- a/main.go +++ b/main.go @@ -40,7 +40,7 @@ func main() { time_spent := kingpin.Flag("time", "Time spent working on project."). Short('t'). Default("7"). - Int() + Float() date_str := kingpin.Flag("date", "Date when work was done (e.g. 2017-01-31)"). Short('d'). String() diff --git a/timetask/http.go b/timetask/http.go index 6e73276..f236665 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -5,6 +5,7 @@ import ( "net/http/cookiejar" "net/url" "strconv" + "strings" "golang.org/x/net/publicsuffix" ) @@ -97,9 +98,11 @@ func buildSubmissionParams(time_entry TimeEntry) url.Values { time_entry.Date.Format("02/01/06"), // day/month/year ) + time_str := strconv.FormatFloat(time_entry.Time, 'f', 2, 64) + time_european_format := strings.Replace(time_str, ".", ",", 0) v.Set( "f_time0", - strconv.Itoa(time_entry.Time), + time_european_format, ) var billable string diff --git a/timetask/time_entry.go b/timetask/time_entry.go index ff0ad1f..bb7a741 100644 --- a/timetask/time_entry.go +++ b/timetask/time_entry.go @@ -10,7 +10,7 @@ type TimeEntry struct { Task int WorkType int Date time.Time - Time int + Time float64 Billable bool Description string } @@ -19,7 +19,7 @@ func NewTimeEntry( profile Profile, project Project, date time.Time, - time int, + time float64, description string, ) TimeEntry { return TimeEntry{ -- cgit v1.2.3 From 81791bbf4291e28691ed2cfaaad357613c915174 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 17:33:19 +0200 Subject: main.go: Move version string to `VERSION` variable Didn't really like having the application version intermixed with regular code. This separates it and makes it easier to see. --- main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index e685d25..63fdfe5 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,8 @@ import ( "gopkg.in/alecthomas/kingpin.v2" ) +var VERSION string = "0.1.0" + type Config struct { Auth struct { Username string @@ -47,7 +49,7 @@ func main() { description := kingpin.Flag("description", "Description of work."). Short('m'). String() - kingpin.Version("0.1.0") + kingpin.Version(VERSION) kingpin.Parse() // Submit time entry -- cgit v1.2.3 From cce89ff8bb9b23a9efb69c740039027b6cdcc5be Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 17:36:44 +0200 Subject: Update TODO --- TODO | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TODO b/TODO index ca62220..0b8b561 100644 --- a/TODO +++ b/TODO @@ -1,11 +1,11 @@ TODO 2017.06.03: -- Command line arguments: - - Project alias (required) - - Time (required) - - Date (optional, format: 2017-01-31) - - Description (optional) +v Command line arguments: (2017.06.03) + v Project alias (required) + v Time (required) + v Date (optional, format: 2017-01-31) + v Description (optional) - Handle failing responses from the server (show errors to the user) - Config -- cgit v1.2.3 From 633bd9a65820a82c9d36ce4cdbcdead236a16774 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 17:38:06 +0200 Subject: Update TODO --- TODO | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO b/TODO index 0b8b561..4dd2fba 100644 --- a/TODO +++ b/TODO @@ -14,3 +14,5 @@ v Command line arguments: (2017.06.03) - Load the config from XDG - Make `PasswordCmd` work + +- Request and list module IDs and names for a given project alias -- cgit v1.2.3 From 50f55979af1950150306e9fd9835f4f4dc441cf5 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 18:17:33 +0200 Subject: main.go: Run goimports --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 63fdfe5..eaef1fc 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( - "io/ioutil" "fmt" + "io/ioutil" "log" "os" "time" -- cgit v1.2.3 From 570118505bc765feda76ddcdcaef3fa0d181ec6b Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 18:30:32 +0200 Subject: Update TODO --- TODO | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO b/TODO index 4dd2fba..e3f5895 100644 --- a/TODO +++ b/TODO @@ -16,3 +16,5 @@ v Command line arguments: (2017.06.03) - Make `PasswordCmd` work - Request and list module IDs and names for a given project alias + +- Format float error ("700") -- cgit v1.2.3 From 6d138ee3c53913ef63565f66fed1b79d339b3230 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 18:31:17 +0200 Subject: buildSubmissionParams(): Fix time string replacement bug We ran into a bug where submitting "7" as the time spent would become "700" on the site. This was because our `n` value of 0 wasn't replacing the "." in the string. Thus "7.00" became "7.00" after the replacement. Not right. I misunderstood what that argument was doing and what the word "empty" in the documentation meant. Change the `n` value to replace all "."s in the string. --- timetask/http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timetask/http.go b/timetask/http.go index f236665..8c41b4f 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -99,7 +99,7 @@ func buildSubmissionParams(time_entry TimeEntry) url.Values { ) time_str := strconv.FormatFloat(time_entry.Time, 'f', 2, 64) - time_european_format := strings.Replace(time_str, ".", ",", 0) + time_european_format := strings.Replace(time_str, ".", ",", -1) v.Set( "f_time0", time_european_format, -- cgit v1.2.3 From b0858ffc7b79f3baf6d1f15962ab90a35275707e Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 18:35:53 +0200 Subject: Update TODO --- TODO | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO b/TODO index e3f5895..73845f3 100644 --- a/TODO +++ b/TODO @@ -17,4 +17,4 @@ v Command line arguments: (2017.06.03) - Request and list module IDs and names for a given project alias -- Format float error ("700") +v Format float error ("700") (2017.06.03) -- cgit v1.2.3 From 12ef5ad5d62297af90e7a4c819bd5c2835a00a05 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 18:55:51 +0200 Subject: Add `MaybeWriteConfig()` A new function that will write a new config.toml file to the XDG_CONFIG_HOME directory. Currently it checks to see whether our config file is present. If not and our config directory isn't present, it creates it. Still need to get this to actually write the config file. Also, we won't want to call it by default in main() like we're doing now. Will likely want to hide it behind a `--write-config` flag. --- config.go | 19 +++++++++++++++++++ main.go | 1 + 2 files changed, 20 insertions(+) create mode 100644 config.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..21f0406 --- /dev/null +++ b/config.go @@ -0,0 +1,19 @@ +package main + +import ( + "os" + "path/filepath" + + "github.com/goulash/xdg" +) + +func MaybeWriteConfig() { + path := xdg.FindConfig("timetasker/config.toml") + + if path == "" { + path = filepath.Join(xdg.ConfigHome, "timetasker") + if _, err := os.Stat(path); os.IsNotExist(err) { + os.Mkdir(path, 0700) + } + } +} diff --git a/main.go b/main.go index eaef1fc..0c8a8e9 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ var config Config func main() { var err error + MaybeWriteConfig() loadConfig() // Parse command line arguments -- cgit v1.2.3 From 557d6fda17c5368d7f76e823bca00335afb7ee0c Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 19:14:53 +0200 Subject: MaybeWriteConfig(): Write an empty config file If no existing config file is found, write a sample config file to XDG_CONFIG_HOME/timetasker/config.toml. --- config.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/config.go b/config.go index 21f0406..f26e560 100644 --- a/config.go +++ b/config.go @@ -1,13 +1,32 @@ package main import ( + "io/ioutil" "os" "path/filepath" "github.com/goulash/xdg" ) -func MaybeWriteConfig() { +const emptyConfig = `[auth] +username = "" +password_cmd = "" + + +[profile] +person_id = # ADD PERSON ID + + +[projects.example] +client = # ADD CLIENT ID +project = # ADD PROJECT ID +module = # ADD MODULE ID +task = 0 +work_type = # ADD WORK TYPE ID +billable = true +` + +func MaybeWriteConfig() error { path := xdg.FindConfig("timetasker/config.toml") if path == "" { @@ -15,5 +34,13 @@ func MaybeWriteConfig() { if _, err := os.Stat(path); os.IsNotExist(err) { os.Mkdir(path, 0700) } + + config_path := filepath.Join(path, "config.toml") + err := ioutil.WriteFile(config_path, []byte(emptyConfig), 0644) + if err != nil { + return err + } } + + return nil } -- cgit v1.2.3 From e14edbd02a61fd1d999df1a4c242e30ca721046b Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 19:18:16 +0200 Subject: main.go: Handle errors from MaybeWriteConfig() If the function results in an error, print it and exit. --- main.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 0c8a8e9..1d90f94 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,13 @@ var config Config func main() { var err error - MaybeWriteConfig() + err = MaybeWriteConfig() + if err != nil { + fmt.Println("Could not write config file") + fmt.Println(err) + os.Exit(1) + } + loadConfig() // Parse command line arguments -- cgit v1.2.3 From 26b206b94c95904fd85b2a3e2c63d87c817afc32 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 19:26:05 +0200 Subject: Move `Config` & `loadConfig()` to config.go Now that we have a 'config.go' file, it makes more sense for these two to live in that file. Change `loadConfig()` to return an error instead of printing it to the log. --- config.go | 22 ++++++++++++++++++++++ main.go | 18 ------------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/config.go b/config.go index f26e560..0aa76f1 100644 --- a/config.go +++ b/config.go @@ -5,9 +5,21 @@ import ( "os" "path/filepath" + "github.com/teddywing/timetasker/timetask" + + "github.com/BurntSushi/toml" "github.com/goulash/xdg" ) +type Config struct { + Auth struct { + Username string + PasswordCmd string `toml:"password_cmd"` + } + Profile timetask.Profile + Projects map[string]timetask.Project +} + const emptyConfig = `[auth] username = "" password_cmd = "" @@ -44,3 +56,13 @@ func MaybeWriteConfig() error { return nil } + +func loadConfig() error { + config = Config{} + _, err := toml.DecodeFile("config2.toml", &config) + if err != nil { + return err + } + + return nil +} diff --git a/main.go b/main.go index 1d90f94..5eae47f 100644 --- a/main.go +++ b/main.go @@ -9,21 +9,11 @@ import ( "github.com/teddywing/timetasker/timetask" - "github.com/BurntSushi/toml" "gopkg.in/alecthomas/kingpin.v2" ) var VERSION string = "0.1.0" -type Config struct { - Auth struct { - Username string - PasswordCmd string `toml:"password_cmd"` - } - Profile timetask.Profile - Projects map[string]timetask.Project -} - var config Config func main() { @@ -110,11 +100,3 @@ func main() { body, err = ioutil.ReadAll(resp.Body) log.Println(string(body)) } - -func loadConfig() { - config = Config{} - _, err := toml.DecodeFile("config2.toml", &config) - if err != nil { - log.Println(err) - } -} -- cgit v1.2.3 From 2becb927295f9f8b642614b5e2ce608da14b9f54 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 19:28:05 +0200 Subject: Make `MaybeWriteConfig()` a private function I figured it would be a good idea to make this function and `loadConfig()` consistent. Since `loadConfig()` is private, make this one private also. --- config.go | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index 0aa76f1..e2d2f94 100644 --- a/config.go +++ b/config.go @@ -38,7 +38,7 @@ work_type = # ADD WORK TYPE ID billable = true ` -func MaybeWriteConfig() error { +func maybeWriteConfig() error { path := xdg.FindConfig("timetasker/config.toml") if path == "" { diff --git a/main.go b/main.go index 5eae47f..00b663f 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,7 @@ var config Config func main() { var err error - err = MaybeWriteConfig() + err = maybeWriteConfig() if err != nil { fmt.Println("Could not write config file") fmt.Println(err) -- cgit v1.2.3 From 855728d1be71170a80ac4b725553b7ff67e85dc4 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 19:29:46 +0200 Subject: main.go: Handle errors coming from `loadConfig()` --- main.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 00b663f..895786d 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,12 @@ func main() { os.Exit(1) } - loadConfig() + err = loadConfig() + if err != nil { + fmt.Println("Could not load config file") + fmt.Println(err) + os.Exit(1) + } // Parse command line arguments project_alias := kingpin.Flag( -- cgit v1.2.3 From fa85a708dff621e4d75f19bdbd45bba5947685c0 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 19:42:06 +0200 Subject: config.go: Extract config file paths to functions Put these constructed paths into reusable functions for easier usage. --- config.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index e2d2f94..43f7689 100644 --- a/config.go +++ b/config.go @@ -38,16 +38,24 @@ work_type = # ADD WORK TYPE ID billable = true ` +func configDir() string { + return filepath.Join(xdg.ConfigHome, "timetasker") +} + +func configFile() string { + return filepath.Join(configDir(), "config.toml") +} + func maybeWriteConfig() error { path := xdg.FindConfig("timetasker/config.toml") if path == "" { - path = filepath.Join(xdg.ConfigHome, "timetasker") + path = configDir() if _, err := os.Stat(path); os.IsNotExist(err) { os.Mkdir(path, 0700) } - config_path := filepath.Join(path, "config.toml") + config_path := configFile() err := ioutil.WriteFile(config_path, []byte(emptyConfig), 0644) if err != nil { return err -- cgit v1.2.3 From 43cf753e794c37a8422e42389b077f1d20edc7af Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 19:48:09 +0200 Subject: main.go: Only call `maybeWriteConfig()` when `--write-config` is given We shouldn't automatically force writing the config file. Only do so if the user asks for it to be done. NOTE: there's a problem here, because `-p` is required but it shouldn't be in this specific case. --- main.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 895786d..a2fafc8 100644 --- a/main.go +++ b/main.go @@ -19,13 +19,6 @@ var config Config func main() { var err error - err = maybeWriteConfig() - if err != nil { - fmt.Println("Could not write config file") - fmt.Println(err) - os.Exit(1) - } - err = loadConfig() if err != nil { fmt.Println("Could not load config file") @@ -51,9 +44,24 @@ func main() { description := kingpin.Flag("description", "Description of work."). Short('m'). String() + write_config_description := fmt.Sprintf( + "Initialise a new config file template at %s", + configFile(), + ) + write_config := kingpin.Flag("write-config", write_config_description). + Bool() kingpin.Version(VERSION) kingpin.Parse() + if *write_config { + err = maybeWriteConfig() + if err != nil { + fmt.Println("Could not write config file") + fmt.Println(err) + os.Exit(1) + } + } + // Submit time entry project, ok := config.Projects[*project_alias] if !ok { -- cgit v1.2.3 From 57f64ce855309b27ea483af81cfdb1d38d1db7f3 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 20:32:17 +0200 Subject: main.go: If `--write-config` is passed, do work and exit Ensure that we're not actually submitting a time entry if the `--write-config` flag was passed. In that case, we just want to write the empty config file and exit successfully. --- main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index a2fafc8..90bb653 100644 --- a/main.go +++ b/main.go @@ -60,8 +60,9 @@ func main() { fmt.Println(err) os.Exit(1) } - } + os.Exit(0) + } // Submit time entry project, ok := config.Projects[*project_alias] if !ok { -- cgit v1.2.3 From bcb86d1e025c6928018f94c5634eed88e50c0a58 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 20:33:17 +0200 Subject: main.go: Custom `Required()` for `--project` argument As far as I've been able to figure out, Kingpin doesn't have a mechanism for dependent arguments or conditional requireds. Thus there's no way to say "--position is only required if --write-config isn't passed". In that case, write our own "required" check, to enforce the presence of "--project" only if "--write-config" isn't passed. We duplicate the message that Kingpin provides from `Required()` and leverage its error formatting (we should probably use this for our other error messages too). The only difference is that `--position=POSITION` won't appear on the first line of the Help text, which would have emphasised the fact that it's required. It's possible to configure Kingpin's help text via templates, but I don't think that's worth the trouble at this point. --- main.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 90bb653..38cd527 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,6 @@ func main() { "Project alias defined in config.toml.", ). Short('p'). - Required(). String() time_spent := kingpin.Flag("time", "Time spent working on project."). Short('t'). @@ -53,6 +52,10 @@ func main() { kingpin.Version(VERSION) kingpin.Parse() + if *project_alias == "" && !*write_config { + kingpin.Fatalf("required flag --project not provided, try --help") + } + if *write_config { err = maybeWriteConfig() if err != nil { -- cgit v1.2.3 From 4750436d77ccf469770d8a531f5e264b8a3aecd8 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 20:58:34 +0200 Subject: main(): Change error handling to use Kingpin error helpers In order to maintain a consistent format when echoing error messages, use the Kingpin helpers to do so. Rewrite our error message code to this effect. This has the added benefit of making our code shorter. --- main.go | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/main.go b/main.go index 38cd527..882efde 100644 --- a/main.go +++ b/main.go @@ -20,11 +20,7 @@ func main() { var err error err = loadConfig() - if err != nil { - fmt.Println("Could not load config file") - fmt.Println(err) - os.Exit(1) - } + kingpin.FatalIfError(err, "Could not load config file") // Parse command line arguments project_alias := kingpin.Flag( @@ -58,18 +54,14 @@ func main() { if *write_config { err = maybeWriteConfig() - if err != nil { - fmt.Println("Could not write config file") - fmt.Println(err) - os.Exit(1) - } + kingpin.FatalIfError(err, "Could not write config file") os.Exit(0) } // Submit time entry project, ok := config.Projects[*project_alias] if !ok { - fmt.Printf("Project '%s' not found\n", *project_alias) + kingpin.Errorf("project '%s' not found", *project_alias) os.Exit(1) } @@ -80,10 +72,11 @@ func main() { date = time.Now() } else { date, err = time.Parse("2006-01-02", *date_str) - if err != nil { - fmt.Printf("Date '%s' could not be parsed. Example: -d 2017-01-31\n", *date_str) - os.Exit(1) - } + kingpin.FatalIfError( + err, + "Date '%s' could not be parsed. Example: -d 2017-01-31\n", + *date_str, + ) } time_entry := timetask.NewTimeEntry( @@ -98,9 +91,7 @@ func main() { config.Auth.Username, config.Auth.PasswordCmd, ) - if err != nil { - log.Fatalln(err) - } + kingpin.FatalIfError(err, "Login request failed") log.Printf("%+v\n", resp) defer resp.Body.Close() @@ -108,9 +99,7 @@ func main() { log.Println(string(body)) resp, err = timetask.SubmitTimeEntry(*client, time_entry) - if err != nil { - log.Fatalln(err) - } + kingpin.FatalIfError(err, "Time entry submission request failed") log.Printf("%+v\n", resp) defer resp.Body.Close() -- cgit v1.2.3 From 4c41339fd3bbe6e3f351626fa25dc475f2aa07ee Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:02:54 +0200 Subject: Update TODO --- TODO | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO b/TODO index 73845f3..0dfc5e9 100644 --- a/TODO +++ b/TODO @@ -9,7 +9,7 @@ v Command line arguments: (2017.06.03) - Handle failing responses from the server (show errors to the user) - Config - - A `--write-config` or similar option that generates and write a bare + v A `--write-config` or similar option that generates and write a bare config for users to use - Load the config from XDG -- cgit v1.2.3 From 403c92bf583cec66677d93ea470ec58312c8240b Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:14:26 +0200 Subject: main.go: Fix whitespace Thanks gofmt --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 882efde..ae18a87 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,7 @@ func main() { description := kingpin.Flag("description", "Description of work."). Short('m'). String() - write_config_description := fmt.Sprintf( + write_config_description := fmt.Sprintf( "Initialise a new config file template at %s", configFile(), ) -- cgit v1.2.3 From f5b1fb3d0ef37ed44b8f7302343dcd5cd16bd725 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:36:20 +0200 Subject: Make the `password_cmd` config setting work Given a string from `password_cmd`, we should execute it as a shell command and assume the result is a password. That password then gets submitted to TimeTask's login form for authentication. Had to do a bit of wrangling to get the command to execute by `exec.Command()`, but managed to get it working in a bit of a roundabout way. Found these resources in my searches: - https://stackoverflow.com/questions/39930109/golang-execute-command#39930127 https://stackoverflow.com/questions/28783637/how-to-make-golang-execute-a-string - https://stackoverflow.com/questions/20437336/how-to-execute-system-command-in-golang-with-unknown-arguments - https://github.com/codeskyblue/go-sh --- main.go | 5 ++++- password_cmd.go | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 password_cmd.go diff --git a/main.go b/main.go index ae18a87..ab0fc4a 100644 --- a/main.go +++ b/main.go @@ -87,9 +87,12 @@ func main() { *description, ) + password, err := passwordCmd(config.Auth.PasswordCmd) + kingpin.FatalIfError(err, "password command failed") + resp, client, err := timetask.Login( config.Auth.Username, - config.Auth.PasswordCmd, + password, ) kingpin.FatalIfError(err, "Login request failed") log.Printf("%+v\n", resp) diff --git a/password_cmd.go b/password_cmd.go new file mode 100644 index 0000000..821c8f6 --- /dev/null +++ b/password_cmd.go @@ -0,0 +1,22 @@ +package main + +import ( + "os" + "os/exec" +) + +// Execute the given string as a shell command and return the resulting output +func passwordCmd(password_cmd string) (password string, err error) { + shell := os.Getenv("SHELL") + + // `Command` requires us to pass shell arguments as parameters to the + // function, but we don't know what the arguments are because + // `password_cmd` is an arbitrary command. To get around this, we pass the + // password command to the current shell to execute. + output, err := exec.Command(shell, "-c", password_cmd).Output() + if err != nil { + return "", err + } + + return string(output), nil +} -- cgit v1.2.3 From aff5d59c5bf300e0fd706cff626c26fdd5ce0695 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:40:25 +0200 Subject: Update TODO --- TODO | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO b/TODO index 0dfc5e9..11ee69d 100644 --- a/TODO +++ b/TODO @@ -13,7 +13,7 @@ v Command line arguments: (2017.06.03) config for users to use - Load the config from XDG -- Make `PasswordCmd` work +v Make `PasswordCmd` work - Request and list module IDs and names for a given project alias -- cgit v1.2.3 From 8204a34e5360d0370f9a0d94b049a3beaa72fb52 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:44:05 +0200 Subject: loadConfig(): Load from real XDG config Instead of loading the config file I've been testing with in the local project directory, load the real one from XDG_CONFIG_HOME. --- config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.go b/config.go index 43f7689..126b123 100644 --- a/config.go +++ b/config.go @@ -67,7 +67,7 @@ func maybeWriteConfig() error { func loadConfig() error { config = Config{} - _, err := toml.DecodeFile("config2.toml", &config) + _, err := toml.DecodeFile(configFile(), &config) if err != nil { return err } -- cgit v1.2.3 From 7729072c262c0e7b76c25dbb09d6f2c6e7dd3eaf Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:47:04 +0200 Subject: main(): Update error message for `loadConfig()` error * Change the initial capital to lowercase for consistency with the other error messages * Suggest using `--write-config` as a possible solution to the error (in cases where the config file doesn't exist) --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index ab0fc4a..e0e4af1 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ func main() { var err error err = loadConfig() - kingpin.FatalIfError(err, "Could not load config file") + kingpin.FatalIfError(err, "could not load config file, try --write-config") // Parse command line arguments project_alias := kingpin.Flag( -- cgit v1.2.3 From 60b5500b88e9b2b04807d947f943412b99cc09d9 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:51:43 +0200 Subject: main(): Move `loadConfig()` call after CLI argument parsing Otherwise, if users haven't created a config file and they run `--help`, they'll get an error complaining that the config file doesn't exist instead of helpful usage output. --- main.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index e0e4af1..f067867 100644 --- a/main.go +++ b/main.go @@ -19,9 +19,6 @@ var config Config func main() { var err error - err = loadConfig() - kingpin.FatalIfError(err, "could not load config file, try --write-config") - // Parse command line arguments project_alias := kingpin.Flag( "project", @@ -58,6 +55,10 @@ func main() { os.Exit(0) } + + err = loadConfig() + kingpin.FatalIfError(err, "could not load config file, try --write-config") + // Submit time entry project, ok := config.Projects[*project_alias] if !ok { -- cgit v1.2.3 From 718172b253657449f54aff58ab7517fbd57519d9 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:54:45 +0200 Subject: Update TODO --- TODO | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index 11ee69d..08cdf7b 100644 --- a/TODO +++ b/TODO @@ -8,10 +8,10 @@ v Command line arguments: (2017.06.03) v Description (optional) - Handle failing responses from the server (show errors to the user) -- Config +v Config (2017.06.03) v A `--write-config` or similar option that generates and write a bare config for users to use - - Load the config from XDG + v Load the config from XDG (2017.06.03) v Make `PasswordCmd` work -- cgit v1.2.3 From 5186b753e9d800ed7fc8ecd02850f3c8a8b40058 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:56:40 +0200 Subject: main(): Change Kingpin error messages to lowercase Remove the initial cap on Kingpin error messages for consistency in our messages. --- main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index f067867..a29c1e6 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,7 @@ func main() { if *write_config { err = maybeWriteConfig() - kingpin.FatalIfError(err, "Could not write config file") + kingpin.FatalIfError(err, "could not write config file") os.Exit(0) } @@ -75,7 +75,7 @@ func main() { date, err = time.Parse("2006-01-02", *date_str) kingpin.FatalIfError( err, - "Date '%s' could not be parsed. Example: -d 2017-01-31\n", + "date '%s' could not be parsed. Example: -d 2017-01-31\n", *date_str, ) } @@ -95,7 +95,7 @@ func main() { config.Auth.Username, password, ) - kingpin.FatalIfError(err, "Login request failed") + kingpin.FatalIfError(err, "login request failed") log.Printf("%+v\n", resp) defer resp.Body.Close() @@ -103,7 +103,7 @@ func main() { log.Println(string(body)) resp, err = timetask.SubmitTimeEntry(*client, time_entry) - kingpin.FatalIfError(err, "Time entry submission request failed") + kingpin.FatalIfError(err, "time entry submission request failed") log.Printf("%+v\n", resp) defer resp.Body.Close() -- cgit v1.2.3 From aa2f0f0bac6cc533b70265c9de60943abefa5931 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 21:57:24 +0200 Subject: main(): Add comment explaining `--project` validation --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index a29c1e6..3b7ace5 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ func main() { kingpin.Version(VERSION) kingpin.Parse() + // Error if no --project unless --write-config was passed if *project_alias == "" && !*write_config { kingpin.Fatalf("required flag --project not provided, try --help") } -- cgit v1.2.3 From 8e7fb7199f1e1f84c914bbaa00fa057e3489f1e8 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 22:13:30 +0200 Subject: main(): Handle errors from HTTP responses Add some simple error handling for known responses from our TimeTask HTTP requests. Check a couple of known strings to determine whether there was an error. If so, exit with a failing error code. Remove our old `log` statements. These were used during debugging to see some output and check responses from TimeTask. We now have an idea of what those responses are, and are handling some of the error cases. Thus the log statements are no longer needed. --- main.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 3b7ace5..861dc8b 100644 --- a/main.go +++ b/main.go @@ -3,8 +3,8 @@ package main import ( "fmt" "io/ioutil" - "log" "os" + "strings" "time" "github.com/teddywing/timetasker/timetask" @@ -97,17 +97,27 @@ func main() { password, ) kingpin.FatalIfError(err, "login request failed") - log.Printf("%+v\n", resp) defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) - log.Println(string(body)) + if strings.Contains( + string(body), + "The username and password don't appear to be valid.", + ) { + kingpin.Errorf("TimeTask authentication failed") + os.Exit(1) + } resp, err = timetask.SubmitTimeEntry(*client, time_entry) kingpin.FatalIfError(err, "time entry submission request failed") - log.Printf("%+v\n", resp) defer resp.Body.Close() body, err = ioutil.ReadAll(resp.Body) - log.Println(string(body)) + if strings.Contains( + string(body), + "No time entries were created.", + ) { + kingpin.Errorf("time entry creation failed") + os.Exit(1) + } } -- cgit v1.2.3 From 7f19527d1a89b44ae559861c5f33cf126c3b011c Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 22:17:50 +0200 Subject: Update TODO --- TODO | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index 08cdf7b..2065140 100644 --- a/TODO +++ b/TODO @@ -7,7 +7,8 @@ v Command line arguments: (2017.06.03) v Date (optional, format: 2017-01-31) v Description (optional) -- Handle failing responses from the server (show errors to the user) +v Handle failing responses from the server (show errors to the user) + (2017.06.03) v Config (2017.06.03) v A `--write-config` or similar option that generates and write a bare config for users to use -- cgit v1.2.3 From 8b608aa4dfdff5e1e17664cff8f2f602d326844c Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 22:30:34 +0200 Subject: Update TODO --- TODO | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO b/TODO index 2065140..1c5c696 100644 --- a/TODO +++ b/TODO @@ -19,3 +19,5 @@ v Make `PasswordCmd` work - Request and list module IDs and names for a given project alias v Format float error ("700") (2017.06.03) + +- Move HTTP errors into http.go -- cgit v1.2.3 From d833ffe6cd5efd97b00f907ed426a88275a8c8db Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 23:08:08 +0200 Subject: Add an option to list "Modules" (sprints & their IDs) Sprints will change with time while the other IDs of a project will stay the same. Thus sprints, which live in the `Module` field, must be updated regularly. In order to facilitate that updating, instead of requiring users to get those IDs directly from the TimeTask website every time, have them use this command to get the names of sprints and their IDs. They can then update the ID manually in their config file. This code makes a request to the endpoint that returns module IDs for the site (the site queries this via AJAX to update its interface). The result of the request is some XML containing the modules and their IDs. For now I'm just printing this out. We'll want to parse the XML and display it in a nicer way. --- main.go | 10 ++++++++++ timetask/http.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/main.go b/main.go index 861dc8b..1521821 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,7 @@ func main() { ) write_config := kingpin.Flag("write-config", write_config_description). Bool() + list_modules := kingpin.Flag("list-modules", "List sprints with IDs").Bool() kingpin.Version(VERSION) kingpin.Parse() @@ -108,6 +109,15 @@ func main() { os.Exit(1) } + // List modules + if *list_modules { + modules, err := timetask.RequestModules(*client, time_entry) + kingpin.FatalIfError(err, "could not retrieve sprints") + fmt.Println(modules) + + os.Exit(0) + } + resp, err = timetask.SubmitTimeEntry(*client, time_entry) kingpin.FatalIfError(err, "time entry submission request failed") diff --git a/timetask/http.go b/timetask/http.go index 8c41b4f..3ff6f4f 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -1,6 +1,7 @@ package timetask import ( + "io/ioutil" "net/http" "net/http/cookiejar" "net/url" @@ -126,3 +127,38 @@ func buildSubmissionParams(time_entry TimeEntry) url.Values { return v } + +func RequestModules( + client http.Client, + time_entry TimeEntry, +) (string, error) { + params := url.Values{ + "module": {"projects"}, + "action": {"listmodulesxref"}, + "f_ID": {strconv.Itoa(time_entry.Project)}, + "f_active": {"t"}, + "f_clientID": {strconv.Itoa(time_entry.Client)}, + "f_personID": {strconv.Itoa(time_entry.PersonID)}, + "f_milestoneID": {""}, + } + modules_url, err := url.Parse(baseURL) + if err != nil { + return "", err + } + + modules_url.RawQuery = params.Encode() + + resp, err := client.Get(modules_url.String()) + if err != nil { + return "", err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + response_body := string(body) + + return response_body, nil +} -- cgit v1.2.3 From aff9a869ffab0eeb8c644826b9250a500468ff1a Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sat, 3 Jun 2017 23:37:32 +0200 Subject: TestLogin(): Fix `Login()` assignment mismatch `Login()` now returns 3 values. Update the test. --- timetask/http_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timetask/http_test.go b/timetask/http_test.go index 604c3c3..36f9e4a 100644 --- a/timetask/http_test.go +++ b/timetask/http_test.go @@ -19,7 +19,7 @@ func init() { func TestLogin(t *testing.T) { t.Skip("No requests") - response, err := Login(username, password) + response, _, err := Login(username, password) if err != nil { t.Fatal(err) } -- cgit v1.2.3 From c113de965dea24c44023a9aabcff7f8ee1f0caa0 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 00:05:39 +0200 Subject: Add module.go for parsing Module XML A new function that parses the XML returned by the `RequestModules()` function. It provides a `Module` type that allows us to interact with modules more easily in code. The `ParseXML()` function will take an XML string and return a slice of `Module`s. Added a test just to facilitate development. Wasn't able to find an easy way to compare slices in Go, so just printed the values and checked the result visually. Not a useful test for future use, but it served its purpose. Eventually it would be nice to find a way to compare structs and have a real pass/fail condition. --- timetask/module.go | 24 ++++++++++++++++++++++++ timetask/module_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 timetask/module.go create mode 100644 timetask/module_test.go diff --git a/timetask/module.go b/timetask/module.go new file mode 100644 index 0000000..adef4e5 --- /dev/null +++ b/timetask/module.go @@ -0,0 +1,24 @@ +package timetask + +import ( + "encoding/xml" +) + +type Module struct { + ID int `xml:"moduleid"` + Name string `xml:"modulename"` +} + +type moduleXML struct { + Modules []Module `xml:"response>item"` +} + +func ParseXML(xml_str string) ([]Module, error) { + modules := moduleXML{} + err := xml.Unmarshal([]byte(xml_str), &modules) + if err != nil { + return nil, err + } + + return modules.Modules, nil +} diff --git a/timetask/module_test.go b/timetask/module_test.go new file mode 100644 index 0000000..6dcfa94 --- /dev/null +++ b/timetask/module_test.go @@ -0,0 +1,50 @@ +package timetask + +import "testing" + +const modules_xml = ` + + + + + + + + + + + + + + + +` + +func TestParseXML(t *testing.T) { + modules, err := ParseXML(modules_xml) + if err != nil { + t.Error(err) + } + + _ = []Module{ // wanted + Module{ + ID: 55555, + Name: "R&D", + }, + Module{ + ID: 77777, + Name: "Sprint 1", + }, + Module{ + ID: 222222, + Name: "Sprint 2", + }, + } + + // Need a way to compare slices + // if modules != wanted { + // t.Errorf("Module parsing failed. Wanted %+v got %+v", wanted, modules) + // } + + t.Logf("%+v\n", modules) +} -- cgit v1.2.3 From 7e711772def7a5d711500c3c6c37ea41b57ac598 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 00:10:14 +0200 Subject: Change `ParseXML()` name to `ModuleParseXML()` Prefix the function name to make it more obvious what it relates to. Since this function lives in the `timetask` module and will be used in contexts that have nothing to do with Modules. --- timetask/module.go | 2 +- timetask/module_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/timetask/module.go b/timetask/module.go index adef4e5..4dde57a 100644 --- a/timetask/module.go +++ b/timetask/module.go @@ -13,7 +13,7 @@ type moduleXML struct { Modules []Module `xml:"response>item"` } -func ParseXML(xml_str string) ([]Module, error) { +func ModuleParseXML(xml_str string) ([]Module, error) { modules := moduleXML{} err := xml.Unmarshal([]byte(xml_str), &modules) if err != nil { diff --git a/timetask/module_test.go b/timetask/module_test.go index 6dcfa94..cee87c5 100644 --- a/timetask/module_test.go +++ b/timetask/module_test.go @@ -20,8 +20,8 @@ const modules_xml = ` ` -func TestParseXML(t *testing.T) { - modules, err := ParseXML(modules_xml) +func TestModuleParseXML(t *testing.T) { + modules, err := ModuleParseXML(modules_xml) if err != nil { t.Error(err) } -- cgit v1.2.3 From eb82e5e447dff67d5bcd1a2d5c9c52990a840e29 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 00:19:35 +0200 Subject: RequestModules(): Pretty print modules Parse the module XML with `ModuleParseXML()`. Take the resulting `[]Module` slice and use it to generate a string of the following format: ID Module 55555 R&D 77777 Sprint 1 222222 Sprint 2 This string is what gets printed to the console, which makes it rather easy to read the modules that are available for the given project and grab the appropriate ID to put into your config file. --- timetask/http.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/timetask/http.go b/timetask/http.go index 3ff6f4f..575ceae 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -1,6 +1,8 @@ package timetask import ( + "bytes" + "fmt" "io/ioutil" "net/http" "net/http/cookiejar" @@ -160,5 +162,18 @@ func RequestModules( } response_body := string(body) - return response_body, nil + modules, err := ModuleParseXML(response_body) + if err != nil { + return "", err + } + + var module_buf bytes.Buffer + module_buf.WriteString("ID\tModule\n") + for _, module := range modules { + module_buf.WriteString( + fmt.Sprintf("%d\t%s\n", module.ID, module.Name), + ) + } + + return module_buf.String(), nil } -- cgit v1.2.3 From 6a62cc9aef3684ef0bd72e6ab79157a699b01c72 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 00:25:43 +0200 Subject: Update TODO --- TODO | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO b/TODO index 1c5c696..63d68fb 100644 --- a/TODO +++ b/TODO @@ -16,7 +16,7 @@ v Config (2017.06.03) v Make `PasswordCmd` work -- Request and list module IDs and names for a given project alias +v Request and list module IDs and names for a given project alias (2017.06.03) v Format float error ("700") (2017.06.03) -- cgit v1.2.3 From d4a9537b5671fafe04335b9f91c5d5869d4a2358 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 00:38:28 +0200 Subject: Move HTTP response body errors into http.go Take the errors in `main()` that check the response body contents for known error strings and put them in their respective functions in "timetask/http.go". Didn't really make sense to me that these functions were returning HTTP responses that we weren't really using in a meaningful way. Instead they should just do their thing and let us know if there was a problem. That includes checking to see if there were any non-standard errors, like the ones we had custom-built. Now all that handling and error-making is self-contained, which feels much nicer. --- main.go | 26 ++------------------------ timetask/http.go | 51 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/main.go b/main.go index 1521821..2f37632 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,7 @@ package main import ( "fmt" - "io/ioutil" "os" - "strings" "time" "github.com/teddywing/timetasker/timetask" @@ -93,22 +91,12 @@ func main() { password, err := passwordCmd(config.Auth.PasswordCmd) kingpin.FatalIfError(err, "password command failed") - resp, client, err := timetask.Login( + client, err := timetask.Login( config.Auth.Username, password, ) kingpin.FatalIfError(err, "login request failed") - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if strings.Contains( - string(body), - "The username and password don't appear to be valid.", - ) { - kingpin.Errorf("TimeTask authentication failed") - os.Exit(1) - } - // List modules if *list_modules { modules, err := timetask.RequestModules(*client, time_entry) @@ -118,16 +106,6 @@ func main() { os.Exit(0) } - resp, err = timetask.SubmitTimeEntry(*client, time_entry) + err = timetask.SubmitTimeEntry(*client, time_entry) kingpin.FatalIfError(err, "time entry submission request failed") - - defer resp.Body.Close() - body, err = ioutil.ReadAll(resp.Body) - if strings.Contains( - string(body), - "No time entries were created.", - ) { - kingpin.Errorf("time entry creation failed") - os.Exit(1) - } } diff --git a/timetask/http.go b/timetask/http.go index 575ceae..f0a6548 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -15,18 +15,14 @@ import ( var baseURL string = "https://af83.timetask.com/index.php" -func Login(username, password string) ( - resp *http.Response, - client *http.Client, - err error, -) { +func Login(username, password string) (client *http.Client, err error) { cookies, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) if err != nil { - return nil, nil, err + return nil, err } client = &http.Client{Jar: cookies} - resp, err = client.PostForm( + resp, err := client.PostForm( baseURL, url.Values{ "module": {"people"}, @@ -37,30 +33,53 @@ func Login(username, password string) ( }, ) if err != nil { - return resp, client, err + return client, err } - return resp, client, err + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return client, err + } + + if strings.Contains( + string(body), + "The username and password don't appear to be valid.", + ) { + return client, fmt.Errorf("TimeTask authentication failed") + } + + return client, err } -func SubmitTimeEntry( - client http.Client, - time_entry TimeEntry, -) (resp *http.Response, err error) { +func SubmitTimeEntry(client http.Client, time_entry TimeEntry) error { values := buildSubmissionParams(time_entry) values.Set("module", "time") values.Set("action", "submitmultipletime") - resp, err = client.PostForm( + resp, err := client.PostForm( baseURL, values, ) if err != nil { - return resp, err + return err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if strings.Contains( + string(body), + "No time entries were created.", + ) { + return fmt.Errorf("time entry creation failed") } - return resp, nil + return nil } func buildSubmissionParams(time_entry TimeEntry) url.Values { -- cgit v1.2.3 From 288bceba90874e4221d8a43757b0fe1c82f5af6d Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 00:42:26 +0200 Subject: Update TODO --- TODO | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO b/TODO index 63d68fb..13efe4a 100644 --- a/TODO +++ b/TODO @@ -20,4 +20,4 @@ v Request and list module IDs and names for a given project alias (2017.06.03) v Format float error ("700") (2017.06.03) -- Move HTTP errors into http.go +v Move HTTP errors into http.go (2017.06.03) -- cgit v1.2.3 From 161d11abfad116a219b490ad83e0a30461bcd455 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 00:51:03 +0200 Subject: README: Rewrite the opening description Remove references to the project being abandoned. Briefly describe it. --- README.md | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1b7f244..5c4cd42 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,13 @@ Timetasker CLI ============== -## [ABANDONED] -This project has been abandoned. What I wanted was a nice command line interface -to be able to submit time sheets using [Time Task](https://timetask.com), but -the result wasn't really what I had hoped for. Ultimately I didn't see this tool -saving me much more time (if any) compared with using the website. +Provides a nice command line interface to submit time sheets using [Time +Task](https://timetask.com). This command will submit a single time entry. -My idea was to have the executable generate a weekly time sheet as YAML with -prepopulated defaults. This file could then be modified by the user if necessary -and submitted to the website via the CLI tool. - -The project is succeeded by a [Chrome +The project improves upon a [Chrome extension](https://github.com/teddywing/chrome-timetasker) that auto-fills the time sheet form on the website. -Posting this code in the event that it becomes useful to anyone. - ## License Copyright © 2017 Teddy Wing. Licensed under the GNU GPLv3+ (see the included -- cgit v1.2.3 From d0bb458eb592c76d1b1fed7b9ab692f9cb5b4838 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 00:57:22 +0200 Subject: README: Add Usage section Show some examples of how to use the program. --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 5c4cd42..1d0b99e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,21 @@ extension](https://github.com/teddywing/chrome-timetasker) that auto-fills the time sheet form on the website. +## Usage +This will submit a time entry for the "example" project on the current day with +a duration of 7 hours and an empty description: + + $ timetasker --project example + +Here we set a custom time of 4.5 hours: + + $ timetasker --project example --time 4.5 + +Now we specify a date and add a description: + + $ timetasker --project example --date 2017-05-31 --description "Worked on Timetasker" + + ## License Copyright © 2017 Teddy Wing. Licensed under the GNU GPLv3+ (see the included COPYING file). -- cgit v1.2.3 From f01841d43182709ba64cd46a5a4d02b11728e826 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 01:15:28 +0200 Subject: README: Add Configuration section Explain the program's configuration and how to configure it using data from TimeTask. --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/README.md b/README.md index 1d0b99e..6c1416c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,78 @@ Now we specify a date and add a description: $ timetasker --project example --date 2017-05-31 --description "Worked on Timetasker" +## Configuration +Timetasker relies on a configuration file in order to work properly. + +If this is your first time running Timetasker, use this command to generate a +skeleton config: + + $ timetasker --write-config + +This will generate a `$HOME/.config/timetasker/config.toml` file that looks like +this: + + [auth] + username = "" + password_cmd = "" + + + [profile] + person_id = # ADD PERSON ID + + + [projects.example] + client = # ADD CLIENT ID + project = # ADD PROJECT ID + module = # ADD MODULE ID + task = 0 + work_type = # ADD WORK TYPE ID + billable = true + +Fill in the `username` with your TimeTask username. The `password_cmd` should be +a shell command that will output your TimeTask password to STDOUT. + +Notice the `[projects.example]` line? That's a project alias. If we, for +instance, changed it to `[projects.my-cool-project]`, we could post a time entry +to that project like this: + + $ timetasker --project my-cool-project + +You say you have more than one project? No problem, just copy-paste the entire +`[projects.example]` section and give it a new name. + +To fill in the other configuration options, we're going to have to take a trip +to the TimeTask website (relax, we won't be using it much after this). + +1. Visit `https://*.timetask.com/time/add/` +2. Fill in a single entry, but don't submit it yet +3. Open the Network console in your browser's developer tools +4. Submit your time entry +5. Open the POST request to `https://*.timetask.com/index.php` +6. Take a look at the form data of the request + +You should see something like this: + + module:time + action:submitmultipletime + f_entryIndexes:0 + f_personID0:111111 + f_clientID0:22222 + f_projectID0:333333 + f_moduleID0:444444 + f_taskID0:0 + f_worktypeID0:555555 + f_date0:04/06/17 + f_time0:7 + f_billable0:t + f_description0: + +Copy the numbers into their corresponding fields in your `config.toml` file. + +Once you have a complete config file, you should be all set to start posting +time entries! + + ## License Copyright © 2017 Teddy Wing. Licensed under the GNU GPLv3+ (see the included COPYING file). -- cgit v1.2.3 From 79b5deb4a3f7530efe83c9d90ab45d52b97ccf79 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 01:23:32 +0200 Subject: README: Add Install section Installation instructions. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 6c1416c..d47d1e9 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,18 @@ Once you have a complete config file, you should be all set to start posting time entries! +## Install +Visit the [releases][3] page, download the version corresponding to your +platform, and put the resulting `timetasker` binary on your PATH. + +To install from source, use: + + $ go install github.com/teddywing/timetasker + + ## License Copyright © 2017 Teddy Wing. Licensed under the GNU GPLv3+ (see the included COPYING file). + + +[3]: https://github.com/teddywing/timetasker/releases -- cgit v1.2.3 From b7f6ee30221f208a3505b03390d6e9f0431c448e Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 01:24:57 +0200 Subject: README: Move URLs to bottom of file Makes it a bit easier to read as text. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d47d1e9..f137a08 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,10 @@ Timetasker CLI ============== Provides a nice command line interface to submit time sheets using [Time -Task](https://timetask.com). This command will submit a single time entry. +Task][1]. This command will submit a single time entry. -The project improves upon a [Chrome -extension](https://github.com/teddywing/chrome-timetasker) that auto-fills the -time sheet form on the website. +The project improves upon a [Chrome extension][2] that auto-fills the time sheet +form on the website. ## Usage @@ -110,4 +109,6 @@ Copyright © 2017 Teddy Wing. Licensed under the GNU GPLv3+ (see the included COPYING file). +[1]: https://timetask.com +[2]: https://github.com/teddywing/chrome-timetasker [3]: https://github.com/teddywing/timetasker/releases -- cgit v1.2.3 From bd8fff1d37a48e8fba4559f7fd34f6c2af507806 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 01:55:00 +0200 Subject: README: Add an additional Usage example I thought it would be cool if we showed off an example connecting `timetasker` with another shell command. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index f137a08..a981d56 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,15 @@ Now we specify a date and add a description: $ timetasker --project example --date 2017-05-31 --description "Worked on Timetasker" +And because it's a shell command, we can combine it with other commands. Let's +create a week's worth of time entries starting on Monday May 29th, 2017: + + $ for d in $(ruby -e "require 'date' + > d = Date.new(2017, 5, 29) + > (0..4).each { |i| puts (d + i).strftime('%Y-%m-%d') }"); + > do timetasker --project example --date $d; + > done + ## Configuration Timetasker relies on a configuration file in order to work properly. -- cgit v1.2.3 From 48c53b17b4d77ef2a094eb3fc3bf163e0ca75b29 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 01:55:58 +0200 Subject: README: Improve readability of command line example Split the example code onto multiple lines to improve readability. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a981d56..9e105c8 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ Here we set a custom time of 4.5 hours: Now we specify a date and add a description: - $ timetasker --project example --date 2017-05-31 --description "Worked on Timetasker" + $ timetasker --project example \ + > --date 2017-05-31 \ + > --description "Worked on Timetasker" And because it's a shell command, we can combine it with other commands. Let's create a week's worth of time entries starting on Monday May 29th, 2017: -- cgit v1.2.3 From d575e2535b4ce53e875a6ef6ac21cbbb6e51fca2 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 01:56:55 +0200 Subject: README: Change description wording I personally think this tool "vastly improves" upon the Chrome extension. After all, it's much more flexible, and it lives in the console, both of which are immense pluses. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e105c8..9d89ce2 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Timetasker CLI Provides a nice command line interface to submit time sheets using [Time Task][1]. This command will submit a single time entry. -The project improves upon a [Chrome extension][2] that auto-fills the time sheet -form on the website. +The project vastly improves upon a [Chrome extension][2] that auto-fills the +time sheet form on the website. ## Usage -- cgit v1.2.3 From c715a7219c65293112f377bc16a4bcf9f9f27de5 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 01:58:24 +0200 Subject: README: Improve wording of multiple project configuration paragraph The wording seemed a bit confusing before. Change it to make it clear that you're supposed to add a new "projects" block to the config. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9d89ce2..2c9845a 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ to that project like this: $ timetasker --project my-cool-project -You say you have more than one project? No problem, just copy-paste the entire -`[projects.example]` section and give it a new name. +You say you have more than one project? No problem, just copy-paste an +additional `[projects.example]` section and give it a new name. To fill in the other configuration options, we're going to have to take a trip to the TimeTask website (relax, we won't be using it much after this). -- cgit v1.2.3 From 10cfa06cb6b6da7ea437f32414af84e62d31ed44 Mon Sep 17 00:00:00 2001 From: Teddy Wing Date: Sun, 4 Jun 2017 02:00:09 +0200 Subject: README: Improve wording of Configuration step "Open" sounded a bit ambiguous to me. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c9845a..4c34af7 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ to the TimeTask website (relax, we won't be using it much after this). 2. Fill in a single entry, but don't submit it yet 3. Open the Network console in your browser's developer tools 4. Submit your time entry -5. Open the POST request to `https://*.timetask.com/index.php` +5. View the POST request to `https://*.timetask.com/index.php` 6. Take a look at the form data of the request You should see something like this: -- cgit v1.2.3