diff --git a/.gitignore b/.gitignore index b364f0c..889ce71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ cmd/asana/asana -/.idea \ No newline at end of file +/.idea + +.DS_Store \ No newline at end of file diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..1a59aeb --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +golang 1.23.0 diff --git a/asana.go b/asana.go index 07d3021..8c5e9b1 100644 --- a/asana.go +++ b/asana.go @@ -15,11 +15,10 @@ import ( "os" "strings" - "github.com/rs/xid" - + "dario.cat/mergo" "github.com/google/go-querystring/query" - "github.com/imdario/mergo" "github.com/pkg/errors" + "github.com/rs/xid" ) const ( @@ -34,9 +33,10 @@ func (f Feature) String() string { } const ( - NewTaskSubtypes Feature = "new_task_subtypes" - NewSections Feature = "new_sections" - StringIDs Feature = "string_ids" + NewTaskSubtypes Feature = "new_task_subtypes" + NewSections Feature = "new_sections" + StringIDs Feature = "string_ids" + ProjectPrivacySetting Feature = "project_privacy_setting" ) // Client is the root client for the Asana API. The nested HTTPClient should provide diff --git a/cmd/asana/main.go b/cmd/asana/main.go index bc0a8b0..3abf138 100644 --- a/cmd/asana/main.go +++ b/cmd/asana/main.go @@ -1,176 +1,180 @@ package main import ( - "encoding/json" - "fmt" - "github.com/jessevdk/go-flags" - "log" - "mime" - "net/http" - "net/url" - "os" - "path/filepath" - - "bitbucket.org/mikehouston/asana-go" + "encoding/json" + "fmt" + "github.com/jessevdk/go-flags" + "log" + "mime" + "net/http" + "net/url" + "os" + "path/filepath" + + "bitbucket.org/mikehouston/asana-go" +) + +var ( + True = true ) var options struct { - Token string `long:"token" description:"Personal Access Token used to authorize access to the API" env:"ASANA_TOKEN" required:"true"` + Token string `long:"token" description:"Personal Access Token used to authorize access to the API" env:"ASANA_TOKEN" required:"true"` - Workspace []string `long:"workspace" short:"w" description:"Workspace to access"` - Project []string `long:"project" short:"p" description:"Project to access"` - Task []string `long:"task" short:"t" description:"Task to access"` + Workspace []string `long:"workspace" short:"w" description:"Workspace to access"` + Project []string `long:"project" short:"p" description:"Project to access"` + Task []string `long:"task" short:"t" description:"Task to access"` - Attach string `long:"attach" description:"Attach a file to a task"` - AddSection string `long:"add-section" description:"Add a new section to a project"` + Attach string `long:"attach" description:"Attach a file to a task"` + AddSection string `long:"add-section" description:"Add a new section to a project"` - Stories bool `long:"stories" description:"List stories for a task"` - Clean bool `long:"clean" description:"Clean all stories from a task"` + Stories bool `long:"stories" description:"List stories for a task"` + Clean bool `long:"clean" description:"Clean all stories from a task"` - Debug bool `short:"d" long:"debug" description:"Show debug information"` - Verbose []bool `short:"v" long:"verbose" description:"Show verbose output"` + Debug bool `short:"d" long:"debug" description:"Show debug information"` + Verbose []bool `short:"v" long:"verbose" description:"Show verbose output"` } func authenticate(req *http.Request) (*url.URL, error) { - req.Header.Add("Authorization", "Bearer "+options.Token) - return nil, nil + req.Header.Add("Authorization", "Bearer "+options.Token) + return nil, nil } func check(err error) { - if err != nil { - log.Fatal(err) - } + if err != nil { + log.Fatal(err) + } } func main() { - if _, err := flags.Parse(&options); err != nil { - return - } - - // Create a client - client := asana.NewClient(&http.Client{ - Transport: &http.Transport{ - Proxy: authenticate, - }, - }) - if options.Debug { - client.Debug = true - client.DefaultOptions.Pretty = true - } - client.Verbose = options.Verbose - client.DefaultOptions.Enable = []asana.Feature{asana.StringIDs, asana.NewSections, asana.NewTaskSubtypes} - - // Load a task object - if options.Task == nil { - - // Load a project object - if options.Project == nil { - - // Load a workspace object - if options.Workspace == nil { - check(ListWorkspaces(client)) - return - } - - for _, w := range options.Workspace { - workspace := &asana.Workspace{ID: w} - check(ListProjects(client, workspace)) - } - return - } - - for _, p := range options.Project { - project := &asana.Project{ID: p} - - if options.AddSection != "" { - request := &asana.SectionBase{ - Name: options.AddSection, - } - - _, err := project.CreateSection(client, request) - check(err) - return - } - - fmtProject(client, project) - } - return - } - - for _, t := range options.Task { - task := &asana.Task{ID: t} - check(task.Fetch(client)) - - fmt.Printf("Task %s: %q\n", task.ID, task.Name) - if options.Attach != "" { - addAttachment(task, client) - return - } - if options.Stories { - listStories(task, client) - } - if options.Clean { - cleanStories(task, client) - } - - fmtTask(task, client) - } + if _, err := flags.Parse(&options); err != nil { + return + } + + // Create a client + client := asana.NewClient(&http.Client{ + Transport: &http.Transport{ + Proxy: authenticate, + }, + }) + if options.Debug { + client.DefaultOptions.Debug = &True + client.DefaultOptions.Pretty = &True + } + client.Verbose = options.Verbose + client.DefaultOptions.Enable = []asana.Feature{asana.StringIDs, asana.NewSections, asana.NewTaskSubtypes, asana.ProjectPrivacySetting} + + // Load a task object + if options.Task == nil { + + // Load a project object + if options.Project == nil { + + // Load a workspace object + if options.Workspace == nil { + check(ListWorkspaces(client)) + return + } + + for _, w := range options.Workspace { + workspace := &asana.Workspace{ID: w} + check(ListProjects(client, workspace)) + } + return + } + + for _, p := range options.Project { + project := &asana.Project{ID: p} + + if options.AddSection != "" { + request := &asana.SectionBase{ + Name: options.AddSection, + } + + _, err := project.CreateSection(client, request) + check(err) + return + } + + fmtProject(client, project) + } + return + } + + for _, t := range options.Task { + task := &asana.Task{ID: t} + check(task.Fetch(client)) + + fmt.Printf("Task %s: %q\n", task.ID, task.Name) + if options.Attach != "" { + addAttachment(task, client) + return + } + if options.Stories { + listStories(task, client) + } + if options.Clean { + cleanStories(task, client) + } + + fmtTask(task, client) + } } func listStories(task *asana.Task, client *asana.Client) { - stories, _, _ := task.Stories(client) - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - - for _, s := range stories { - fmt.Printf("Story %s (%s):\n", s.ID, s.CreatedBy.Name) - check(enc.Encode(s)) - } + stories, _, _ := task.Stories(client) + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + + for _, s := range stories { + fmt.Printf("Story %s (%s):\n", s.ID, s.CreatedBy.Name) + check(enc.Encode(s)) + } } func cleanStories(task *asana.Task, client *asana.Client) { - stories, _, _ := task.Stories(client) - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") + stories, _, _ := task.Stories(client) + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") - for _, s := range stories { - check(s.Delete(client)) - } + for _, s := range stories { + check(s.Delete(client)) + } } func fmtProject(client *asana.Client, project *asana.Project) { - fmt.Println("\nSections:") - check(ListSections(client, project)) - fmt.Println("\nTasks:") - check(ListTasks(client, project)) + fmt.Println("\nSections:") + check(ListSections(client, project)) + fmt.Println("\nTasks:") + check(ListTasks(client, project)) } func fmtTask(task *asana.Task, client *asana.Client) { - fmt.Printf(" Completed: %v\n", task.Completed) - if task.Completed != nil && !*task.Completed { - fmt.Printf(" Due: %s\n", task.DueAt) - } - if task.Notes != "" { - fmt.Printf(" Notes: %q\n", task.Notes) - } - // Get subtasks - subtasks, nextPage, err := task.Subtasks(client) - check(err) - _ = nextPage - for _, subtask := range subtasks { - fmt.Printf(" Subtask %s: %q\n", subtask.ID, subtask.Name) - } + fmt.Printf(" Completed: %v\n", task.Completed) + if task.Completed != nil && !*task.Completed { + fmt.Printf(" Due: %s\n", task.DueAt) + } + if task.Notes != "" { + fmt.Printf(" Notes: %q\n", task.Notes) + } + // Get subtasks + subtasks, nextPage, err := task.Subtasks(client) + check(err) + _ = nextPage + for _, subtask := range subtasks { + fmt.Printf(" Subtask %s: %q\n", subtask.ID, subtask.Name) + } } func addAttachment(task *asana.Task, client *asana.Client) { - f, err := os.Open(options.Attach) - check(err) - defer f.Close() - a, err := task.CreateAttachment(client, &asana.NewAttachment{ - Reader: f, - FileName: f.Name(), - ContentType: mime.TypeByExtension(filepath.Ext(f.Name())), - }) - check(err) - fmt.Printf("Attachment added: %+v", a) + f, err := os.Open(options.Attach) + check(err) + defer f.Close() + a, err := task.CreateAttachment(client, &asana.NewAttachment{ + Reader: f, + FileName: f.Name(), + ContentType: mime.TypeByExtension(filepath.Ext(f.Name())), + }) + check(err) + fmt.Printf("Attachment added: %+v", a) } diff --git a/customfields.go b/customfields.go index 3be6c81..2890523 100644 --- a/customfields.go +++ b/customfields.go @@ -200,7 +200,7 @@ type AddProjectLocalCustomFieldRequest struct { } func (p *Project) AddProjectLocalCustomField(client *Client, request *AddProjectLocalCustomFieldRequest) (*CustomFieldSetting, error) { - client.trace("Attach custom field %q to project %q", request.CustomField, p.ID) + client.trace("Attach custom field %q to project %q", request.CustomField.Name, p.ID) // Custom request encoding m := map[string]interface{}{} diff --git a/customfields_test.go b/customfields_test.go index 3552323..01f7ae0 100644 --- a/customfields_test.go +++ b/customfields_test.go @@ -1,49 +1,49 @@ package asana import ( - "encoding/json" - "testing" + "encoding/json" + "testing" ) func TestCustomFieldBase_Precision_ParseZero(t *testing.T) { - cf := &CustomFieldBase{} - if err := json.Unmarshal([]byte(` + cf := &CustomFieldBase{} + if err := json.Unmarshal([]byte(` { "precision": 0 } `), cf); err != nil { - t.Fatal(err) - } + t.Fatal(err) + } - if cf.Precision == nil || *cf.Precision != 0 { - t.Errorf("Expected Precision to be a pointer to the integer zero, but saw %v", cf.Precision) - } + if cf.Precision == nil || *cf.Precision != 0 { + t.Errorf("Expected Precision to be a pointer to the integer zero, but saw %v", cf.Precision) + } } func TestCustomFieldBase_Precision_ParseMissing(t *testing.T) { - cf := &CustomFieldBase{} - if err := json.Unmarshal([]byte(` + cf := &CustomFieldBase{} + if err := json.Unmarshal([]byte(` { "name": "name" } `), cf); err != nil { - t.Fatal(err) - } + t.Fatal(err) + } - if cf.Precision != nil { - t.Errorf("Expected Precision to be a nil, but saw %v", cf.Precision) - } + if cf.Precision != nil { + t.Errorf("Expected Precision to be a nil, but saw %v", cf.Precision) + } } func TestCustomFieldBase_Precision_SerializeZero(t *testing.T) { - val := 0 - cf := &CustomFieldBase{Precision: &val} - if bs, err := json.Marshal(cf); err != nil { - t.Fatal(err) - } else { - if string(bs) != `{"precision":0,"resource_subtype":""}` { - t.Errorf("Expected Precision to be a zero, but saw %v", string(bs)) - } - } + val := 0 + cf := &CustomFieldBase{Precision: &val} + if bs, err := json.Marshal(cf); err != nil { + t.Fatal(err) + } else { + if string(bs) != `{"precision":0,"resource_subtype":""}` { + t.Errorf("Expected Precision to be a zero, but saw %v", string(bs)) + } + } } diff --git a/go.mod b/go.mod index 29ed825..885af70 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,18 @@ module bitbucket.org/mikehouston/asana-go -go 1.19 +go 1.23 require ( + dario.cat/mergo v1.0.1 github.com/google/go-querystring v1.1.0 - github.com/imdario/mergo v0.3.13 - github.com/jessevdk/go-flags v1.4.0 + github.com/h2non/gock v1.2.0 + github.com/jessevdk/go-flags v1.6.1 github.com/pkg/errors v0.9.1 - github.com/rs/xid v1.4.0 - golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7 + github.com/rs/xid v1.5.0 + golang.org/x/oauth2 v0.22.0 ) require ( - github.com/golang/protobuf v1.5.2 // indirect - golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.1 // indirect + github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect + golang.org/x/sys v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index d167dec..f532dcf 100644 --- a/go.sum +++ b/go.sum @@ -1,37 +1,26 @@ -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= +github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E= -golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7 h1:dtndE8FcEta75/4kHF3AbpuWzV6f1LjnLrM4pe2SZrw= -golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/members.go b/members.go new file mode 100644 index 0000000..da6d35b --- /dev/null +++ b/members.go @@ -0,0 +1,94 @@ +package asana + +type AccessLevel string + +const ( + AccessLevelAdmin AccessLevel = "admin" + AccessLevelEditor AccessLevel = "editor" + AccessLevelCommenter AccessLevel = "commenter" + AccessLevelViewer AccessLevel = "viewer" +) + +type ProjectMember struct { + // Read-only. Globally unique ID of the object + ID string `json:"gid,omitempty"` + + // Read-only. The base type of this resource + ResourceType string `json:"resource_type,omitempty"` + + // Read-only. The name of the object. + Name string `json:"name,omitempty"` +} + +type ProjectMembership struct { + // Read-only. Globally unique ID of the object + ID string `json:"gid,omitempty"` + + // Read-only. The base type of this resource + ResourceType string `json:"resource_type,omitempty"` + + // The project associated with this membership + Parent *Project `json:"parent"` + + // A member object represents either a team or user. + Member *ProjectMember `json:"member"` + + // Whether the member has admin, editor, commenter, or viewer access to the project. + AccessLevel AccessLevel `json:"access_level,omitempty"` + + // Read-only. Type of the membership. + ResourceSubtype string `json:"resource_subtype,omitempty"` +} + +type membershipsRequestParams struct { + // Globally unique identifier for goal, project, or portfolio + Parent string `json:"parent"` + + // Optional - Globally unique identifier for team or user. + Member string `json:"member,omitempty"` +} + +func (p *Project) Memberships(client *Client, options ...*Options) ([]*ProjectMembership, *NextPage, error) { + client.trace("Listing memberships in project %s...\n", p.ID) + var result []*ProjectMembership + + // Make the request + query := membershipsRequestParams{ + Parent: p.ID, + } + nextPage, err := client.get("/memberships", query, &result, options...) + return result, nextPage, err +} + +type CreateMembershipRequest struct { + MemberID string + + // Whether the member has admin, editor, commenter, or viewer access to the project. + AccessLevel *AccessLevel `json:"access_level,omitempty"` +} + +type createMembershipRequest struct { + + // The gid of the user or team + Member string `json:"member"` + + // The gid of the goal or project to add the member to. + Parent string `json:"parent"` + + // Whether the member has admin, editor, commenter, or viewer access to the project. + AccessLevel *AccessLevel `json:"access_level,omitempty"` +} + +func (p *Project) CreateMembership(c *Client, req CreateMembershipRequest, options ...*Options) (*ProjectMembership, error) { + c.info("Creating Membership for entity %q in project %q\n", req.MemberID, p.ID) + + data := &createMembershipRequest{ + Member: req.MemberID, + Parent: p.ID, + AccessLevel: req.AccessLevel, + } + result := &ProjectMembership{} + + err := c.post("/memberships", data, result) + return result, err +} diff --git a/members_test.go b/members_test.go new file mode 100644 index 0000000..b6fba62 --- /dev/null +++ b/members_test.go @@ -0,0 +1,43 @@ +package asana + +import ( + "net/http" + "testing" + + "github.com/h2non/gock" +) + +type o map[string]any + +func TestProject_Memberships(t *testing.T) { + defer gock.Off() + + gock.New("https://app.asana.com"). + Get("/api/1.0/memberships"). + Reply(200). + JSON(o{"data": []o{{ + "gid": "12345", + "resource_type": "team", + "parent": o{"gid": "63627", "resource_type": "project", "name": "test"}, + "member": o{"gid": "12345", "resource_type": "team", "name": "team1"}, + "access_level": "admin", + "resource_subtype": "project_membership", + }}}) + + project := &Project{} + + client := NewClient(http.DefaultClient) + memberships, _, err := project.Memberships(client) + if err != nil { + t.Error(err) + } + + if len(memberships) != 1 { + t.Errorf("Expected 1 membership but found %d", len(memberships)) + } + + m := memberships[0] + if m.ID != "12345" { + t.Errorf("Expected membership ID 12345 but saw %s", m.ID) + } +} diff --git a/projects.go b/projects.go index 81b0bf6..820c962 100644 --- a/projects.go +++ b/projects.go @@ -29,6 +29,15 @@ const ( ViewTimeline View = "timeline" ) +// PrivacySetting indicates the privacy setting of a project +type PrivacySetting string + +const ( + PrivacySettingWorkspace PrivacySetting = "public_to_workspace" + PrivacySettingTeam PrivacySetting = "private_to_team" + PrivacySettingPrivate PrivacySetting = "private" +) + // ProjectBase contains the parts of Project which are not related to a specific instance type ProjectBase struct { @@ -68,10 +77,10 @@ type ProjectBase struct { // object. Notes string `json:"notes,omitempty"` - // True if the project is public to the organization. If false, do not - // share this project with other users in this organization without - // explicitly checking to see if they have access. - Public *bool `json:"public,omitempty"` + // The privacy setting of the project. Can be one of: public_to_workspace, private_to_team, private + // + // Note: Administrators in your organization may restrict the values of privacy_setting. + PrivacySetting PrivacySetting `json:"privacy_setting,omitempty"` // The day on which this project starts. This takes a date with format // YYYY-MM-DD. @@ -123,6 +132,8 @@ type Project struct { // Read-only. Array of Custom Field Settings (in compact form). CustomFieldSettings []*CustomFieldSetting `json:"custom_field_settings,omitempty"` + // Deprecated in favour of calling the /memberships endpoint + // // Read-only. Array of users who are members of this project. Members []*User `json:"members,omitempty"` @@ -159,6 +170,12 @@ type Project struct { // Create-only. The team that this project is shared with. This field only // exists for projects in organizations. Team *Team `json:"team,omitempty"` + + // Deprecated in favour of PrivacySetting + // True if the project is public to the organization. If false, do not + // share this project with other users in this organization without + // explicitly checking to see if they have access. + Public *bool `json:"public,omitempty"` } func (p *Project) GetID() string {