[rotang] Adding in a enable/disable knob to the rota config.

This to make it possible to migrate all configurations without having
the jobs messing with calendars and sending e-mails.


Bug: 845549
Change-Id: I7cd67de89b8ffac6088b9b5720a748f4101ac034
Reviewed-on: https://chromium-review.googlesource.com/1232799
Commit-Queue: Ola Karlsson <olakar@chromium.org>
Reviewed-by: Tiffany Zhang <zhangtiff@chromium.org>
Cr-Commit-Position: refs/heads/master@{#17817}
diff --git a/go/src/infra/appengine/rotang/cmd/handlers/job_email.go b/go/src/infra/appengine/rotang/cmd/handlers/job_email.go
index ab93e4e..4b8b70c 100644
--- a/go/src/infra/appengine/rotang/cmd/handlers/job_email.go
+++ b/go/src/infra/appengine/rotang/cmd/handlers/job_email.go
@@ -42,7 +42,7 @@
 
 // notifyEmail figures out if a notification should be sent for the specified shift.
 func (h *State) notifyEmail(ctx context.Context, cfg *rotang.Configuration, t time.Time) error {
-	if cfg.Config.Email.DaysBeforeNotify == 0 {
+	if cfg.Config.Email.DaysBeforeNotify == 0 || !cfg.Config.Enabled {
 		return nil
 	}
 	t = t.UTC().Add(time.Duration(cfg.Config.Email.DaysBeforeNotify) * 24 * time.Hour)
diff --git a/go/src/infra/appengine/rotang/cmd/handlers/job_email_test.go b/go/src/infra/appengine/rotang/cmd/handlers/job_email_test.go
index 9d06321..2248e0e 100644
--- a/go/src/infra/appengine/rotang/cmd/handlers/job_email_test.go
+++ b/go/src/infra/appengine/rotang/cmd/handlers/job_email_test.go
@@ -59,6 +59,7 @@
 			Config: rotang.Config{
 				Description: "Test",
 				Name:        "Test Rota",
+				Enabled:     true,
 			},
 		},
 	}, {
@@ -79,6 +80,7 @@
 			Config: rotang.Config{
 				Description: "Test",
 				Name:        "Test Rota",
+				Enabled:     true,
 				Email: rotang.Email{
 					Subject:          "Some subject",
 					Body:             "Some Body",
@@ -120,6 +122,57 @@
 			},
 		},
 	}, {
+		name: "No email if rota is not enabled",
+		time: midnight,
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		cfg: &rotang.Configuration{
+			Config: rotang.Config{
+				Description: "Test",
+				Name:        "Test Rota",
+				Email: rotang.Email{
+					Subject:          "Some subject",
+					Body:             "Some Body",
+					DaysBeforeNotify: 4,
+				},
+				Shifts: rotang.ShiftConfig{
+					StartTime: midnight,
+					Length:    1,
+					Skip:      2,
+					Shifts: []rotang.Shift{
+						{
+							Name:     "MTV all day",
+							Duration: 8 * time.Hour,
+						},
+					},
+				},
+			},
+			Members: []rotang.ShiftMember{
+				{
+					Email: "oncaller@oncall.com",
+				},
+			},
+		},
+		memberPool: []rotang.Member{
+			{
+				Email: "oncaller@oncall.com",
+			},
+		},
+		shifts: []rotang.ShiftEntry{
+			{
+				Name: "MTV All Day",
+				OnCall: []rotang.ShiftMember{
+					{
+						Email: "oncaller@oncall.com",
+					},
+				},
+				StartTime: midnight.Add(4 * 24 * time.Hour),
+				EndTime:   midnight.Add(8*time.Hour + 4*24*time.Hour),
+			},
+		},
+	}, {
 		name: "Email success",
 		time: midnight,
 		ctx: &router.Context{
@@ -129,6 +182,7 @@
 		cfg: &rotang.Configuration{
 			Config: rotang.Config{
 				Description: "Test",
+				Enabled:     true,
 				Name:        "Test Rota",
 				Email: rotang.Email{
 					Subject: `Upcoming On-call shift for rotation: {{.RotaName}} {{.ShiftEntry.StartTime.In .Member.TZ}} to {{.ShiftEntry.EndTime.In .Member.TZ}}`,
@@ -195,6 +249,7 @@
 			Config: rotang.Config{
 				Description: "Test",
 				Name:        "Test Rota",
+				Enabled:     true,
 				Email: rotang.Email{
 					Subject:          "Some subject",
 					Body:             "Some Body",
@@ -246,6 +301,7 @@
 			Config: rotang.Config{
 				Description: "Test",
 				Name:        "Test Rota",
+				Enabled:     true,
 				Email: rotang.Email{
 					Subject:          "Some subject",
 					Body:             "Some Body",
@@ -320,6 +376,7 @@
 			Config: rotang.Config{
 				Description: "Test",
 				Name:        "Test Rota",
+				Enabled:     true,
 				Email: rotang.Email{
 					Subject:          "Some subject",
 					Body:             "Some Body",
@@ -474,6 +531,7 @@
 			Config: rotang.Config{
 				Description: "Test",
 				Name:        "Test Rota",
+				Enabled:     true,
 				Email: rotang.Email{
 					Subject:          "Some subject",
 					Body:             "Some Body",
diff --git a/go/src/infra/appengine/rotang/cmd/handlers/templates/pages/modifyrota.html b/go/src/infra/appengine/rotang/cmd/handlers/templates/pages/modifyrota.html
index b2cb3eb..c93696a 100644
--- a/go/src/infra/appengine/rotang/cmd/handlers/templates/pages/modifyrota.html
+++ b/go/src/infra/appengine/rotang/cmd/handlers/templates/pages/modifyrota.html
@@ -304,7 +304,6 @@
             <td>
               <input type="number" name="shiftMembers" value="{{with .Rota}}{{.Config.Shifts.ShiftMembers}}{{else}}2{{end}}" required><small><i> nr of members to schedule</i></small>
             </td>
-          </tr>
           <tr><td>Generator:</td>
             <td>
               <select name="generator">
diff --git a/go/src/infra/appengine/rotang/pkg/datastore/datastore.go b/go/src/infra/appengine/rotang/pkg/datastore/datastore.go
index 858567a..555c9a6 100644
--- a/go/src/infra/appengine/rotang/pkg/datastore/datastore.go
+++ b/go/src/infra/appengine/rotang/pkg/datastore/datastore.go
@@ -356,6 +356,50 @@
 	}, children)
 }
 
+// EnableRota enables jobs to consider the specified rotation.
+func (s *Store) EnableRota(ctx context.Context, name string) error {
+	return s.changeRotaState(ctx, name, true)
+}
+
+// DisableRota disables jobs for the specified rotation.
+func (s *Store) DisableRota(ctx context.Context, name string) error {
+	return s.changeRotaState(ctx, name, false)
+}
+
+// RotaEnabled returns the state of the specified rota.
+func (s *Store) RotaEnabled(ctx context.Context, name string) (bool, error) {
+	if err := ctx.Err(); err != nil {
+		return false, err
+	}
+
+	cfg, err := s.RotaConfig(ctx, name)
+	if err != nil {
+		return false, err
+	}
+
+	if len(cfg) != 1 {
+		return false, status.Errorf(codes.Internal, "Unexpected number of configurations returned")
+	}
+	return cfg[0].Config.Enabled, nil
+
+}
+
+func (s *Store) changeRotaState(ctx context.Context, name string, state bool) error {
+	if err := ctx.Err(); err != nil {
+		return err
+	}
+
+	cfg, err := s.RotaConfig(ctx, name)
+	if err != nil {
+		return err
+	}
+	if len(cfg) != 1 {
+		return status.Errorf(codes.Internal, "Unexpected number of configurations returned")
+	}
+	cfg[0].Config.Enabled = state
+	return s.UpdateRotaConfig(ctx, cfg[0])
+}
+
 // AddRotaMember adds a members to the specified rota.
 func (s *Store) AddRotaMember(ctx context.Context, rota string, member *rotang.ShiftMember) error {
 	if err := ctx.Err(); err != nil {
diff --git a/go/src/infra/appengine/rotang/pkg/datastore/datastore_test.go b/go/src/infra/appengine/rotang/pkg/datastore/datastore_test.go
index fd18fb5..921b982 100644
--- a/go/src/infra/appengine/rotang/pkg/datastore/datastore_test.go
+++ b/go/src/infra/appengine/rotang/pkg/datastore/datastore_test.go
@@ -408,6 +408,84 @@
 	}
 }
 
+func TestChangeRotaState(t *testing.T) {
+	ctx := newTestContext()
+	ctxCancel, cancel := context.WithCancel(ctx)
+	cancel()
+
+	s := New(ctx)
+
+	tests := []struct {
+		name     string
+		fail     bool
+		rota     string
+		ctx      context.Context
+		cfg      *rotang.Configuration
+		testFunc func(context.Context, string) error
+		want     bool
+	}{{
+		name:     "Enable Success",
+		ctx:      ctx,
+		testFunc: s.EnableRota,
+		rota:     "Test Rota",
+		cfg: &rotang.Configuration{
+			Config: rotang.Config{
+				Name:    "Test Rota",
+				Enabled: false,
+			},
+		},
+		want: true,
+	}, {
+		name:     "Canceled Context",
+		fail:     true,
+		ctx:      ctxCancel,
+		testFunc: s.EnableRota,
+		cfg: &rotang.Configuration{
+			Config: rotang.Config{
+				Name:    "Test Rota",
+				Enabled: false,
+			},
+		},
+	}, {
+		name:     "Disable success",
+		ctx:      ctx,
+		rota:     "Test Rota",
+		testFunc: s.DisableRota,
+		cfg: &rotang.Configuration{
+			Config: rotang.Config{
+				Name: "Test Rota",
+			},
+		},
+	},
+	}
+
+	for _, tst := range tests {
+		t.Run(tst.name, func(t *testing.T) {
+			if err := s.CreateRotaConfig(ctx, tst.cfg); err != nil {
+				t.Fatalf("%s: CreateRotaConfig(ctx, _) failed: %v", tst.name, err)
+			}
+			defer s.DeleteRotaConfig(ctx, tst.cfg.Config.Name)
+			err := tst.testFunc(tst.ctx, tst.rota)
+			if got, want := (err != nil), tst.fail; got != want {
+				t.Fatalf("%s: tstFunc(ctx, %q) = %t, want: %t, err: %v", tst.name, tst.rota, got, want, err)
+			}
+			if err != nil {
+				return
+			}
+
+			got, err := s.RotaEnabled(ctx, tst.rota)
+			if err != nil {
+				t.Fatalf("%s: RotaEnabled(ctx, %q) failed: %v", tst.name, tst.rota, err)
+			}
+
+			if got != tst.want {
+				t.Fatalf("%s: RotaEnabled(ctx, %q) = %t want: %t", tst.name, tst.rota, got, tst.want)
+			}
+
+		})
+	}
+}
+
 func TestMember(t *testing.T) {
 	ctx := newTestContext()
 	ctxCancel, cancel := context.WithCancel(ctx)
diff --git a/go/src/infra/appengine/rotang/rotang.go b/go/src/infra/appengine/rotang/rotang.go
index ff9afe9..3467ce2 100644
--- a/go/src/infra/appengine/rotang/rotang.go
+++ b/go/src/infra/appengine/rotang/rotang.go
@@ -38,6 +38,7 @@
 	ShiftsToSchedule int
 	Shifts           ShiftConfig
 	Expiration       int
+	Enabled          bool
 }
 
 // ShiftConfig holds the Shift configuration.
@@ -146,6 +147,12 @@
 	DeleteRotaMember(ctx context.Context, rota, email string) error
 	// MemberOf returns the rotations the specified email is a member of.
 	MemberOf(ctx context.Context, email string) ([]string, error)
+	// EnableRota enables jobs to consider rotation.
+	EnableRota(ctx context.Context, rota string) error
+	// DisableRota disables jobs for rotation.
+	DisableRota(ctx context.Context, rota string) error
+	// RotaEnabled returns the Enabled state of a rota.
+	RotaEnabled(ctx context.Context, rota string) (bool, error)
 }
 
 // MemberStorer defines the member store interface.