aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTeddy Wing2017-06-04 02:07:50 +0200
committerTeddy Wing2017-06-04 02:07:50 +0200
commit02e4fb5d0d95b8c5c5442ee0a97b960f1296c236 (patch)
tree4cf323694692322036b80d2d921ff966096c6c36
parent055301ca09d57b759b290d897bbb7560460251ca (diff)
parent9b6a6543e351308939bd420243507368b0669e63 (diff)
downloadtimetasker-02e4fb5d0d95b8c5c5442ee0a97b960f1296c236.tar.bz2
Merge branch 'timetasker-daily'
-rw-r--r--README.md128
-rw-r--r--TODO23
-rw-r--r--config.go76
-rw-r--r--main.go118
-rw-r--r--password_cmd.go22
-rw-r--r--templates/timesheet.yml.tmpl11
-rw-r--r--templates/weekly_timesheet.yml.tmpl3
-rw-r--r--timetask/fields.go85
-rw-r--r--timetask/generator.go44
-rw-r--r--timetask/http.go239
-rw-r--r--timetask/http_test.go4
-rw-r--r--timetask/module.go24
-rw-r--r--timetask/module_test.go50
-rw-r--r--timetask/profile.go5
-rw-r--r--timetask/project.go10
-rw-r--r--timetask/time_entry.go65
16 files changed, 586 insertions, 321 deletions
diff --git a/README.md b/README.md
index 1b7f244..4c34af7 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..13efe4a
--- /dev/null
+++ b/TODO
@@ -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
+}
diff --git a/main.go b/main.go
index 5a9f0d1..2f37632 100644
--- a/main.go
+++ b/main.go
@@ -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&amp;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&amp;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
}