forked from golang/build
/
dash.go
289 lines (255 loc) · 6.99 KB
/
dash.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
// 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"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
"google.golang.org/appengine/user"
)
func findEmail(ctx context.Context, data *godash.Data) string {
u := user.Current(ctx)
if u != nil {
return data.Reviewers.Preferred(u.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 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 loadData(ctx context.Context) (*godash.Data, error) {
cache, err := getCache(ctx, "gzdata")
if err != nil {
return nil, err
}
return parseData(cache)
}
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 := appengine.NewContext(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 {
log.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 {
log.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 := user.LoginURL(ctx, "/dash")
if err != nil {
http.Error(w, err.Error(), 500)
}
logout, err := user.LogoutURL(ctx, "/dash")
if err != nil {
http.Error(w, err.Error(), 500)
}
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 {
log.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
}
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
}