diff options
| -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  } | 
