| // Copyright 2012 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 dashboard |
| |
| // This file handles the front page. |
| |
| import ( |
| "bytes" |
| "html/template" |
| "io" |
| "net/http" |
| "strings" |
| "sync" |
| "time" |
| |
| "appengine" |
| "appengine/datastore" |
| "appengine/user" |
| ) |
| |
| func init() { |
| http.HandleFunc("/", handleFront) |
| http.HandleFunc("/favicon.ico", http.NotFound) |
| } |
| |
| // maximum number of active CLs to show in person-specific tables. |
| const maxCLs = 100 |
| |
| func handleFront(w http.ResponseWriter, r *http.Request) { |
| c := appengine.NewContext(r) |
| |
| data := &frontPageData{ |
| Reviewers: personList, |
| User: user.Current(c).Email, |
| IsAdmin: user.IsAdmin(c), |
| } |
| var currentPerson string |
| u := data.User |
| you := "you" |
| if e := r.FormValue("user"); e != "" { |
| u = e |
| you = e |
| } |
| currentPerson, data.UserIsReviewer = emailToPerson[u] |
| if !data.UserIsReviewer { |
| currentPerson = u |
| } |
| |
| var wg sync.WaitGroup |
| errc := make(chan error, 10) |
| activeCLs := datastore.NewQuery("CL"). |
| Filter("Closed =", false). |
| Order("-Modified") |
| |
| tableFetch := func(index int, f func(tbl *clTable) error) { |
| wg.Add(1) |
| go func() { |
| defer wg.Done() |
| start := time.Now() |
| if err := f(&data.Tables[index]); err != nil { |
| errc <- err |
| } |
| data.Timing[index] = time.Now().Sub(start) |
| }() |
| } |
| |
| data.Tables[0].Title = "CLs assigned to " + you + " for review" |
| if data.UserIsReviewer { |
| tableFetch(0, func(tbl *clTable) error { |
| q := activeCLs.Filter("Reviewer =", currentPerson).Limit(maxCLs) |
| tbl.Assignable = true |
| _, err := q.GetAll(c, &tbl.CLs) |
| return err |
| }) |
| } |
| |
| tableFetch(1, func(tbl *clTable) error { |
| q := activeCLs |
| if data.UserIsReviewer { |
| q = q.Filter("Author =", currentPerson) |
| } else { |
| q = q.Filter("Owner =", currentPerson) |
| } |
| q = q.Limit(maxCLs) |
| tbl.Title = "CLs sent by " + you |
| tbl.Assignable = true |
| _, err := q.GetAll(c, &tbl.CLs) |
| return err |
| }) |
| |
| tableFetch(2, func(tbl *clTable) error { |
| q := activeCLs.Limit(50) |
| tbl.Title = "Other active CLs" |
| tbl.Assignable = true |
| if _, err := q.GetAll(c, &tbl.CLs); err != nil { |
| return err |
| } |
| // filter |
| for i := len(tbl.CLs) - 1; i >= 0; i-- { |
| cl := tbl.CLs[i] |
| if cl.Owner == currentPerson || cl.Author == currentPerson || cl.Reviewer == currentPerson { |
| // Preserve order. |
| copy(tbl.CLs[i:], tbl.CLs[i+1:]) |
| tbl.CLs = tbl.CLs[:len(tbl.CLs)-1] |
| } |
| } |
| return nil |
| }) |
| |
| tableFetch(3, func(tbl *clTable) error { |
| q := datastore.NewQuery("CL"). |
| Filter("Closed =", true). |
| Order("-Modified"). |
| Limit(10) |
| tbl.Title = "Recently closed CLs" |
| tbl.Assignable = false |
| _, err := q.GetAll(c, &tbl.CLs) |
| return err |
| }) |
| |
| // Not really a table fetch. |
| tableFetch(0, func(_ *clTable) error { |
| var err error |
| data.LogoutURL, err = user.LogoutURL(c, "/") |
| return err |
| }) |
| |
| wg.Wait() |
| |
| select { |
| case err := <-errc: |
| c.Errorf("%v", err) |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| default: |
| } |
| |
| var b bytes.Buffer |
| if err := frontPage.ExecuteTemplate(&b, "front", &data); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| |
| io.Copy(w, &b) |
| } |
| |
| type frontPageData struct { |
| Tables [4]clTable |
| Timing [4]time.Duration |
| |
| Reviewers []string |
| UserIsReviewer bool |
| |
| User, LogoutURL string // actual logged in user |
| IsAdmin bool |
| } |
| |
| type clTable struct { |
| Title string |
| Assignable bool |
| CLs []*CL |
| } |
| |
| var frontPage = template.Must(template.New("front").Funcs(template.FuncMap{ |
| "selected": func(a, b string) string { |
| if a == b { |
| return "selected" |
| } |
| return "" |
| }, |
| "shortemail": func(s string) string { |
| if i := strings.Index(s, "@"); i >= 0 { |
| s = s[:i] |
| } |
| return s |
| }, |
| }).Parse(` |
| <!doctype html> |
| <html> |
| <head> |
| <title>Go code reviews</title> |
| <link rel="icon" type="image/png" href="/static/icon.png" /> |
| <style type="text/css"> |
| body { |
| font-family: Helvetica, sans-serif; |
| } |
| img#gopherstamp { |
| float: right; |
| height: auto; |
| width: 250px; |
| } |
| h1, h2, h3 { |
| color: #777; |
| margin-bottom: 0; |
| } |
| table { |
| border-spacing: 0; |
| } |
| td { |
| vertical-align: top; |
| padding: 2px 5px; |
| } |
| tr.unreplied td.email { |
| border-left: 2px solid blue; |
| } |
| tr.pending td { |
| background: #fc8; |
| } |
| tr.failed td { |
| background: #f88; |
| } |
| tr.saved td { |
| background: #8f8; |
| } |
| .cls { |
| margin-top: 0; |
| } |
| a { |
| color: blue; |
| text-decoration: none; /* no link underline */ |
| } |
| address { |
| font-size: 10px; |
| text-align: right; |
| } |
| .email { |
| font-family: monospace; |
| } |
| </style> |
| <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> |
| </head> |
| <body> |
| |
| <img id="gopherstamp" src="/static/gopherstamp.jpg" /> |
| <h1>Go code reviews</h1> |
| |
| <table class="cls"> |
| {{range $i, $tbl := .Tables}} |
| <tr><td colspan="5"><h3>{{$tbl.Title}}</h3></td></tr> |
| {{if .CLs}} |
| {{range $cl := .CLs}} |
| <tr id="cl-{{$cl.Number}}" class="{{if not $i}}{{if not .Reviewed}}unreplied{{end}}{{end}}"> |
| <td class="email">{{$cl.DisplayOwner}}</td> |
| <td> |
| {{if $tbl.Assignable}} |
| <select id="cl-rev-{{$cl.Number}}" {{if not $.UserIsReviewer}}disabled{{end}}> |
| <option></option> |
| {{range $.Reviewers}} |
| <option {{selected . $cl.Reviewer}}>{{.}}</option> |
| {{end}} |
| </select> |
| <script type="text/javascript"> |
| $(function() { |
| $('#cl-rev-{{$cl.Number}}').change(function() { |
| var r = $(this).val(); |
| var row = $('tr#cl-{{$cl.Number}}'); |
| row.addClass('pending'); |
| $.post('/assign', { |
| 'cl': '{{$cl.Number}}', |
| 'r': r |
| }).success(function() { |
| row.removeClass('pending'); |
| row.addClass('saved'); |
| }).error(function() { |
| row.removeClass('pending'); |
| row.addClass('failed'); |
| }); |
| }); |
| }); |
| </script> |
| {{end}} |
| </td> |
| <td> |
| <a href="http://codereview.appspot.com/{{.Number}}/" title="{{ printf "%s" .Description}}">{{.Number}}: {{.FirstLineHTML}}</a> |
| {{if and .LGTMs $tbl.Assignable}}<br /><span style="font-size: smaller;">LGTMs: {{.LGTMHTML}}</span>{{end}} |
| {{if and .NotLGTMs $tbl.Assignable}}<br /><span style="font-size: smaller; color: #f74545;">NOT LGTMs: {{.NotLGTMHTML}}</span>{{end}} |
| {{if .LastUpdateBy}}<br /><span style="font-size: smaller; color: #777777;">(<span title="{{.LastUpdateBy}}">{{.LastUpdateBy | shortemail}}</span>) {{.LastUpdate}}</span>{{end}} |
| </td> |
| <td title="Last modified">{{.ModifiedAgo}}</td> |
| <td>{{if $.IsAdmin}}<a href="/update-cl?cl={{.Number}}" title="Update this CL">⟳</a>{{end}}</td> |
| </tr> |
| {{end}} |
| {{else}} |
| <tr><td colspan="5"><em>none</em></td></tr> |
| {{end}} |
| {{end}} |
| </table> |
| |
| <hr /> |
| <address> |
| You are <span class="email">{{.User}}</span> · <a href="{{.LogoutURL}}">logout</a><br /> |
| datastore timing: {{range .Timing}} {{.}}{{end}} |
| </address> |
| |
| </body> |
| </html> |
| `)) |