Skip to content

Commit

Permalink
✨ Surface assessment tags on Archetype resource (#502)
Browse files Browse the repository at this point in the history
* Use TagRefs on the Archetype resource
* Add virtual tags inherited from assessments to the Tags field on the
Archetype resource
* Fixed a bug where an application would be denied membership in an
archetype that wasn't a strict subset of another archetype.

---------

Signed-off-by: Sam Lucidi <[email protected]>
  • Loading branch information
mansam authored Oct 4, 2023
1 parent 2c86e5f commit ff4b675
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 30 deletions.
71 changes: 51 additions & 20 deletions api/archetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,24 @@ func (h ArchetypeHandler) Get(ctx *gin.Context) {
return
}

resolver, err := assessment.NewQuestionnaireResolver(h.DB(ctx))
questionnaires, err := assessment.NewQuestionnaireResolver(h.DB(ctx))
if err != nil {
_ = ctx.Error(err)
return
}

tags, err := assessment.NewTagResolver(h.DB(ctx))
if err != nil {
_ = ctx.Error(err)
}

resolver := assessment.NewArchetypeResolver(m, tags)

r := Archetype{}
r.With(m)
r.WithApplications(applications)
r.Assessed = resolver.Assessed(m.Assessments)
r.WithAssessmentTags(resolver.AssessmentTags())
r.Assessed = questionnaires.Assessed(m.Assessments)
h.Respond(ctx, http.StatusOK, r)
}

Expand All @@ -93,11 +101,15 @@ func (h ArchetypeHandler) List(ctx *gin.Context) {
return
}

resolver, err := assessment.NewQuestionnaireResolver(h.DB(ctx))
questionnaires, err := assessment.NewQuestionnaireResolver(h.DB(ctx))
if err != nil {
_ = ctx.Error(err)
return
}
tags, err := assessment.NewTagResolver(h.DB(ctx))
if err != nil {
_ = ctx.Error(err)
}

membership := assessment.NewMembershipResolver(h.DB(ctx))
resources := []Archetype{}
Expand All @@ -109,9 +121,11 @@ func (h ArchetypeHandler) List(ctx *gin.Context) {
_ = ctx.Error(err)
return
}
resolver := assessment.NewArchetypeResolver(m, tags)
r.With(m)
r.WithApplications(applications)
r.Assessed = resolver.Assessed(m.Assessments)
r.WithAssessmentTags(resolver.AssessmentTags())
r.Assessed = questionnaires.Assessed(m.Assessments)
resources = append(resources, r)
}

Expand Down Expand Up @@ -326,17 +340,17 @@ func (h ArchetypeHandler) AssessmentCreate(ctx *gin.Context) {
// Archetype REST resource.
type Archetype struct {
Resource
Name string `json:"name" yaml:"name"`
Description string `json:"description" yaml:"description"`
Comments string `json:"comments" yaml:"comments"`
Tags []Ref `json:"tags" yaml:"tags"`
CriteriaTags []Ref `json:"criteriaTags" yaml:"criteriaTags"`
Stakeholders []Ref `json:"stakeholders" yaml:"stakeholders"`
StakeholderGroups []Ref `json:"stakeholderGroups" yaml:"stakeholderGroups"`
Applications []Ref `json:"applications" yaml:"applications"`
Assessments []Ref `json:"assessments" yaml:"assessments"`
Assessed bool `json:"assessed"`
Review *Ref `json:"review"`
Name string `json:"name" yaml:"name"`
Description string `json:"description" yaml:"description"`
Comments string `json:"comments" yaml:"comments"`
Tags []TagRef `json:"tags" yaml:"tags"`
Criteria []TagRef `json:"criteria" yaml:"criteria"`
Stakeholders []Ref `json:"stakeholders" yaml:"stakeholders"`
StakeholderGroups []Ref `json:"stakeholderGroups" yaml:"stakeholderGroups"`
Applications []Ref `json:"applications" yaml:"applications"`
Assessments []Ref `json:"assessments" yaml:"assessments"`
Assessed bool `json:"assessed"`
Review *Ref `json:"review"`
}

//
Expand All @@ -346,13 +360,17 @@ func (r *Archetype) With(m *model.Archetype) {
r.Name = m.Name
r.Description = m.Description
r.Comments = m.Comments
r.Tags = []Ref{}
r.Tags = []TagRef{}
for _, t := range m.Tags {
r.Tags = append(r.Tags, r.ref(t.ID, &t))
ref := TagRef{}
ref.With(t.ID, t.Name, "", false)
r.Tags = append(r.Tags, ref)
}
r.CriteriaTags = []Ref{}
r.Criteria = []TagRef{}
for _, t := range m.CriteriaTags {
r.CriteriaTags = append(r.CriteriaTags, r.ref(t.ID, &t))
ref := TagRef{}
ref.With(t.ID, t.Name, "", false)
r.Criteria = append(r.Criteria, ref)
}
r.Stakeholders = []Ref{}
for _, s := range m.Stakeholders {
Expand Down Expand Up @@ -383,6 +401,16 @@ func (r *Archetype) WithApplications(apps []model.Application) {
}
}

//
// WithAssessmentTags updates the Archetype resource with tags inherited from assessments.
func (r *Archetype) WithAssessmentTags(tags []model.Tag) {
for _, t := range tags {
ref := TagRef{}
ref.With(t.ID, t.Name, SourceAssessment, true)
r.Tags = append(r.Tags, ref)
}
}

//
// Model builds a model from the resource.
func (r *Archetype) Model() (m *model.Archetype) {
Expand All @@ -393,6 +421,9 @@ func (r *Archetype) Model() (m *model.Archetype) {
}
m.ID = r.ID
for _, ref := range r.Tags {
if ref.Virtual {
continue
}
m.Tags = append(
m.Tags,
model.Tag{
Expand All @@ -401,7 +432,7 @@ func (r *Archetype) Model() (m *model.Archetype) {
},
})
}
for _, ref := range r.CriteriaTags {
for _, ref := range r.Criteria {
m.CriteriaTags = append(
m.CriteriaTags,
model.Tag{
Expand Down
4 changes: 2 additions & 2 deletions api/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,8 @@ func (r *Ref) With(id uint, name string) {
type TagRef struct {
ID uint `json:"id" binding:"required"`
Name string `json:"name"`
Source string `json:"source"`
Virtual bool `json:"virtual,omitempty"`
Source string `json:"source,omitempty" yaml:"source,omitempty"`
Virtual bool `json:"virtual,omitempty" yaml:"virtual,omitempty"`
}

//
Expand Down
38 changes: 38 additions & 0 deletions assessment/archetype.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package assessment

import "github.com/konveyor/tackle2-hub/model"

//
// NewArchetypeResolver creates a new ArchetypeResolver.
func NewArchetypeResolver(archetype *model.Archetype, tags *TagResolver) (a *ArchetypeResolver) {
a = &ArchetypeResolver{
archetype: archetype,
tagResolver: tags,
}
return
}

//
// ArchetypeResolver wraps an Archetype model
// with assessment-related functionality.
type ArchetypeResolver struct {
archetype *model.Archetype
tagResolver *TagResolver
}

//
// AssessmentTags returns the list of tags that the archetype should
// inherit from the answers given to its assessments.
func (r *ArchetypeResolver) AssessmentTags() (tags []model.Tag) {
seenTags := make(map[uint]bool)
for _, assessment := range r.archetype.Assessments {
aTags := r.tagResolver.Assessment(&assessment)
for _, t := range aTags {
if _, found := seenTags[t.ID]; !found {
seenTags[t.ID] = true
tags = append(tags, t)
}
}
}
return
}
4 changes: 2 additions & 2 deletions assessment/membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (r *MembershipResolver) Archetypes(m *model.Application) (archetypes []mode

matches := []model.Archetype{}
for _, a := range r.archetypes {
if appTags.Superset(r.tagSets[a.ID]) {
if appTags.Superset(r.tagSets[a.ID], false) {
matches = append(matches, a)
}
}
Expand All @@ -69,7 +69,7 @@ loop:
}
a1tags := r.tagSets[a1.ID]
a2tags := r.tagSets[a2.ID]
if a1tags.Subset(a2tags) {
if a1tags.Subset(a2tags, true) {
continue loop
}
}
Expand Down
2 changes: 1 addition & 1 deletion assessment/questionnaire.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ loop:
answered.Add(a.QuestionnaireID)
}
}
assessed = answered.Superset(r.requiredQuestionnaires)
assessed = answered.Superset(r.requiredQuestionnaires, false)

return
}
15 changes: 10 additions & 5 deletions assessment/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ func (r Set) Size() int {

//
// Add a member to the set.
func (r Set) Add(member uint) {
r.members[member] = true
func (r Set) Add(members ...uint) {
for _, member := range members {
r.members[member] = true
}
}

//
Expand All @@ -35,7 +37,10 @@ func (r Set) Contains(element uint) bool {

//
// Superset tests whether every element of other is in the set.
func (r Set) Superset(other Set) bool {
func (r Set) Superset(other Set, strict bool) bool {
if strict && r.Size() <= other.Size() {
return false
}
for m := range other.members {
if !r.Contains(m) {
return false
Expand All @@ -46,8 +51,8 @@ func (r Set) Superset(other Set) bool {

//
// Subset tests whether every element of this set is in the other.
func (r Set) Subset(other Set) bool {
return other.Superset(r)
func (r Set) Subset(other Set, strict bool) bool {
return other.Superset(r, strict)
}

//
Expand Down
46 changes: 46 additions & 0 deletions assessment/set_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package assessment

import (
"github.com/onsi/gomega"
"testing"
)

func TestSet_Superset(t *testing.T) {
g := gomega.NewGomegaWithT(t)

a := NewSet()
b := NewSet()
a.Add(1, 2, 3, 4)
b.Add(1, 2, 3, 4)

g.Expect(a.Superset(b, false)).To(gomega.BeTrue())
g.Expect(b.Superset(a, false)).To(gomega.BeTrue())
g.Expect(a.Superset(b, true)).To(gomega.BeFalse())
g.Expect(b.Superset(a, true)).To(gomega.BeFalse())

a.Add(5)
g.Expect(a.Superset(b, false)).To(gomega.BeTrue())
g.Expect(a.Superset(b, true)).To(gomega.BeTrue())
g.Expect(b.Superset(a, false)).To(gomega.BeFalse())
g.Expect(b.Superset(a, true)).To(gomega.BeFalse())
}

func TestSet_Subset(t *testing.T) {
g := gomega.NewGomegaWithT(t)

a := NewSet()
b := NewSet()
a.Add(1, 2, 3, 4)
b.Add(1, 2, 3, 4)

g.Expect(a.Subset(b, false)).To(gomega.BeTrue())
g.Expect(b.Subset(a, false)).To(gomega.BeTrue())
g.Expect(a.Subset(b, true)).To(gomega.BeFalse())
g.Expect(b.Subset(a, true)).To(gomega.BeFalse())

b.Add(5)
g.Expect(a.Subset(b, false)).To(gomega.BeTrue())
g.Expect(a.Subset(b, true)).To(gomega.BeTrue())
g.Expect(b.Subset(a, false)).To(gomega.BeFalse())
g.Expect(b.Subset(a, true)).To(gomega.BeFalse())
}

0 comments on commit ff4b675

Please sign in to comment.