Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial commit

  • Loading branch information...
commit b847a80c0b5772f13cc12ff763ad647d6cd1c0a5 0 parents
@chrisb authored
5 README
@@ -0,0 +1,5 @@
+Refuddle is a small Sinatra app that acts as a web-based client for Unfuddle. The goal was to develop a project-agnostic view of one's Unfuddle tickets that fit with the OSX style. I recommend using something like Fluid (www.fluidapp.com) to create a SSB (single-site browser) for the app.
+
+Contributions are welcome!
+
+Enjoy!
BIN  public/images/grad.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/images/toolbar.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 public/javascripts/application.js
@@ -0,0 +1,61 @@
+var inspCell;
+
+var formatSummary = function (elCell, oRecord, oColumn, oData) {
+ if(oData.include('[hold]')) {
+ elCell.innerHTML = oData.replace('[hold]','').strip();
+ } else {
+ elCell.innerHTML = oData;
+ }
+};
+
+
+var formatStatus = function (elCell, oRecord, oColumn, oData) {
+ if(oRecord._oData.summary.include('[hold]')) {
+ elCell.innerHTML = 'hold';
+ elCell.parentNode.parentNode.addClassName('hold');
+ } else {
+ elCell.innerHTML = oData;
+ }
+};
+
+var ticketsDataTable;
+
+YAHOO.util.Event.addListener(window, "load", function() {
+
+ var myColumnDefsY = [
+ { key:"number", width:20, label: "#", sortable: true },
+ { key:"status", width:100, label: "Status", formatter: formatStatus, sortable: true },
+ { key:"summary", label: "Summary", formatter: formatSummary, sortable: true }
+ ];
+
+ var myDataSource = new YAHOO.util.DataSource("/tickets.js");
+
+ myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
+ myDataSource.connXhrMode = "queueRequests";
+ myDataSource.responseSchema = {
+ resultsList: "items",
+ fields: [
+ { key: 'number' },
+ { key: 'status' },
+ { key: 'summary' },
+ { key: 'project_id' },
+ { key: 'id' }
+ ]
+ };
+
+ ticketsDataTable = new YAHOO.widget.ScrollingDataTable("yscrolling", myColumnDefsY, myDataSource, {
+ height:"20em",
+ width: "100%",
+ selectionMode: 'single',
+ initialRequest: ""
+ });
+
+ //ticketsDataTable.subscribe("rowMouseoverEvent", this.singleSelectDataTable.onEventHighlightRow);
+ //ticketsDataTable.subscribe("rowMouseoutEvent", this.singleSelectDataTable.onEventUnhighlightRow);
+ ticketsDataTable.subscribe("rowClickEvent", ticketsDataTable.onEventSelectRow );
+ ticketsDataTable.subscribe("rowClickEvent", function (oArgs) {
+ var oRecord = this.getRecord(oArgs.target);
+ var url = '/ticket/'+oRecord.getData('project_id')+'/'+oRecord.getData('number')+'.js'
+ new Ajax.Updater('ticket-detail', url, { method: 'get' } );
+ });
+});
210 public/stylesheets/application.css
@@ -0,0 +1,210 @@
+html, body {
+ font-family: 'Lucida Grande','Helvetica Neue',sans-serif;
+ margin-width: 0px;
+ margin: 0px;
+ background-color: #fafafa;
+ padding: 0px;
+}
+
+* {
+ cursor: default;
+}
+
+#login {
+ width: 450px;
+ background-color: #fafafa;
+ border: 1px solid #f2f2f2;
+ margin-left: auto;
+ margin-right: auto;
+ margin-top: 50px;
+ padding: 14px;
+}
+
+#login p.submit {
+ margin-top: 24px;
+ text-align: center;
+}
+
+#toolbar {
+ background-image: url(/images/toolbar.png);
+ height: 25px;
+ font-weight: bold;
+ color: #fff;
+ text-shadow: #000 1px 1px 1px;
+ line-height: 25px;
+ padding-left: 12px;
+ padding-right: 12px;
+}
+
+#toolbar #logout {
+ float: right;
+}
+
+#ticket-detail .no-ticket {
+ margin-top: 60px;
+ margin-left: auto;
+ margin-right: auto;
+ text-align: center;
+}
+
+#ticket-detail .no-ticket span {
+ background-color: #525252;
+ color: #fff;
+ font-weight: bold;
+ padding: 10px;
+ font-size: 2.0em;
+ border-radius: 12px;
+}
+
+#ticket-detail {
+ border-top: 1px solid #aaa;
+}
+
+#toolbar #logout a {
+ background-color: #777;
+ border-radius: 8px;
+ border: 1px solid #525252;
+ cursor: pointer;
+ padding-left: 8px;
+ padding-right: 8px;
+ color: #fff;
+ text-decoration: none;
+ text-shadow: none;
+ font-size: 0.8em;
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+#login p.info {
+ color: #525252;
+ font-size: 0.9em;
+ line-height: 2em;
+ margin-bottom: 50px !important;
+}
+
+#login h1 {
+ margin: 0px;
+ padding: 0px;
+ text-align: center;
+ text-shadow: #fff 1px 1px 2px;
+}
+
+#login p {
+ color: #525252;
+}
+
+#login p label {
+ float: left;
+ width: 130px;
+ text-align: right;
+ margin-right: 12px;
+ font-weight: bold;
+ line-height: 24px;
+ color: #000;
+}
+
+#ticket-detail .header {
+ border-bottom: 1px solid #f2f2f2;
+ margin-bottom: 10px;
+ padding-bottom: 10px;
+ background-color: #fafafa;
+}
+
+#ticket-detail .header .line .number {
+ opacity: 0.5;
+ float: left;
+ text-align: right;
+ width: 50px;
+ margin-right: 10px;
+}
+
+#ticket-detail .description {
+ margin-left: 30px;
+ margin-right: 30px;
+ border: 1px solid #f2f2f2;
+ background-color: #fafafa;
+ padding: 14px;
+}
+
+#ticket-detail .header .line {
+ font-size: 1.2em;
+ padding: 14px;
+}
+
+#ticket-detail .header .line div {
+ display: inline;
+}
+
+#ticket-detail .header p {
+ color: #525252;
+ padding: 2px;
+ margin: 0px;
+}
+
+#ticket-detail .header p label {
+ width: 130px;
+ text-align: right;
+ color: #222;
+ font-weight: bold;
+ float: left;
+ margin-right: 10px;
+}
+
+
+/* YUI styles */
+
+.yui-skin-sam .yui-dt table {
+ width: 100%;
+ border: 0px !important;
+ overflow-x: hidden !important;
+}
+
+.yui-skin-sam .yui-dt-scrollable .yui-dt-bd {
+ border-bottom: 0px !important;
+ border-left: 0px !important;
+ border-right: 0px !important;
+}
+
+.yui-skin-sam tr.yui-dt-odd {
+ background-color: #edf3fe !important;
+}
+
+.yui-skin-sam .yui-dt table th {
+ text-align: left;
+ border-bottom: 1px solid #aaa;
+ text-shadow: #fff 1px 1px 1px;
+}
+
+.yui-skin-sam .yui-dt tr.hold td {
+ opacity: 0.4;
+}
+
+.yui-dt-col-number {
+ text-align: right !important;
+}
+
+.yui-skin-sam .yui-dt td {
+ border-right: none !important;
+ /* border-bottom: 1px solid #fafafa; */
+}
+
+.yui-skin-sam .yui-dt-scrollable .yui-dt-data tr.yui-dt-last td {
+ border-bottom: 0px !important;
+}
+
+.yui-skin-sam .yui-dt tr.yui-dt-first td {
+ border-top: 0px !important;
+}
+
+.yui-skin-sam .yui-dt-scrollable .yui-dt-hd {
+ border-top: 0px !important;
+ border-left: 0px !important;
+ border-right: 0px !important;
+}
+
+.yui-skin-sam th .yui-dt-liner {
+ font-size: 0.9em;
+ height: 16px;
+ line-height: 16px;
+ padding: 1px 10px !important;
+}
56 refuddle.rb
@@ -0,0 +1,56 @@
+require 'rubygems'
+require 'rest_client'
+require 'json'
+require 'sinatra'
+require 'haml'
+
+def unfuddle_call_api(request,passthru=false)
+ puts "[API] #{request}"
+ data = RestClient.get("#{unfuddle_base_url}#{request}")
+ return passthru ? data : JSON.parse(data)
+end
+
+def unfuddle_base_url
+ "http://#{session[:username]}:#{session[:password]}@#{session[:subdomain]}.unfuddle.com/api/v1"
+end
+
+# enable :sessions # we meed more than 4K of data
+use Rack::Session::Pool
+
+get '/login' do
+ haml(:login)
+end
+
+get '/logout' do
+ [ :subdomain, :username, :password ].each { |k| session[k] = nil }
+ redirect '/'
+end
+
+post '/login' do
+ session[:subdomain] = params[:subdomain]
+ session[:username] = params[:username]
+ session[:password] = params[:password]
+
+ session[:projects] = {}
+ session[:people] = {}
+ session[:milestones] = {}
+
+ unfuddle_call_api('/projects.json').each { |p| session[:projects][p['id']] = p['title'] }
+ unfuddle_call_api('/people.json').each { |p| session[:people][p['id']] = p['first_name']+' '+p['last_name'] }
+ unfuddle_call_api('/milestones.json').each { |m| session[:milestones][m['project_id']].nil? ? session[:milestones][m['project_id']] = { m['id'] => m['title'] } : session[:milestones][m['project_id']][m['id']] = m['title'] }
+
+ redirect '/'
+end
+
+get '/' do
+ session[:subdomain] ? haml(:index) : redirect('/login')
+end
+
+get '/tickets.js' do
+ { :items => unfuddle_call_api('/ticket_reports/dynamic.json?conditions_string=assignee-eq-current,status-neq-closed,status-neq-resolved')['groups'].first['tickets'] }.to_json
+end
+
+get '/ticket/:project_id/:number.js' do
+ @ticket = unfuddle_call_api("/projects/#{params[:project_id]}/tickets/by_number/#{params[:number]}.json")
+ haml :detail, :layout => false
+end
27 views/detail.haml
@@ -0,0 +1,27 @@
+- if @ticket.nil?
+ .no-ticket
+ %span Please select a ticket above.
+- else
+ .header
+ .line
+ .number="##{@ticket['number']}"
+ .summary= @ticket['summary']
+ .status= @ticket['status']
+ %p
+ %label Milestone:
+ - milestone = session[:milestones][@ticket['project_id']][@ticket['milestone_id']]
+ = milestone.nil? ? "(none)" : milestone
+ %p
+ %label Reported by:
+ = session[:people][@ticket['reporter_id']]
+ %p
+ %label Assigned to:
+ = session[:people][@ticket['assignee_id']]
+ %p
+ %label Created:
+ = DateTime.parse(@ticket['created_at']).strftime("%B %d %Y %I:%M%p")
+ %p
+ %label Last Updated:
+ = DateTime.parse(@ticket['updated_at']).strftime("%B %d %Y %I:%M%p")
+
+ .description= @ticket['description']
10 views/index.haml
@@ -0,0 +1,10 @@
+#toolbar
+ Connected as:
+ = "#{session[:username]} (#{session[:subdomain]})"
+ #logout
+ %a{ :href => '/logout' } Log Out
+
+#yscrolling
+
+#ticket-detail
+ = haml(:detail)
27 views/layout.haml
@@ -0,0 +1,27 @@
+!!! 5
+%head
+ %title Unfuddle Tickets
+
+ %link{ :rel => 'stylesheet', :type => 'text/css', :href => 'http://yui.yahooapis.com/2.8.2r1/build/fonts/fonts-min.css' }
+ %link{ :rel => 'stylesheet', :type => 'text/css', :href => 'http://yui.yahooapis.com/2.8.2r1/build/button/assets/skins/sam/button.css' }
+ %link{ :rel => 'stylesheet', :type => 'text/css', :href => 'http://yui.yahooapis.com/2.8.2r1/build/datatable/assets/skins/sam/datatable.css' }
+
+ %script{ :type => 'text/javascript', :src => 'http://yui.yahooapis.com/2.8.2r1/build/yahoo-dom-event/yahoo-dom-event.js' }
+ %script{ :type => 'text/javascript', :src => 'http://yui.yahooapis.com/2.8.2r1/build/element/element-min.js' }
+ %script{ :type => 'text/javascript', :src => 'http://yui.yahooapis.com/2.8.2r1/build/button/button-min.js' }
+ %script{ :type => 'text/javascript', :src => 'http://yui.yahooapis.com/2.8.2r1/build/datasource/datasource-min.js' }
+ %script{ :type => 'text/javascript', :src => 'http://yui.yahooapis.com/2.8.2r1/build/datatable/datatable-min.js' }
+ %script{ :type => 'text/javascript', :src => 'http://yui.yahooapis.com/2.8.2r1/build/json/json-min.js' }
+ %script{ :type => 'text/javascript', :src => 'http://yui.yahooapis.com/2.8.2r1/build/connection/connection-min.js' }
+
+ %script{ :type => 'text/javascript', :src => 'http://prototypejs.org/assets/2009/8/31/prototype.js' }
+
+ %script{ :type => 'text/javascript', :src => 'javascripts/application.js' }
+ %link{ :rel => 'stylesheet', :href => '/stylesheets/application.css', :type => 'text/css', :media => 'screen, projection' }
+%body.yui-skin-sam
+ %div.container
+ -# %div.navigation
+ -# %a{ :hfref => "/tickets" } Unfuddle Tickets
+ -# %a{ :hfref => "/performance" } Performance
+ -# %a{ :hfref => "/transactions" } Transactions
+ = yield
15 views/login.haml
@@ -0,0 +1,15 @@
+%form#login{ :action => '/login', :method => 'post' }
+ %h1 Refuddle
+ %p.info This is a tool I built to provide a better interface to Unfuddle. Due to the nature of the Unfuddle API I will need your username and password. If you're not comfortable providing this info... well, that sucks. Perhaps try changing your Unfuddle password to something else?
+ %p
+ %label{ :for => 'subdomain' } Unfuddle URL:
+ %input{ :type => 'text', :name => 'subdomain' }
+ \.unfuddle.com
+ %p
+ %label{ :for => 'username' } Username:
+ %input{ :type => 'text', :name => 'username' }
+ %p
+ %label{ :for => 'password' } Password:
+ %input{ :type => 'password', :name => 'password' }
+ %p.submit
+ %input{ :type => 'submit', :value => "Start Refuddling!" }
Please sign in to comment.
Something went wrong with that request. Please try again.