BugChomper utility - rewrite in Go

BUG=skia:
R=jcgregorio@google.com

Author: borenet@google.com

Review URL: https://codereview.chromium.org/274693002

git-svn-id: http://skia.googlecode.com/svn/trunk@14715 2bbb7eff-a529-9590-31e7-b0007b416f81
This commit is contained in:
commit-bot@chromium.org 2014-05-13 18:03:45 +00:00
parent 90bebbb9ed
commit 5132461627
9 changed files with 1219 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,72 @@
table#buglist {
border-collapse: collapse;
border-style: solid;
border-color: rgba(0, 0, 0, 1.0);
border-width: 3px;
width: 80%;
margin-left: 10%;
margin-right: 10%;
}
tr {
border-color: rgba(0, 0, 0, 1.0);
border-style: dashed;
border-width: 1px 3px;
}
tr.priority_Critical {
background-color: rgba(255, 0, 0, 0.3);
}
tr.priority_High {
background-color: rgba(255, 165, 0, 0.3);
}
tr.priority_Medium {
background-color: rgba(255, 255, 0, 0.3);
}
tr.priority_Low {
background-color: rgba(0, 255, 0, 0.3);
}
tr.priority_Never {
background-color: rgba(190, 190, 190, 0.3);
}
tbody {
background-color: rgba(190, 190, 190, 0.1);
}
tr.priority_row {
background-color: rgba(190, 190, 190, 0.1);
border-style: solid;
}
tr.tr_head {
background-color: rgba(190, 190, 190, 0.5);
}
#table_header {
text-align: center;
}
td {
padding: 5px;
}
td.priority_td {
text-align: center;
}
a {
color: black;
}
a:visited {
color: black;
}
a:hover {
text-decoration: none;
}

View File

@ -0,0 +1,314 @@
/**
* TableDnD plug-in for JQuery, allows you to drag and drop table rows
* You can set up various options to control how the system will work
* Copyright © Denis Howlett <denish@isocra.com>
* Licensed like jQuery, see http://docs.jquery.com/License.
*
* Configuration options:
*
* onDragStyle
* This is the style that is assigned to the row during drag. There are limitations to the styles that can be
* associated with a row (such as you can't assign a border—well you can, but it won't be
* displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
* a map (as used in the jQuery css(...) function).
* onDropStyle
* This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
* to what you can do. Also this replaces the original style, so again consider using onDragClass which
* is simply added and then removed on drop.
* onDragClass
* This class is added for the duration of the drag and then removed when the row is dropped. It is more
* flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
* is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
* stylesheet.
* onDrop
* Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
* and the row that was dropped. You can work out the new order of the rows by using
* table.rows.
* onDragStart
* Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
* table and the row which the user has started to drag.
* onAllowDrop
* Pass a function that will be called as a row is over another row. If the function returns true, allow
* dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
* the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
* scrollAmount
* This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
* window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
* FF3 beta)
*
* Other ways to control behaviour:
*
* Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
* that you don't want to be draggable.
*
* Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
* <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
* an ID as must all the rows.
*
* Known problems:
* - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
*
* Version 0.2: 2008-02-20 First public version
* Version 0.3: 2008-02-07 Added onDragStart option
* Made the scroll amount configurable (default is 5 as before)
* Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
* Added onAllowDrop to control dropping
* Fixed a bug which meant that you couldn't set the scroll amount in both directions
* Added serialise method
*/
jQuery.tableDnD = {
/** Keep hold of the current table being dragged */
currentTable : null,
/** Keep hold of the current drag object if any */
dragObject: null,
/** The current mouse offset */
mouseOffset: null,
/** Remember the old value of Y so that we don't do too much processing */
oldY: 0,
/** Actually build the structure */
build: function(options) {
// Make sure options exists
options = options || {};
// Set up the defaults if any
this.each(function() {
// Remember the options
this.tableDnDConfig = {
onDragStyle: options.onDragStyle,
onDropStyle: options.onDropStyle,
// Add in the default class for whileDragging
onDragClass: options.onDragClass ? options.onDragClass : "tDnD_whileDrag",
onDrop: options.onDrop,
onDragStart: options.onDragStart,
scrollAmount: options.scrollAmount ? options.scrollAmount : 5
};
// Now make the rows draggable
jQuery.tableDnD.makeDraggable(this);
});
// Now we need to capture the mouse up and mouse move event
// We can use bind so that we don't interfere with other event handlers
jQuery(document)
.bind('mousemove', jQuery.tableDnD.mousemove)
.bind('mouseup', jQuery.tableDnD.mouseup);
// Don't break the chain
return this;
},
/** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
makeDraggable: function(table) {
// Now initialise the rows
var rows = table.rows; //getElementsByTagName("tr")
var config = table.tableDnDConfig;
for (var i=0; i<rows.length; i++) {
// To make non-draggable rows, add the nodrag class (eg for Category and Header rows)
// inspired by John Tarr and Famic
var nodrag = $(rows[i]).hasClass("nodrag");
if (! nodrag) { //There is no NoDnD attribute on rows I want to drag
jQuery(rows[i]).mousedown(function(ev) {
if (ev.target.tagName == "TD") {
jQuery.tableDnD.dragObject = this;
jQuery.tableDnD.currentTable = table;
jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
if (config.onDragStart) {
// Call the onDrop method if there is one
config.onDragStart(table, this);
}
return false;
}
}).css("cursor", "move"); // Store the tableDnD object
}
}
},
/** Get the mouse coordinates from the event (allowing for browser differences) */
mouseCoords: function(ev){
if(ev.pageX || ev.pageY){
return {x:ev.pageX, y:ev.pageY};
}
return {
x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
y:ev.clientY + document.body.scrollTop - document.body.clientTop
};
},
/** Given a target element and a mouse event, get the mouse offset from that element.
To do this we need the element's position and the mouse position */
getMouseOffset: function(target, ev) {
ev = ev || window.event;
var docPos = this.getPosition(target);
var mousePos = this.mouseCoords(ev);
return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
},
/** Get the position of an element by going up the DOM tree and adding up all the offsets */
getPosition: function(e){
var left = 0;
var top = 0;
/** Safari fix -- thanks to Luis Chato for this! */
if (e.offsetHeight == 0) {
/** Safari 2 doesn't correctly grab the offsetTop of a table row
this is detailed here:
http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
note that firefox will return a text node as a first child, so designing a more thorough
solution may need to take that into account, for now this seems to work in firefox, safari, ie */
e = e.firstChild; // a table cell
}
while (e.offsetParent){
left += e.offsetLeft;
top += e.offsetTop;
e = e.offsetParent;
}
left += e.offsetLeft;
top += e.offsetTop;
return {x:left, y:top};
},
mousemove: function(ev) {
if (jQuery.tableDnD.dragObject == null) {
return;
}
var dragObj = jQuery(jQuery.tableDnD.dragObject);
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
var mousePos = jQuery.tableDnD.mouseCoords(ev);
var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
//auto scroll the window
var yOffset = window.pageYOffset;
if (document.all) {
// Windows version
//yOffset=document.body.scrollTop;
if (typeof document.compatMode != 'undefined' &&
document.compatMode != 'BackCompat') {
yOffset = document.documentElement.scrollTop;
}
else if (typeof document.body != 'undefined') {
yOffset=document.body.scrollTop;
}
}
if (mousePos.y-yOffset < config.scrollAmount) {
window.scrollBy(0, -config.scrollAmount);
} else {
var windowHeight = window.innerHeight ? window.innerHeight
: document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
window.scrollBy(0, config.scrollAmount);
}
}
if (y != jQuery.tableDnD.oldY) {
// work out if we're going up or down...
var movingDown = y > jQuery.tableDnD.oldY;
// update the old value
jQuery.tableDnD.oldY = y;
// update the style to show we're dragging
if (config.onDragClass) {
dragObj.addClass(config.onDragClass);
} else {
dragObj.css(config.onDragStyle);
}
// If we're over a row then move the dragged row to there so that the user sees the
// effect dynamically
var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
if (currentRow) {
// TODO worry about what happens when there are multiple TBODIES
if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
} else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
}
}
}
return false;
},
/** We're only worried about the y position really, because we can only move rows up and down */
findDropTargetRow: function(draggedRow, y) {
var rows = jQuery.tableDnD.currentTable.rows;
for (var i=0; i<rows.length; i++) {
var row = rows[i];
var rowY = this.getPosition(row).y;
var rowHeight = parseInt(row.offsetHeight)/2;
if (row.offsetHeight == 0) {
rowY = this.getPosition(row.firstChild).y;
rowHeight = parseInt(row.firstChild.offsetHeight)/2;
}
// Because we always have to insert before, we need to offset the height a bit
if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
// that's the row we're over
// If it's the same as the current row, ignore it
if (row == draggedRow) {return null;}
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
if (config.onAllowDrop) {
if (config.onAllowDrop(draggedRow, row)) {
return row;
} else {
return null;
}
} else {
// If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
var nodrop = $(row).hasClass("nodrop");
if (! nodrop) {
return row;
} else {
return null;
}
}
return row;
}
}
return null;
},
mouseup: function(e) {
if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
var droppedRow = jQuery.tableDnD.dragObject;
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
// If we have a dragObject, then we need to release it,
// The row will already have been moved to the right place so we just reset stuff
if (config.onDragClass) {
jQuery(droppedRow).removeClass(config.onDragClass);
} else {
jQuery(droppedRow).css(config.onDropStyle);
}
jQuery.tableDnD.dragObject = null;
if (config.onDrop) {
// Call the onDrop method if there is one
config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
}
jQuery.tableDnD.currentTable = null; // let go of the table too
}
},
serialize: function() {
if (jQuery.tableDnD.currentTable) {
var result = "";
var tableId = jQuery.tableDnD.currentTable.id;
var rows = jQuery.tableDnD.currentTable.rows;
for (var i=0; i<rows.length; i++) {
if (result.length > 0) result += "&";
result += tableId + '[]=' + rows[i].id;
}
return result;
} else {
return "Error: No Table id set, you need to set an id on your table and every row";
}
}
}
jQuery.fn.extend(
{
tableDnD : jQuery.tableDnD.build
}
);

11
tools/bug_chomper/run_server.sh Executable file
View File

@ -0,0 +1,11 @@
if [[ -z `which go` ]]; then
echo "Please install Go before running the server."
exit 1
fi
go get github.com/gorilla/securecookie
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd $DIR
GOPATH="$GOPATH:$DIR" go run $DIR/src/server/server.go $@

View File

@ -0,0 +1,303 @@
// Copyright (c) 2014 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.
/*
Utilities for interacting with the GoogleCode issue tracker.
Example usage:
issueTracker := issue_tracker.MakeIssueTraker(myOAuthConfigFile)
authURL := issueTracker.MakeAuthRequestURL()
// Visit the authURL to obtain an authorization code.
issueTracker.UpgradeCode(code)
// Now issueTracker can be used to retrieve and edit issues.
*/
package issue_tracker
import (
"bytes"
"code.google.com/p/goauth2/oauth"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
)
// BugPriorities are the possible values for "Priority-*" labels for issues.
var BugPriorities = []string{"Critical", "High", "Medium", "Low", "Never"}
var apiScope = []string{
"https://www.googleapis.com/auth/projecthosting",
"https://www.googleapis.com/auth/userinfo.email",
}
const issueApiURL = "https://www.googleapis.com/projecthosting/v2/projects/"
const issueURL = "https://code.google.com/p/skia/issues/detail?id="
const personApiURL = "https://www.googleapis.com/userinfo/v2/me"
// Enum for determining whether a label has been added, removed, or is
// unchanged.
const (
labelAdded = iota
labelRemoved
labelUnchanged
)
// loadOAuthConfig reads the OAuth given config file path and returns an
// appropriate oauth.Config.
func loadOAuthConfig(oauthConfigFile string) (*oauth.Config, error) {
errFmt := "failed to read OAuth config file: %s"
fileContents, err := ioutil.ReadFile(oauthConfigFile)
if err != nil {
return nil, fmt.Errorf(errFmt, err)
}
var decodedJson map[string]struct {
AuthURL string `json:"auth_uri"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
TokenURL string `json:"token_uri"`
}
if err := json.Unmarshal(fileContents, &decodedJson); err != nil {
return nil, fmt.Errorf(errFmt, err)
}
config, ok := decodedJson["web"]
if !ok {
return nil, fmt.Errorf(errFmt, err)
}
return &oauth.Config{
ClientId: config.ClientId,
ClientSecret: config.ClientSecret,
Scope: strings.Join(apiScope, " "),
AuthURL: config.AuthURL,
TokenURL: config.TokenURL,
}, nil
}
// Issue contains information about an issue.
type Issue struct {
Id int `json:"id"`
Project string `json:"projectId"`
Title string `json:"title"`
Labels []string `json:"labels"`
}
// URL returns the URL of a given issue.
func (i Issue) URL() string {
return issueURL + strconv.Itoa(i.Id)
}
// IssueList represents a list of issues from the IssueTracker.
type IssueList struct {
TotalResults int `json:"totalResults"`
Items []*Issue `json:"items"`
}
// IssueTracker is the primary point of contact with the issue tracker,
// providing methods for authenticating to and interacting with it.
type IssueTracker struct {
OAuthConfig *oauth.Config
OAuthTransport *oauth.Transport
}
// MakeIssueTracker creates and returns an IssueTracker with authentication
// configuration from the given authConfigFile.
func MakeIssueTracker(authConfigFile string, redirectURL string) (*IssueTracker, error) {
oauthConfig, err := loadOAuthConfig(authConfigFile)
if err != nil {
return nil, fmt.Errorf(
"failed to create IssueTracker: %s", err)
}
oauthConfig.RedirectURL = redirectURL
return &IssueTracker{
OAuthConfig: oauthConfig,
OAuthTransport: &oauth.Transport{Config: oauthConfig},
}, nil
}
// MakeAuthRequestURL returns an authentication request URL which can be used
// to obtain an authorization code via user sign-in.
func (it IssueTracker) MakeAuthRequestURL() string {
// NOTE: Need to add XSRF protection if we ever want to run this on a public
// server.
return it.OAuthConfig.AuthCodeURL(it.OAuthConfig.RedirectURL)
}
// IsAuthenticated determines whether the IssueTracker has sufficient
// permissions to retrieve and edit Issues.
func (it IssueTracker) IsAuthenticated() bool {
return it.OAuthTransport.Token != nil
}
// UpgradeCode exchanges the single-use authorization code, obtained by
// following the URL obtained from IssueTracker.MakeAuthRequestURL, for a
// multi-use, session token. This is required before IssueTracker can retrieve
// and edit issues.
func (it *IssueTracker) UpgradeCode(code string) error {
token, err := it.OAuthTransport.Exchange(code)
if err == nil {
it.OAuthTransport.Token = token
return nil
} else {
return fmt.Errorf(
"failed to exchange single-user auth code: %s", err)
}
}
// GetLoggedInUser retrieves the email address of the authenticated user.
func (it IssueTracker) GetLoggedInUser() (string, error) {
errFmt := "error retrieving user email: %s"
if !it.IsAuthenticated() {
return "", fmt.Errorf(errFmt, "User is not authenticated!")
}
resp, err := it.OAuthTransport.Client().Get(personApiURL)
if err != nil {
return "", fmt.Errorf(errFmt, err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf(errFmt, fmt.Sprintf(
"user data API returned code %d: %v", resp.StatusCode, string(body)))
}
userInfo := struct {
Email string `json:"email"`
}{}
if err := json.Unmarshal(body, &userInfo); err != nil {
return "", fmt.Errorf(errFmt, err)
}
return userInfo.Email, nil
}
// GetBug retrieves the Issue with the given ID from the IssueTracker.
func (it IssueTracker) GetBug(project string, id int) (*Issue, error) {
errFmt := fmt.Sprintf("error retrieving issue %d: %s", id, "%s")
if !it.IsAuthenticated() {
return nil, fmt.Errorf(errFmt, "user is not authenticated!")
}
requestURL := issueApiURL + project + "/issues/" + strconv.Itoa(id)
resp, err := it.OAuthTransport.Client().Get(requestURL)
if err != nil {
return nil, fmt.Errorf(errFmt, err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(errFmt, fmt.Sprintf(
"issue tracker returned code %d:%v", resp.StatusCode, string(body)))
}
var issue Issue
if err := json.Unmarshal(body, &issue); err != nil {
return nil, fmt.Errorf(errFmt, err)
}
return &issue, nil
}
// GetBugs retrieves all Issues with the given owner from the IssueTracker,
// returning an IssueList.
func (it IssueTracker) GetBugs(project string, owner string) (*IssueList, error) {
errFmt := "error retrieving issues: %s"
if !it.IsAuthenticated() {
return nil, fmt.Errorf(errFmt, "user is not authenticated!")
}
params := map[string]string{
"owner": url.QueryEscape(owner),
"can": "open",
"maxResults": "9999",
}
requestURL := issueApiURL + project + "/issues?"
first := true
for k, v := range params {
if first {
first = false
} else {
requestURL += "&"
}
requestURL += k + "=" + v
}
resp, err := it.OAuthTransport.Client().Get(requestURL)
if err != nil {
return nil, fmt.Errorf(errFmt, err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(errFmt, fmt.Sprintf(
"issue tracker returned code %d:%v", resp.StatusCode, string(body)))
}
var bugList IssueList
if err := json.Unmarshal(body, &bugList); err != nil {
return nil, fmt.Errorf(errFmt, err)
}
return &bugList, nil
}
// SubmitIssueChanges creates a comment on the given Issue which modifies it
// according to the contents of the passed-in Issue struct.
func (it IssueTracker) SubmitIssueChanges(issue *Issue, comment string) error {
errFmt := "Error updating issue " + strconv.Itoa(issue.Id) + ": %s"
if !it.IsAuthenticated() {
return fmt.Errorf(errFmt, "user is not authenticated!")
}
oldIssue, err := it.GetBug(issue.Project, issue.Id)
if err != nil {
return fmt.Errorf(errFmt, err)
}
postData := struct {
Content string `json:"content"`
Updates struct {
Title *string `json:"summary"`
Labels []string `json:"labels"`
} `json:"updates"`
}{
Content: comment,
}
if issue.Title != oldIssue.Title {
postData.Updates.Title = &issue.Title
}
// TODO(borenet): Add other issue attributes, eg. Owner.
labels := make(map[string]int)
for _, label := range issue.Labels {
labels[label] = labelAdded
}
for _, label := range oldIssue.Labels {
if _, ok := labels[label]; ok {
labels[label] = labelUnchanged
} else {
labels[label] = labelRemoved
}
}
labelChanges := make([]string, 0)
for labelName, present := range labels {
if present == labelRemoved {
labelChanges = append(labelChanges, "-"+labelName)
} else if present == labelAdded {
labelChanges = append(labelChanges, labelName)
}
}
if len(labelChanges) > 0 {
postData.Updates.Labels = labelChanges
}
postBytes, err := json.Marshal(&postData)
if err != nil {
return fmt.Errorf(errFmt, err)
}
requestURL := issueApiURL + issue.Project + "/issues/" +
strconv.Itoa(issue.Id) + "/comments"
resp, err := it.OAuthTransport.Client().Post(
requestURL, "application/json", bytes.NewReader(postBytes))
if err != nil {
return fmt.Errorf(errFmt, err)
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf(errFmt, fmt.Sprintf(
"Issue tracker returned code %d:%v", resp.StatusCode, string(body)))
}
return nil
}

View File

@ -0,0 +1,376 @@
// Copyright (c) 2014 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.
/*
Serves a webpage for easy management of Skia bugs.
WARNING: This server is NOT secure and should not be made publicly
accessible.
*/
package main
import (
"encoding/json"
"flag"
"fmt"
"html/template"
"issue_tracker"
"log"
"net/http"
"net/url"
"path"
"path/filepath"
"strconv"
"strings"
"time"
)
import "github.com/gorilla/securecookie"
const certFile = "certs/cert.pem"
const keyFile = "certs/key.pem"
const issueComment = "Edited by BugChomper"
const oauthCallbackPath = "/oauth2callback"
const oauthConfigFile = "oauth_client_secret.json"
const defaultPort = 8000
const localHost = "127.0.0.1"
const maxSessionLen = time.Duration(3600 * time.Second)
const priorityPrefix = "Priority-"
const project = "skia"
const cookieName = "BugChomperCookie"
var scheme = "http"
var curdir, _ = filepath.Abs(".")
var templatePath, _ = filepath.Abs("templates")
var templates = template.Must(template.ParseFiles(
path.Join(templatePath, "bug_chomper.html"),
path.Join(templatePath, "submitted.html"),
path.Join(templatePath, "error.html")))
var hashKey = securecookie.GenerateRandomKey(32)
var blockKey = securecookie.GenerateRandomKey(32)
var secureCookie = securecookie.New(hashKey, blockKey)
// SessionState contains data for a given session.
type SessionState struct {
IssueTracker *issue_tracker.IssueTracker
OrigRequestURL string
SessionStart time.Time
}
// getAbsoluteURL returns the absolute URL of the given Request.
func getAbsoluteURL(r *http.Request) string {
return scheme + "://" + r.Host + r.URL.Path
}
// getOAuth2CallbackURL returns a callback URL to be used by the OAuth2 login
// page.
func getOAuth2CallbackURL(r *http.Request) string {
return scheme + "://" + r.Host + oauthCallbackPath
}
func saveSession(session *SessionState, w http.ResponseWriter, r *http.Request) error {
encodedSession, err := secureCookie.Encode(cookieName, session)
if err != nil {
return fmt.Errorf("unable to encode session state: %s", err)
}
cookie := &http.Cookie{
Name: cookieName,
Value: encodedSession,
Domain: strings.Split(r.Host, ":")[0],
Path: "/",
HttpOnly: true,
}
http.SetCookie(w, cookie)
return nil
}
// makeSession creates a new session for the Request.
func makeSession(w http.ResponseWriter, r *http.Request) (*SessionState, error) {
log.Println("Creating new session.")
// Create the session state.
issueTracker, err := issue_tracker.MakeIssueTracker(
oauthConfigFile, getOAuth2CallbackURL(r))
if err != nil {
return nil, fmt.Errorf("unable to create IssueTracker for session: %s", err)
}
session := SessionState{
IssueTracker: issueTracker,
OrigRequestURL: getAbsoluteURL(r),
SessionStart: time.Now(),
}
// Encode and store the session state.
if err := saveSession(&session, w, r); err != nil {
return nil, err
}
return &session, nil
}
// getSession retrieves the active SessionState or creates and returns a new
// SessionState.
func getSession(w http.ResponseWriter, r *http.Request) (*SessionState, error) {
cookie, err := r.Cookie(cookieName)
if err != nil {
log.Println("No cookie found! Starting new session.")
return makeSession(w, r)
}
var session SessionState
if err := secureCookie.Decode(cookieName, cookie.Value, &session); err != nil {
log.Printf("Invalid or corrupted session. Starting another: %s", err.Error())
return makeSession(w, r)
}
currentTime := time.Now()
if currentTime.Sub(session.SessionStart) > maxSessionLen {
log.Printf("Session starting at %s is expired. Starting another.",
session.SessionStart.Format(time.RFC822))
return makeSession(w, r)
}
saveSession(&session, w, r)
return &session, nil
}
// reportError serves the error page with the given message.
func reportError(w http.ResponseWriter, msg string, code int) {
errData := struct {
Code int
CodeString string
Message string
}{
Code: code,
CodeString: http.StatusText(code),
Message: msg,
}
w.WriteHeader(code)
err := templates.ExecuteTemplate(w, "error.html", errData)
if err != nil {
log.Println("Failed to display error.html!!")
}
}
// makeBugChomperPage builds and serves the BugChomper page.
func makeBugChomperPage(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r)
if err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
return
}
issueTracker := session.IssueTracker
user, err := issueTracker.GetLoggedInUser()
if err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
return
}
log.Println("Loading bugs for " + user)
bugList, err := issueTracker.GetBugs(project, user)
if err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
return
}
bugsById := make(map[string]*issue_tracker.Issue)
bugsByPriority := make(map[string][]*issue_tracker.Issue)
for _, bug := range bugList.Items {
bugsById[strconv.Itoa(bug.Id)] = bug
var bugPriority string
for _, label := range bug.Labels {
if strings.HasPrefix(label, priorityPrefix) {
bugPriority = label[len(priorityPrefix):]
}
}
if _, ok := bugsByPriority[bugPriority]; !ok {
bugsByPriority[bugPriority] = make(
[]*issue_tracker.Issue, 0)
}
bugsByPriority[bugPriority] = append(
bugsByPriority[bugPriority], bug)
}
bugsJson, err := json.Marshal(bugsById)
if err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
return
}
data := struct {
Title string
User string
BugsJson template.JS
BugsByPriority *map[string][]*issue_tracker.Issue
Priorities []string
PriorityPrefix string
}{
Title: "BugChomper",
User: user,
BugsJson: template.JS(string(bugsJson)),
BugsByPriority: &bugsByPriority,
Priorities: issue_tracker.BugPriorities,
PriorityPrefix: priorityPrefix,
}
if err := templates.ExecuteTemplate(w, "bug_chomper.html", data); err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
return
}
}
// authIfNeeded determines whether the current user is logged in. If not, it
// redirects to a login page. Returns true if the user is redirected and false
// otherwise.
func authIfNeeded(w http.ResponseWriter, r *http.Request) bool {
session, err := getSession(w, r)
if err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
return false
}
issueTracker := session.IssueTracker
if !issueTracker.IsAuthenticated() {
loginURL := issueTracker.MakeAuthRequestURL()
log.Println("Redirecting for login:", loginURL)
http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
return true
}
return false
}
// submitData attempts to submit data from a POST request to the IssueTracker.
func submitData(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r)
if err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
return
}
issueTracker := session.IssueTracker
edits := r.FormValue("all_edits")
var editsMap map[string]*issue_tracker.Issue
if err := json.Unmarshal([]byte(edits), &editsMap); err != nil {
errMsg := "Could not parse edits from form response: " + err.Error()
reportError(w, errMsg, http.StatusInternalServerError)
return
}
data := struct {
Title string
Message string
BackLink string
}{}
if len(editsMap) == 0 {
data.Title = "No Changes Submitted"
data.Message = "You didn't change anything!"
data.BackLink = ""
if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
return
}
return
}
errorList := make([]error, 0)
for issueId, newIssue := range editsMap {
log.Println("Editing issue " + issueId)
if err := issueTracker.SubmitIssueChanges(newIssue, issueComment); err != nil {
errorList = append(errorList, err)
}
}
if len(errorList) > 0 {
errorStrings := ""
for _, err := range errorList {
errorStrings += err.Error() + "\n"
}
errMsg := "Not all changes could be submitted: \n" + errorStrings
reportError(w, errMsg, http.StatusInternalServerError)
return
}
data.Title = "Submitted Changes"
data.Message = "Your changes were submitted to the issue tracker."
data.BackLink = ""
if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
return
}
return
}
// handleBugChomper handles HTTP requests for the bug_chomper page.
func handleBugChomper(w http.ResponseWriter, r *http.Request) {
if authIfNeeded(w, r) {
return
}
switch r.Method {
case "GET":
makeBugChomperPage(w, r)
case "POST":
submitData(w, r)
}
}
// handleOAuth2Callback handles callbacks from the OAuth2 sign-in.
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r)
if err != nil {
reportError(w, err.Error(), http.StatusInternalServerError)
}
issueTracker := session.IssueTracker
invalidLogin := "Invalid login credentials"
params, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
reportError(w, invalidLogin+": "+err.Error(), http.StatusForbidden)
return
}
code, ok := params["code"]
if !ok {
reportError(w, invalidLogin+": redirect did not include auth code.",
http.StatusForbidden)
return
}
log.Println("Upgrading auth token:", code[0])
if err := issueTracker.UpgradeCode(code[0]); err != nil {
errMsg := "failed to upgrade token: " + err.Error()
reportError(w, errMsg, http.StatusForbidden)
return
}
if err := saveSession(session, w, r); err != nil {
reportError(w, "failed to save session: "+err.Error(),
http.StatusInternalServerError)
return
}
http.Redirect(w, r, session.OrigRequestURL, http.StatusTemporaryRedirect)
return
}
// handleRoot is the handler function for all HTTP requests at the root level.
func handleRoot(w http.ResponseWriter, r *http.Request) {
log.Println("Fetching " + r.URL.Path)
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
handleBugChomper(w, r)
return
}
http.NotFound(w, r)
}
// Run the BugChomper server.
func main() {
var public bool
flag.BoolVar(
&public, "public", false, "Make this server publicly accessible.")
flag.Parse()
http.HandleFunc("/", handleRoot)
http.HandleFunc(oauthCallbackPath, handleOAuth2Callback)
http.Handle("/res/", http.FileServer(http.Dir(curdir)))
port := ":" + strconv.Itoa(defaultPort)
log.Println("Server is running at " + scheme + "://" + localHost + port)
var err error
if public {
log.Println("WARNING: This server is not secure and should not be made " +
"publicly accessible.")
scheme = "https"
err = http.ListenAndServeTLS(port, certFile, keyFile, nil)
} else {
scheme = "http"
err = http.ListenAndServe(localHost+port, nil)
}
if err != nil {
log.Println(err.Error())
}
}

View File

@ -0,0 +1,118 @@
<html>
<head>
<title>{{.Title}}</title>
<link rel="stylesheet" type="text/css" href="res/style.css" />
<link rel="icon" type="image/ico" href="res/favicon.ico" />
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script type="text/javascript" src="res/third_party/jquery.tablednd.js"></script>
<script type="text/javascript">
"use strict";
var issues = {{.BugsJson}};
var edited = {};
function edit_label(bug_id, old_value, new_value) {
console.log("issue[" + bug_id + "]: " + old_value + " -> " + new_value);
if (!edited[bug_id]) {
edited[bug_id] = JSON.parse(JSON.stringify(issues[bug_id]));
}
var old_index = edited[bug_id]["labels"].indexOf(old_value);
if (old_index > -1) {
edited[bug_id]["labels"][old_index] = new_value;
} else {
edited[bug_id]["labels"].push(new_value)
}
if (JSON.stringify(issues[bug_id]) == JSON.stringify(edited[bug_id])) {
console.log("Not changing " + bug_id);
delete edited[bug_id]
}
document.getElementById("all_edits").value = JSON.stringify(edited);
}
</script>
</head>
<body>
<h1>BugChomper</h1>
<form method="post">
<input type="hidden" name="all_edits" id="all_edits" value="{}" />
<input type="submit" value="Submit changes to issue tracker" />
</form>
<table id="buglist">
<thead>
<tr id="table_header" class="nodrag tr_head">
<td colspan=3><h2>Open bugs for {{.User}}</h2></td>
</tr>
<tr id="table_subheader" class="nodrag tr_head">
<td>ID</td>
<td>Priority</td>
<td>Title</td>
</tr>
</thead>
<tbody>
{{with $all_data := .}}
{{range $index, $priority := index $all_data.Priorities}}
<tr id="priority_{{$priority}}"
class="{{if eq $index 0}}nodrop{{else}}{{end}} nodrag priority_row priority_{{$priority}}"
>
<td colspan=3 class="priority_td">Priority {{$priority}}</td>
</tr>
{{range $index, $bug := index $all_data.BugsByPriority $priority}}
<tr id="{{$bug.Id}}" class="priority_{{$priority}}">
<td id="id_{{$bug.Id}}">
<a href="{{$bug.URL}}" target="_blank">{{$bug.Id}}</a>
</td>
<td id="priority_{{$bug.Id}}">{{$priority}}</td>
<td id="title_{{$bug.Id}}">{{$bug.Title}}</td>
</tr>
{{end}}
{{end}}
{{end}}
</tbody>
</table>
<script type="text/javascript">
$(document).ready(function() {
$("#buglist").tableDnD({
onDrop: function(table, dropped_row) {
var id = dropped_row.id;
var css_priority_prefix = "priority_"
var new_priority = null;
var dropped_index = null;
var thead_rows = table.tHead.rows;
var tbody_rows = table.tBodies[0].rows;
var all_rows = [];
for (var i = 0; i < thead_rows.length; i++) {
all_rows.push(thead_rows[i]);
}
for (var i = 0; i < tbody_rows.length; i++) {
all_rows.push(tbody_rows[i]);
}
for (var i = 0; i < all_rows.length; i++) {
if (all_rows[i].id) {
if (all_rows[i].id.indexOf(css_priority_prefix) == 0) {
new_priority = all_rows[i].id.substring(css_priority_prefix.length);
}
if (all_rows[i].id == id) {
break;
}
} else {
console.warn("No id for:");
console.warn(all_rows[i]);
}
}
if (new_priority) {
priority_td = document.getElementById(css_priority_prefix + id);
old_priority = priority_td.innerHTML;
if (priority_td && new_priority != old_priority) {
priority_td.innerHTML = new_priority;
document.getElementById(id).className = css_priority_prefix + new_priority;
edit_label(id, "{{.PriorityPrefix}}" + old_priority, "{{.PriorityPrefix}}" + new_priority);
}
}
}
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,12 @@
<html>
<head>
<title>Error {{.Code}}: {{.CodeString}}</title>
<link rel="stylesheet" type="text/css" href="res/style.css" />
<link rel="icon" type="image/ico" href="res/favicon.ico" />
</head>
<body>
<h1>Error {{.Code}}: {{.CodeString}}</h1>
{{.Message}}
<br/>
</body>
</html>

View File

@ -0,0 +1,13 @@
<html>
<head>
<title>{{.Title}}</title>
<link rel="stylesheet" type="text/css" href="res/style.css" />
<link rel="icon" type="image/ico" href="res/favicon.ico" />
</head>
<body>
<h1>{{.Title}}</h1>
{{.Message}}
<br/>
<a href="{{.BackLink}}">Go back</a>
</body>
</html>