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:
parent
90bebbb9ed
commit
5132461627
BIN
tools/bug_chomper/res/favicon.ico
Normal file
BIN
tools/bug_chomper/res/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 B |
72
tools/bug_chomper/res/style.css
Normal file
72
tools/bug_chomper/res/style.css
Normal 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;
|
||||
}
|
314
tools/bug_chomper/res/third_party/jquery.tablednd.js
vendored
Normal file
314
tools/bug_chomper/res/third_party/jquery.tablednd.js
vendored
Normal 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
11
tools/bug_chomper/run_server.sh
Executable 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 $@
|
||||
|
303
tools/bug_chomper/src/issue_tracker/issue_tracker.go
Normal file
303
tools/bug_chomper/src/issue_tracker/issue_tracker.go
Normal 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
|
||||
}
|
376
tools/bug_chomper/src/server/server.go
Normal file
376
tools/bug_chomper/src/server/server.go
Normal 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())
|
||||
}
|
||||
}
|
118
tools/bug_chomper/templates/bug_chomper.html
Normal file
118
tools/bug_chomper/templates/bug_chomper.html
Normal 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>
|
12
tools/bug_chomper/templates/error.html
Normal file
12
tools/bug_chomper/templates/error.html
Normal 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>
|
13
tools/bug_chomper/templates/submitted.html
Normal file
13
tools/bug_chomper/templates/submitted.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user