[rotang] Add a handler for Deleting rotations.


Bug: 845549
Change-Id: I002a74b4d01a439b1658e8d76a67e610203e4268
Reviewed-on: https://chromium-review.googlesource.com/1227863
Reviewed-by: Ola Karlsson <olakar@chromium.org>
Reviewed-by: Tiffany Zhang <zhangtiff@chromium.org>
Commit-Queue: Ola Karlsson <olakar@chromium.org>
Cr-Commit-Position: refs/heads/master@{#17687}
diff --git a/cmd/app/app.go b/cmd/app/app.go
index 924e2d7..9d8812e 100644
--- a/cmd/app/app.go
+++ b/cmd/app/app.go
@@ -17,7 +17,9 @@
 	"golang.org/x/net/context"
 
 	"go.chromium.org/gae/service/mail"
+	"go.chromium.org/luci/appengine/gaeauth/server"
 	"go.chromium.org/luci/appengine/gaemiddleware/standard"
+	"go.chromium.org/luci/server/auth"
 	"go.chromium.org/luci/server/router"
 	"go.chromium.org/luci/server/templates"
 )
@@ -51,7 +53,7 @@
 
 	tmw := middleware.Extend(templates.WithTemplates(&templates.Bundle{
 		Loader: templates.FileSystemLoader("../handlers/templates"),
-	}))
+	}), auth.Authenticate(server.UsersAPIAuthMethod{}))
 
 	// Sort out the generators.
 	gs := algo.New()
@@ -74,8 +76,10 @@
 	r.GET("/upload", tmw, h.HandleUpload)
 	r.GET("/list", tmw, h.HandleList)
 	r.GET("/createrota", tmw, h.HandleCreateRota)
+	r.GET("/managerota", tmw, h.HandleManageRota)
 
 	r.POST("/createrota", tmw, h.HandleCreateRota)
+	r.POST("/deleterota", tmw, h.HandleDeleteRota)
 	r.POST("/upload", tmw, h.HandleUpload)
 
 	http.DefaultServeMux.Handle("/", r)
diff --git a/cmd/handlers/handle_createrota.go b/cmd/handlers/handle_createrota.go
index 7a9ac50..fa1cc9d 100644
--- a/cmd/handlers/handle_createrota.go
+++ b/cmd/handlers/handle_createrota.go
@@ -12,8 +12,8 @@
 	"strings"
 	"time"
 
-	"go.chromium.org/gae/service/user"
 	"go.chromium.org/luci/common/logging"
+	"go.chromium.org/luci/server/auth"
 	"go.chromium.org/luci/server/router"
 	"go.chromium.org/luci/server/templates"
 	"google.golang.org/grpc/codes"
@@ -130,7 +130,7 @@
 			http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		usr := user.Current(ctx.Context)
+		usr := auth.CurrentUser(ctx.Context)
 		if usr == nil {
 			http.Error(ctx.Writer, "login required", http.StatusForbidden)
 			return
@@ -162,7 +162,7 @@
 		return
 	}
 
-	usr := user.Current(ctx.Context)
+	usr := auth.CurrentUser(ctx.Context)
 	if usr == nil {
 		http.Error(ctx.Writer, "Login required", http.StatusForbidden)
 		return
diff --git a/cmd/handlers/handle_createrota_test.go b/cmd/handlers/handle_createrota_test.go
index 0a6b792..6e94b07 100644
--- a/cmd/handlers/handle_createrota_test.go
+++ b/cmd/handlers/handle_createrota_test.go
@@ -11,7 +11,9 @@
 	"time"
 
 	"github.com/kylelemons/godebug/pretty"
-	"go.chromium.org/gae/service/user"
+	"go.chromium.org/luci/auth/identity"
+	"go.chromium.org/luci/server/auth"
+	"go.chromium.org/luci/server/auth/authtest"
 	"go.chromium.org/luci/server/router"
 	"go.chromium.org/luci/server/templates"
 	"golang.org/x/net/context"
@@ -380,21 +382,21 @@
 		t.Fatalf("New failed: %v", err)
 	}
 
-	tu := user.GetTestable(ctx)
-
 	for _, tst := range tests {
 		t.Run(tst.name, func(t *testing.T) {
 			for _, m := range tst.memberPool {
 				if err := h.memberStore(ctx).CreateMember(ctx, &m); err != nil {
 					t.Fatalf("%s: CreateMember(ctx, _) failed: %v", tst.name, err)
 				}
+				defer h.memberStore(ctx).DeleteMember(ctx, m.Email)
 			}
 			tst.ctx.Context = templates.Use(tst.ctx.Context, &templates.Bundle{
 				Loader: templates.FileSystemLoader(templatesLocation),
 			}, nil)
 			if tst.user != "" {
-				tu.Login(tst.user, "", false)
-				defer tu.Logout()
+				tst.ctx.Context = auth.WithState(tst.ctx.Context, &authtest.FakeState{
+					Identity: identity.Identity("user:" + tst.user),
+				})
 			}
 			h.HandleCreateRota(tst.ctx)
 
@@ -411,8 +413,6 @@
 	ctxCancel, cancel := context.WithCancel(ctx)
 	cancel()
 
-	tu := user.GetTestable(ctx)
-
 	testTime, err := time.Parse("15:04", "13:37")
 	if err != nil {
 		t.Fatalf("time.Parse() failed: %v", err)
@@ -573,8 +573,9 @@
 				Loader: templates.FileSystemLoader(templatesLocation),
 			}, nil)
 			if tst.user != "" {
-				tu.Login(tst.user, "", false)
-				defer tu.Logout()
+				tst.ctx.Context = auth.WithState(tst.ctx.Context, &authtest.FakeState{
+					Identity: identity.Identity("user:" + tst.user),
+				})
 			}
 			tst.ctx.Request = httptest.NewRequest("POST", "/createrota", nil)
 			tst.ctx.Request.Form = tst.values
diff --git a/cmd/handlers/handle_deleterota.go b/cmd/handlers/handle_deleterota.go
new file mode 100644
index 0000000..a344a99
--- /dev/null
+++ b/cmd/handlers/handle_deleterota.go
@@ -0,0 +1,69 @@
+package handlers
+
+import (
+	"net/http"
+
+	"go.chromium.org/luci/server/auth"
+	"go.chromium.org/luci/server/router"
+)
+
+// HandleDeleteRota handles deletion of a rotation configuration
+func (h *State) HandleDeleteRota(ctx *router.Context) {
+	if err := ctx.Context.Err(); err != nil {
+		http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	if ctx.Request.Method != "POST" {
+		http.Error(ctx.Writer, "HandleDeleteRota handles only POST requests", http.StatusBadRequest)
+		return
+	}
+
+	if err := ctx.Request.ParseForm(); err != nil {
+		http.Error(ctx.Writer, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	rotaName := ctx.Request.FormValue("name")
+	if rotaName == "" {
+		http.Error(ctx.Writer, "`name` not set", http.StatusBadRequest)
+		return
+	}
+	rotas, err := h.configStore(ctx.Context).RotaConfig(ctx.Context, rotaName)
+	if err != nil {
+		http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	if len(rotas) != 1 {
+		http.Error(ctx.Writer, "Unexpected number of rotations returned", http.StatusInternalServerError)
+		return
+	}
+	rota := rotas[0]
+
+	usr := auth.CurrentUser(ctx.Context)
+	if usr == nil {
+		http.Error(ctx.Writer, "login required", http.StatusForbidden)
+		return
+	}
+
+	isOwner := false
+	for _, o := range rota.Config.Owners {
+		if o == usr.Email {
+			isOwner = true
+			break
+		}
+	}
+
+	if !isOwner {
+		http.Error(ctx.Writer, "not in the rotation owners", http.StatusForbidden)
+		return
+	}
+
+	if err := h.configStore(ctx.Context).DeleteRotaConfig(ctx.Context, rotaName); err != nil {
+		http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	http.Redirect(ctx.Writer, ctx.Request, h.selfURL+"/managerota", http.StatusOK)
+}
diff --git a/cmd/handlers/handle_deleterota_test.go b/cmd/handlers/handle_deleterota_test.go
new file mode 100644
index 0000000..5e89aec
--- /dev/null
+++ b/cmd/handlers/handle_deleterota_test.go
@@ -0,0 +1,207 @@
+package handlers
+
+import (
+	"infra/appengine/rotang"
+	"infra/appengine/rotang/pkg/algo"
+	"infra/appengine/rotang/pkg/datastore"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"sort"
+	"testing"
+
+	"github.com/kylelemons/godebug/pretty"
+	"go.chromium.org/luci/auth/identity"
+	"go.chromium.org/luci/server/auth"
+	"go.chromium.org/luci/server/auth/authtest"
+	"go.chromium.org/luci/server/router"
+	"go.chromium.org/luci/server/templates"
+	"golang.org/x/net/context"
+)
+
+func TestHandleDeleteRota(t *testing.T) {
+	ctx := newTestContext()
+	ctxCancel, cancel := context.WithCancel(ctx)
+	cancel()
+
+	tests := []struct {
+		name     string
+		fail     bool
+		rotaName string
+		user     string
+		ctx      *router.Context
+		cfg      []rotang.Configuration
+		want     []string
+	}{{
+		name: "Context canceled",
+		fail: true,
+		ctx: &router.Context{
+			Context: ctxCancel,
+			Writer:  httptest.NewRecorder(),
+		},
+	}, {
+		name: "Delete success",
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		rotaName: "Test Rota",
+		user:     "test@user.com",
+		cfg: []rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name:   "Test Rota",
+					Owners: []string{"test@user.com", "another@user.com"},
+				},
+			}, {
+				Config: rotang.Config{
+					Name:   "Another Rota",
+					Owners: []string{"test@user.com", "another@user.com"},
+				},
+			},
+		},
+		want: []string{"Another Rota"},
+	}, {
+		name: "Not owner",
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		rotaName: "Test Rota",
+		fail:     true,
+		user:     "test@user.com",
+		cfg: []rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name:   "Test Rota",
+					Owners: []string{"another@user.com"},
+				},
+			}, {
+				Config: rotang.Config{
+					Name:   "Another Rota",
+					Owners: []string{"test@user.com", "another@user.com"},
+				},
+			},
+		},
+	}, {
+		name: "Not logged in",
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		rotaName: "Test Rota",
+		fail:     true,
+		cfg: []rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name:   "Test Rota",
+					Owners: []string{"test@user.com", "another@user.com"},
+				},
+			}, {
+				Config: rotang.Config{
+					Name:   "Another Rota",
+					Owners: []string{"test@user.com", "another@user.com"},
+				},
+			},
+		},
+	}, {
+		name: "No rota name set",
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		fail: true,
+		cfg: []rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name:   "Test Rota",
+					Owners: []string{"test@user.com", "another@user.com"},
+				},
+			}, {
+				Config: rotang.Config{
+					Name:   "Another Rota",
+					Owners: []string{"test@user.com", "another@user.com"},
+				},
+			},
+		},
+	}, {
+		name: "Rota does not exist",
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		rotaName: "Non Existing",
+		fail:     true,
+		cfg: []rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name:   "Test Rota",
+					Owners: []string{"test@user.com", "another@user.com"},
+				},
+			}, {
+				Config: rotang.Config{
+					Name:   "Another Rota",
+					Owners: []string{"test@user.com", "another@user.com"},
+				},
+			},
+		},
+	},
+	}
+
+	opts := Options{
+		URL:        "http://localhost:8080",
+		Generators: &algo.Generators{},
+	}
+	setupStoreHandlers(&opts, datastore.New)
+	h, err := New(&opts)
+	if err != nil {
+		t.Fatalf("New failed: %v", err)
+	}
+
+	for _, tst := range tests {
+		t.Run(tst.name, func(t *testing.T) {
+			for _, c := range tst.cfg {
+				if err := h.configStore(ctx).CreateRotaConfig(ctx, &c); err != nil {
+					t.Fatalf("%s: CreateRotaConfig(ctx, _) failed: %v", tst.name, err)
+				}
+				defer h.configStore(ctx).DeleteRotaConfig(ctx, c.Config.Name)
+			}
+			tst.ctx.Context = templates.Use(tst.ctx.Context, &templates.Bundle{
+				Loader: templates.FileSystemLoader(templatesLocation),
+			}, nil)
+			if tst.user != "" {
+				tst.ctx.Context = auth.WithState(tst.ctx.Context, &authtest.FakeState{
+					Identity: identity.Identity("user:" + tst.user),
+				})
+			}
+			tst.ctx.Request = httptest.NewRequest("POST", "/deleterota", nil)
+			tst.ctx.Request.Form = url.Values{
+				"name": {tst.rotaName},
+			}
+			h.HandleDeleteRota(tst.ctx)
+
+			recorder := tst.ctx.Writer.(*httptest.ResponseRecorder)
+			if got, want := (recorder.Code != http.StatusOK), tst.fail; got != want {
+				t.Fatalf("%s: HandleDeleteRota(ctx) = %t want: %t , res: %v", tst.name, got, want, recorder.Body)
+			}
+			if recorder.Code != http.StatusOK {
+				return
+			}
+
+			rs, err := h.configStore(ctx).RotaConfig(ctx, "")
+			if err != nil {
+				t.Fatalf("%s: RotaConfig(ctx, %q) failed: %v", tst.name, "", err)
+			}
+			var got []string
+			for _, c := range rs {
+				got = append(got, c.Config.Name)
+			}
+			sort.Strings(got)
+			sort.Strings(tst.want)
+			if diff := pretty.Compare(tst.want, got); diff != "" {
+				t.Errorf("%s: HandleDeleteRota(ctx) differ -want +got, %s", tst.name, diff)
+			}
+
+		})
+	}
+}
diff --git a/cmd/handlers/handle_index.go b/cmd/handlers/handle_index.go
index d58b4d3..a3eb718 100644
--- a/cmd/handlers/handle_index.go
+++ b/cmd/handlers/handle_index.go
@@ -8,7 +8,7 @@
 	"infra/appengine/rotang"
 	"net/http"
 
-	"go.chromium.org/gae/service/user"
+	"go.chromium.org/luci/server/auth"
 	"go.chromium.org/luci/server/router"
 	"go.chromium.org/luci/server/templates"
 	"google.golang.org/grpc/codes"
@@ -27,7 +27,7 @@
 		Oncallers []rotang.ShiftMember
 	}{}
 
-	usr := user.Current(ctx.Context)
+	usr := auth.CurrentUser(ctx.Context)
 	if usr == nil {
 		templates.MustRender(ctx.Context, ctx.Writer, "pages/index.html", templates.Args{"Rotas": res})
 		return
diff --git a/cmd/handlers/handle_index_test.go b/cmd/handlers/handle_index_test.go
index 80c3cc2..ea8ad7e 100644
--- a/cmd/handlers/handle_index_test.go
+++ b/cmd/handlers/handle_index_test.go
@@ -13,8 +13,9 @@
 	"net/http/httptest"
 	"testing"
 
-	"go.chromium.org/gae/service/user"
-
+	"go.chromium.org/luci/auth/identity"
+	"go.chromium.org/luci/server/auth"
+	"go.chromium.org/luci/server/auth/authtest"
 	"go.chromium.org/luci/server/router"
 	"go.chromium.org/luci/server/templates"
 )
@@ -36,8 +37,6 @@
 	ctxCancel, cancel := context.WithCancel(ctx)
 	cancel()
 
-	tt := user.GetTestable(ctx)
-
 	tests := []struct {
 		name       string
 		fail       bool
@@ -136,8 +135,9 @@
 				Loader: templates.FileSystemLoader(templatesLocation),
 			}, nil)
 			if tst.email != "" {
-				tt.Login(tst.email, "", false)
-				defer tt.Logout()
+				tst.ctx.Context = auth.WithState(tst.ctx.Context, &authtest.FakeState{
+					Identity: identity.Identity("user:" + tst.email),
+				})
 			}
 			h.HandleIndex(tst.ctx)
 
diff --git a/cmd/handlers/handle_managerota.go b/cmd/handlers/handle_managerota.go
new file mode 100644
index 0000000..26a32f6
--- /dev/null
+++ b/cmd/handlers/handle_managerota.go
@@ -0,0 +1,43 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package handlers
+
+import (
+	"infra/appengine/rotang"
+	"net/http"
+
+	"go.chromium.org/gae/service/user"
+	"go.chromium.org/luci/server/router"
+	"go.chromium.org/luci/server/templates"
+)
+
+// HandleManageRota handles the rota management interface.
+func (h *State) HandleManageRota(ctx *router.Context) {
+	if err := ctx.Context.Err(); err != nil {
+		http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	usr := user.Current(ctx.Context)
+	if usr == nil {
+		http.Error(ctx.Writer, "not logged in", http.StatusProxyAuthRequired)
+		return
+	}
+
+	rotas, err := h.configStore(ctx.Context).RotaConfig(ctx.Context, "")
+	if err != nil {
+		http.Error(ctx.Writer, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	var permRotas []*rotang.Configuration
+	for _, rota := range rotas {
+		for _, m := range rota.Config.Owners {
+			if usr.Email == m {
+				permRotas = append(permRotas, rota)
+			}
+		}
+	}
+
+	templates.MustRender(ctx.Context, ctx.Writer, "pages/managerota.html", templates.Args{"Rotas": permRotas})
+}
diff --git a/cmd/handlers/handle_managerota_test.go b/cmd/handlers/handle_managerota_test.go
new file mode 100644
index 0000000..9a5db87
--- /dev/null
+++ b/cmd/handlers/handle_managerota_test.go
@@ -0,0 +1,151 @@
+package handlers
+
+import (
+	"infra/appengine/rotang"
+	"infra/appengine/rotang/pkg/algo"
+	"infra/appengine/rotang/pkg/datastore"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"go.chromium.org/gae/service/user"
+	"go.chromium.org/luci/server/router"
+	"go.chromium.org/luci/server/templates"
+	"golang.org/x/net/context"
+)
+
+func TestHandleManageRota(t *testing.T) {
+	ctx := newTestContext()
+	ctxCancel, cancel := context.WithCancel(ctx)
+	cancel()
+
+	tests := []struct {
+		name       string
+		fail       bool
+		ctx        *router.Context
+		cfg        []rotang.Configuration
+		memberPool []rotang.Member
+		user       string
+	}{{
+		name: "Context canceled",
+		fail: true,
+		user: "test@user.com",
+		ctx: &router.Context{
+			Context: ctxCancel,
+			Writer:  httptest.NewRecorder(),
+		},
+	}, {
+		name: "Rotation success",
+		user: "test@user.com",
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		memberPool: []rotang.Member{
+			{
+				Email: "test@user.com",
+			}, {
+				Email: "another@user.com",
+			},
+		},
+		cfg: []rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name:   "Rota one",
+					Owners: []string{"test@user.com"},
+				},
+			}, {
+				Config: rotang.Config{
+					Name:   "Rota Two",
+					Owners: []string{"test@user.com"},
+				},
+			},
+		},
+	}, {
+		name: "Not logged in",
+		fail: true,
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		memberPool: []rotang.Member{
+			{
+				Email: "test@user.com",
+			}, {
+				Email: "another@user.com",
+			},
+		},
+		cfg: []rotang.Configuration{
+			{
+				Config: rotang.Config{
+					Name:   "Rota one",
+					Owners: []string{"test@user.com"},
+				},
+			}, {
+				Config: rotang.Config{
+					Name:   "Rota Two",
+					Owners: []string{"test@user.com"},
+				},
+			},
+		},
+	}, {
+		name: "No rotations",
+		fail: true,
+		user: "test@user.com",
+		ctx: &router.Context{
+			Context: ctx,
+			Writer:  httptest.NewRecorder(),
+		},
+		memberPool: []rotang.Member{
+			{
+				Email: "test@user.com",
+			}, {
+				Email: "another@user.com",
+			},
+		},
+	},
+	}
+
+	opts := Options{
+		URL:        "http://localhost:8080",
+		Generators: &algo.Generators{},
+	}
+	setupStoreHandlers(&opts, datastore.New)
+	h, err := New(&opts)
+	if err != nil {
+		t.Fatalf("New failed: %v", err)
+	}
+
+	tu := user.GetTestable(ctx)
+
+	for _, tst := range tests {
+		t.Run(tst.name, func(t *testing.T) {
+			for _, m := range tst.memberPool {
+				if err := h.memberStore(ctx).CreateMember(ctx, &m); err != nil {
+					t.Fatalf("%s: CreateMember(ctx, _) failed: %v", tst.name, err)
+				}
+				defer h.memberStore(ctx).DeleteMember(ctx, m.Email)
+			}
+			for _, c := range tst.cfg {
+				if err := h.configStore(ctx).CreateRotaConfig(ctx, &c); err != nil {
+					t.Fatalf("%s: CreateRotaConfig(ctx, _) failed: %v", tst.name, err)
+				}
+				defer h.configStore(ctx).DeleteRotaConfig(ctx, c.Config.Name)
+			}
+			tst.ctx.Context = templates.Use(tst.ctx.Context, &templates.Bundle{
+				Loader: templates.FileSystemLoader(templatesLocation),
+			}, nil)
+			if tst.user != "" {
+				tu.Login(tst.user, "", false)
+			}
+			defer tu.Logout()
+
+			h.HandleManageRota(tst.ctx)
+
+			recorder := tst.ctx.Writer.(*httptest.ResponseRecorder)
+			if got, want := (recorder.Code != http.StatusOK), tst.fail; got != want {
+				t.Fatalf("%s: HandleManageRota(ctx) = %t want: %t, res: %v", tst.name, got, want, recorder.Body)
+			}
+		})
+	}
+}
diff --git a/cmd/handlers/templates/pages/index.html b/cmd/handlers/templates/pages/index.html
index 81b31a7..bd14a86 100644
--- a/cmd/handlers/templates/pages/index.html
+++ b/cmd/handlers/templates/pages/index.html
@@ -14,7 +14,7 @@
 </p>
 <a href="upload">Upload Legacy JSON rota configuraton</a><br>
 <a href="list">List configurations in backend store</a><br>
-<a href="createrota">Create a rotation</a><br>
+<a href="managerota">Manage rotations</a><br>
 
 {{if .Rotas}}
 <h2>Rotations you are a member of</h2>
diff --git a/cmd/handlers/templates/pages/managerota.html b/cmd/handlers/templates/pages/managerota.html
new file mode 100644
index 0000000..ac58183
--- /dev/null
+++ b/cmd/handlers/templates/pages/managerota.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<html>
+<head>
+  <title>Modify Rotation</title>
+</head>
+<body>
+
+<h2>Rotations you manage </h2>
+
+{{if not .Rotas}}
+<i>No rotas managed</i>
+{{end}}
+
+{{with .Rotas}}
+<h3>Rota table</h3>
+<table class="table">
+  <thead>
+    <tr>
+      <th>Name</th>
+     </tr>
+  </thead>
+  <tbody>
+  {{range $c := .}}
+     <tr>
+       <td>{{$c.Config.Name}}</td>
+       <td>
+         <form action="modifyrota" method="post">
+            <button type="submit" name="name" value="{{$c.Config.Name}}">Modify</button>
+         </form>
+       </td>
+       <td>
+         <form action="deleterota" method="post">
+            <button type="submit" name="name" value="{{$c.Config.Name}}">Delete</button>
+         </form>
+       </td>
+     </tr>
+   {{end}}
+  </tbody>
+</table>
+{{end}}
+
+<br>
+<br>
+
+<a href="createrota">Create new rotation</a><br>
+
+</body>
+</html>
diff --git a/rotang.go b/rotang.go
index cf8a042..a44e36d 100644
--- a/rotang.go
+++ b/rotang.go
@@ -33,7 +33,7 @@
 	Description    string
 	Calendar       string
 	TokenID        string
-	Owners         []string
+	Owners         []string // TODO(olakar): Change from email to groups.
 	Email          Email
 	DaysToSchedule int
 	Shifts         ShiftConfig