blob: c26c104f24ae083973c6b4b6af555484edd402e7 [file] [log] [blame]
// Copyright 2016 The Go 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 devapp
import (
"fmt"
"html/template"
"io/ioutil"
"net/http"
"sort"
"strings"
"time"
"github.com/google/go-github/github"
"golang.org/x/build/godash"
"golang.org/x/net/context"
)
type pkgLogger interface {
// This needs to be x/net/context because App Engine Standard still runs on
// Go 1.6.
Infof(context.Context, string, ...interface{})
Errorf(context.Context, string, ...interface{})
Criticalf(context.Context, string, ...interface{})
}
var logger pkgLogger
func findEmail(ctx context.Context, data *godash.Data) string {
email := currentUserEmail(ctx)
if email != "" {
return data.Reviewers.Preferred(email)
}
return ""
}
type itemsByMilestone struct {
list []*godash.Item
milestones []string
}
func (x itemsByMilestone) Len() int { return len(x.list) }
func (x itemsByMilestone) Swap(i, j int) { x.list[i], x.list[j] = x.list[j], x.list[i] }
func (x itemsByMilestone) Less(i, j int) bool { return x.index(x.list[i]) < x.index(x.list[j]) }
func (x itemsByMilestone) index(i *godash.Item) int {
if i.Issue == nil {
return len(x.milestones)
}
milestone := i.Issue.Milestone
for i, m := range x.milestones {
if m == milestone {
return i
}
}
return len(x.milestones)
}
type byDate []*github.Milestone
func (x byDate) Len() int { return len(x) }
func (x byDate) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byDate) Less(i, j int) bool {
a, b := x[i].DueOn, x[j].DueOn
if a == nil {
return false
}
if b == nil {
return true
}
return a.Before(*b)
}
func loadData(ctx context.Context) (*godash.Data, error) {
cache, err := getCache(ctx, "gzdata")
if err != nil {
return nil, err
}
return parseData(cache)
}
func datedMilestones(milestones []*github.Milestone) []string {
milestones = append([]*github.Milestone{}, milestones...)
sort.Stable(byDate(milestones))
var names []string
for _, m := range milestones {
if m.Title != nil {
names = append(names, *m.Title)
}
}
return names
}
func parseData(cache *Cache) (*godash.Data, error) {
data := &godash.Data{Reviewers: &godash.Reviewers{}}
return data, unpackCache(cache, &data)
}
func showDash(w http.ResponseWriter, req *http.Request) {
ctx := getContext(req)
req.ParseForm()
data, err := loadData(ctx)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// Load information about logged-in user.
var d display
d.email = findEmail(ctx, data)
d.data = data
d.activeMilestones = data.GetActiveMilestones()
// TODO(quentin): Load the user's preferences into d.pref.
tmpl, err := ioutil.ReadFile("template/dash.html")
if err != nil {
logger.Errorf(ctx, "reading template: %v", err)
return
}
t, err := template.New("main").Funcs(template.FuncMap{
"css": d.css,
"join": d.join,
"mine": d.mine,
"muted": d.muted,
"old": d.old,
"replace": strings.Replace,
"second": d.second,
"short": d.short,
"since": d.since,
"ghemail": d.ghemail,
"release": d.release,
}).Parse(string(tmpl))
if err != nil {
logger.Errorf(ctx, "parsing template: %v", err)
return
}
groups := data.GroupData(true, true)
var filtered []*godash.Group
for _, group := range groups {
if group.Dir == "closed" || group.Dir == "proposal" {
continue
}
sort.Stable(itemsByMilestone{group.Items, datedMilestones(data.Milestones)})
filtered = append(filtered, group)
}
login, err := loginURL(ctx, "/dash")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
logout, err := logoutURL(ctx, "/dash")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
tData := struct {
User string
Now string
Login, Logout string
Dirs []*godash.Group
}{
d.email,
data.Now.UTC().Format(time.UnixDate),
login, logout,
filtered,
}
if err := t.Execute(w, tData); err != nil {
logger.Errorf(ctx, "execute: %v", err)
http.Error(w, "error executing template", 500)
return
}
}
// display holds state needed to compute the displayed HTML.
// The methods here are turned into functions for the template to call.
// Not all methods need the display state; being methods just keeps
// them all in one place.
type display struct {
email string
data *godash.Data
activeMilestones []string
pref UserPref
}
// short returns a shortened email address by removing @domain.
// Input can be string or []string; output is same.
func (d *display) short(s interface{}) interface{} {
switch s := s.(type) {
case string:
return d.data.Reviewers.Shorten(s)
case []string:
v := make([]string, len(s))
for i, t := range s {
v[i] = d.short(t).(string)
}
return v
default:
return s
}
}
// css returns name if cond is true; otherwise it returns the empty string.
// It is intended for use in generating css class names (or not).
func (d *display) css(name string, cond bool) string {
if cond {
return name
}
return ""
}
// old returns css class "old" t is too long ago.
func (d *display) old(t time.Time) string {
return d.css("old", time.Since(t) > 7*24*time.Hour)
}
// join is like strings.Join but takes arguments in the reverse order,
// enabling {{list | join ","}}.
func (d *display) join(sep string, list []string) string {
return strings.Join(list, sep)
}
// since returns the elapsed time since t as a number of days.
func (d *display) since(t time.Time) string {
// NOTE(rsc): Considered changing the unit (hours, days, weeks)
// but that made it harder to scan through the table.
// If it's always days, that's one less thing you have to read.
// Otherwise 1 week might be misread as worse than 6 hours.
dt := time.Since(t)
return fmt.Sprintf("%.1f days ago", float64(dt)/float64(24*time.Hour))
}
// second returns the css class "second" if the index is non-zero
// (so really "second" here means "not first").
func (d *display) second(index int) string {
return d.css("second", index > 0)
}
// mine returns the css class "mine" if the email address is the logged-in user.
// It also returns "unassigned" for an unassigned reviewer.
func (d *display) mine(email string) string {
if long := d.data.Reviewers.Resolve(email); long != "" {
email = long
}
if d.data.Reviewers.Preferred(email) == d.email {
return "mine"
}
if email == "" {
return "unassigned"
}
return ""
}
// ghemail converts a GitHub login name into an e-mail address, or
// "@username" if the e-mail address is unknown.
func (d *display) ghemail(login string) string {
if login == "" {
return login
}
if addr := d.data.Reviewers.ResolveGitHub(login); addr != "" {
return addr
}
return "@" + login
}
// muted returns the css class "muted" if the directory is muted.
func (d *display) muted(dir string) string {
for _, m := range d.pref.Muted {
if m == dir {
return "muted"
}
}
return ""
}
// release returns the css class "release" if the issue is related to the release.
func (d *display) release(milestone string) string {
for _, m := range d.activeMilestones {
if m == milestone {
return "release"
}
}
return ""
}
// UserPref holds user preferences; stored in the datastore under email address.
type UserPref struct {
Muted []string
}