diff options
author | Teddy Wing | 2017-06-04 02:07:50 +0200 |
---|---|---|
committer | Teddy Wing | 2017-06-04 02:07:50 +0200 |
commit | 02e4fb5d0d95b8c5c5442ee0a97b960f1296c236 (patch) | |
tree | 4cf323694692322036b80d2d921ff966096c6c36 | |
parent | 055301ca09d57b759b290d897bbb7560460251ca (diff) | |
parent | 9b6a6543e351308939bd420243507368b0669e63 (diff) | |
download | timetasker-02e4fb5d0d95b8c5c5442ee0a97b960f1296c236.tar.bz2 |
Merge branch 'timetasker-daily'
-rw-r--r-- | README.md | 128 | ||||
-rw-r--r-- | TODO | 23 | ||||
-rw-r--r-- | config.go | 76 | ||||
-rw-r--r-- | main.go | 118 | ||||
-rw-r--r-- | password_cmd.go | 22 | ||||
-rw-r--r-- | templates/timesheet.yml.tmpl | 11 | ||||
-rw-r--r-- | templates/weekly_timesheet.yml.tmpl | 3 | ||||
-rw-r--r-- | timetask/fields.go | 85 | ||||
-rw-r--r-- | timetask/generator.go | 44 | ||||
-rw-r--r-- | timetask/http.go | 239 | ||||
-rw-r--r-- | timetask/http_test.go | 4 | ||||
-rw-r--r-- | timetask/module.go | 24 | ||||
-rw-r--r-- | timetask/module_test.go | 50 | ||||
-rw-r--r-- | timetask/profile.go | 5 | ||||
-rw-r--r-- | timetask/project.go | 10 | ||||
-rw-r--r-- | timetask/time_entry.go | 65 |
16 files changed, 586 insertions, 321 deletions
@@ -1,23 +1,125 @@ 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. - -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 -extension](https://github.com/teddywing/chrome-timetasker) that auto-fills the +Provides a nice command line interface to submit time sheets using [Time +Task][1]. This command will submit a single time entry. + +The project vastly improves upon a [Chrome extension][2] that auto-fills the time sheet form on the website. -Posting this code in the event that it becomes useful to anyone. + +## 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" + +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. + +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 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). + +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. 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: + + 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! + + +## 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). + + +[1]: https://timetask.com +[2]: https://github.com/teddywing/chrome-timetasker +[3]: https://github.com/teddywing/timetasker/releases @@ -0,0 +1,23 @@ +TODO + +2017.06.03: +v Command line arguments: (2017.06.03) + v Project alias (required) + v Time (required) + v Date (optional, format: 2017-01-31) + v Description (optional) + +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 + v Load the config from XDG (2017.06.03) + +v Make `PasswordCmd` work + +v Request and list module IDs and names for a given project alias (2017.06.03) + +v Format float error ("700") (2017.06.03) + +v Move HTTP errors into http.go (2017.06.03) diff --git a/config.go b/config.go new file mode 100644 index 0000000..126b123 --- /dev/null +++ b/config.go @@ -0,0 +1,76 @@ +package main + +import ( + "io/ioutil" + "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 = "" + + +[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 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 = configDir() + if _, err := os.Stat(path); os.IsNotExist(err) { + os.Mkdir(path, 0700) + } + + config_path := configFile() + err := ioutil.WriteFile(config_path, []byte(emptyConfig), 0644) + if err != nil { + return err + } + } + + return nil +} + +func loadConfig() error { + config = Config{} + _, err := toml.DecodeFile(configFile(), &config) + if err != nil { + return err + } + + return nil +} @@ -2,58 +2,110 @@ package main import ( "fmt" - "io/ioutil" - "log" "os" + "time" "github.com/teddywing/timetasker/timetask" - "gopkg.in/yaml.v2" + "gopkg.in/alecthomas/kingpin.v2" ) -type Config struct { - Auth struct { - Username string - PasswordCmd string `yaml:"password_cmd"` - } - Fields timetask.Fields - Defaults timetask.TimeEntry -} +var VERSION string = "0.1.0" var config Config func main() { - loadConfig() + var err error - if len(os.Args) == 1 { - fmt.Println("Not enough arguments") - os.Exit(1) + // Parse command line arguments + project_alias := kingpin.Flag( + "project", + "Project alias defined in config.toml.", + ). + Short('p'). + String() + time_spent := kingpin.Flag("time", "Time spent working on project."). + Short('t'). + Default("7"). + Float() + 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."). + 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() + list_modules := kingpin.Flag("list-modules", "List sprints with IDs").Bool() + 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") } - file_path := os.Args[len(os.Args)-1] - file, err := ioutil.ReadFile(file_path) - if err != nil { - log.Println(err) + if *write_config { + err = maybeWriteConfig() + kingpin.FatalIfError(err, "could not write config file") + + os.Exit(0) } - time_entries := []timetask.TimeEntry{} - err = yaml.Unmarshal(file, &time_entries) - if err != nil { - log.Println(err) + err = loadConfig() + kingpin.FatalIfError(err, "could not load config file, try --write-config") + + // Submit time entry + project, ok := config.Projects[*project_alias] + if !ok { + kingpin.Errorf("project '%s' not found", *project_alias) + os.Exit(1) } - log.Printf("%+v", time_entries) + var date time.Time - // timetask.SubmitTimeEntries(config.Fields, time_entries) + // 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) + kingpin.FatalIfError( + err, + "date '%s' could not be parsed. Example: -d 2017-01-31\n", + *date_str, + ) + } - timetask.GenerateWeeklyTimesheet(os.Stdout, config.Defaults) -} + time_entry := timetask.NewTimeEntry( + config.Profile, + project, + date, + *time_spent, + *description, + ) + + password, err := passwordCmd(config.Auth.PasswordCmd) + kingpin.FatalIfError(err, "password command failed") -func loadConfig() { - config_str, err := ioutil.ReadFile("config.yml") - config = Config{} - err = yaml.Unmarshal(config_str, &config) - if err != nil { - log.Println(err) + client, err := timetask.Login( + config.Auth.Username, + password, + ) + kingpin.FatalIfError(err, "login request failed") + + // 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) } + + err = timetask.SubmitTimeEntry(*client, time_entry) + kingpin.FatalIfError(err, "time entry submission request failed") } 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 +} 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/fields.go b/timetask/fields.go deleted file mode 100644 index fb3a026..0000000 --- a/timetask/fields.go +++ /dev/null @@ -1,85 +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 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/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) - } -} diff --git a/timetask/http.go b/timetask/http.go index 83946ad..f0a6548 100644 --- a/timetask/http.go +++ b/timetask/http.go @@ -1,8 +1,9 @@ package timetask import ( + "bytes" "fmt" - "log" + "io/ioutil" "net/http" "net/http/cookiejar" "net/url" @@ -12,15 +13,17 @@ 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) (client *http.Client, err error) { cookies, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) if err != nil { return nil, err } - client := http.Client{Jar: cookies} - resp, err = client.PostForm( - "https://af83.timetask.com/index.php", + client = &http.Client{Jar: cookies} + resp, err := client.PostForm( + baseURL, url.Values{ "module": {"people"}, "action": {"loginsubmit"}, @@ -30,112 +33,166 @@ func Login(username, password string) (resp *http.Response, err error) { }, ) if err != nil { - return resp, err + return client, err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return client, err } - return resp, 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 SubmitTimeEntries(fields Fields, time_entries []TimeEntry) (resp *http.Response, err error) { - v := buildSubmissionParams(fields, time_entries) +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( + baseURL, + values, + ) + if err != nil { + return err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } - v.Set("module", "time") - v.Set("action", "submitmultipletime") + if strings.Contains( + string(body), + "No time entries were created.", + ) { + return fmt.Errorf("time entry creation failed") + } - return nil, nil + return nil } -func buildSubmissionParams(fields Fields, time_entries []TimeEntry) url.Values { +func buildSubmissionParams(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( + "f_personID0", + strconv.Itoa(time_entry.PersonID), + ) - v.Set( - fmt.Sprintf("f_projectID%d", i), - strconv.Itoa(project.ID), - ) + v.Set( + "f_clientID0", + strconv.Itoa(time_entry.Client), + ) - v.Set( - fmt.Sprintf("f_moduleID%d", i), - strconv.Itoa(module.ID), - ) + v.Set( + "f_projectID0", + strconv.Itoa(time_entry.Project), + ) - v.Set( - fmt.Sprintf("f_taskID%d", i), - strconv.Itoa(task.ID), - ) + v.Set( + "f_moduleID0", + strconv.Itoa(time_entry.Module), + ) - v.Set( - fmt.Sprintf("f_worktypeID%d", i), - strconv.Itoa(work_type.ID), - ) + v.Set( + "f_taskID0", + strconv.Itoa(time_entry.Task), + ) - v.Set( - fmt.Sprintf("f_date%d", i), - entry.Date.Format("02/01/06"), // day/month/year - ) + v.Set( + "f_worktypeID0", + strconv.Itoa(time_entry.WorkType), + ) - v.Set( - fmt.Sprintf("f_time%d", i), - strconv.Itoa(entry.Time), - ) + v.Set( + "f_date0", + time_entry.Date.Format("02/01/06"), // day/month/year + ) - v.Set( - fmt.Sprintf("f_billable%d", i), - billable, - ) + time_str := strconv.FormatFloat(time_entry.Time, 'f', 2, 64) + time_european_format := strings.Replace(time_str, ".", ",", -1) + v.Set( + "f_time0", + time_european_format, + ) - v.Set( - fmt.Sprintf("f_description%d", i), - entry.Description, - ) + 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, + ) + + v.Set("f_entryIndexes", "0") 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) + + 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 +} diff --git a/timetask/http_test.go b/timetask/http_test.go index c54617f..36f9e4a 100644 --- a/timetask/http_test.go +++ b/timetask/http_test.go @@ -17,7 +17,9 @@ func init() { } func TestLogin(t *testing.T) { - response, err := Login(username, password) + t.Skip("No requests") + + response, _, err := Login(username, password) if err != nil { t.Fatal(err) } diff --git a/timetask/module.go b/timetask/module.go new file mode 100644 index 0000000..4dde57a --- /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 ModuleParseXML(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..cee87c5 --- /dev/null +++ b/timetask/module_test.go @@ -0,0 +1,50 @@ +package timetask + +import "testing" + +const modules_xml = `<?xml version="1.0" encoding="UTF-8" ?> +<ajax-response> + <response type="object" id="ModuleList"> + <item> + <moduleid><![CDATA[55555]]></moduleid> + <modulename><![CDATA[R&D]]></modulename> + </item> + <item> + <moduleid><![CDATA[77777]]></moduleid> + <modulename><![CDATA[Sprint 1]]></modulename> + </item> + <item> + <moduleid><![CDATA[222222]]></moduleid> + <modulename><![CDATA[Sprint 2]]></modulename> + </item> + </response> +</ajax-response>` + +func TestModuleParseXML(t *testing.T) { + modules, err := ModuleParseXML(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) +} diff --git a/timetask/profile.go b/timetask/profile.go new file mode 100644 index 0000000..9267ee6 --- /dev/null +++ b/timetask/profile.go @@ -0,0 +1,5 @@ +package timetask + +type Profile struct { + PersonID int `toml:"person_id"` +} 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 +} diff --git a/timetask/time_entry.go b/timetask/time_entry.go index 17a8e0c..bb7a741 100644 --- a/timetask/time_entry.go +++ b/timetask/time_entry.go @@ -3,50 +3,35 @@ package timetask import "time" type TimeEntry struct { - Client string - Project string - Module string - Task string - WorkType string `yaml:"work_type"` + PersonID int + Client int + Project int + Module int + Task int + WorkType int Date time.Time - Time int + Time float64 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 +func NewTimeEntry( + profile Profile, + project Project, + date time.Time, + time float64, + 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, } - - 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 } |