Skip to content

Commit

Permalink
Enable importing app stakeholders
Browse files Browse the repository at this point in the history
Adds two fields to the import CSV immediately before tags.
One is for the Owner and should be empty or contain a single
value in the form 'John Doe <[email protected]>'. The second
is for contributors, and should be empty or a list of one or
more values in the previous format, separated by commas. As
this would be a comma-delimited field within a CSV, the whole
value should be surrounded by quotes.

Fixes #538

Signed-off-by: Sam Lucidi <[email protected]>
  • Loading branch information
mansam committed Nov 7, 2023
1 parent ee70713 commit c4d9129
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 3 deletions.
12 changes: 9 additions & 3 deletions api/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const (
RecordTypeDependency = "2"
)

const (
ExpectedFieldCount = 17
)

//
// Import Statuses
const (
Expand Down Expand Up @@ -270,8 +274,8 @@ func (h ImportHandler) UploadCSV(ctx *gin.Context) {
var imp model.Import
switch row[0] {
case RecordTypeApplication:
// Check row format - length, expecting 15 fields + tags
if len(row) < 15 {
// Check row format - length, expecting 17 fields + tags
if len(row) < ExpectedFieldCount {
h.Respond(ctx, http.StatusBadRequest, gin.H{"errorMessage": "Invalid Application Import CSV format."})
return
}
Expand Down Expand Up @@ -396,10 +400,12 @@ func (h ImportHandler) applicationFromRow(fileName string, row []string) (app mo
RepositoryURL: row[12],
RepositoryBranch: row[13],
RepositoryPath: row[14],
Owner: row[15],
Contributors: row[16],
}

// Tags
for i := 15; i < len(row); i++ {
for i := ExpectedFieldCount; i < len(row); i++ {
if i%2 == 0 {
tag := model.ImportTag{
Name: row[i],
Expand Down
84 changes: 84 additions & 0 deletions importer/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,54 @@ func (m *Manager) createApplication(imp *model.Import) (ok bool) {
}
}

if imp.Owner != "" {
name, email, parsed := parseStakeholder(imp.Owner)
if !parsed {
imp.ErrorMessage = fmt.Sprintf("Could not parse Owner '%s'.", imp.Owner)
return
}
owner, found := m.findStakeholder(email)
if !found {
if imp.ImportSummary.CreateEntities {
var err error
owner, err = m.createStakeholder(name, email)
if err != nil {
imp.ErrorMessage = fmt.Sprintf("Owner '%s' could not be created.", imp.Owner)
return
}
} else {
imp.ErrorMessage = fmt.Sprintf("Owner '%s' could not be found.", imp.Owner)
return
}
}
app.OwnerID = &owner.ID
}
if imp.Contributors != "" {
fields := strings.Split(imp.Contributors, ",")
for _, f := range fields {
name, email, parsed := parseStakeholder(f)
if !parsed {
imp.ErrorMessage = fmt.Sprintf("Could not parse Contributor '%s'.", f)
return
}
contributor, found := m.findStakeholder(email)
if !found {
if imp.ImportSummary.CreateEntities {
var err error
contributor, err = m.createStakeholder(name, email)
if err != nil {
imp.ErrorMessage = fmt.Sprintf("Contributor '%s' could not be created.", imp.Owner)
return
}
} else {
imp.ErrorMessage = fmt.Sprintf("Contributor '%s' could not be found.", imp.Owner)
return
}
}
app.Contributors = append(app.Contributors, contributor)
}
}

result := m.DB.Create(app)
if result.Error != nil {
imp.ErrorMessage = result.Error.Error()
Expand All @@ -295,6 +343,25 @@ func (m *Manager) createApplication(imp *model.Import) (ok bool) {
return
}

func (m *Manager) createStakeholder(name string, email string) (stakeholder model.Stakeholder, err error) {
stakeholder.Name = name
stakeholder.Email = email
err = m.DB.Create(&stakeholder).Error
if err != nil {
err = liberr.Wrap(err)
}
return
}

func (m *Manager) findStakeholder(email string) (stakeholder model.Stakeholder, found bool) {
result := m.DB.First(&stakeholder, "email = ?", email)
if result.Error != nil {
return
}
found = true
return
}

//
// normalizedName transforms given name to be comparable as same with similar names
// Example: normalizedName(" F oo-123 bar! ") returns "foo123bar!"
Expand All @@ -304,3 +371,20 @@ func normalizedName(name string) (normName string) {
normName = invalidSymbols.ReplaceAllString(normName, "")
return
}

//
// parseStakeholder attempts to parse a stakeholder's name and an email address
// out of a string like `John Smith <[email protected]>`. The pattern is very
// simple and treats anything before the first bracket as the name,
// and anything within the brackets as the email.
func parseStakeholder(s string) (name string, email string, parsed bool) {
pattern := regexp.MustCompile("(.+)\\s<(.+@.+)>")
matches := pattern.FindStringSubmatch(strings.TrimSpace(s))
if len(matches) != 3 {
return
}
parsed = true
name = matches[1]
email = strings.ToLower(matches[2])
return
}
2 changes: 2 additions & 0 deletions migration/v11/model/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ type Import struct {
RepositoryURL string
RepositoryBranch string
RepositoryPath string
Owner string
Contributors string
}

func (r *Import) AsMap() (m map[string]interface{}) {
Expand Down

0 comments on commit c4d9129

Please sign in to comment.