Skip to content

Instantly share code, notes, and snippets.

@tosch
Created January 29, 2010 14:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tosch/289771 to your computer and use it in GitHub Desktop.
Save tosch/289771 to your computer and use it in GitHub Desktop.
#--
# Copyright (c) 2010, John Mettraux, jmettraux@gmail.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#++
# featured in
# http://jmettraux.wordpress.com/2010/01/29/barley/
require 'rubygems'
#
# wrapping up users handling
#
class Users
def self.[](id)
all[id.to_s]
end
def self.all
@@users ||= load
end
def self.load
JSON.parse(File.read('users.json'))
end
def self.write
return unless @@users.kind_of?(Hash)
File.open('users.json', 'w') do |file|
file.write(JSON.generate(@@users))
end
end
def self.add(options_from_twitter)
all
@@users[options_from_twitter['id']] = {
'id' => options_from_twitter['id'],
'screen_name' => options_from_twitter['screen_name'],
'name' => options_from_twitter['name'],
'profile_image_url' => options_from_twitter['profile_image_url']
}
write
@@users = load
options_from_twitter['id']
end
end
#
# the workflow engine
#
require 'ruote'
require 'ruote/part/storage_participant'
require 'ruote/storage/fs_storage'
ENGINE = Ruote::Engine.new(
Ruote::Worker.new(Ruote::FsStorage.new('ruote_data')))
#
# the workflow participants
#
ENGINE.register_participant('trace') do |workitem|
(workitem.fields['trace'] ||= []) << [
Time.now.strftime('%Y-%m-%d %H:%M:%S'),
workitem.fields['next'],
workitem.fields['task'] ]
end
ENGINE.register_participant('.+', Ruote::StorageParticipant)
PART = Ruote::StorageParticipant.new(ENGINE)
# a handy pointer into the workitems
#
# the (only) process definition
#
PDEF = Ruote.process_definition :name => 'barely' do
cursor do
trace
participant '${f:next}'
rewind :if => '${f:next}'
end
end
#
# web resources (thanks to Sinatra)
#
require 'cgi'
require 'sinatra'
require 'haml'
use_in_file_templates!
def h (s)
Rack::Utils.escape_html(s)
end
def sort (workitems)
workitems.sort! do |wi0, wi1|
(wi0.fields['last'] || '') <=> (wi1.fields['last'] || '')
end
end
set :haml, { :format => :html5 }
get '/' do
redirect '/work'
end
get '/work' do
@workitems = PART.all
sort(@workitems)
haml :work
end
get '/work/:thing' do
t = params[:thing]
@workitems = if Users[t]
PART.by_participant(t)
else
PART.by_field('subject', t)
end
sort(@workitems)
haml :work
end
post '/new' do
redirect '/login' unless session[:user]
if n = params['next']
wfid = ENGINE.launch(
PDEF,
'next' => n,
'subject' => params['subject'],
'task' => params['task'],
'last' => Ruote.now_to_utc_s)
end
sleep 0.5
redirect '/work'
end
post '/work' do
redirect '/login' unless session[:user]
fei = params['fei']
fei = Ruote::FlowExpressionId.from_id(fei, ENGINE.context.engine_id)
workitem = PART[fei]
# fetch workitem from storage
if params['action'] == 'resume'
workitem.fields['next'] = params['next']
workitem.fields['task'] = params['task']
workitem.fields['last'] = Ruote.now_to_utc_s
PART.reply(workitem)
else # params['action'] == 'terminate'
workitem.fields.delete('next')
PART.reply(workitem)
end
sleep 0.5
redirect '/work'
end
# twitter integration
require 'twitter_oauth'
use Rack::Session::Cookie,
:domain => COOKIE_DOMAIN,
:path => COOKIE_PATH,
:expire_after => 60 * 60 * 24 * 365,
:secret => COOKIE_SECRET
def twit_client(options = {})
options.merge!(
:consumer_key => TWITTER_CONSUMER_KEY,
:consumer_secret => TWITTER_CONSUMER_SECRET
)
@@twit_client ||= TwitterOAuth::Client.new(options)
end
get '/login' do
request_token = twit_client.request_token(:oauth_callback => TWITTER_OAUTH_CALLBACK_URL)
session[:twit_request_token] = request_token.token
session[:twit_request_secret] = request_token.secret
redirect request_token.authorize_url.gsub('authorize', 'authenticate')
end
get '/logout' do
session[:twit_request_token] = nil
session[:twit_request_secret] = nil
session[:twit_access_token] = nil
session[:twit_access_secret] = nil
session[:user] = nil
redirect '/work'
end
get '/oauth' do
if session[:twit_request_token].nil? and session[:twit_request_secret].nil?
redirect '/login'
return
end
begin
access_token = twit_client.authorize(
session[:twit_request_token],
session[:twit_request_secret],
:oauth_verifier => params[:oauth_verifier]
)
rescue OAuth::Unauthorized
end
if twit_client.authorized?
session[:twit_access_token] = access_token.token
session[:twit_access_secret] = access_token.secret
session[:user] = Users.add(twit_client.info).to_s
redirect '/work'
else
redirect '/login'
end
end
__END__
@@work
%html
%head
%title barley
%script( src='http://code.jquery.com/jquery-1.4.1.min.js' )
%link( href='http://gist.github.com/raw/286506/13f8e0a14ccef2ffa3345223e1fc6f3f2582206f/reset.css' type='text/css' rel='stylesheet' )
%style
:sass
body
font-family: "helvetica neue", helvetica
font-size: 14pt
margin-left: 20%
margin-right: 20%
margin-top: 20pt
background: #C0DEED url('http://a3.twimg.com/a/1264550348/images/bg-clouds.png') repeat-x
p
margin-bottom: 5pt
input[type='text']
width: 100%
img
width: 38px
#login img
:width 165px
a
color: black
text-decoration: none
a:visited
color: black
text-decoration: none
a:active
color: black
text-decoration: none
#barley
font-size: 350%
font-weight: lighter
color: white
padding-left: 2pt
padding-bottom: 7pt
#buttons
font-size: 90%
color: white
margin-bottom: 14pt
#buttons a
color: white
#buttons a:visited
color: white
.workitem
margin-bottom: 7pt
.workitem > *
float: left
.workitem:after
display: block
clear: both
visibility: hidden
content: ''
.wi_info
margin-left: 3pt
.wi_user
font-weight: bold
.wi_task
opacity: 0.37
cursor: pointer
.wi_wfid
font-size: 70%
vertical-align: middle
font-weight: lighter
table
width: 100%
tr.buttons > td
text-align: center
padding-top: 4pt
td
vertical-align: middle
td.constrained
width: 1%
padding-right: 1em
td.label
font-weight: lighter
.trace
opacity: 0.37
margin-bottom: 4pt
cursor: pointer
.trace_detail
padding-left: 2pt
border-left: 2.5pt solid #8EC1DA
.trace_step
width: 100%
.trace_step_time
font-size: 70%
.trace_step_user
font-weight: bold
opacity: 0.6
.trace_step_task
opacity: 0.37
%body
#barley
%span{ :onclick => "document.location.href = '/work';", :style => 'cursor: pointer;' } barley
#login
- if session[:user]
%a{ :href => '/logout' } logout
- else
%a{ :href => '/login'}
%img{ :src => '/sign_in_with_twitter.gif', :alt => 'sign in with twitter' }
#message
#{@message}
- if session[:user]
#buttons
%a{ :href => '', :onclick => "$('#new_form').slideToggle(); $('#new_next').focus(); return false;" } new
|
%a{ :href => '/work' } all
#new_form{ :style => 'display: none;' }
%form{ :action => '/new', :method => 'POST' }
%table
%tr
%td.constrained{ :rowspan => 2 }
%select#new_next{ :name => 'next', :onchange => "this.options[selectedIndex].select();" }
- Users.all.each do |id, u|
- uavatar = u['profile_image_url']
%option{ :id => "new_#{id}", :value => id } #{h(u['screen_name'])} (#{h(u['name'])})
:javascript
document.getElementById('new_#{id}').select = function () {
$('#new_avatar').get(0).src = '#{uavatar}';
$('#new_subject').focus();
}
- current_user = Users[session[:user]] || Users.all.first[1]
:javascript
$('#new_next').get(0).value = #{current_user['id']};
%td.constrained{ :rowspan => 2 }
%img#new_avatar{ :src => current_user['profile_image_url'] }
%td.constrained.label
subject
%td
%input{ :id => 'new_subject', :type => 'text', :name => 'subject', :value => '' }
%tr
%td.constrained.label
task
%td
%input{ :type => 'text', :name => 'task', :value => '' }
%tr.buttons
%td{ :colspan => 4 }
%input{ :type => 'submit', :value => 'launch' }
#work
- @workitems.each do |workitem|
- wid = "workitem#{workitem.fei.hash.to_s}"
- user = Users[workitem.participant_name]
.workitem
.wi_user_image
%img{ :src => user['profile_image_url'], :class => 'wi_user' }
.wi_info
.wi_first_line
%span.wi_user
%a{ :href => "/work/#{h workitem.participant_name}" } #{h user['screen_name']}
%span.wi_subject
%a{ :href => "/work/#{CGI.escape(workitem.fields['subject'])}" } #{h workitem.fields['subject']}
%span.wi_task{ :onclick => "$('##{wid}').slideToggle(); $('#next_#{wid}').focus();" }
#{h workitem.fields['task']}
.wi_second_line
%span.wi_wfid
#{workitem.fei.wfid}
- t = Rufus.to_ruby_time(workitem.fields['last'])
- ago = Rufus.to_duration_string(Time.now - t.to_time, :drop_seconds => true)
- ago = (ago.strip == '') ? 'a few seconds' : ago
(#{ago} ago)
.workitem_form{ :style => 'display: none;', :id => wid }
.trace{ :onclick => "$('#trace#{wid}').slideToggle();" }
- names = workitem.fields['trace'].collect { |e| Users[e[1]]['screen_name'] }
#{names.join(' &#187; ')}
.trace_detail{ :id => "trace#{wid}", :style => 'display: none;' }
- workitem.fields['trace'].each do |time, user, task|
- t = Rufus.to_ruby_time(time)
- ago = Rufus.to_duration_string(Time.now - t.to_time, :drop_seconds => true)
- u = Users[user]
%p.trace_step
%span.trace_step_time #{h time} (#{ago} ago)
%span.trace_step_user #{h(u['screen_name'])} (#{h(u['name'])})
%span.trace_step_task #{h task}
- if session[:user]
%form{ :action => '/work', :method => 'POST' }
%input{ :type => 'hidden', :name => 'fei', :value => workitem.fei.to_storage_id }
%table
%tr
%td.constrained{ :rowspan => 2 }
%select{ :id => "next_#{wid}", :name => 'next', :onchange => "this.options[selectedIndex].select();" }
- Users.all.each do |id, u|
- uavatar = u['profile_image_url']
%option{ :id => "u_#{id}_#{wid}", :value => id } #{h(u['screen_name'])} (#{h(u['name'])})
:javascript
document.getElementById('u_#{id}_#{wid}').select = function () {
$('#avatar_#{wid}').get(0).src = '#{uavatar}';
$('#task_#{wid}').focus();
}
:javascript
$('#next_#{wid}').get(0).value = '#{workitem.participant_name}';
%td.constrained{ :rowspan => 2 }
%img{ :src => Users[workitem.participant_name]['profile_image_url'], :id => "avatar_#{wid}" }
%td.constrained.label
subject
%td
#{h workitem.fields['subject']}
%tr
%td.constrained.label
task
%td
%input{ :id => "task_#{wid}", :type => 'text', :name => 'task', :value => workitem.fields['task'] }
%tr.buttons
%td{ :colspan => 4 }
%input{ :type => 'submit', :name => 'action', :value => 'resume' }
or
%input{ :type => 'submit', :name => 'action', :value => 'terminate' }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment