Skip to content

Instantly share code, notes, and snippets.

@TMorgan99
Created March 24, 2009 19:56
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 TMorgan99/84339 to your computer and use it in GitHub Desktop.
Save TMorgan99/84339 to your computer and use it in GitHub Desktop.
Build Agility, as per the tutorial
#!/bin/sh
## ========================================================
## Build Agility, as per the tutorial
## ========================================================
## run this script from a fresh directory.
## It will unload the heplers here, and build the app in the agility
## It is meant to be run in stages, where it runs into an 'exit'
## just comment over, and start again for the next step
rails --version # Rails 2.2.2
# hobo --version ## !! bug! not an option!
gem list hobo
## part 0. Setup the helper tools.
# sed_model stream-edit the model code.
cat >sed_add_action <<"RUBY" && chmod +x sed_add_action
#!/usr/bin/env ruby
## Insert input (as from heredoc) to the controller listed.
## Append an action method to a controller
require 'rubygems'
require 'activesupport'
include ActiveSupport::Inflector
model = ARGV.shift
code = $stdin.read
code.gsub! /^/, ' ' # indent
file = "./app/controllers/#{model.underscore.pluralize}_controller.rb"
tag = /(^end$)/
inp = File.read file
File.open( file, 'w' ) { |f|
f.puts inp.sub( tag, code + $/ + '\1' )
}
RUBY
cat >sed_model <<"RUBY" && chmod +x sed_model
#!/usr/bin/env ruby
## Read input (as from heredoc)
## input gives a general description of the changes to be applied to the various models.
## First line names the model, and optional verb
## verb
## (blank) -- ensure the model exists, and add any remaining lines to the association section.
## model: -- run the hobo_model generator, supplying the remaining arguments.
## model_resource: -- hobo_model_resource generator
## add_(fields)
## route: replace auto_actions code as supplied ( in the controller)
## create/update/destroy/view: replace permission code as supplied.
require 'rubygems'
require 'activesupport'
include ActiveSupport::Inflector
migration_name = ARGV.shift
migration_option = ARGV.shift
model = String.new
verb = String.new
h = Hash.new # model information
v = Hash.new # scoped vars
gen = Array.new # generators
log = String.new # collect command output, but ignore it.
puts ">>> Applying [#{migration_name}] migration ..."
## ========================================================
# == Parse the input heredoc
$stdin.read.each do |line|
case line.chomp!
when /^\s*$/ # then empty line
when /^\s*#/ # then comment line
when /^(\w+)=(.*)$/ # set runtime option
var, val = $1, $2
v[ var.to_sym ] = val
when /^(\w+):(\w+:?)?\s*(.*)?$/
model, verb, eol = $1, $2, $3
h[ model ] ||= Hash.new # create sub-hash for model, if needed.
case verb
when nil then verb = 'assoc'
when /model|model_resource/ # hobo generators
verb.chomp! ':'
gen << "script/generate hobo_#{verb} #{model.underscore} #{eol}"
verb = 'assoc'
else verb.chomp! ':'
end
h[ model ][ verb ] ||= [] # append (reset) verb value
else
h[ model ][ verb ] << line # append to verb value
end
end
## ========================================================
# === Apply generators first
gen.each do |spec|
puts "--> ruby #{spec}"
%x[#{ spec } |tee -a log/hobo.log]
end
## ========================================================
# === Apply actions to models
h.each do |model, sub_h|
# -- controller
file = "app/controllers/#{model.underscore.pluralize}_controller.rb"
if route = sub_h.delete( 'route' )
code = route.join $/
tag = /^\s*auto_actions.*$/
inp = File.read file
# remove previous code
inp.gsub!( /^\s*auto_actions_for.*$/, '' )
# replace
inp.sub!( tag, $/ + code )
File.open( file, 'w' ) { |f| f.puts inp }
end
# -- model
file = "app/models/#{model.underscore}.rb"
inp = File.read file
# - associations ( append to association section )
# -- TODO: remove dups?
if assoc = sub_h.delete( 'assoc' )
code = assoc.join $/
tag = /^(\s*fields do)$/
inp.sub!( tag, $/ + code + $/ + '\1' )
end
# - fields
if field = sub_h.delete( 'field' )
code = field.join $/
tag = /^(\s*fields do)$/
inp.sub!( tag, '\1' + $/ + ' ' + code )
end
if field = sub_h.delete( 'field_drop' )
tag = /^((\ *)fields do(.*?)\2end$)/m
inp =~ tag
old_fields, indent = $1, $2
new_fields = old_fields
field.each { |f| new_fields.sub!( /^(\s*#{f}\b)/, '##' + '\1' ) }
inp.sub!( tag, new_fields )
end
# - permissions
%w[ create update destroy view ].each do |perm|
if code = sub_h.delete( perm )
tag = /^((\ *)def #{perm}_permitted\?(.*?)\2end$)/m
inp =~ tag
old_method, indent = $1, $2
lines = old_method.split( $/ )
# replace internal line with the new code.
lines[ 1 .. -2 ] = code.map { |line| indent + line }
new_method = lines.join $/
inp.sub! old_method, new_method
end
end
File.open( file, 'w' ) { |f| f.puts inp }
end
# run hobo's migration
opt = migration_option + "\n" if migration_option
puts IO.popen( "echo '#{opt}m\n\n' |script/generate hobo_migration", "w+" ).read
RUBY
# -----------------------------------------------------------------------------
#
# -----------------------------------------------------------------------------
# gem install hobo # hobo 0.8.5
hobo Agility && cd Agility
../sed_model 'Hobo User Model' <<MODEL
# migrate the db to get it started
MODEL
# ------------ setup cucumber for the first feature
# --- create a feature story, step definitions, and a blueprint sheet
./script/generate cucumber
cat > features/part-one.feature <<FEATURE
Feature: Part One
* Track multiple projects
* Each projects has a collection of stories
* Stories are just a brief chunk of text
* A story can be assigned a current status and a set of outstanding tasks
* Tasks can be assigned to users
* Users can get an easy heads up of the tasks they are assigned to
Scenario: * Outline of project scope
Given a Project has a name
And a Project has a collection of stories
And a Story has a title
And a Story has a body
And a Story has a status
And a Story has a collection of tasks
And a Task has a description
And a Task has a collection of users
Then all is well
FEATURE
cat > features/step_definitions/my_steps.rb <<STEPS
Given /^an? (\w+)\s+has an? (\w+)$/ do | model, attribute_name |
o = model.constantize.make
o.should be_a_kind_of( Hobo::Model )
o.should respond_to( attribute_name )
end
Given /^an? (\w+)\s+has a collection of (\w+)$/ do | model, collection_name |
o = model.constantize.make( collection_name.to_s => (1..3).map {
collection_name.classify.constantize.make
} )
o.should be_a_kind_of( Hobo::Model )
end
Then /^all is well$/ do
end
STEPS
cat >features/support/blueprints.rb <<BLUEPRINTS
require 'faker'
require 'machinist'
# --- Shams
Sham.define do
title { Faker::Lorem.words(5).join(' ') }
name { Faker::Name.name }
body { Faker::Lorem.paragraphs(3).join("\n\n") }
status { Faker::Lorem.words(1).first }
description { Faker::Lorem.sentence }
project_name {|index| "Project #{index}" }
email_address { Faker::Internet.email }
end
Project.blueprint {
name { Sham.project_name }
}
Story.blueprint {
project
title; body; status
}
Task.blueprint {
story
description
}
User.blueprint {
name; email_address
}
BLUEPRINTS
## ---- We need to generate our models.
../sed_model 'First models' <<MODEL
Project:model_resource name:string
Story:model_resource title:string body:text status:string
Task:model_resource description:string
MODEL
## -- check out our features
rake -s features
## ----- We need to associate these models
../sed_model 'First model -- associations' <<MODEL
Project:
has_many :stories, :dependent => :destroy
Story:
belongs_to :project
has_many :tasks, :dependent => :destroy
Task:
belongs_to :story
has_many :task_assignments, :dependent => :destroy
has_many :users, :through => :task_assignments, :accessible => true
TaskAssignment:model
belongs_to :user
belongs_to :task
User:
has_many :task_assignments, :dependent => :destroy
has_many :tasks, :through => :task_assignments
MODEL
## --- now, our features pass!
rake -s features
echo 'Part 1-3 completed' #&& exit
# ===== Part 4 =====
# -- Don't have a nice way to edit the dryml ... I just overwrite the files.
cat >>app/views/taglibs/application.dryml <<DRYML
<extend tag="card" for="Task">
<old-card merge>
<append-body:>
<div class="users">
Assigned users: <repeat:users join=", "><a/></repeat><else>None</else>
</div>
</append-body:>
</old-card>
</extend>
DRYML
cat >app/views/users/show.dryml <<DRYML
<show-page>
<content-body:>
<h3><Your/> Assigned Tasks</h3>
<repeat with="&@user.tasks.group_by(&:story)">
<h4>Story: <a with="&this_key"/></h4>
<collection/>
</repeat>
</content-body:>
</show-page>
DRYML
cat >app/views/projects/show.dryml <<DRYML
<show-page>
<collection: replace>
<table-plus with="&@stories" fields="this, tasks.count, status">
<empty-message:>No stories match your criteria</empty-message:>
</table-plus>
</collection:>
</show-page>
DRYML
## this was not mentioned in the tutorial at this point,
## but it should have been. It is not mentioned until later
## But with BDD, these things would have been caught?
cat > app/views/tasks/edit.dryml <<DRYML
<edit-page>
<form:>
<cancel: with="&this.story"/>
</form:>
</edit-page>
DRYML
../sed_add_action Project <<RUBY
def show
@project = find_instance
@stories = @project.stories.apply_scopes(
:search => [ params[:search], :title ],
:order_by => parse_sort_param( :title, :status )
)
end
RUBY
echo 'Part 4 completed' #&& exit
### --- Add rake task to load testing data into db
cat >lib/tasks/test_data.rake <<RAKE
desc 'Run Machinist to load up data'
task :load_records => [ 'environment' ] do
require 'features/support/blueprints'
User.make :name => 'Admin' # Admin is the first user
6.times { User.make }
5.times { Project.make }
users = User.all; users.shift # all but the Admin
projects = Project.all
projects.each do |prj|
4.times { Story.make :project => prj } # ensure 4 stories per project
end
6.times { Story.make :project => projects.rand } # more stories on any project
stories = Story.all
( stories.size * 4 ).times do # make tasks; average 4 per story
n = ( 0.25 < rand ) ? 1 : 3 # assign 3 users 25% of the time
Task.make :story => stories.rand,
:users => users.shuffle[ 0, n ] # users will be distinct
end
end
RAKE
rake load_records
echo 'Part 4 data loader completed' #&& exit
## ====== Part 5 ======
../sed_model 'Story status menu' 'drop status' <<MODEL
StoryStatus:model_resource name:string
StoryStatus:route:
auto_actions :write_only
Story:
belongs_to :status, :class_name => "StoryStatus"
Story:field_drop
status
MODEL
cat >> 'features/support/blueprints.rb' <<BLUEPRINT
StoryStatus.blueprint do
name # status-names are specified in the rake task; no sham needed here.
end
# revised model # ruby will toss the previous declaration
Story.blueprint {
project
status
title; body
}
BLUEPRINT
# The menu is working in the edit-story page now.
# It would be nice though if we had a ajaxified editor right on the story page.
cat >app/views/stories/show.dryml <<DRYML
<show-page>
<field-list: tag="editor"/>
</show-page>
DRYML
cat >>lib/tasks/test_data.rake <<RAKE
# distribute a model, (using it's belongs_to field) over the list of possibilities
# generates SQL like this.
# UPDATE "stories" SET "status_id" = 5 WHERE ("stories"."id" IN (9,10,13,15,16))
# UPDATE "stories" SET "status_id" = 6 WHERE ("stories"."id" IN (NULL))
# UPDATE "stories" SET "status_id" = 1 WHERE ("stories"."id" IN (2,6,8,14,18))
def distribute( model, belongs_to, list )
belongs_to = "#{belongs_to}_id".to_s # for some annoying reason, rails can't handle foriegn_keys in update_all
list = list.map &:id
ids = model.all.map &:id
k = Hash.new
k[list.shift], ids = ids.partition { rand( list.size) < 1 } until list.empty?
k.each_pair do | key, members |
model.update_all( { belongs_to => key }, { :id => members } ) unless members.empty?
end
end
desc 'Run Machinist to load up data'
task :load_story_status => [ 'environment' ] do
require 'features/support/blueprints'
%w(new accepted discussion implementation user_testing deployed rejected).
each { |status| StoryStatus.make :name => status }
distribute( Story, 'status', StoryStatus.all )
end
RAKE
rake load_story_status
echo 'Part 5a - Story Status menu completed' #&& exit
## ==============================
## Filtering stories by status
cat >app/views/projects/show.dryml <<DRYML
<show-page>
<collection: replace>
<table-plus with="&@stories" fields="this, tasks.count, status">
<prepend-header:>
<div class="filter">
Display by status:
<filter-menu param-name="status" options="&StoryStatus.all"/>
</div>
</prepend-header:>
<empty-message:>No stories match your criteria</empty-message:>
</table-plus>
</collection:>
</show-page>
DRYML
## To make the filter look right, add this:
cat >public/stylesheets/application.css <<CSS
.show-page.project .filter {float: left;}
.show-page.project .filter form, .show-page.project .filter form div {display: inline;}
CSS
### Controller fixes?
## I wanted to add 'task counter' to the list of 'order-by'
## -- but how to specify this?
## -- needs to be a virtual attribute somehow
### DRYML fixes?
## Also, the StoryStatus menu filter refreshes to the 'All' choice when something is checked
## and reselecting 'All' does not clear off the filter.
../sed_add_action Project <<RUBY
def show
@project = find_instance
@stories = @project.stories.apply_scopes(
:search => [params[:search], :title],
:status_is => params[:status],
:order_by => parse_sort_param(:title, :status)
)
end
RUBY
echo 'Part 5b - Filtering stories by status completed' #&& exit
## =================
# Task re-ordering
./script/plugin install acts_as_list
../sed_model 'acts_as_list' <<MODEL
Task:
# set the protected flag on this attribute, to avoid UI generation.
# ( should have really happened in the plugin .... )
attr_protected :postion
acts_as_list :scope => :story
Story:
has_many :tasks, :dependent => :destroy, :order => :position
MODEL
## AAL will automagically take care of initializion of the position field.
## Although the Guest is not permitted to adjust position, the JavaSctipt is enabled for her
# # this has been fixed by the attr_protected flag, so is no longer needed.
## well. I thought the attr_protected flag would do the trick
## I thought it was before ...
cat >>app/views/taglibs/application.dryml <<DRYML
<extend tag="form" for="Task">
<old-form merge>
<field-list: fields="description, users"/>
</old-form>
</extend>
DRYML
# Markdown / Textile formatting of stories
## I skipped this part -- but don't you have an HTML tag for this that uses javascript rich-text editor?
## nicEdit? some vestage is found in the git clone
echo 'Part 5c - Task re-ordering completed' #&& exit
## ============================
# Part 6 -- Project Ownership
../sed_model 'Project Ownership' <<MODEL
Project:
belongs_to :owner, :class_name => "User", :creator => true
Project:create:
owner_is? acting_user
Project:update:
acting_user.administrator? || ( owner_is?(acting_user) && !owner_changed?)
Project:destroy:
acting_user.administrator? || owner_is?(acting_user)
User:
has_many :projects, :class_name => "Project", :foreign_key => "owner_id"
MODEL
cat > app/views/front/index.dryml <<DRYML
<page title="Home">
<body: class="front-page"/>
<content:>
<header class="content-header">
<h1>Welcome to <app-name/></h1>
<section class="welcome-message">
<h3>[[ The Agile Hobo --- up and running ]]</h3>
</section>
</header>
<section with="&current_user" class="content-body" if="&logged_in?">
<h3>Your Projects</h3>
<collection:projects><card without-creator-link/></collection>
<a:projects action="new">New Project</a>
<h3>Projects you have joined</h3>
<collection:joined-projects><card without-creator-link/></collection>
</section>
<section class="content-body" if="&!logged_in?">
You will certainly be able to see more once you have logged in.
</section>
</content:>
</page>
DRYML
## now, lets distribute our existing Projects amoung the Users
cat >>lib/tasks/test_data.rake <<RAKE
desc 'Run Machinist to load up data'
task :load_owner => [ 'environment' ] do
owner_list = User.all
owner_list.shift # don't overwork the long suffering Admin
distribute( Project, 'owner', owner_list )
end
RAKE
rake load_owner
echo 'Part 6 -- Project Ownership completed' #&& exit
# ========================================
# Part 7 -- Granting read access to others
../sed_model 'Grant read access' <<MODEL
ProjectMembership:model_resource
ProjectMembership:route
auto_actions :write_only
ProjectMembership:
belongs_to :project
belongs_to :user
# --- Permissions -- only the project owner (and admins) can manipulate these:
ProjectMembership:create:
acting_user.administrator? || acting_user == project.owner
ProjectMembership:update:
acting_user.administrator? || acting_user == project.owner
ProjectMembership:destroy:
acting_user.administrator? || acting_user == project.owner
ProjectMembership:view:
true
# === The other ends of those two belongs-to associations:
Project:
has_many :memberships, :class_name => "ProjectMembership", :dependent => :destroy
has_many :members, :through => :memberships, :source => :user
User:
has_many :project_memberships, :dependent => :destroy
has_many :joined_projects, :through => :project_memberships, :source => :project
# --- View permission on projects, stories and tasks according to project membership.
Project:view:
acting_user.administrator? || acting_user == owner || acting_user.in?(members)
Story:view:
project.viewable_by? acting_user
Task:view:
story.viewable_by? acting_user
# Modify the actions provided by the projects controller to:
Project:route
auto_actions :show, :edit, :update, :destroy
auto_actions_for :owner, [:new, :create]
MODEL
cat >>lib/tasks/test_data.rake <<RAKE
# the usual suspects have joined all projects
desc 'Run Machinist to load up data'
task :load_member => [ 'environment' ] do
[ 2, 3, 4 ].each do | member_id |
member = User.find member_id
member.joined_projects << Project.all
end
end
RAKE
rake load_member
## The view layer
cat >app/viewhints/project_hints.rb <<RUBY
class ProjectHints < Hobo::ViewHints
children :stories, :memberships
end
RUBY
cat > app/views/projects/show.dryml <<DRYML
<show-page>
<collection: replace>
<table-plus with="&@stories" fields="this, tasks.count, status">
<prepend-header:>
<div class="filter">
Display by status:
<filter-menu param-name="status" options="&StoryStatus.all"/>
</div>
</prepend-header:>
<empty-message:>No stories match your criteria</empty-message:>
</table-plus>
</collection:>
<aside:>
<h2>Project Members</h2>
<collection:memberships part="members">
<card><heading:><a:user/></heading:></card>
</collection>
<form:memberships.new update="members" reset-form refocus-form>
<div>
Add a member: <name-one:user complete-target="&@project" completer="new_member_name"/>
</div>
</form>
</aside:>
</show-page>
DRYML
## name-one:user completer working, but what happens when I press enter?
## seems to need a User. find_by_name somewhere in the mix
../sed_add_action Project <<RUBY
autocomplete :new_member_name do
project = find_instance
hobo_completions :name, User.without_joined_project(project).is_not(project.owner)
end
RUBY
echo 'Part 7 -- Granting read access completed' #&& exit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment