Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions backend/src/models/coordination_team.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package models

import (
"go.mongodb.org/mongo-driver/bson/primitive"
)

// CoordinationTeam represents a dedicated coordination team separate from normal teams.
// It maps a set of coordinator members to the IDs of teams they coordinate.
type CoordinationTeam struct {
ID primitive.ObjectID `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"`

// Coordinator is the single member that acts as coordinator for this coordination team.
Coordinator *TeamMember `json:"coordinator,omitempty" bson:"coordinator,omitempty"`

// CoordinatedMembers holds IDs of members (from models.Member) that belong to
// this coordination team for the current event.
CoordinatedMembers []primitive.ObjectID `json:"coordinatedMembers" bson:"coordinatedMembers"`
}

// HasCoordinator returns true if the given member is a coordinator in this coordination team.
func (ct *CoordinationTeam) HasCoordinator(member primitive.ObjectID) bool {
if ct.Coordinator == nil {
return false
}
return ct.Coordinator.Member == member
}

// HasCoordinatedTeam returns true if this coordination team coordinates the provided team id.
func (ct *CoordinationTeam) HasCoordinatedMember(memberID primitive.ObjectID) bool {
for _, m := range ct.CoordinatedMembers {
if m == memberID {
return true
}
}
return false
}
36 changes: 36 additions & 0 deletions backend/src/mongodb/company.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,42 @@ func (c *CompaniesType) GetCompanies(compOptions GetCompaniesOptions) ([]*models
return companies, nil
}

// GetCompaniesByMembers returns companies that have a participation with any of the provided member IDs.
// If eventID is provided, only participations for that event are considered.
func (c *CompaniesType) GetCompaniesByMembers(memberIDs []primitive.ObjectID, eventID *int) ([]*models.Company, error) {
ctx := context.Background()
var companies = make([]*models.Company, 0)

filter := bson.M{}

if eventID != nil {
filter["participations"] = bson.M{"$elemMatch": bson.M{"member": bson.M{"$in": memberIDs}, "event": *eventID}}
} else {
filter["participations.member"] = bson.M{"$in": memberIDs}
}

cur, err := c.Collection.Find(ctx, filter)
if err != nil {
return nil, err
}

for cur.Next(ctx) {
var comp models.Company
if err := cur.Decode(&comp); err != nil {
return nil, err
}
companies = append(companies, &comp)
}

if err := cur.Err(); err != nil {
return nil, err
}

cur.Close(ctx)

return companies, nil
}

// Transforms a models.Company into a models.CompanyPublic. If eventID != nil, returns only the participation for that event, if announced.
// Otherwise, returns all participations in which they were announced
func companyToPublic(company models.Company, eventID *int) (*models.CompanyPublic, error) {
Expand Down
231 changes: 231 additions & 0 deletions backend/src/mongodb/coordination_team.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package mongodb

import (
"context"
"errors"

"github.com/sinfo/deck2/src/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)

// CoordinationTeamsType contains database info for coordination teams
type CoordinationTeamsType struct {
Collection *mongo.Collection
}

// CreateCoordinationTeam inserts a new coordination team
func (c *CoordinationTeamsType) CreateCoordinationTeam(name string) (*models.CoordinationTeam, error) {
ctx := context.Background()

insertResult, err := c.Collection.InsertOne(ctx, bson.M{
"name": name,
// coordinator is intentionally omitted until set
"coordinatedMembers": []primitive.ObjectID{},
})
if err != nil {
return nil, err
}

id := insertResult.InsertedID.(primitive.ObjectID)
return c.GetCoordinationTeam(id)
}

// GetCoordinationTeam retrieves a coordination team by ID
func (c *CoordinationTeamsType) GetCoordinationTeam(id primitive.ObjectID) (*models.CoordinationTeam, error) {
ctx := context.Background()
var ct models.CoordinationTeam
if err := c.Collection.FindOne(ctx, bson.M{"_id": id}).Decode(&ct); err != nil {
return nil, err
}
return &ct, nil
}

// GetCoordinationTeamsByMember finds coordination teams that include the given member id
func (c *CoordinationTeamsType) GetCoordinationTeamsByMember(memberID primitive.ObjectID) ([]*models.CoordinationTeam, error) {
ctx := context.Background()
cur, err := c.Collection.Find(ctx, bson.M{"coordinatedMembers": memberID})
if err != nil {
return nil, err
}

var res []*models.CoordinationTeam
for cur.Next(ctx) {
var ct models.CoordinationTeam
if err := cur.Decode(&ct); err != nil {
return nil, err
}
res = append(res, &ct)
}
cur.Close(ctx)
return res, nil
}

// GetCoordinationTeamsByCoordinator finds coordination teams where the provided member is a coordinator
func (c *CoordinationTeamsType) GetCoordinationTeamsByCoordinator(memberID primitive.ObjectID) ([]*models.CoordinationTeam, error) {
ctx := context.Background()
cur, err := c.Collection.Find(ctx, bson.M{"coordinator.member": memberID})
if err != nil {
return nil, err
}

var res []*models.CoordinationTeam
for cur.Next(ctx) {
var ct models.CoordinationTeam
if err := cur.Decode(&ct); err != nil {
return nil, err
}
res = append(res, &ct)
}
cur.Close(ctx)
return res, nil
}

// AddCoordinatedMember adds a member id to the coordinatedMembers list
func (c *CoordinationTeamsType) AddCoordinatedMember(coordTeamID, memberID primitive.ObjectID) (*models.CoordinationTeam, error) {
ctx := context.Background()

ct, err := c.GetCoordinationTeam(coordTeamID)
if err != nil {
return nil, err
}

for _, m := range ct.CoordinatedMembers {
if m == memberID {
return nil, errors.New("already exists")
}
}

ct.CoordinatedMembers = append(ct.CoordinatedMembers, memberID)

if _, err := c.Collection.UpdateOne(ctx, bson.M{"_id": coordTeamID}, bson.M{"$set": bson.M{"coordinatedMembers": ct.CoordinatedMembers}}); err != nil {
return nil, err
}

return c.GetCoordinationTeam(coordTeamID)
}

// RemoveCoordinatedMember removes a member id from the coordinatedMembers list
func (c *CoordinationTeamsType) RemoveCoordinatedMember(coordTeamID, memberID primitive.ObjectID) (*models.CoordinationTeam, error) {
ctx := context.Background()

ct, err := c.GetCoordinationTeam(coordTeamID)
if err != nil {
return nil, err
}

found := false
newList := make([]primitive.ObjectID, 0, len(ct.CoordinatedMembers))
for _, m := range ct.CoordinatedMembers {
if m == memberID {
found = true
continue
}
newList = append(newList, m)
}

if !found {
return nil, errors.New("not found")
}

if _, err := c.Collection.UpdateOne(ctx, bson.M{"_id": coordTeamID}, bson.M{"$set": bson.M{"coordinatedMembers": newList}}); err != nil {
return nil, err
}

return c.GetCoordinationTeam(coordTeamID)
}

// SetCoordinator sets the single coordinator for this coordination team. It replaces existing coordinators.
// If name is non-empty it will also update the coordination team's name in the same operation.
func (c *CoordinationTeamsType) SetCoordinator(coordTeamID primitive.ObjectID, member models.TeamMember, name string) (*models.CoordinationTeam, error) {
ctx := context.Background()

if member.Role != models.RoleCoordinator {
member.Role = models.RoleCoordinator
}

set := bson.M{"coordinator": member}
if len(name) > 0 {
set["name"] = name
}

if _, err := c.Collection.UpdateOne(ctx, bson.M{"_id": coordTeamID}, bson.M{"$set": set}); err != nil {
return nil, err
}

return c.GetCoordinationTeam(coordTeamID)
}

// RemoveCoordinator removes a coordinator by member id
func (c *CoordinationTeamsType) RemoveCoordinator(coordTeamID, memberID primitive.ObjectID) (*models.CoordinationTeam, error) {
ctx := context.Background()

ct, err := c.GetCoordinationTeam(coordTeamID)
if err != nil {
return nil, err
}

if ct.Coordinator == nil || ct.Coordinator.Member != memberID {
return nil, errors.New("not found")
}

// unset the coordinator field
if _, err := c.Collection.UpdateOne(ctx, bson.M{"_id": coordTeamID}, bson.M{"$unset": bson.M{"coordinator": ""}}); err != nil {
return nil, err
}

return c.GetCoordinationTeam(coordTeamID)
}

// UpdateName updates the coordination team's name
func (c *CoordinationTeamsType) UpdateName(coordTeamID primitive.ObjectID, name string) (*models.CoordinationTeam, error) {
ctx := context.Background()

if _, err := c.Collection.UpdateOne(ctx, bson.M{"_id": coordTeamID}, bson.M{"$set": bson.M{"name": name}}); err != nil {
return nil, err
}

return c.GetCoordinationTeam(coordTeamID)
}

// GetAllCoordinationTeams lists all coordination teams
func (c *CoordinationTeamsType) GetAllCoordinationTeams() ([]*models.CoordinationTeam, error) {
ctx := context.Background()
cur, err := c.Collection.Find(ctx, bson.M{})
if err != nil {
return nil, err
}

var res []*models.CoordinationTeam
for cur.Next(ctx) {
var ct models.CoordinationTeam
if err := cur.Decode(&ct); err != nil {
return nil, err
}
res = append(res, &ct)
}
cur.Close(ctx)
return res, nil
}

// DeleteCoordinationTeam deletes a coordination team and returns the deleted document
func (c *CoordinationTeamsType) DeleteCoordinationTeam(coordTeamID primitive.ObjectID) (*models.CoordinationTeam, error) {
ctx := context.Background()

ct, err := c.GetCoordinationTeam(coordTeamID)
if err != nil {
return nil, err
}

delRes, err := c.Collection.DeleteOne(ctx, bson.M{"_id": coordTeamID})
if err != nil {
return nil, err
}

if delRes.DeletedCount != 1 {
return nil, errors.New("could not delete coordination team")
}

return ct, nil
}
6 changes: 6 additions & 0 deletions backend/src/mongodb/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ var (
Notifications *NotificationsType
//Templates is an instance of a mongodb collection
Templates *TemplateType
// CoordinationTeams is an instance of coordination teams collection
CoordinationTeams *CoordinationTeamsType
)

var (
Expand Down Expand Up @@ -179,6 +181,10 @@ func InitializeDatabase() {
Collection: db.Collection("templates"),
}

CoordinationTeams = &CoordinationTeamsType{
Collection: db.Collection("coordinationTeams"),
}

// Ensure index for categories
if err := Categories.EnsureIndexes(); err != nil {
log.Println("Warning: failed to ensure categories indexes:", err)
Expand Down
27 changes: 12 additions & 15 deletions backend/src/mongodb/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,20 @@ func (n *NotificationsType) Notify(author primitive.ObjectID, data CreateNotific
}
}

// notify coordination on the author's team
for _, teamID := range event.Teams {
team, err := Teams.GetTeam(teamID)
if err != nil || !team.HasMember(author) {
continue
}

coordinators := team.GetMembersByRole(models.RoleCoordinator)

for _, coordinator := range coordinators {

// notify authors only if not running on production mode
if config.Production && coordinator.Member == author {
// Notify coordination teams: coordination logic lives in separate collection
// Find coordination teams that include the author as a coordinated member
// and notify those coordination teams' coordinators only.
coordTeams, err := CoordinationTeams.GetCoordinationTeamsByMember(author)
if err == nil {
for _, coordTeam := range coordTeams {
if coordTeam.Coordinator == nil {
continue
}

n.NotifyMember(coordinator.Member, data)
// do not notify the author themselves in production
if config.Production && coordTeam.Coordinator.Member == author {
continue
}
n.NotifyMember(coordTeam.Coordinator.Member, data)
}
}

Expand Down
Loading