From a7b9d613b3f8ead3664f27474c416ef8bb1d6ccb Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Wed, 6 Mar 2024 04:48:04 -0500 Subject: [PATCH 01/13] wip: draft of sending notifications on discussion reply --- pkg/graph/generated/generated.go | 574 +++++++++++++++++- pkg/graph/resolvers/activity.resolvers.go | 17 + pkg/graph/resolvers/plan_discussion.go | 100 +-- pkg/graph/schema/types/activity.graphql | 11 +- pkg/models/new_discussion_replied_meta.go | 67 ++ pkg/notifications/activity.go | 7 + .../new_discussion_replied_meta.go | 23 + .../new_discussion_replied_meta_test.go | 77 +++ src/gql/gen/graphql.ts | 13 +- 9 files changed, 841 insertions(+), 48 deletions(-) create mode 100644 pkg/models/new_discussion_replied_meta.go create mode 100644 pkg/notifications/new_discussion_replied_meta.go create mode 100644 pkg/notifications/new_discussion_replied_meta_test.go diff --git a/pkg/graph/generated/generated.go b/pkg/graph/generated/generated.go index 2eb0ed6a48..c0cbd6fecd 100644 --- a/pkg/graph/generated/generated.go +++ b/pkg/graph/generated/generated.go @@ -49,6 +49,7 @@ type ResolverRoot interface { ExistingModelLinks() ExistingModelLinksResolver ModelPlan() ModelPlanResolver Mutation() MutationResolver + NewDiscussionRepliedActivityMeta() NewDiscussionRepliedActivityMetaResolver OperationalNeed() OperationalNeedResolver OperationalSolution() OperationalSolutionResolver PlanBasics() PlanBasicsResolver @@ -270,6 +271,16 @@ type ComplexityRoot struct { AgreedDts func(childComplexity int) int } + NewDiscussionRepliedActivityMeta struct { + Content func(childComplexity int) int + Discussion func(childComplexity int) int + DiscussionID func(childComplexity int) int + Reply func(childComplexity int) int + ReplyID func(childComplexity int) int + Type func(childComplexity int) int + Version func(childComplexity int) int + } + OperationalNeed struct { CreatedBy func(childComplexity int) int CreatedByUserAccount func(childComplexity int) int @@ -1178,6 +1189,11 @@ type MutationResolver interface { MarkAllNotificationsAsRead(ctx context.Context) ([]*models.UserNotification, error) UpdateUserNotificationPreferences(ctx context.Context, changes map[string]interface{}) (*models.UserNotificationPreferences, error) } +type NewDiscussionRepliedActivityMetaResolver interface { + Discussion(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.PlanDiscussion, error) + + Reply(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.DiscussionReply, error) +} type OperationalNeedResolver interface { Solutions(ctx context.Context, obj *models.OperationalNeed, includeNotNeeded bool) ([]*models.OperationalSolution, error) } @@ -2681,6 +2697,55 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.NDAInfo.AgreedDts(childComplexity), true + case "NewDiscussionRepliedActivityMeta.content": + if e.complexity.NewDiscussionRepliedActivityMeta.Content == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.Content(childComplexity), true + + case "NewDiscussionRepliedActivityMeta.discussion": + if e.complexity.NewDiscussionRepliedActivityMeta.Discussion == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.Discussion(childComplexity), true + + case "NewDiscussionRepliedActivityMeta.discussionID": + if e.complexity.NewDiscussionRepliedActivityMeta.DiscussionID == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.DiscussionID(childComplexity), true + + case "NewDiscussionRepliedActivityMeta.reply": + if e.complexity.NewDiscussionRepliedActivityMeta.Reply == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.Reply(childComplexity), true + + case "NewDiscussionRepliedActivityMeta.replyID": + if e.complexity.NewDiscussionRepliedActivityMeta.ReplyID == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.ReplyID(childComplexity), true + + case "NewDiscussionRepliedActivityMeta.type": + if e.complexity.NewDiscussionRepliedActivityMeta.Type == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.Type(childComplexity), true + + case "NewDiscussionRepliedActivityMeta.version": + if e.complexity.NewDiscussionRepliedActivityMeta.Version == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.Version(childComplexity), true + case "OperationalNeed.createdBy": if e.complexity.OperationalNeed.CreatedBy == nil { break @@ -9354,7 +9419,7 @@ enum ActivityType { """ ActivityMetaData is a type that represents all the data that can be captured in an Activity """ -union ActivityMetaData = ActivityMetaBaseStruct | TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta +union ActivityMetaData = ActivityMetaBaseStruct | TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta | NewDiscussionRepliedActivityMeta type TaggedInPlanDiscussionActivityMeta { version: Int! @@ -9374,6 +9439,15 @@ type TaggedInDiscussionReplyActivityMeta { content: String! } +type NewDiscussionRepliedActivityMeta { + version: Int! + type: ActivityType! + discussionID: UUID! + discussion: PlanDiscussion! + replyID: UUID! + reply: DiscussionReply! + content: String! +} type ActivityMetaBaseStruct { version: Int! @@ -23386,6 +23460,368 @@ func (ec *executionContext) fieldContext_NDAInfo_agreedDts(ctx context.Context, return fc, nil } +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_version(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_version(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Version, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_version(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_type(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(models.ActivityType) + fc.Result = res + return ec.marshalNActivityType2githubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋmodelsᚐActivityType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ActivityType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_discussionID(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_discussionID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.DiscussionID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_discussionID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type UUID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_discussion(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_discussion(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.NewDiscussionRepliedActivityMeta().Discussion(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.PlanDiscussion) + fc.Result = res + return ec.marshalNPlanDiscussion2ᚖgithubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋmodelsᚐPlanDiscussion(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_discussion(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_PlanDiscussion_id(ctx, field) + case "modelPlanID": + return ec.fieldContext_PlanDiscussion_modelPlanID(ctx, field) + case "content": + return ec.fieldContext_PlanDiscussion_content(ctx, field) + case "userRole": + return ec.fieldContext_PlanDiscussion_userRole(ctx, field) + case "userRoleDescription": + return ec.fieldContext_PlanDiscussion_userRoleDescription(ctx, field) + case "replies": + return ec.fieldContext_PlanDiscussion_replies(ctx, field) + case "isAssessment": + return ec.fieldContext_PlanDiscussion_isAssessment(ctx, field) + case "createdBy": + return ec.fieldContext_PlanDiscussion_createdBy(ctx, field) + case "createdByUserAccount": + return ec.fieldContext_PlanDiscussion_createdByUserAccount(ctx, field) + case "createdDts": + return ec.fieldContext_PlanDiscussion_createdDts(ctx, field) + case "modifiedBy": + return ec.fieldContext_PlanDiscussion_modifiedBy(ctx, field) + case "modifiedByUserAccount": + return ec.fieldContext_PlanDiscussion_modifiedByUserAccount(ctx, field) + case "modifiedDts": + return ec.fieldContext_PlanDiscussion_modifiedDts(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PlanDiscussion", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_replyID(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_replyID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ReplyID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_replyID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type UUID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_reply(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_reply(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.NewDiscussionRepliedActivityMeta().Reply(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.DiscussionReply) + fc.Result = res + return ec.marshalNDiscussionReply2ᚖgithubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋmodelsᚐDiscussionReply(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_reply(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_DiscussionReply_id(ctx, field) + case "discussionID": + return ec.fieldContext_DiscussionReply_discussionID(ctx, field) + case "content": + return ec.fieldContext_DiscussionReply_content(ctx, field) + case "userRole": + return ec.fieldContext_DiscussionReply_userRole(ctx, field) + case "userRoleDescription": + return ec.fieldContext_DiscussionReply_userRoleDescription(ctx, field) + case "isAssessment": + return ec.fieldContext_DiscussionReply_isAssessment(ctx, field) + case "createdBy": + return ec.fieldContext_DiscussionReply_createdBy(ctx, field) + case "createdByUserAccount": + return ec.fieldContext_DiscussionReply_createdByUserAccount(ctx, field) + case "createdDts": + return ec.fieldContext_DiscussionReply_createdDts(ctx, field) + case "modifiedBy": + return ec.fieldContext_DiscussionReply_modifiedBy(ctx, field) + case "modifiedByUserAccount": + return ec.fieldContext_DiscussionReply_modifiedByUserAccount(ctx, field) + case "modifiedDts": + return ec.fieldContext_DiscussionReply_modifiedDts(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DiscussionReply", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_content(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_content(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Content, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_content(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _OperationalNeed_id(ctx context.Context, field graphql.CollectedField, obj *models.OperationalNeed) (ret graphql.Marshaler) { fc, err := ec.fieldContext_OperationalNeed_id(ctx, field) if err != nil { @@ -59829,6 +60265,11 @@ func (ec *executionContext) _ActivityMetaData(ctx context.Context, sel ast.Selec return graphql.Null } return ec._TaggedInDiscussionReplyActivityMeta(ctx, sel, obj) + case *models.NewDiscussionRepliedActivityMeta: + if obj == nil { + return graphql.Null + } + return ec._NewDiscussionRepliedActivityMeta(ctx, sel, obj) default: panic(fmt.Errorf("unexpected type %T", obj)) } @@ -62171,6 +62612,137 @@ func (ec *executionContext) _NDAInfo(ctx context.Context, sel ast.SelectionSet, return out } +var newDiscussionRepliedActivityMetaImplementors = []string{"NewDiscussionRepliedActivityMeta", "ActivityMetaData"} + +func (ec *executionContext) _NewDiscussionRepliedActivityMeta(ctx context.Context, sel ast.SelectionSet, obj *models.NewDiscussionRepliedActivityMeta) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, newDiscussionRepliedActivityMetaImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("NewDiscussionRepliedActivityMeta") + case "version": + out.Values[i] = ec._NewDiscussionRepliedActivityMeta_version(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "type": + out.Values[i] = ec._NewDiscussionRepliedActivityMeta_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "discussionID": + out.Values[i] = ec._NewDiscussionRepliedActivityMeta_discussionID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "discussion": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._NewDiscussionRepliedActivityMeta_discussion(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "replyID": + out.Values[i] = ec._NewDiscussionRepliedActivityMeta_replyID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "reply": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._NewDiscussionRepliedActivityMeta_reply(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "content": + out.Values[i] = ec._NewDiscussionRepliedActivityMeta_content(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var operationalNeedImplementors = []string{"OperationalNeed"} func (ec *executionContext) _OperationalNeed(ctx context.Context, sel ast.SelectionSet, obj *models.OperationalNeed) graphql.Marshaler { diff --git a/pkg/graph/resolvers/activity.resolvers.go b/pkg/graph/resolvers/activity.resolvers.go index 024431a59f..4c89512241 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -6,6 +6,7 @@ package resolvers import ( "context" + "fmt" "github.com/cmsgov/mint-app/pkg/appcontext" "github.com/cmsgov/mint-app/pkg/authentication" @@ -18,6 +19,16 @@ func (r *activityResolver) ActorUserAccount(ctx context.Context, obj *models.Act return UserAccountGetByIDLOADER(ctx, obj.ActorID) } +// Discussion is the resolver for the discussion field. +func (r *newDiscussionRepliedActivityMetaResolver) Discussion(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.PlanDiscussion, error) { + panic(fmt.Errorf("not implemented: Discussion - discussion")) +} + +// Reply is the resolver for the reply field. +func (r *newDiscussionRepliedActivityMetaResolver) Reply(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.DiscussionReply, error) { + panic(fmt.Errorf("not implemented: Reply - reply")) +} + // Discussion is the resolver for the discussion field. func (r *taggedInDiscussionReplyActivityMetaResolver) Discussion(ctx context.Context, obj *models.TaggedInDiscussionReplyActivityMeta) (*models.PlanDiscussion, error) { logger := appcontext.ZLogger(ctx) @@ -39,6 +50,11 @@ func (r *taggedInPlanDiscussionActivityMetaResolver) Discussion(ctx context.Cont // Activity returns generated.ActivityResolver implementation. func (r *Resolver) Activity() generated.ActivityResolver { return &activityResolver{r} } +// NewDiscussionRepliedActivityMeta returns generated.NewDiscussionRepliedActivityMetaResolver implementation. +func (r *Resolver) NewDiscussionRepliedActivityMeta() generated.NewDiscussionRepliedActivityMetaResolver { + return &newDiscussionRepliedActivityMetaResolver{r} +} + // TaggedInDiscussionReplyActivityMeta returns generated.TaggedInDiscussionReplyActivityMetaResolver implementation. func (r *Resolver) TaggedInDiscussionReplyActivityMeta() generated.TaggedInDiscussionReplyActivityMetaResolver { return &taggedInDiscussionReplyActivityMetaResolver{r} @@ -50,5 +66,6 @@ func (r *Resolver) TaggedInPlanDiscussionActivityMeta() generated.TaggedInPlanDi } type activityResolver struct{ *Resolver } +type newDiscussionRepliedActivityMetaResolver struct{ *Resolver } type taggedInDiscussionReplyActivityMetaResolver struct{ *Resolver } type taggedInPlanDiscussionActivityMetaResolver struct{ *Resolver } diff --git a/pkg/graph/resolvers/plan_discussion.go b/pkg/graph/resolvers/plan_discussion.go index 7ee8d3e7a7..066d8c5e0a 100644 --- a/pkg/graph/resolvers/plan_discussion.go +++ b/pkg/graph/resolvers/plan_discussion.go @@ -454,63 +454,73 @@ func CreateDiscussionReply( if err != nil { return reply, err } + // Create Activity and notifications in the DB _, notificationErr := notifications.ActivityTaggedInDiscussionReplyCreate(ctx, tx, principal.Account().ID, discussion.ID, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) if notificationErr != nil { return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) } - go func() { - - replyUser := principal.Account() - commonName := replyUser.CommonName - if err != nil { - logger.Error("error sending discussion reply emails. Unable to retrieve modelPlan", - zap.String("replyID", reply.ID.String()), - zap.Error(err)) - } + pref, err := UserNotificationPreferencesGetByUserID(ctx, principal.Account().ID) + if err != nil { + return nil, fmt.Errorf("unable to get user notification preference, Notification not created %w", err) + } - errReplyEmail := sendDiscussionReplyEmails( - ctx, - store, - logger, - emailService, - emailTemplateService, - addressBook, - discussion, - reply, - modelPlan, - replyUser, - ) - if errReplyEmail != nil { - logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", - zap.String("discussionID", discussion.ID.String()), - zap.Error(errReplyEmail)) - } + // Early return if + if !pref.NewDiscussionReply.SendEmail() { + go func() { + replyUser := principal.Account() + commonName := replyUser.CommonName - err = sendPlanDiscussionTagEmails( - ctx, - store, - false, - logger, - emailService, - emailTemplateService, - addressBook, - reply.Content, - reply.DiscussionID, - modelPlan, - commonName, - reply.UserRole.Humanize(models.ValueOrEmpty(reply.UserRoleDescription)), - ) - - if err != nil { if err != nil { + logger.Error("error sending discussion reply emails. Unable to retrieve modelPlan", + zap.String("replyID", reply.ID.String()), + zap.Error(err)) + } + + errReplyEmail := sendDiscussionReplyEmails( + ctx, + store, + logger, + emailService, + emailTemplateService, + addressBook, + discussion, + reply, + modelPlan, + replyUser, + ) + if errReplyEmail != nil { logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", zap.String("discussionID", discussion.ID.String()), - zap.Error(err)) + zap.Error(errReplyEmail)) } - } - }() + + err = sendPlanDiscussionTagEmails( + ctx, + store, + false, + logger, + emailService, + emailTemplateService, + addressBook, + reply.Content, + reply.DiscussionID, + modelPlan, + commonName, + reply.UserRole.Humanize(models.ValueOrEmpty(reply.UserRoleDescription)), + ) + + if err != nil { + if err != nil { + logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", + zap.String("discussionID", discussion.ID.String()), + zap.Error(err)) + } + } + }() + } + return reply, err }) if err != nil { diff --git a/pkg/graph/schema/types/activity.graphql b/pkg/graph/schema/types/activity.graphql index 96da5d3d19..726a03b169 100644 --- a/pkg/graph/schema/types/activity.graphql +++ b/pkg/graph/schema/types/activity.graphql @@ -13,7 +13,7 @@ enum ActivityType { """ ActivityMetaData is a type that represents all the data that can be captured in an Activity """ -union ActivityMetaData = ActivityMetaBaseStruct | TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta +union ActivityMetaData = ActivityMetaBaseStruct | TaggedInPlanDiscussionActivityMeta | TaggedInDiscussionReplyActivityMeta | NewDiscussionRepliedActivityMeta type TaggedInPlanDiscussionActivityMeta { version: Int! @@ -33,6 +33,15 @@ type TaggedInDiscussionReplyActivityMeta { content: String! } +type NewDiscussionRepliedActivityMeta { + version: Int! + type: ActivityType! + discussionID: UUID! + discussion: PlanDiscussion! + replyID: UUID! + reply: DiscussionReply! + content: String! +} type ActivityMetaBaseStruct { version: Int! diff --git a/pkg/models/new_discussion_replied_meta.go b/pkg/models/new_discussion_replied_meta.go new file mode 100644 index 0000000000..068f54db61 --- /dev/null +++ b/pkg/models/new_discussion_replied_meta.go @@ -0,0 +1,67 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "errors" + + "github.com/google/uuid" +) + +// NewDiscussionRepliedActivityMeta represents the notification data that is relevant to a new Discussion Reply +type NewDiscussionRepliedActivityMeta struct { + ActivityMetaBaseStruct + discussionRelation + ReplyID uuid.UUID `json:"replyID"` + Content string `json:"content"` +} + +// newNewDiscussionRepliedActivityMeta creates a New NewDiscussionRepliedActivityMeta +func newNewDiscussionRepliedActivityMeta(discussionID uuid.UUID, replyID uuid.UUID, content string) *NewDiscussionRepliedActivityMeta { + version := 0 //iterate this if this type ever updates + return &NewDiscussionRepliedActivityMeta{ + ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityTaggedInDiscussionReply, version), + discussionRelation: NewDiscussionRelation(discussionID), + ReplyID: replyID, + Content: content, + } + +} + +// NewNewDiscussionRepliedActivity creates a New New Discussion Replied type of Activity +func NewNewDiscussionRepliedActivity(actorID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, content string) *Activity { + return &Activity{ + baseStruct: NewBaseStruct(actorID), + ActorID: actorID, + EntityID: discussionID, + ActivityType: ActivityTaggedInDiscussionReply, + MetaData: newNewDiscussionRepliedActivityMeta(discussionID, replyID, content), + } +} + +// Future Enhancement: --> Refactor these all to have a generic scan / value + +// Value allows us to satisfy the valuer interface so we can write to the database +// We need to do a specific implementation instead of relying on the implementation of the embedded struct, as that will only serialize the common data +func (d NewDiscussionRepliedActivityMeta) Value() (driver.Value, error) { + + j, err := json.Marshal(d) + return j, err +} + +// Scan implements the scanner interface so we can translate the JSONb from the db to an object in GO +func (d *NewDiscussionRepliedActivityMeta) Scan(src interface{}) error { + if src == nil { + return nil + } + source, ok := src.([]byte) + if !ok { + return errors.New("type assertion .([]byte) failed") + } + err := json.Unmarshal(source, d) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/notifications/activity.go b/pkg/notifications/activity.go index 81bccc657c..07b8d42cc4 100644 --- a/pkg/notifications/activity.go +++ b/pkg/notifications/activity.go @@ -93,6 +93,13 @@ func parseRawActivityMetaData(activityType models.ActivityType, rawMetaDataJSON return nil, err } return &meta, nil + case models.ActivityNewDiscussionReply: + // Deserialize the raw JSON into NewDiscussionReplyActivityMeta + meta := models.NewDiscussionRepliedActivityMeta{} + if err := json.Unmarshal(rawData, &meta); err != nil { + return nil, err + } + return &meta, nil // Add cases for other ActivityTypes as needed diff --git a/pkg/notifications/new_discussion_replied_meta.go b/pkg/notifications/new_discussion_replied_meta.go new file mode 100644 index 0000000000..0b36cc2847 --- /dev/null +++ b/pkg/notifications/new_discussion_replied_meta.go @@ -0,0 +1,23 @@ +package notifications + +import ( + "context" + + "github.com/google/uuid" + + "github.com/cmsgov/mint-app/pkg/models" + "github.com/cmsgov/mint-app/pkg/sqlutils" +) + +// ActivityNewDiscussionRepliedCreate creates an activity for when a Discussion is replied to. +func ActivityNewDiscussionRepliedCreate(ctx context.Context, np sqlutils.NamedPreparer, actorID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, discussionContent models.TaggedHTML, getPreferencesFunc GetUserNotificationPreferencesFunc) (*models.Activity, error) { + + activity := models.NewNewDiscussionRepliedActivity(actorID, discussionID, replyID, discussionContent.RawContent.String()) + + retActivity, actErr := activityCreate(ctx, np, activity) + if actErr != nil { + return nil, actErr + } + + return retActivity, nil +} diff --git a/pkg/notifications/new_discussion_replied_meta_test.go b/pkg/notifications/new_discussion_replied_meta_test.go new file mode 100644 index 0000000000..81f21efa15 --- /dev/null +++ b/pkg/notifications/new_discussion_replied_meta_test.go @@ -0,0 +1,77 @@ +package notifications + +import ( + "context" + + "github.com/google/uuid" + + "github.com/cmsgov/mint-app/pkg/models" +) + +func (suite *NotificationsSuite) TestActivityNewDiscussionRepliedCreate() { + + tag1EUA := "SKZO" + tag1Principal, err := suite.testConfigs.GetTestPrincipal(suite.testConfigs.Store, tag1EUA) + suite.NoError(err) + tag1Label := "Alexander Stark" + tag1Type := models.TagTypeUserAccount + tag1 := `@` + tag1Label + `` + + tag2EUA := "FAKE" + tag2Principal, err := suite.testConfigs.GetTestPrincipal(suite.testConfigs.Store, tag2EUA) + suite.NoError(err) + tag2Label := "Terry Thompson" + tag2Type := models.TagTypeUserAccount + tag2 := `@` + tag2Label + `` + + tag3ID := "CONNECT" + tag3Label := "Salesforce CONNECT" + tag3Type := models.TagTypePossibleSolution + tag3 := `@` + tag3Label + `` + + htmlMention := `

Hey ` + tag1 + `! Will you be able to join the meeting next week? If not, can you contact ` + tag2 + ` to let them know?

We are planning on using the ` + tag3 + `solution.` + tag1 + tag1 + + // We have made a mention with 5 Mentions. This should only create 5 tags in the database + taggedContent, err := models.NewTaggedContentFromString(htmlMention) + suite.NoError(err) + + //Note: this will fail without properly updating the mentions to point to the DB. + // We can't test that here because it is part of the resolver package, which calls this package + input := models.TaggedHTML(taggedContent) + + // we are just choosing a valid UUID to set for the entityID + discussionID := uuid.New() + replyID := uuid.New() + actorID := suite.testConfigs.Principal.Account().ID + + mockFunc := func(ctx context.Context, user_id uuid.UUID) (*models.UserNotificationPreferences, error) { + // Return mock data, all notifications enabled + return models.NewUserNotificationPreferences(user_id), nil + } + + testActivity, err := ActivityNewDiscussionRepliedCreate(suite.testConfigs.Context, suite.testConfigs.Store, actorID, discussionID, replyID, input, mockFunc) + + suite.NoError(err) + suite.NotNil(testActivity) + suite.EqualValues(models.ActivityNewDiscussionReply, testActivity.ActivityType) + // Assert meta data is not deserialized here + suite.Nil(testActivity.MetaData) + //Assert meta data can be deserialized + suite.NotNil(testActivity.MetaDataRaw) + meta, err := parseRawActivityMetaData(testActivity.ActivityType, testActivity.MetaDataRaw) + suite.NoError(err) + suite.NotNil(meta) + + actorNots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Principal) + suite.NoError(err) + suite.EqualValues(0, actorNots.NumUnreadNotifications()) + + tag1Nots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, tag1Principal) + suite.NoError(err) + suite.EqualValues(1, tag1Nots.NumUnreadNotifications()) + + tag2Nots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, tag2Principal) + suite.NoError(err) + suite.EqualValues(1, tag2Nots.NumUnreadNotifications()) + +} diff --git a/src/gql/gen/graphql.ts b/src/gql/gen/graphql.ts index b5c9bed5a1..111e323719 100644 --- a/src/gql/gen/graphql.ts +++ b/src/gql/gen/graphql.ts @@ -61,7 +61,7 @@ export type ActivityMetaBaseStruct = { }; /** ActivityMetaData is a type that represents all the data that can be captured in an Activity */ -export type ActivityMetaData = ActivityMetaBaseStruct | TaggedInDiscussionReplyActivityMeta | TaggedInPlanDiscussionActivityMeta; +export type ActivityMetaData = ActivityMetaBaseStruct | NewDiscussionRepliedActivityMeta | TaggedInDiscussionReplyActivityMeta | TaggedInPlanDiscussionActivityMeta; /** ActivityType represents the possible activities that happen in application that might result in a notification */ export enum ActivityType { @@ -915,6 +915,17 @@ export type NdaInfo = { agreedDts?: Maybe; }; +export type NewDiscussionRepliedActivityMeta = { + __typename: 'NewDiscussionRepliedActivityMeta'; + content: Scalars['String']['output']; + discussion: PlanDiscussion; + discussionID: Scalars['UUID']['output']; + reply: DiscussionReply; + replyID: Scalars['UUID']['output']; + type: ActivityType; + version: Scalars['Int']['output']; +}; + export enum NonClaimsBasedPayType { ADVANCED_PAYMENT = 'ADVANCED_PAYMENT', BUNDLED_EPISODE_OF_CARE = 'BUNDLED_EPISODE_OF_CARE', From 0aa147b0093daeba2513b972b1f2e2457fbe97c5 Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Tue, 12 Mar 2024 15:10:46 -0400 Subject: [PATCH 02/13] wip: implementing PR feedback --- pkg/graph/generated/generated.go | 237 ++++++++++++++++++++++ pkg/graph/resolvers/activity.resolvers.go | 12 ++ pkg/graph/resolvers/plan_discussion.go | 84 ++++---- pkg/graph/schema/types/activity.graphql | 2 + src/gql/gen/graphql.ts | 2 + 5 files changed, 299 insertions(+), 38 deletions(-) diff --git a/pkg/graph/generated/generated.go b/pkg/graph/generated/generated.go index c0cbd6fecd..e993bf1782 100644 --- a/pkg/graph/generated/generated.go +++ b/pkg/graph/generated/generated.go @@ -275,6 +275,8 @@ type ComplexityRoot struct { Content func(childComplexity int) int Discussion func(childComplexity int) int DiscussionID func(childComplexity int) int + ModelPlan func(childComplexity int) int + ModelPlanID func(childComplexity int) int Reply func(childComplexity int) int ReplyID func(childComplexity int) int Type func(childComplexity int) int @@ -1190,6 +1192,9 @@ type MutationResolver interface { UpdateUserNotificationPreferences(ctx context.Context, changes map[string]interface{}) (*models.UserNotificationPreferences, error) } type NewDiscussionRepliedActivityMetaResolver interface { + ModelPlanID(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (uuid.UUID, error) + ModelPlan(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.ModelPlan, error) + Discussion(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.PlanDiscussion, error) Reply(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.DiscussionReply, error) @@ -2718,6 +2723,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.NewDiscussionRepliedActivityMeta.DiscussionID(childComplexity), true + case "NewDiscussionRepliedActivityMeta.modelPlan": + if e.complexity.NewDiscussionRepliedActivityMeta.ModelPlan == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.ModelPlan(childComplexity), true + + case "NewDiscussionRepliedActivityMeta.modelPlanID": + if e.complexity.NewDiscussionRepliedActivityMeta.ModelPlanID == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.ModelPlanID(childComplexity), true + case "NewDiscussionRepliedActivityMeta.reply": if e.complexity.NewDiscussionRepliedActivityMeta.Reply == nil { break @@ -9442,6 +9461,8 @@ type TaggedInDiscussionReplyActivityMeta { type NewDiscussionRepliedActivityMeta { version: Int! type: ActivityType! + modelPlanID: UUID! + modelPlan: ModelPlan! discussionID: UUID! discussion: PlanDiscussion! replyID: UUID! @@ -23548,6 +23569,150 @@ func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_type(c return fc, nil } +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_modelPlanID(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_modelPlanID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.NewDiscussionRepliedActivityMeta().ModelPlanID(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_modelPlanID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type UUID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_modelPlan(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_modelPlan(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.NewDiscussionRepliedActivityMeta().ModelPlan(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.ModelPlan) + fc.Result = res + return ec.marshalNModelPlan2ᚖgithubᚗcomᚋcmsgovᚋmintᚑappᚋpkgᚋmodelsᚐModelPlan(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_modelPlan(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ModelPlan_id(ctx, field) + case "modelName": + return ec.fieldContext_ModelPlan_modelName(ctx, field) + case "abbreviation": + return ec.fieldContext_ModelPlan_abbreviation(ctx, field) + case "archived": + return ec.fieldContext_ModelPlan_archived(ctx, field) + case "createdBy": + return ec.fieldContext_ModelPlan_createdBy(ctx, field) + case "createdByUserAccount": + return ec.fieldContext_ModelPlan_createdByUserAccount(ctx, field) + case "createdDts": + return ec.fieldContext_ModelPlan_createdDts(ctx, field) + case "modifiedBy": + return ec.fieldContext_ModelPlan_modifiedBy(ctx, field) + case "modifiedByUserAccount": + return ec.fieldContext_ModelPlan_modifiedByUserAccount(ctx, field) + case "modifiedDts": + return ec.fieldContext_ModelPlan_modifiedDts(ctx, field) + case "basics": + return ec.fieldContext_ModelPlan_basics(ctx, field) + case "generalCharacteristics": + return ec.fieldContext_ModelPlan_generalCharacteristics(ctx, field) + case "participantsAndProviders": + return ec.fieldContext_ModelPlan_participantsAndProviders(ctx, field) + case "beneficiaries": + return ec.fieldContext_ModelPlan_beneficiaries(ctx, field) + case "opsEvalAndLearning": + return ec.fieldContext_ModelPlan_opsEvalAndLearning(ctx, field) + case "collaborators": + return ec.fieldContext_ModelPlan_collaborators(ctx, field) + case "documents": + return ec.fieldContext_ModelPlan_documents(ctx, field) + case "discussions": + return ec.fieldContext_ModelPlan_discussions(ctx, field) + case "payments": + return ec.fieldContext_ModelPlan_payments(ctx, field) + case "status": + return ec.fieldContext_ModelPlan_status(ctx, field) + case "isFavorite": + return ec.fieldContext_ModelPlan_isFavorite(ctx, field) + case "isCollaborator": + return ec.fieldContext_ModelPlan_isCollaborator(ctx, field) + case "crs": + return ec.fieldContext_ModelPlan_crs(ctx, field) + case "tdls": + return ec.fieldContext_ModelPlan_tdls(ctx, field) + case "prepareForClearance": + return ec.fieldContext_ModelPlan_prepareForClearance(ctx, field) + case "nameHistory": + return ec.fieldContext_ModelPlan_nameHistory(ctx, field) + case "operationalNeeds": + return ec.fieldContext_ModelPlan_operationalNeeds(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ModelPlan", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _NewDiscussionRepliedActivityMeta_discussionID(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_discussionID(ctx, field) if err != nil { @@ -62633,6 +62798,78 @@ func (ec *executionContext) _NewDiscussionRepliedActivityMeta(ctx context.Contex if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "modelPlanID": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._NewDiscussionRepliedActivityMeta_modelPlanID(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "modelPlan": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._NewDiscussionRepliedActivityMeta_modelPlan(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "discussionID": out.Values[i] = ec._NewDiscussionRepliedActivityMeta_discussionID(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/pkg/graph/resolvers/activity.resolvers.go b/pkg/graph/resolvers/activity.resolvers.go index 4c89512241..6d8026ee42 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -8,6 +8,8 @@ import ( "context" "fmt" + "github.com/google/uuid" + "github.com/cmsgov/mint-app/pkg/appcontext" "github.com/cmsgov/mint-app/pkg/authentication" "github.com/cmsgov/mint-app/pkg/graph/generated" @@ -19,6 +21,16 @@ func (r *activityResolver) ActorUserAccount(ctx context.Context, obj *models.Act return UserAccountGetByIDLOADER(ctx, obj.ActorID) } +// ModelPlanID is the resolver for the modelPlanID field. +func (r *newDiscussionRepliedActivityMetaResolver) ModelPlanID(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (uuid.UUID, error) { + panic(fmt.Errorf("not implemented: ModelPlanID - modelPlanID")) +} + +// ModelPlan is the resolver for the modelPlan field. +func (r *newDiscussionRepliedActivityMetaResolver) ModelPlan(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.ModelPlan, error) { + panic(fmt.Errorf("not implemented: ModelPlan - modelPlan")) +} + // Discussion is the resolver for the discussion field. func (r *newDiscussionRepliedActivityMetaResolver) Discussion(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.PlanDiscussion, error) { panic(fmt.Errorf("not implemented: Discussion - discussion")) diff --git a/pkg/graph/resolvers/plan_discussion.go b/pkg/graph/resolvers/plan_discussion.go index 066d8c5e0a..25fde92b6b 100644 --- a/pkg/graph/resolvers/plan_discussion.go +++ b/pkg/graph/resolvers/plan_discussion.go @@ -456,18 +456,22 @@ func CreateDiscussionReply( } // Create Activity and notifications in the DB - _, notificationErr := notifications.ActivityTaggedInDiscussionReplyCreate(ctx, tx, principal.Account().ID, discussion.ID, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) + _, notificationErr := notifications.ActivityTaggedInDiscussionReplyCreate(ctx, tx, discussion.CreatedBy, discussion.ID, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) if notificationErr != nil { return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) } - pref, err := UserNotificationPreferencesGetByUserID(ctx, principal.Account().ID) + _, err = notifications.ActivityNewDiscussionRepliedCreate(ctx, tx, discussion.CreatedBy, discussion.ID, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) + if notificationErr != nil { + return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) + } + + pref, err := UserNotificationPreferencesGetByUserID(ctx, discussion.CreatedBy) if err != nil { return nil, fmt.Errorf("unable to get user notification preference, Notification not created %w", err) } - // Early return if - if !pref.NewDiscussionReply.SendEmail() { + if pref.NewDiscussionReply.SendEmail() || pref.TaggedInDiscussionReply.SendEmail() { go func() { replyUser := principal.Account() commonName := replyUser.CommonName @@ -478,44 +482,48 @@ func CreateDiscussionReply( zap.Error(err)) } - errReplyEmail := sendDiscussionReplyEmails( - ctx, - store, - logger, - emailService, - emailTemplateService, - addressBook, - discussion, - reply, - modelPlan, - replyUser, - ) - if errReplyEmail != nil { - logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", - zap.String("discussionID", discussion.ID.String()), - zap.Error(errReplyEmail)) + if pref.NewDiscussionReply.SendEmail() { + errReplyEmail := sendDiscussionReplyEmails( + ctx, + store, + logger, + emailService, + emailTemplateService, + addressBook, + discussion, + reply, + modelPlan, + replyUser, + ) + if errReplyEmail != nil { + logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", + zap.String("discussionID", discussion.ID.String()), + zap.Error(errReplyEmail)) + } } - err = sendPlanDiscussionTagEmails( - ctx, - store, - false, - logger, - emailService, - emailTemplateService, - addressBook, - reply.Content, - reply.DiscussionID, - modelPlan, - commonName, - reply.UserRole.Humanize(models.ValueOrEmpty(reply.UserRoleDescription)), - ) + if pref.TaggedInDiscussion.SendEmail() { + err = sendPlanDiscussionTagEmails( + ctx, + store, + false, + logger, + emailService, + emailTemplateService, + addressBook, + reply.Content, + reply.DiscussionID, + modelPlan, + commonName, + reply.UserRole.Humanize(models.ValueOrEmpty(reply.UserRoleDescription)), + ) - if err != nil { if err != nil { - logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", - zap.String("discussionID", discussion.ID.String()), - zap.Error(err)) + if err != nil { + logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", + zap.String("discussionID", discussion.ID.String()), + zap.Error(err)) + } } } }() diff --git a/pkg/graph/schema/types/activity.graphql b/pkg/graph/schema/types/activity.graphql index 726a03b169..595f4b8532 100644 --- a/pkg/graph/schema/types/activity.graphql +++ b/pkg/graph/schema/types/activity.graphql @@ -36,6 +36,8 @@ type TaggedInDiscussionReplyActivityMeta { type NewDiscussionRepliedActivityMeta { version: Int! type: ActivityType! + modelPlanID: UUID! + modelPlan: ModelPlan! discussionID: UUID! discussion: PlanDiscussion! replyID: UUID! diff --git a/src/gql/gen/graphql.ts b/src/gql/gen/graphql.ts index 111e323719..b90fa6a6e0 100644 --- a/src/gql/gen/graphql.ts +++ b/src/gql/gen/graphql.ts @@ -920,6 +920,8 @@ export type NewDiscussionRepliedActivityMeta = { content: Scalars['String']['output']; discussion: PlanDiscussion; discussionID: Scalars['UUID']['output']; + modelPlan: ModelPlan; + modelPlanID: Scalars['UUID']['output']; reply: DiscussionReply; replyID: Scalars['UUID']['output']; type: ActivityType; From 7c03b21871d7101fa2648a15378446bd260fefb4 Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Wed, 13 Mar 2024 12:57:01 -0400 Subject: [PATCH 03/13] wip: implement various PR feedback --- pkg/graph/generated/generated.go | 44 +++---------------- pkg/graph/resolvers/activity.resolvers.go | 24 ++++++---- pkg/graph/resolvers/plan_discussion.go | 2 +- pkg/models/new_discussion_replied_meta.go | 8 ++-- .../new_discussion_replied_meta.go | 15 ++++++- .../new_discussion_replied_meta_test.go | 37 ++-------------- 6 files changed, 44 insertions(+), 86 deletions(-) diff --git a/pkg/graph/generated/generated.go b/pkg/graph/generated/generated.go index e993bf1782..83d097d328 100644 --- a/pkg/graph/generated/generated.go +++ b/pkg/graph/generated/generated.go @@ -1192,7 +1192,6 @@ type MutationResolver interface { UpdateUserNotificationPreferences(ctx context.Context, changes map[string]interface{}) (*models.UserNotificationPreferences, error) } type NewDiscussionRepliedActivityMetaResolver interface { - ModelPlanID(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (uuid.UUID, error) ModelPlan(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.ModelPlan, error) Discussion(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.PlanDiscussion, error) @@ -23583,7 +23582,7 @@ func (ec *executionContext) _NewDiscussionRepliedActivityMeta_modelPlanID(ctx co }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.NewDiscussionRepliedActivityMeta().ModelPlanID(rctx, obj) + return obj.ModelPlanID, nil }) if err != nil { ec.Error(ctx, err) @@ -23604,8 +23603,8 @@ func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_modelP fc = &graphql.FieldContext{ Object: "NewDiscussionRepliedActivityMeta", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type UUID does not have child fields") }, @@ -62799,41 +62798,10 @@ func (ec *executionContext) _NewDiscussionRepliedActivityMeta(ctx context.Contex atomic.AddUint32(&out.Invalids, 1) } case "modelPlanID": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._NewDiscussionRepliedActivityMeta_modelPlanID(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue + out.Values[i] = ec._NewDiscussionRepliedActivityMeta_modelPlanID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "modelPlan": field := field diff --git a/pkg/graph/resolvers/activity.resolvers.go b/pkg/graph/resolvers/activity.resolvers.go index 6d8026ee42..550556fa60 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -6,7 +6,6 @@ package resolvers import ( "context" - "fmt" "github.com/google/uuid" @@ -21,24 +20,21 @@ func (r *activityResolver) ActorUserAccount(ctx context.Context, obj *models.Act return UserAccountGetByIDLOADER(ctx, obj.ActorID) } -// ModelPlanID is the resolver for the modelPlanID field. -func (r *newDiscussionRepliedActivityMetaResolver) ModelPlanID(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (uuid.UUID, error) { - panic(fmt.Errorf("not implemented: ModelPlanID - modelPlanID")) -} - // ModelPlan is the resolver for the modelPlan field. func (r *newDiscussionRepliedActivityMetaResolver) ModelPlan(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.ModelPlan, error) { - panic(fmt.Errorf("not implemented: ModelPlan - modelPlan")) + return ModelPlanGetByIDLOADER(ctx, obj.ModelPlanID) } // Discussion is the resolver for the discussion field. func (r *newDiscussionRepliedActivityMetaResolver) Discussion(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.PlanDiscussion, error) { - panic(fmt.Errorf("not implemented: Discussion - discussion")) + logger := appcontext.ZLogger(ctx) + return PlanDiscussionGetByID(ctx, r.store, logger, obj.DiscussionID) } // Reply is the resolver for the reply field. func (r *newDiscussionRepliedActivityMetaResolver) Reply(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (*models.DiscussionReply, error) { - panic(fmt.Errorf("not implemented: Reply - reply")) + logger := appcontext.ZLogger(ctx) + return DiscussionReplyGetByID(ctx, r.store, logger, obj.ReplyID) } // Discussion is the resolver for the discussion field. @@ -81,3 +77,13 @@ type activityResolver struct{ *Resolver } type newDiscussionRepliedActivityMetaResolver struct{ *Resolver } type taggedInDiscussionReplyActivityMetaResolver struct{ *Resolver } type taggedInPlanDiscussionActivityMetaResolver struct{ *Resolver } + +// !!! WARNING !!! +// The code below was going to be deleted when updating resolvers. It has been copied here so you have +// one last chance to move it out of harms way if you want. There are two reasons this happens: +// - When renaming or deleting a resolver the old code will be put in here. You can safely delete +// it when you're done. +// - You have helper methods in this file. Move them out to keep these resolver files clean. +func (r *newDiscussionRepliedActivityMetaResolver) ModelPlanID(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (uuid.UUID, error) { + return obj.ModelPlanID, nil +} diff --git a/pkg/graph/resolvers/plan_discussion.go b/pkg/graph/resolvers/plan_discussion.go index 25fde92b6b..7ca61f69b2 100644 --- a/pkg/graph/resolvers/plan_discussion.go +++ b/pkg/graph/resolvers/plan_discussion.go @@ -461,7 +461,7 @@ func CreateDiscussionReply( return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) } - _, err = notifications.ActivityNewDiscussionRepliedCreate(ctx, tx, discussion.CreatedBy, discussion.ID, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) + _, err = notifications.ActivityNewDiscussionRepliedCreate(ctx, tx, discussion.CreatedBy, discussion.ModelPlanID, discussion.ID, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) if notificationErr != nil { return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) } diff --git a/pkg/models/new_discussion_replied_meta.go b/pkg/models/new_discussion_replied_meta.go index 068f54db61..66cc9ca24d 100644 --- a/pkg/models/new_discussion_replied_meta.go +++ b/pkg/models/new_discussion_replied_meta.go @@ -12,16 +12,18 @@ import ( type NewDiscussionRepliedActivityMeta struct { ActivityMetaBaseStruct discussionRelation + modelPlanRelation ReplyID uuid.UUID `json:"replyID"` Content string `json:"content"` } // newNewDiscussionRepliedActivityMeta creates a New NewDiscussionRepliedActivityMeta -func newNewDiscussionRepliedActivityMeta(discussionID uuid.UUID, replyID uuid.UUID, content string) *NewDiscussionRepliedActivityMeta { +func newNewDiscussionRepliedActivityMeta(modelPlanID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, content string) *NewDiscussionRepliedActivityMeta { version := 0 //iterate this if this type ever updates return &NewDiscussionRepliedActivityMeta{ ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityTaggedInDiscussionReply, version), discussionRelation: NewDiscussionRelation(discussionID), + modelPlanRelation: NewModelPlanRelation(modelPlanID), ReplyID: replyID, Content: content, } @@ -29,13 +31,13 @@ func newNewDiscussionRepliedActivityMeta(discussionID uuid.UUID, replyID uuid.UU } // NewNewDiscussionRepliedActivity creates a New New Discussion Replied type of Activity -func NewNewDiscussionRepliedActivity(actorID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, content string) *Activity { +func NewNewDiscussionRepliedActivity(actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, content string) *Activity { return &Activity{ baseStruct: NewBaseStruct(actorID), ActorID: actorID, EntityID: discussionID, ActivityType: ActivityTaggedInDiscussionReply, - MetaData: newNewDiscussionRepliedActivityMeta(discussionID, replyID, content), + MetaData: newNewDiscussionRepliedActivityMeta(modelPlanID, discussionID, replyID, content), } } diff --git a/pkg/notifications/new_discussion_replied_meta.go b/pkg/notifications/new_discussion_replied_meta.go index 0b36cc2847..9f54bd215d 100644 --- a/pkg/notifications/new_discussion_replied_meta.go +++ b/pkg/notifications/new_discussion_replied_meta.go @@ -2,6 +2,7 @@ package notifications import ( "context" + "fmt" "github.com/google/uuid" @@ -10,14 +11,24 @@ import ( ) // ActivityNewDiscussionRepliedCreate creates an activity for when a Discussion is replied to. -func ActivityNewDiscussionRepliedCreate(ctx context.Context, np sqlutils.NamedPreparer, actorID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, discussionContent models.TaggedHTML, getPreferencesFunc GetUserNotificationPreferencesFunc) (*models.Activity, error) { +func ActivityNewDiscussionRepliedCreate(ctx context.Context, np sqlutils.NamedPreparer, actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, discussionReplyContent models.TaggedHTML, getPreferencesFunc GetUserNotificationPreferencesFunc) (*models.Activity, error) { - activity := models.NewNewDiscussionRepliedActivity(actorID, discussionID, replyID, discussionContent.RawContent.String()) + activity := models.NewNewDiscussionRepliedActivity(actorID, modelPlanID, discussionID, replyID, discussionReplyContent.RawContent.String()) retActivity, actErr := activityCreate(ctx, np, activity) if actErr != nil { return nil, actErr } + pref, err := getPreferencesFunc(ctx, actorID) + if err != nil { + return nil, fmt.Errorf("unable to get user notification preference, Notification not created %w", err) + } + + _, err = userNotificationCreate(ctx, np, retActivity, actorID, pref.NewDiscussionReply) + if err != nil { + return nil, err + } + return retActivity, nil } diff --git a/pkg/notifications/new_discussion_replied_meta_test.go b/pkg/notifications/new_discussion_replied_meta_test.go index 81f21efa15..23c43dad38 100644 --- a/pkg/notifications/new_discussion_replied_meta_test.go +++ b/pkg/notifications/new_discussion_replied_meta_test.go @@ -9,30 +9,9 @@ import ( ) func (suite *NotificationsSuite) TestActivityNewDiscussionRepliedCreate() { + html := `

Hey there! Are you available for a quick sync on thjis issue? Thanks!

` - tag1EUA := "SKZO" - tag1Principal, err := suite.testConfigs.GetTestPrincipal(suite.testConfigs.Store, tag1EUA) - suite.NoError(err) - tag1Label := "Alexander Stark" - tag1Type := models.TagTypeUserAccount - tag1 := `@` + tag1Label + `` - - tag2EUA := "FAKE" - tag2Principal, err := suite.testConfigs.GetTestPrincipal(suite.testConfigs.Store, tag2EUA) - suite.NoError(err) - tag2Label := "Terry Thompson" - tag2Type := models.TagTypeUserAccount - tag2 := `@` + tag2Label + `` - - tag3ID := "CONNECT" - tag3Label := "Salesforce CONNECT" - tag3Type := models.TagTypePossibleSolution - tag3 := `@` + tag3Label + `` - - htmlMention := `

Hey ` + tag1 + `! Will you be able to join the meeting next week? If not, can you contact ` + tag2 + ` to let them know?

We are planning on using the ` + tag3 + `solution.` + tag1 + tag1 - - // We have made a mention with 5 Mentions. This should only create 5 tags in the database - taggedContent, err := models.NewTaggedContentFromString(htmlMention) + taggedContent, err := models.NewTaggedContentFromString(html) suite.NoError(err) //Note: this will fail without properly updating the mentions to point to the DB. @@ -40,6 +19,7 @@ func (suite *NotificationsSuite) TestActivityNewDiscussionRepliedCreate() { input := models.TaggedHTML(taggedContent) // we are just choosing a valid UUID to set for the entityID + modelPlanID := uuid.New() discussionID := uuid.New() replyID := uuid.New() actorID := suite.testConfigs.Principal.Account().ID @@ -49,7 +29,7 @@ func (suite *NotificationsSuite) TestActivityNewDiscussionRepliedCreate() { return models.NewUserNotificationPreferences(user_id), nil } - testActivity, err := ActivityNewDiscussionRepliedCreate(suite.testConfigs.Context, suite.testConfigs.Store, actorID, discussionID, replyID, input, mockFunc) + testActivity, err := ActivityNewDiscussionRepliedCreate(suite.testConfigs.Context, suite.testConfigs.Store, actorID, modelPlanID, discussionID, replyID, input, mockFunc) suite.NoError(err) suite.NotNil(testActivity) @@ -65,13 +45,4 @@ func (suite *NotificationsSuite) TestActivityNewDiscussionRepliedCreate() { actorNots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Principal) suite.NoError(err) suite.EqualValues(0, actorNots.NumUnreadNotifications()) - - tag1Nots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, tag1Principal) - suite.NoError(err) - suite.EqualValues(1, tag1Nots.NumUnreadNotifications()) - - tag2Nots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, tag2Principal) - suite.NoError(err) - suite.EqualValues(1, tag2Nots.NumUnreadNotifications()) - } From 27f21d5d6d1976024d674b091db4b59a3ea8fcfd Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Wed, 13 Mar 2024 13:17:40 -0400 Subject: [PATCH 04/13] wip: corrected actor ID to be reply creator ID and added discussion creator ID to meta --- pkg/graph/generated/generated.go | 76 ++++++++++++++++--- pkg/graph/resolvers/plan_discussion.go | 2 +- pkg/graph/schema/types/activity.graphql | 1 + pkg/models/new_discussion_replied_meta.go | 12 +-- .../new_discussion_replied_meta.go | 4 +- src/gql/gen/graphql.ts | 1 + 6 files changed, 79 insertions(+), 17 deletions(-) diff --git a/pkg/graph/generated/generated.go b/pkg/graph/generated/generated.go index 83d097d328..2994e9b102 100644 --- a/pkg/graph/generated/generated.go +++ b/pkg/graph/generated/generated.go @@ -272,15 +272,16 @@ type ComplexityRoot struct { } NewDiscussionRepliedActivityMeta struct { - Content func(childComplexity int) int - Discussion func(childComplexity int) int - DiscussionID func(childComplexity int) int - ModelPlan func(childComplexity int) int - ModelPlanID func(childComplexity int) int - Reply func(childComplexity int) int - ReplyID func(childComplexity int) int - Type func(childComplexity int) int - Version func(childComplexity int) int + Content func(childComplexity int) int + Discussion func(childComplexity int) int + DiscussionCreatorID func(childComplexity int) int + DiscussionID func(childComplexity int) int + ModelPlan func(childComplexity int) int + ModelPlanID func(childComplexity int) int + Reply func(childComplexity int) int + ReplyID func(childComplexity int) int + Type func(childComplexity int) int + Version func(childComplexity int) int } OperationalNeed struct { @@ -2715,6 +2716,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.NewDiscussionRepliedActivityMeta.Discussion(childComplexity), true + case "NewDiscussionRepliedActivityMeta.discussionCreatorID": + if e.complexity.NewDiscussionRepliedActivityMeta.DiscussionCreatorID == nil { + break + } + + return e.complexity.NewDiscussionRepliedActivityMeta.DiscussionCreatorID(childComplexity), true + case "NewDiscussionRepliedActivityMeta.discussionID": if e.complexity.NewDiscussionRepliedActivityMeta.DiscussionID == nil { break @@ -9463,6 +9471,7 @@ type NewDiscussionRepliedActivityMeta { modelPlanID: UUID! modelPlan: ModelPlan! discussionID: UUID! + discussionCreatorID: UUID! discussion: PlanDiscussion! replyID: UUID! reply: DiscussionReply! @@ -23756,6 +23765,50 @@ func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_discus return fc, nil } +func (ec *executionContext) _NewDiscussionRepliedActivityMeta_discussionCreatorID(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_discussionCreatorID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.DiscussionCreatorID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(uuid.UUID) + fc.Result = res + return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_discussionCreatorID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "NewDiscussionRepliedActivityMeta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type UUID does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _NewDiscussionRepliedActivityMeta_discussion(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_discussion(ctx, field) if err != nil { @@ -62843,6 +62896,11 @@ func (ec *executionContext) _NewDiscussionRepliedActivityMeta(ctx context.Contex if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "discussionCreatorID": + out.Values[i] = ec._NewDiscussionRepliedActivityMeta_discussionCreatorID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "discussion": field := field diff --git a/pkg/graph/resolvers/plan_discussion.go b/pkg/graph/resolvers/plan_discussion.go index 7ca61f69b2..088d0eacde 100644 --- a/pkg/graph/resolvers/plan_discussion.go +++ b/pkg/graph/resolvers/plan_discussion.go @@ -461,7 +461,7 @@ func CreateDiscussionReply( return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) } - _, err = notifications.ActivityNewDiscussionRepliedCreate(ctx, tx, discussion.CreatedBy, discussion.ModelPlanID, discussion.ID, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) + _, err = notifications.ActivityNewDiscussionRepliedCreate(ctx, tx, reply.CreatedBy, discussion.ModelPlanID, discussion.ID, discussion.CreatedBy, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) if notificationErr != nil { return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) } diff --git a/pkg/graph/schema/types/activity.graphql b/pkg/graph/schema/types/activity.graphql index 595f4b8532..ff4bc09bd2 100644 --- a/pkg/graph/schema/types/activity.graphql +++ b/pkg/graph/schema/types/activity.graphql @@ -39,6 +39,7 @@ type NewDiscussionRepliedActivityMeta { modelPlanID: UUID! modelPlan: ModelPlan! discussionID: UUID! + discussionCreatorID: UUID! discussion: PlanDiscussion! replyID: UUID! reply: DiscussionReply! diff --git a/pkg/models/new_discussion_replied_meta.go b/pkg/models/new_discussion_replied_meta.go index 66cc9ca24d..c99298b69a 100644 --- a/pkg/models/new_discussion_replied_meta.go +++ b/pkg/models/new_discussion_replied_meta.go @@ -13,17 +13,19 @@ type NewDiscussionRepliedActivityMeta struct { ActivityMetaBaseStruct discussionRelation modelPlanRelation - ReplyID uuid.UUID `json:"replyID"` - Content string `json:"content"` + DiscussionCreatorID uuid.UUID `json:"discussionCreatorID"` + ReplyID uuid.UUID `json:"replyID"` + Content string `json:"content"` } // newNewDiscussionRepliedActivityMeta creates a New NewDiscussionRepliedActivityMeta -func newNewDiscussionRepliedActivityMeta(modelPlanID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, content string) *NewDiscussionRepliedActivityMeta { +func newNewDiscussionRepliedActivityMeta(modelPlanID uuid.UUID, discussionID uuid.UUID, discussionCreatorID uuid.UUID, replyID uuid.UUID, content string) *NewDiscussionRepliedActivityMeta { version := 0 //iterate this if this type ever updates return &NewDiscussionRepliedActivityMeta{ ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityTaggedInDiscussionReply, version), discussionRelation: NewDiscussionRelation(discussionID), modelPlanRelation: NewModelPlanRelation(modelPlanID), + DiscussionCreatorID: discussionCreatorID, ReplyID: replyID, Content: content, } @@ -31,13 +33,13 @@ func newNewDiscussionRepliedActivityMeta(modelPlanID uuid.UUID, discussionID uui } // NewNewDiscussionRepliedActivity creates a New New Discussion Replied type of Activity -func NewNewDiscussionRepliedActivity(actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, content string) *Activity { +func NewNewDiscussionRepliedActivity(actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, discussionCreatorID uuid.UUID, replyID uuid.UUID, content string) *Activity { return &Activity{ baseStruct: NewBaseStruct(actorID), ActorID: actorID, EntityID: discussionID, ActivityType: ActivityTaggedInDiscussionReply, - MetaData: newNewDiscussionRepliedActivityMeta(modelPlanID, discussionID, replyID, content), + MetaData: newNewDiscussionRepliedActivityMeta(modelPlanID, discussionID, discussionCreatorID, replyID, content), } } diff --git a/pkg/notifications/new_discussion_replied_meta.go b/pkg/notifications/new_discussion_replied_meta.go index 9f54bd215d..dec9e95db9 100644 --- a/pkg/notifications/new_discussion_replied_meta.go +++ b/pkg/notifications/new_discussion_replied_meta.go @@ -11,9 +11,9 @@ import ( ) // ActivityNewDiscussionRepliedCreate creates an activity for when a Discussion is replied to. -func ActivityNewDiscussionRepliedCreate(ctx context.Context, np sqlutils.NamedPreparer, actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, discussionReplyContent models.TaggedHTML, getPreferencesFunc GetUserNotificationPreferencesFunc) (*models.Activity, error) { +func ActivityNewDiscussionRepliedCreate(ctx context.Context, np sqlutils.NamedPreparer, actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, discussionCreatorID uuid.UUID, replyID uuid.UUID, discussionReplyContent models.TaggedHTML, getPreferencesFunc GetUserNotificationPreferencesFunc) (*models.Activity, error) { - activity := models.NewNewDiscussionRepliedActivity(actorID, modelPlanID, discussionID, replyID, discussionReplyContent.RawContent.String()) + activity := models.NewNewDiscussionRepliedActivity(actorID, modelPlanID, discussionID, discussionCreatorID, replyID, discussionReplyContent.RawContent.String()) retActivity, actErr := activityCreate(ctx, np, activity) if actErr != nil { diff --git a/src/gql/gen/graphql.ts b/src/gql/gen/graphql.ts index b90fa6a6e0..0659805fb6 100644 --- a/src/gql/gen/graphql.ts +++ b/src/gql/gen/graphql.ts @@ -919,6 +919,7 @@ export type NewDiscussionRepliedActivityMeta = { __typename: 'NewDiscussionRepliedActivityMeta'; content: Scalars['String']['output']; discussion: PlanDiscussion; + discussionCreatorID: Scalars['UUID']['output']; discussionID: Scalars['UUID']['output']; modelPlan: ModelPlan; modelPlanID: Scalars['UUID']['output']; From e749625371adb757435308a954a33aacc24a0e91 Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Wed, 13 Mar 2024 13:39:27 -0400 Subject: [PATCH 05/13] fix: corrected notification recipient from actor to discussion creator --- pkg/notifications/new_discussion_replied_meta.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/notifications/new_discussion_replied_meta.go b/pkg/notifications/new_discussion_replied_meta.go index dec9e95db9..1141d9ac1a 100644 --- a/pkg/notifications/new_discussion_replied_meta.go +++ b/pkg/notifications/new_discussion_replied_meta.go @@ -25,7 +25,7 @@ func ActivityNewDiscussionRepliedCreate(ctx context.Context, np sqlutils.NamedPr return nil, fmt.Errorf("unable to get user notification preference, Notification not created %w", err) } - _, err = userNotificationCreate(ctx, np, retActivity, actorID, pref.NewDiscussionReply) + _, err = userNotificationCreate(ctx, np, retActivity, discussionCreatorID, pref.NewDiscussionReply) if err != nil { return nil, err } From e89ad21ee5ae2b676d8c573f75605e1147dc36e7 Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Wed, 13 Mar 2024 17:34:51 -0400 Subject: [PATCH 06/13] feat: testing preview for discussion reply notification --- pkg/graph/generated/generated.go | 76 ++---------- pkg/graph/resolvers/plan_discussion.go | 110 +++++++++--------- pkg/graph/schema/types/activity.graphql | 1 - pkg/models/new_discussion_replied_meta.go | 14 +-- .../new_discussion_replied_meta.go | 12 +- .../new_discussion_replied_meta_test.go | 37 +++--- pkg/notifications/notifications_test.go | 13 +++ src/gql/gen/graphql.ts | 1 - 8 files changed, 102 insertions(+), 162 deletions(-) diff --git a/pkg/graph/generated/generated.go b/pkg/graph/generated/generated.go index 2994e9b102..83d097d328 100644 --- a/pkg/graph/generated/generated.go +++ b/pkg/graph/generated/generated.go @@ -272,16 +272,15 @@ type ComplexityRoot struct { } NewDiscussionRepliedActivityMeta struct { - Content func(childComplexity int) int - Discussion func(childComplexity int) int - DiscussionCreatorID func(childComplexity int) int - DiscussionID func(childComplexity int) int - ModelPlan func(childComplexity int) int - ModelPlanID func(childComplexity int) int - Reply func(childComplexity int) int - ReplyID func(childComplexity int) int - Type func(childComplexity int) int - Version func(childComplexity int) int + Content func(childComplexity int) int + Discussion func(childComplexity int) int + DiscussionID func(childComplexity int) int + ModelPlan func(childComplexity int) int + ModelPlanID func(childComplexity int) int + Reply func(childComplexity int) int + ReplyID func(childComplexity int) int + Type func(childComplexity int) int + Version func(childComplexity int) int } OperationalNeed struct { @@ -2716,13 +2715,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.NewDiscussionRepliedActivityMeta.Discussion(childComplexity), true - case "NewDiscussionRepliedActivityMeta.discussionCreatorID": - if e.complexity.NewDiscussionRepliedActivityMeta.DiscussionCreatorID == nil { - break - } - - return e.complexity.NewDiscussionRepliedActivityMeta.DiscussionCreatorID(childComplexity), true - case "NewDiscussionRepliedActivityMeta.discussionID": if e.complexity.NewDiscussionRepliedActivityMeta.DiscussionID == nil { break @@ -9471,7 +9463,6 @@ type NewDiscussionRepliedActivityMeta { modelPlanID: UUID! modelPlan: ModelPlan! discussionID: UUID! - discussionCreatorID: UUID! discussion: PlanDiscussion! replyID: UUID! reply: DiscussionReply! @@ -23765,50 +23756,6 @@ func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_discus return fc, nil } -func (ec *executionContext) _NewDiscussionRepliedActivityMeta_discussionCreatorID(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_discussionCreatorID(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.DiscussionCreatorID, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(uuid.UUID) - fc.Result = res - return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_NewDiscussionRepliedActivityMeta_discussionCreatorID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "NewDiscussionRepliedActivityMeta", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type UUID does not have child fields") - }, - } - return fc, nil -} - func (ec *executionContext) _NewDiscussionRepliedActivityMeta_discussion(ctx context.Context, field graphql.CollectedField, obj *models.NewDiscussionRepliedActivityMeta) (ret graphql.Marshaler) { fc, err := ec.fieldContext_NewDiscussionRepliedActivityMeta_discussion(ctx, field) if err != nil { @@ -62896,11 +62843,6 @@ func (ec *executionContext) _NewDiscussionRepliedActivityMeta(ctx context.Contex if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } - case "discussionCreatorID": - out.Values[i] = ec._NewDiscussionRepliedActivityMeta_discussionCreatorID(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } case "discussion": field := field diff --git a/pkg/graph/resolvers/plan_discussion.go b/pkg/graph/resolvers/plan_discussion.go index 088d0eacde..17a331f4b8 100644 --- a/pkg/graph/resolvers/plan_discussion.go +++ b/pkg/graph/resolvers/plan_discussion.go @@ -461,73 +461,69 @@ func CreateDiscussionReply( return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) } - _, err = notifications.ActivityNewDiscussionRepliedCreate(ctx, tx, reply.CreatedBy, discussion.ModelPlanID, discussion.ID, discussion.CreatedBy, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) + discussionCreatorPref, err := UserNotificationPreferencesGetByUserID(ctx, discussion.CreatedBy) + if err != nil { + return nil, fmt.Errorf("unable to get user notification preference, Notification not created %w", err) + } + + _, err = notifications.ActivityNewDiscussionRepliedCreate(ctx, tx, reply.CreatedBy, discussion.ModelPlanID, discussion.ID, discussion.CreatedBy, reply.ID, reply.Content, discussionCreatorPref) if notificationErr != nil { return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) } - pref, err := UserNotificationPreferencesGetByUserID(ctx, discussion.CreatedBy) - if err != nil { - return nil, fmt.Errorf("unable to get user notification preference, Notification not created %w", err) - } + go func() { + replyUser := principal.Account() + commonName := replyUser.CommonName - if pref.NewDiscussionReply.SendEmail() || pref.TaggedInDiscussionReply.SendEmail() { - go func() { - replyUser := principal.Account() - commonName := replyUser.CommonName + if err != nil { + logger.Error("error sending discussion reply emails. Unable to retrieve modelPlan", + zap.String("replyID", reply.ID.String()), + zap.Error(err)) + } - if err != nil { - logger.Error("error sending discussion reply emails. Unable to retrieve modelPlan", - zap.String("replyID", reply.ID.String()), - zap.Error(err)) + if discussionCreatorPref.NewDiscussionReply.SendEmail() { + errReplyEmail := sendDiscussionReplyEmails( + ctx, + store, + logger, + emailService, + emailTemplateService, + addressBook, + discussion, + reply, + modelPlan, + replyUser, + ) + if errReplyEmail != nil { + logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", + zap.String("discussionID", discussion.ID.String()), + zap.Error(errReplyEmail)) } + } - if pref.NewDiscussionReply.SendEmail() { - errReplyEmail := sendDiscussionReplyEmails( - ctx, - store, - logger, - emailService, - emailTemplateService, - addressBook, - discussion, - reply, - modelPlan, - replyUser, - ) - if errReplyEmail != nil { - logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", - zap.String("discussionID", discussion.ID.String()), - zap.Error(errReplyEmail)) - } - } + err = sendPlanDiscussionTagEmails( + ctx, + store, + false, + logger, + emailService, + emailTemplateService, + addressBook, + reply.Content, + reply.DiscussionID, + modelPlan, + commonName, + reply.UserRole.Humanize(models.ValueOrEmpty(reply.UserRoleDescription)), + ) - if pref.TaggedInDiscussion.SendEmail() { - err = sendPlanDiscussionTagEmails( - ctx, - store, - false, - logger, - emailService, - emailTemplateService, - addressBook, - reply.Content, - reply.DiscussionID, - modelPlan, - commonName, - reply.UserRole.Humanize(models.ValueOrEmpty(reply.UserRoleDescription)), - ) - - if err != nil { - if err != nil { - logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", - zap.String("discussionID", discussion.ID.String()), - zap.Error(err)) - } - } + if err != nil { + if err != nil { + logger.Error("error sending tagged in plan discussion reply emails to tagged users and teams", + zap.String("discussionID", discussion.ID.String()), + zap.Error(err)) } - }() - } + } + }() return reply, err }) diff --git a/pkg/graph/schema/types/activity.graphql b/pkg/graph/schema/types/activity.graphql index ff4bc09bd2..595f4b8532 100644 --- a/pkg/graph/schema/types/activity.graphql +++ b/pkg/graph/schema/types/activity.graphql @@ -39,7 +39,6 @@ type NewDiscussionRepliedActivityMeta { modelPlanID: UUID! modelPlan: ModelPlan! discussionID: UUID! - discussionCreatorID: UUID! discussion: PlanDiscussion! replyID: UUID! reply: DiscussionReply! diff --git a/pkg/models/new_discussion_replied_meta.go b/pkg/models/new_discussion_replied_meta.go index c99298b69a..c4ad4016d2 100644 --- a/pkg/models/new_discussion_replied_meta.go +++ b/pkg/models/new_discussion_replied_meta.go @@ -13,19 +13,17 @@ type NewDiscussionRepliedActivityMeta struct { ActivityMetaBaseStruct discussionRelation modelPlanRelation - DiscussionCreatorID uuid.UUID `json:"discussionCreatorID"` - ReplyID uuid.UUID `json:"replyID"` - Content string `json:"content"` + ReplyID uuid.UUID `json:"replyID"` + Content string `json:"content"` } // newNewDiscussionRepliedActivityMeta creates a New NewDiscussionRepliedActivityMeta -func newNewDiscussionRepliedActivityMeta(modelPlanID uuid.UUID, discussionID uuid.UUID, discussionCreatorID uuid.UUID, replyID uuid.UUID, content string) *NewDiscussionRepliedActivityMeta { +func newNewDiscussionRepliedActivityMeta(modelPlanID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, content string) *NewDiscussionRepliedActivityMeta { version := 0 //iterate this if this type ever updates return &NewDiscussionRepliedActivityMeta{ ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityTaggedInDiscussionReply, version), discussionRelation: NewDiscussionRelation(discussionID), modelPlanRelation: NewModelPlanRelation(modelPlanID), - DiscussionCreatorID: discussionCreatorID, ReplyID: replyID, Content: content, } @@ -33,13 +31,13 @@ func newNewDiscussionRepliedActivityMeta(modelPlanID uuid.UUID, discussionID uui } // NewNewDiscussionRepliedActivity creates a New New Discussion Replied type of Activity -func NewNewDiscussionRepliedActivity(actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, discussionCreatorID uuid.UUID, replyID uuid.UUID, content string) *Activity { +func NewNewDiscussionRepliedActivity(actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, content string) *Activity { return &Activity{ baseStruct: NewBaseStruct(actorID), ActorID: actorID, EntityID: discussionID, - ActivityType: ActivityTaggedInDiscussionReply, - MetaData: newNewDiscussionRepliedActivityMeta(modelPlanID, discussionID, discussionCreatorID, replyID, content), + ActivityType: ActivityNewDiscussionReply, + MetaData: newNewDiscussionRepliedActivityMeta(modelPlanID, discussionID, replyID, content), } } diff --git a/pkg/notifications/new_discussion_replied_meta.go b/pkg/notifications/new_discussion_replied_meta.go index 1141d9ac1a..5d7cc6917c 100644 --- a/pkg/notifications/new_discussion_replied_meta.go +++ b/pkg/notifications/new_discussion_replied_meta.go @@ -2,7 +2,6 @@ package notifications import ( "context" - "fmt" "github.com/google/uuid" @@ -11,21 +10,16 @@ import ( ) // ActivityNewDiscussionRepliedCreate creates an activity for when a Discussion is replied to. -func ActivityNewDiscussionRepliedCreate(ctx context.Context, np sqlutils.NamedPreparer, actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, discussionCreatorID uuid.UUID, replyID uuid.UUID, discussionReplyContent models.TaggedHTML, getPreferencesFunc GetUserNotificationPreferencesFunc) (*models.Activity, error) { +func ActivityNewDiscussionRepliedCreate(ctx context.Context, np sqlutils.NamedPreparer, actorID uuid.UUID, modelPlanID uuid.UUID, discussionID uuid.UUID, discussionCreatorID uuid.UUID, replyID uuid.UUID, discussionReplyContent models.TaggedHTML, userPreferences *models.UserNotificationPreferences) (*models.Activity, error) { - activity := models.NewNewDiscussionRepliedActivity(actorID, modelPlanID, discussionID, discussionCreatorID, replyID, discussionReplyContent.RawContent.String()) + activity := models.NewNewDiscussionRepliedActivity(actorID, modelPlanID, discussionID, replyID, discussionReplyContent.RawContent.String()) retActivity, actErr := activityCreate(ctx, np, activity) if actErr != nil { return nil, actErr } - pref, err := getPreferencesFunc(ctx, actorID) - if err != nil { - return nil, fmt.Errorf("unable to get user notification preference, Notification not created %w", err) - } - - _, err = userNotificationCreate(ctx, np, retActivity, discussionCreatorID, pref.NewDiscussionReply) + _, err := userNotificationCreate(ctx, np, retActivity, discussionCreatorID, userPreferences.NewDiscussionReply) if err != nil { return nil, err } diff --git a/pkg/notifications/new_discussion_replied_meta_test.go b/pkg/notifications/new_discussion_replied_meta_test.go index 23c43dad38..7838d2f2c2 100644 --- a/pkg/notifications/new_discussion_replied_meta_test.go +++ b/pkg/notifications/new_discussion_replied_meta_test.go @@ -1,48 +1,47 @@ package notifications import ( - "context" - "github.com/google/uuid" "github.com/cmsgov/mint-app/pkg/models" ) func (suite *NotificationsSuite) TestActivityNewDiscussionRepliedCreate() { - html := `

Hey there! Are you available for a quick sync on thjis issue? Thanks!

` + html := `

Hey there! Are you available for a quick sync on this issue? Thanks!

` taggedContent, err := models.NewTaggedContentFromString(html) suite.NoError(err) - //Note: this will fail without properly updating the mentions to point to the DB. + // Note: this will fail without properly updating the mentions to point to the DB. // We can't test that here because it is part of the resolver package, which calls this package input := models.TaggedHTML(taggedContent) - // we are just choosing a valid UUID to set for the entityID - modelPlanID := uuid.New() + modelPlanID := uuid.New() // We are just choosing a valid UUID to set for the entityID discussionID := uuid.New() replyID := uuid.New() actorID := suite.testConfigs.Principal.Account().ID + userPrefs := models.NewUserNotificationPreferences(actorID) - mockFunc := func(ctx context.Context, user_id uuid.UUID) (*models.UserNotificationPreferences, error) { - // Return mock data, all notifications enabled - return models.NewUserNotificationPreferences(user_id), nil - } - - testActivity, err := ActivityNewDiscussionRepliedCreate(suite.testConfigs.Context, suite.testConfigs.Store, actorID, modelPlanID, discussionID, replyID, input, mockFunc) + discussionCreator, err := suite.testConfigs.GetTestPrincipal(suite.testConfigs.Store, "FRED") + suite.NoError(err) + testActivity, err := ActivityNewDiscussionRepliedCreate(suite.testConfigs.Context, suite.testConfigs.Store, actorID, modelPlanID, discussionID, discussionCreator.Account().ID, replyID, input, userPrefs) suite.NoError(err) + + // Assert about notification object creation suite.NotNil(testActivity) suite.EqualValues(models.ActivityNewDiscussionReply, testActivity.ActivityType) - // Assert meta data is not deserialized here - suite.Nil(testActivity.MetaData) - //Assert meta data can be deserialized - suite.NotNil(testActivity.MetaDataRaw) - meta, err := parseRawActivityMetaData(testActivity.ActivityType, testActivity.MetaDataRaw) - suite.NoError(err) - suite.NotNil(meta) + // Assert about notification delivery actorNots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, suite.testConfigs.Principal) suite.NoError(err) suite.EqualValues(0, actorNots.NumUnreadNotifications()) + + recipientNots, err := UserNotificationCollectionGetByUser(suite.testConfigs.Context, suite.testConfigs.Store, discussionCreator) + suite.NoError(err) + suite.EqualValues(1, recipientNots.NumUnreadNotifications()) + + // Assert about meta data values + meta := suite.deserializeActivityMetadata(testActivity) + suite.EqualValues(input.RawContent.String(), meta.(*models.NewDiscussionRepliedActivityMeta).Content) } diff --git a/pkg/notifications/notifications_test.go b/pkg/notifications/notifications_test.go index fafc0e77c5..3f07e3d42b 100644 --- a/pkg/notifications/notifications_test.go +++ b/pkg/notifications/notifications_test.go @@ -3,6 +3,8 @@ package notifications import ( "testing" + "github.com/cmsgov/mint-app/pkg/models" + "github.com/stretchr/testify/suite" "github.com/cmsgov/mint-app/pkg/testconfig" @@ -27,3 +29,14 @@ func TestNotificationsSuite(t *testing.T) { suite.Run(t, rs) } + +func (suite *NotificationsSuite) deserializeActivityMetadata(testActivity *models.Activity) models.ActivityMetaData { + suite.Nil(testActivity.MetaData) // Assert meta data is not deserialized here + suite.NotNil(testActivity.MetaDataRaw) // Assert meta data can be deserialized + + meta, err := parseRawActivityMetaData(testActivity.ActivityType, testActivity.MetaDataRaw) + suite.NoError(err) + suite.NotNil(meta) + + return meta +} diff --git a/src/gql/gen/graphql.ts b/src/gql/gen/graphql.ts index 0659805fb6..b90fa6a6e0 100644 --- a/src/gql/gen/graphql.ts +++ b/src/gql/gen/graphql.ts @@ -919,7 +919,6 @@ export type NewDiscussionRepliedActivityMeta = { __typename: 'NewDiscussionRepliedActivityMeta'; content: Scalars['String']['output']; discussion: PlanDiscussion; - discussionCreatorID: Scalars['UUID']['output']; discussionID: Scalars['UUID']['output']; modelPlan: ModelPlan; modelPlanID: Scalars['UUID']['output']; From 64fa54412543005ceacd48381c06b7bec56b94bc Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Thu, 14 Mar 2024 11:36:56 -0400 Subject: [PATCH 07/13] chore: updated postman collection --- MINT.postman_collection.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/MINT.postman_collection.json b/MINT.postman_collection.json index 17e52d8906..89c6fb0313 100644 --- a/MINT.postman_collection.json +++ b/MINT.postman_collection.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "32329f70-986e-430d-8a0b-4509ca71dab1", + "_postman_id": "e2043604-4cd9-443c-9267-db489feb92d8", "name": "MINT", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "20320964" + "_exporter_id": "20435042" }, "item": [ { @@ -2089,7 +2089,8 @@ "// pm.collectionVariables.set(\"operationalSolutionSubtaskID\"+i, subtaskID);", "// }" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2099,7 +2100,7 @@ "body": { "mode": "graphql", "graphql": { - "query": "query currentUser {\n currentUser {\n notifications {\n numUnreadNotifications\n notifications {\n __typename\n id\n isRead\n inAppSent\n emailSent\n activity {\n activityType\n entityID\n actorID\n metaData {\n __typename\n ... on TaggedInPlanDiscussionActivityMeta{\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n content\n }\n ... on TaggedInDiscussionReplyActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n replyID\n content\n }\n }\n createdByUserAccount {\n commonName\n }\n }\n createdByUserAccount {\n commonName\n }\n }\n }\n }\n}\n", + "query": "query currentUser {\n currentUser {\n notifications {\n numUnreadNotifications\n notifications {\n __typename\n id\n isRead\n inAppSent\n emailSent\n activity {\n activityType\n entityID\n actorID\n metaData {\n __typename\n ... on TaggedInPlanDiscussionActivityMeta{\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n content\n }\n ... on TaggedInDiscussionReplyActivityMeta {\n version\n type\n modelPlanID\n modelPlan{\n modelName\n } \n discussionID\n replyID\n content\n }\n ... on NewDiscussionRepliedActivityMeta {\n version\n type\n discussionID\n replyID\n content\n } \n }\n createdByUserAccount {\n commonName\n }\n }\n createdByUserAccount {\n commonName\n }\n }\n }\n }\n}\n", "variables": "" } }, From 528020dea19ecf4aa62ea123148dcf728af293da Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Thu, 14 Mar 2024 11:52:41 -0400 Subject: [PATCH 08/13] fix: corrected activity meta base struct type --- pkg/models/new_discussion_replied_meta.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/models/new_discussion_replied_meta.go b/pkg/models/new_discussion_replied_meta.go index c4ad4016d2..69e7d10878 100644 --- a/pkg/models/new_discussion_replied_meta.go +++ b/pkg/models/new_discussion_replied_meta.go @@ -21,7 +21,7 @@ type NewDiscussionRepliedActivityMeta struct { func newNewDiscussionRepliedActivityMeta(modelPlanID uuid.UUID, discussionID uuid.UUID, replyID uuid.UUID, content string) *NewDiscussionRepliedActivityMeta { version := 0 //iterate this if this type ever updates return &NewDiscussionRepliedActivityMeta{ - ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityTaggedInDiscussionReply, version), + ActivityMetaBaseStruct: NewActivityMetaBaseStruct(ActivityNewDiscussionReply, version), discussionRelation: NewDiscussionRelation(discussionID), modelPlanRelation: NewModelPlanRelation(modelPlanID), ReplyID: replyID, From 084e43fbaa2215e7ac0cfd3389852c88eb6faea4 Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Thu, 14 Mar 2024 16:38:48 -0400 Subject: [PATCH 09/13] chore: added comment to helper function --- pkg/notifications/notifications_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/notifications/notifications_test.go b/pkg/notifications/notifications_test.go index 3f07e3d42b..c03687dbcf 100644 --- a/pkg/notifications/notifications_test.go +++ b/pkg/notifications/notifications_test.go @@ -30,6 +30,9 @@ func TestNotificationsSuite(t *testing.T) { suite.Run(t, rs) } +// deserializeActivityMetadata is a helper function to deserialize the metadata of an activity +// It asserts that the metadata is not deserialized, and that the raw metadata is deserialized +// This method is used to test the deserialization of the metadata of an activity in notification unit tests func (suite *NotificationsSuite) deserializeActivityMetadata(testActivity *models.Activity) models.ActivityMetaData { suite.Nil(testActivity.MetaData) // Assert meta data is not deserialized here suite.NotNil(testActivity.MetaDataRaw) // Assert meta data can be deserialized From 0fc1ebd199a55c5b3a487284268225f01ff9f82c Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Thu, 14 Mar 2024 16:41:56 -0400 Subject: [PATCH 10/13] chore: deleted unused generated resolver --- pkg/graph/resolvers/activity.resolvers.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pkg/graph/resolvers/activity.resolvers.go b/pkg/graph/resolvers/activity.resolvers.go index 77c60e0c52..2def93ad38 100644 --- a/pkg/graph/resolvers/activity.resolvers.go +++ b/pkg/graph/resolvers/activity.resolvers.go @@ -7,8 +7,6 @@ package resolvers import ( "context" - "github.com/google/uuid" - "github.com/cmsgov/mint-app/pkg/appcontext" "github.com/cmsgov/mint-app/pkg/authentication" "github.com/cmsgov/mint-app/pkg/graph/generated" @@ -87,13 +85,3 @@ type activityResolver struct{ *Resolver } type newDiscussionRepliedActivityMetaResolver struct{ *Resolver } type taggedInDiscussionReplyActivityMetaResolver struct{ *Resolver } type taggedInPlanDiscussionActivityMetaResolver struct{ *Resolver } - -// !!! WARNING !!! -// The code below was going to be deleted when updating resolvers. It has been copied here so you have -// one last chance to move it out of harms way if you want. There are two reasons this happens: -// - When renaming or deleting a resolver the old code will be put in here. You can safely delete -// it when you're done. -// - You have helper methods in this file. Move them out to keep these resolver files clean. -func (r *newDiscussionRepliedActivityMetaResolver) ModelPlanID(ctx context.Context, obj *models.NewDiscussionRepliedActivityMeta) (uuid.UUID, error) { - return obj.ModelPlanID, nil -} From de8033b3f1db1a048c750de212eefd4dc9a0849b Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Thu, 14 Mar 2024 16:43:34 -0400 Subject: [PATCH 11/13] fix: revereted incorrectly changed user ID assignment --- pkg/graph/resolvers/plan_discussion.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/graph/resolvers/plan_discussion.go b/pkg/graph/resolvers/plan_discussion.go index c7cc0ffdf2..f940f73000 100644 --- a/pkg/graph/resolvers/plan_discussion.go +++ b/pkg/graph/resolvers/plan_discussion.go @@ -456,7 +456,7 @@ func CreateDiscussionReply( } // Create Activity and notifications in the DB - _, notificationErr := notifications.ActivityTaggedInDiscussionReplyCreate(ctx, tx, discussion.CreatedBy, discussion.ModelPlanID, discussion.ID, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) + _, notificationErr := notifications.ActivityTaggedInDiscussionReplyCreate(ctx, tx, principal.Account().ID, discussion.ModelPlanID, discussion.ID, reply.ID, reply.Content, loaders.UserNotificationPreferencesGetByUserID) if notificationErr != nil { return nil, fmt.Errorf("unable to generate notifications, %w", notificationErr) } From 4856232bd9362511b29d01100715cd24682927df Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Thu, 14 Mar 2024 16:49:39 -0400 Subject: [PATCH 12/13] removed irrelevant comment --- pkg/notifications/new_discussion_replied_meta_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/notifications/new_discussion_replied_meta_test.go b/pkg/notifications/new_discussion_replied_meta_test.go index 7838d2f2c2..36b5ba981f 100644 --- a/pkg/notifications/new_discussion_replied_meta_test.go +++ b/pkg/notifications/new_discussion_replied_meta_test.go @@ -11,9 +11,6 @@ func (suite *NotificationsSuite) TestActivityNewDiscussionRepliedCreate() { html := `

Hey there! Are you available for a quick sync on this issue? Thanks!

` taggedContent, err := models.NewTaggedContentFromString(html) suite.NoError(err) - - // Note: this will fail without properly updating the mentions to point to the DB. - // We can't test that here because it is part of the resolver package, which calls this package input := models.TaggedHTML(taggedContent) modelPlanID := uuid.New() // We are just choosing a valid UUID to set for the entityID From f7c1a2900fe84017e402d9de3df75eb2eb3e35c1 Mon Sep 17 00:00:00 2001 From: Tom Brooks Date: Thu, 14 Mar 2024 16:54:42 -0400 Subject: [PATCH 13/13] chore: added more assertions to unit test --- pkg/notifications/new_discussion_replied_meta_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/notifications/new_discussion_replied_meta_test.go b/pkg/notifications/new_discussion_replied_meta_test.go index 36b5ba981f..80234515cf 100644 --- a/pkg/notifications/new_discussion_replied_meta_test.go +++ b/pkg/notifications/new_discussion_replied_meta_test.go @@ -41,4 +41,7 @@ func (suite *NotificationsSuite) TestActivityNewDiscussionRepliedCreate() { // Assert about meta data values meta := suite.deserializeActivityMetadata(testActivity) suite.EqualValues(input.RawContent.String(), meta.(*models.NewDiscussionRepliedActivityMeta).Content) + suite.EqualValues(discussionID, meta.(*models.NewDiscussionRepliedActivityMeta).DiscussionID) + suite.EqualValues(modelPlanID, meta.(*models.NewDiscussionRepliedActivityMeta).ModelPlanID) + suite.EqualValues(replyID, meta.(*models.NewDiscussionRepliedActivityMeta).ReplyID) }