From ff4b6755371919b7cab5f194751c5e2722f03267 Mon Sep 17 00:00:00 2001 From: Samuel Lucidi Date: Wed, 4 Oct 2023 17:04:36 -0400 Subject: [PATCH] :sparkles: Surface assessment tags on Archetype resource (#502) * 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 --- api/archetype.go | 71 ++++++++++++++++++++++++++----------- api/base.go | 4 +-- assessment/archetype.go | 38 ++++++++++++++++++++ assessment/membership.go | 4 +-- assessment/questionnaire.go | 2 +- assessment/set.go | 15 +++++--- assessment/set_test.go | 46 ++++++++++++++++++++++++ 7 files changed, 150 insertions(+), 30 deletions(-) create mode 100644 assessment/archetype.go create mode 100644 assessment/set_test.go diff --git a/api/archetype.go b/api/archetype.go index 5218ebc6e..2388e0572 100644 --- a/api/archetype.go +++ b/api/archetype.go @@ -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) } @@ -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{} @@ -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) } @@ -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"` } // @@ -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 { @@ -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) { @@ -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{ @@ -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{ diff --git a/api/base.go b/api/base.go index 260836f8b..5e0c9607e 100644 --- a/api/base.go +++ b/api/base.go @@ -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"` } // diff --git a/assessment/archetype.go b/assessment/archetype.go new file mode 100644 index 000000000..550330d25 --- /dev/null +++ b/assessment/archetype.go @@ -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 +} diff --git a/assessment/membership.go b/assessment/membership.go index e6eeee83a..2119e0011 100644 --- a/assessment/membership.go +++ b/assessment/membership.go @@ -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) } } @@ -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 } } diff --git a/assessment/questionnaire.go b/assessment/questionnaire.go index d1fa86d01..c1e1a3a49 100644 --- a/assessment/questionnaire.go +++ b/assessment/questionnaire.go @@ -60,7 +60,7 @@ loop: answered.Add(a.QuestionnaireID) } } - assessed = answered.Superset(r.requiredQuestionnaires) + assessed = answered.Superset(r.requiredQuestionnaires, false) return } diff --git a/assessment/set.go b/assessment/set.go index c71006248..5a49ee930 100644 --- a/assessment/set.go +++ b/assessment/set.go @@ -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 + } } // @@ -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 @@ -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) } // diff --git a/assessment/set_test.go b/assessment/set_test.go new file mode 100644 index 000000000..b87df2ac2 --- /dev/null +++ b/assessment/set_test.go @@ -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()) +}