adding thunderdome into git

master
Andrew Coleman 2009-12-02 22:16:20 -06:00
commit dc2c316246
9 changed files with 576 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2009 Andrew Coleman
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.

96
README.markdown Normal file
View File

@ -0,0 +1,96 @@
# Welcome to the ThunderDome
This is an administrative panel for Rails styled models. In a nutshell, this application shows you a bunch of models in a list and lets you perform basic filtering, paginated displays and the usual edit/update/destroy functions.
The ThunderDome is released under the MIT license. See the `LICENSE` file for the complete text.
## Requirements
* Ruby
* Sinatra
* HAML
* Web browser with or without JavaScript support
## Paginated Viewing
All models get paginated views. 100 records per-page. Filtering is available. It's all basic, but it really gets the job done and gets out of your way.
Sorting, by default, is `id ASC`. If you would like to change it, add a class method called `custom_order` that returns a string of the order you would like to use.
## Creation And Editing
The ThunderDome will introspect all of the objects and infer what needs to be done on each model and build a form with all of the attributes listed in alphabetical order. If a column is a boolean, it displays a checkbox, strings get a text field and texts get a text area. Parent model relationships (yes, it does those!) get a select.
## Has-Many / Belongs-To's
It will handle parent-child relationships. In the child paginated views, it will display the parent's ID as an integer, but it's an abbreviation and as long as your model responds to `to_s` it will put that into the full definition in the `<abbr>` tag. In the edit view, it will provide a `<select>` list of models you can pick from. To facilitate this, you must provide a `for_select` class method. It's pretty simple, here is an example of one:
class Agency < ActiveRecord::Base
def self.for_select
all(:order => 'name ASC').collect do |a|
[ a.name, a.id.to_s ]
end
end
def self.custom_order
'agencies.code ASC'
end
end
It is important to note that i called `to_s` on the integer `id`. Trust me, this works and without the conversion it will not remember to pre-select values.
## Has And Belongs To Many
I will get back to you on this one.
## How does this really work? Really.
You `require` the appropriate files and populate a variable with a list of the models you want, i do the rest. Filtering requires you to add the column names of the columns you want filtered in a different variable.
The trickiest (and only) part of running this is you to appropriately load all of your files in the `one_man.rb` file. I have included a sample.
This application is less than 400 lines, so once you add it, it is unlikely to need to be changed or updated. It depends on nothing in your models and infers everything it needs to operate.
## How do i use this?
You can do with it as you need to, but i have a copy of this inside of my `RAILS_ROOT` directory in a directory called thunderdome. From there, you just configure your `one_man.rb` and your `config/database.yml` file. I will assume you know how to configure your database if you are this far into the game. Here is an example of a `one_man.rb` file that might be useful to some people. For this, i am going to examine NewEgg. Please note that i have never worked on anything for NewEgg. I have no idea what it is actually like. I am going to describe what i think the application structure might be like if it were written in Ruby, which it's not.
# avoid repetition, cache this value
model_dir = File.join(File.dirname(__FILE__), '..', 'app', 'models')
# the main menu is stored the model ProductCategory. It has children
# (submenu) and the parents (ProductCategory models with no parent) are the
# topmost. an example would be like 'COMPUTER HARDWARE' and 'NETWORKING'.
require File.join(model_dir, 'product_category')
# list of manufacturers, for any product.
require File.join(model_dir, 'product_manufacturer')
# for role based access control, you control the permissions here, but
# you configure the roles in the master application and which users belong
# to which roles
require File.join(model_dir, 'permission')
# the actual products you see on the page
require File.join(model_dir, 'product')
# set up your variables all nicely so the app runs with your codes
@@constants = %w(product_category product_manufacturer permission product)
@@sortable_columns = %w(name controller action)
What all of that means is this:
1. You used `require` to load the models into Ruby.
2. You told the `@@constants` variable what the symbolized name of those models are. If that variable does not contain your model, you cannot view it in the application. An exception will be thrown.
3. Any table that has a `name`, `controller`, or `action` column will be filtered on those columns. Watch out and have your indexes ready.
4. On the home page for ThunderDome it will have four entries, alphabetized.
## To Run
It's pretty easy once you configure your initialization file:
userbob@myhost> ruby thunderdome.rb
## What about REST?
I'll get back to you on this one when it matters for this type of thing.

2
config.ru Normal file
View File

@ -0,0 +1,2 @@
require 'thunderdome'
run Sinatra::Application

21
config/database.yml Normal file
View File

@ -0,0 +1,21 @@
development:
adapter: mysql
host: localhost
database: consolo_development
username: andrew
password:
test:
adapter: mysql
database: consolo_test
host: localhost
username: andrew
password:
production:
adapter: mysql
database: consolo_development
host: localhost
username: andrew
password:

View File

@ -0,0 +1,18 @@
<VirtualHost *:80>
ServerAdmin webmaster@consoloservices.com
ServerName thunderdome.consoloservices.com
ErrorLog /var/log/apache2/thunderdome.consoloservices.com-error_log
CustomLog /var/log/apache2/thunderdome.consoloservices.com-access_log common
DocumentRoot /consolo/trunk/thunderdome/public
RackEnv production
<Directory "/consolo/trunk/thunderdome/public">
Options FollowSymlinks
AllowOverride None
Order allow,deny
Allow from all
</Directory>
AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/javascript text/css
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
</VirtualHost>

39
one_man.rb Normal file
View File

@ -0,0 +1,39 @@
# monkey patch plugin for subdomain restrictions
class ActiveRecord::Base
def self.use_for_restricted_subdomains
true
end
end
# load up all of my prerequisites
require File.join(File.dirname(__FILE__), '..', 'lib', 'date_tools')
require File.join(File.dirname(__FILE__), '..', 'lib', 'american_date_monkey_patch')
require File.join(File.dirname(__FILE__), '..', 'lib', 'specialty_strings')
# directories containing my rails models
mdir = File.join(File.dirname(__FILE__), '..', 'app', 'models')
edir = File.join(File.dirname(__FILE__), '..', 'app', 'errands')
# base superclass, all of my models descend from this.
require File.join(mdir, 'consolo_constant.rb')
# custom async job processing engine
require File.join(mdir, 'errand.rb')
require File.join(edir, 'errand_new_site.rb')
require File.join(edir, 'errand_destroy_agency.rb')
# load every class that descends from my superclass
Dir.glob(File.join(mdir, '*.rb')).each do |fname|
next if fname =~ /consolo_constant/i
cmdstr = "grep -E '^class' #{fname} | grep ConsoloConstant"
if system("#{cmdstr} > /dev/null 2>&1")
require fname
@@constants << File.basename(fname, ".rb").capitalize.gsub(/_(.)/) do |s|
$1.capitalize
end
end
end
# sort tables on any of these columns, if they are in the table
@@sortable_columns = %w(code description name category action controller city county state year)

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

379
thunderdome.rb Executable file
View File

@ -0,0 +1,379 @@
#!/usr/bin/env ruby
%w{ rubygems sinatra mysql active_record yaml haml sass }.each do |lib|
require lib
end
configure do
# add your models into this variable in your initialization file
@@constants = []
# add the columns you want to search on into this variable
@@sortable_columns = []
# load up what you want to do in the app
init_file = File.join(File.dirname(__FILE__), 'one_man.rb')
require init_file if File.exists?(init_file)
# connect to the database
begin
dbf = File.join(File.dirname(__FILE__), 'config', 'database.yml')
dbconfig = YAML::load_file(dbf)[Sinatra::Application.environment.to_s]
ActiveRecord::Base.establish_connection(dbconfig)
rescue => exception
$stderr.puts "There was a problem connecting to the database:"
$stderr.puts "* #{exception.message}"
exception.backtrace.each do |msg|
$stderr.puts "-> #{msg}"
end
exit 1
end
@@constants.sort!
end
configure :development do
set :logging, true
ActiveRecord::Base.logger = Logger.new($stderr)
end
configure :production do
log = File.new(File.join(File.dirname(__FILE__), "thunderdome.log"), "a")
$stdout.reopen(log)
$stderr.reopen(log)
ActiveRecord::Base.logger = Logger.new(log)
end
get '/' do
haml :index
end
get '/stylesheet.css' do
content_type 'text/css', :charset => 'utf-8'
sass :stylesheet
end
before do
if params[:klass] and !@@constants.include? params[:klass]
raise "Bad Constant!"
else
true
end
end
helpers do
def constant_form
haml :constant_form
end
def constant_url(pagenum)
"/#{params[:klass]}/#{pagenum.to_i}?q=#{params[:q]}"
end
end
def all_booleans_to_false
@constant.attributes.keys.each do |key|
col = @constant.column_for_attribute(key)
next unless col.type == :boolean
@constant.send("#{key}=", false)
end
end
get '/:klass/edit/:id' do
@klass = params[:klass].constantize
@constant = @klass.find params[:id]
haml :edit
end
put '/:klass/edit/:id' do
@klass = params[:klass].constantize
@constant = @klass.find params[:id]
all_booleans_to_false
params[:constant].each do |name, value|
params[:constant][name] = value.to_date if name =~ /_date$/
end
@constant.attributes = params[:constant]
if @constant.save
redirect "/#{params[:klass]}/#{params[:page].to_i}?q=#{params[:q]}"
else
haml :edit
end
end
get '/:klass/new' do
@klass = params[:klass].constantize
@constant = @klass.new
haml :edit
end
post '/:klass/new' do
@klass = params[:klass].constantize
@constant = @klass.new
all_booleans_to_false
@constant.attributes = params[:constant]
if @constant.save
redirect "/#{params[:klass]}/#{params[:page].to_i}?q=#{params[:q]}"
else
haml :edit
end
end
get '/:klass/:page' do
@klass = params[:klass].constantize
conds, vars = [], {}
if params[:q] and !params[:q].to_s.empty?
vars[:q] = "%#{params[:q]}%"
cols = @klass.columns.collect(&:name)
@@sortable_columns.each do |col|
conds << "#{col} LIKE :q" if cols.include? col
end
end
@max_pages = (@klass.count(:conditions => [ conds.join(' OR '), vars ]) / 100).ceil
page_order = @klass.respond_to?(:custom_order) ? @klass.custom_order : 'id ASC'
@constants = @klass.find :all,
:limit => 100,
:order => page_order,
:offset => (params[:page].to_i * 100),
:conditions => [ conds.join(' OR '), vars ]
haml :show
end
delete '/:klass/:id' do
@klass = params[:klass].constantize
@constant = @klass.find params[:id]
@constant.destroy
redirect "/#{params[:klass]}/#{params[:page].to_i}?q=#{params[:q]}"
end
use_in_file_templates!
__END__
@@ layout
%html
%head
%title ThunderDome
%link{ :rel => 'stylesheet', :href => '/stylesheet.css' }
%body
= yield
@@ index
#header
%h1
Welcome to the ThunderDome!
%span.version== The Dos!
#content
%ol
- @@constants.each do |c|
%li
%a{ :href => "/#{c}/0" }
= c
@@ show
#header
%h1= params[:klass].pluralize
#content
%div#navigation
%a{ :href => '/', :title => 'Home' } Home
|
%a{ :href => "new?page=#{params[:page].to_i}&q=#{params[:q]}" }== New #{params[:klass]}
- if @max_pages > 0
|
Jump to page:
- if params[:page].to_i > 0
%a{ :href => constant_url(0) } First
|
- if params[:page].to_i > 1
%a{ :href => constant_url(params[:page].to_i - 1) } Previous
|
%select{ :id => 'pagenum' }
- (0..@max_pages).each do |page_num|
- if page_num == params[:page].to_i
%option{ :value => page_num, :selected => 'selected' }= page_num
- else
%option{ :value => page_num }= page_num
%form{ :method => 'GET' }
%input{ :type => 'hidden', :name => 'q', :value => params[:q] }
%button{ :onclick => 'this.form.action = "/' + params[:klass] + '/" + document.getElementById("pagenum").value; this.form.submit(); return false' } Go
- if params[:page].to_i < @max_pages - 1
|
%a{ :href => constant_url(params[:page].to_i + 1) } Next
- if params[:page].to_i < @max_pages
|
%a{ :href => constant_url(@max_pages) } Last
|
%form{ :method => 'GET', :action => "/#{params[:klass]}/#{params[:page]}" }
%input{ :type => 'text', :name => 'q', :value => params[:q] }
%button{ :onclick => 'this.form.submit(); return false' } Filter
%form{ :method => 'GET', :action => "/#{params[:klass]}/0" }
%button{ :onclick => 'this.form.submit(); return false' } Reset
- unless @constants.nil? or @constants.empty?
- join_keys = []
- parents = {}
- @constants.first.class.reflect_on_all_associations.select { |x| x.macro == :belongs_to }.each { |x| join_keys << [(x.options[:foreign_key] || "#{x.name}_id"), (x.options[:class_name] || x.name.to_s.camelize)] ; parents[join_keys.last.last] = [] }
- join_keys = join_keys.sort_by { |x| x.first }
- sorted_keys = (@constants.first.attributes.keys.sort rescue []).reject! { |x| x == 'id' or join_keys.any? { |y| y.first == x } }
%table.collectionList{ :cellspacing => 0 }
%tr
%th{ :width => "45px" } &nbsp;
- join_keys.each do |jk|
%th= jk.first
- sorted_keys.each do |field|
%th= field
- @constants.each_with_index do |c, idx|
%tr{ :class => (idx % 2 == 0 ? 'even' : 'odd') }
%td
%a{ :href => "/#{params[:klass]}/edit/#{c.id}?page=#{params[:page].to_i}&q=#{params[:q]}" }
%img{ :src => '/images/document-save.png' }
%a{ :onclick => "if (confirm('Are you sure you want to delete this entry?')) { var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); m.setAttribute('value', 'delete'); f.appendChild(m);f.submit(); };return false;", :href => "/#{params[:klass]}/#{c.id}?page=#{params[:page]}&q=#{params[:q]}" }
%img{ :src => '/images/user-trash.png' }
- join_keys.each do |jk|
%td
- id = c.send(jk[0])
%abbr{ :title => ((parents[jk[1]][id] ||= jk[1].constantize.find(id)) rescue 'NOTFOUND') }= id
- sorted_keys.each do |field|
%td= c.send(field)
@@ edit
- unless @constant.errors.empty?
#errorExplanation
%p The model could not be saved:
%ul
- @constant.errors.each_full do |msg|
%li= msg
#header
%h1== #{@constant.new_record? ? 'Creating' : 'Editing'} #{@klass}
#content
#navigation
%a{ :href => '/' } Home
|
%a{ :href => constant_url(params[:page]) }= params[:klass].pluralize
- if @constant.new_record?
%form{ :action => "/#{params[:klass]}/new", :method => 'POST' }
= constant_form
- else
%form{ :action => "/#{params[:klass]}/edit/#{@constant.id}", :method => 'pOST' }
%input{ :type => 'hidden', :name => '_method', :value => 'PUT' }
= constant_form
@@ constant_form
%input{ :type => 'hidden', :name => 'page', :value => params[:page].to_i }
%input{ :type => 'hidden', :name => 'q', :value => params[:q] }
%table{ :cellspacing => 0 }
- join_keys = []
- @constant.class.reflect_on_all_associations.select{|x| x.macro == :belongs_to}.each do |association|
- column = association.options[:foreign_key] || "#{association.name}_id"
- join_keys << column
- kls = (association.options[:class_name] || association.name.to_s.camelize).constantize
%tr
%td{ :width => '25%', :align => 'right' }
%label{ :for => "constant[#{column}]" }= kls
%td
%select{ :name => "constant[#{column}]", :id => "constant[#{column}]" }
- (kls.for_select(true) rescue kls.for_select).each do |fkname, fkid|
- if @constant.send(column).to_s == fkid
%option{ :value => fkid, :selected => 'selected' }= fkname
- else
%option{ :value => fkid }= fkname
- @constant.attributes.keys.sort.each do |key|
- next if key == 'id' or join_keys.include? key
%tr
%td{ :width => '25%', :align => 'right' }
%label{ :for => "constant[#{key}]" }= key
%td
- col = @constant.column_for_attribute(key)
- if col.type == :boolean
%input{ :type => 'checkbox', :name => "constant[#{key}]", :id => "constant[#{key}]", :checked => @constant.send("#{key}?"), :value => '1' }
- elsif col.type == :date
%input{ :type => 'date', :name => "constant[#{key}]", :id => "constant[#{key}]", :value => @constant.send(key) }
- elsif col.type == :text
%textarea{ :name => "constant[#{key}]", :id => "constant[#{key}]", :rows => 10, :cols => 50 }= @constant.send(key)
- else
- flen = col.limit.nil? ? 30 : col.limit
%input{ :type => 'text', :size => (flen > 50 ? 50 : flen), :maxsize => flen, :name => "constant[#{key}]", :id => "constant[#{key}]", :value => @constant.send(key) }
%input{ :type => 'submit', :value => 'Save' }
@@ stylesheet
body
margin: 0
margin-bottom: 25px
padding: 0
background-color: #f0f0f0
font:
family: "Lucida Grande", "Bitstream Vera Sans", "Verdana"
size: 12px
color: #333
#content
background-color: white
border: 3px solid #aaa
border-top: none
width: 90%
margin-left: auto
margin-right: auto
margin-top: 15px
padding: 5px
#navigation
margin: 10px 25px
form
display: inline
ol
margin-left: 25px
li
&:selected
background: #FCC
a
color: #03c
&:hover
background-color: #03c
color: white
text-decoration: none
img
text-decoration: none
border: none
vertical-align: text-bottom
#header
margin-top: 10px
padding-left: 75px
padding-right: 30px
h1
margin: 0
.version
color: #888
font-size: 16px
.even
background-color: #bbbbbb
.odd
background-color: #dddddd
.collectionList
padding: 2px
width: 100%
font-size: 12px
th
text-align: center
border-bottom: 2px solid #aaa
td
padding: 5px
tr:hover
background-color: #fcffa2
#errorExplanation {
padding: 10px
margin: 10px 0px
border: 2px solid #aaa
font-style: italic
background-color: #d28f8f
color: #8b0000