[:preflight, :setup_directories, :copy_assets, :migrate]
6 |
7 | desc "Test for any dependencies"
8 | task :preflight do # see slicetasks.rb
9 | end
10 |
11 | desc "Setup directories"
12 | task :setup_directories do
13 | puts "Creating directories for host application"
14 | MerbAdmin.mirrored_components.each do |type|
15 | if File.directory?(MerbAdmin.dir_for(type))
16 | if !File.directory?(dst_path = MerbAdmin.app_dir_for(type))
17 | relative_path = dst_path.relative_path_from(Merb.root)
18 | puts "- creating directory :#{type} #{File.basename(Merb.root) / relative_path}"
19 | mkdir_p(dst_path)
20 | end
21 | end
22 | end
23 | end
24 |
25 | # desc "Copy stub files to host application"
26 | # task :stubs do
27 | # puts "Copying stubs for MerbAdmin - resolves any collisions"
28 | # copied, preserved = MerbAdmin.mirror_stubs!
29 | # puts "- no files to copy" if copied.empty? && preserved.empty?
30 | # copied.each { |f| puts "- copied #{f}" }
31 | # preserved.each { |f| puts "! preserved override as #{f}" }
32 | # end
33 |
34 | # desc "Copy stub files and views to host application"
35 | # task :patch => [ "stubs", "freeze:views" ]
36 |
37 | desc "Copy public assets to host application"
38 | task :copy_assets do
39 | puts "Copying assets for MerbAdmin - resolves any collisions"
40 | copied, preserved = MerbAdmin.mirror_public!
41 | puts "- no files to copy" if copied.empty? && preserved.empty?
42 | copied.each { |f| puts "- copied #{f}" }
43 | preserved.each { |f| puts "! preserved override as #{f}" }
44 | end
45 |
46 | desc "Migrate the database"
47 | task :migrate do # see slicetasks.rb
48 | end
49 |
50 | desc "Freeze MerbAdmin into your app (only merb-admin/app)"
51 | task :freeze => [ "freeze:app" ]
52 |
53 | namespace :freeze do
54 |
55 | # desc "Freezes MerbAdmin by installing the gem into application/gems"
56 | # task :gem do
57 | # ENV["GEM"] ||= "merb-admin"
58 | # Rake::Task['slices:install_as_gem'].invoke
59 | # end
60 |
61 | desc "Freezes MerbAdmin by copying all files from merb-admin/app to your application"
62 | task :app do
63 | puts "Copying all merb-admin/app files to your application - resolves any collisions"
64 | copied, preserved = MerbAdmin.mirror_app!
65 | puts "- no files to copy" if copied.empty? && preserved.empty?
66 | copied.each { |f| puts "- copied #{f}" }
67 | preserved.each { |f| puts "! preserved override as #{f}" }
68 | end
69 |
70 | desc "Freeze all views into your application for easy modification"
71 | task :views do
72 | puts "Copying all view templates to your application - resolves any collisions"
73 | copied, preserved = MerbAdmin.mirror_files_for :view
74 | puts "- no files to copy" if copied.empty? && preserved.empty?
75 | copied.each { |f| puts "- copied #{f}" }
76 | preserved.each { |f| puts "! preserved override as #{f}" }
77 | end
78 |
79 | desc "Freeze all models into your application for easy modification"
80 | task :models do
81 | puts "Copying all models to your application - resolves any collisions"
82 | copied, preserved = MerbAdmin.mirror_files_for :model
83 | puts "- no files to copy" if copied.empty? && preserved.empty?
84 | copied.each { |f| puts "- copied #{f}" }
85 | preserved.each { |f| puts "! preserved override as #{f}" }
86 | end
87 |
88 | desc "Freezes MerbAdmin as a gem and copies over merb-admin/app"
89 | task :app_with_gem => [:gem, :app]
90 |
91 | desc "Freezes MerbAdmin by unpacking all files into your application"
92 | task :unpack do
93 | puts "Unpacking MerbAdmin files to your application - resolves any collisions"
94 | copied, preserved = MerbAdmin.unpack_slice!
95 | puts "- no files to copy" if copied.empty? && preserved.empty?
96 | copied.each { |f| puts "- copied #{f}" }
97 | preserved.each { |f| puts "! preserved override as #{f}" }
98 | end
99 |
100 | end
101 |
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/public/javascripts/ordering.js:
--------------------------------------------------------------------------------
1 | addEvent(window, 'load', reorder_init);
2 |
3 | var lis;
4 | var top = 0;
5 | var left = 0;
6 | var height = 30;
7 |
8 | function reorder_init() {
9 | lis = document.getElementsBySelector('ul#orderthese li');
10 | var input = document.getElementsBySelector('input[name=order_]')[0];
11 | setOrder(input.value.split(','));
12 | input.disabled = true;
13 | draw();
14 | // Now initialise the dragging behaviour
15 | var limit = (lis.length - 1) * height;
16 | for (var i = 0; i < lis.length; i++) {
17 | var li = lis[i];
18 | var img = document.getElementById('handle'+li.id);
19 | li.style.zIndex = 1;
20 | Drag.init(img, li, left + 10, left + 10, top + 10, top + 10 + limit);
21 | li.onDragStart = startDrag;
22 | li.onDragEnd = endDrag;
23 | img.style.cursor = 'move';
24 | }
25 | }
26 |
27 | function submitOrderForm() {
28 | var inputOrder = document.getElementsBySelector('input[name=order_]')[0];
29 | inputOrder.value = getOrder();
30 | inputOrder.disabled=false;
31 | }
32 |
33 | function startDrag() {
34 | this.style.zIndex = '10';
35 | this.className = 'dragging';
36 | }
37 |
38 | function endDrag(x, y) {
39 | this.style.zIndex = '1';
40 | this.className = '';
41 | // Work out how far along it has been dropped, using x co-ordinate
42 | var oldIndex = this.index;
43 | var newIndex = Math.round((y - 10 - top) / height);
44 | // 'Snap' to the correct position
45 | this.style.top = (10 + top + newIndex * height) + 'px';
46 | this.index = newIndex;
47 | moveItem(oldIndex, newIndex);
48 | }
49 |
50 | function moveItem(oldIndex, newIndex) {
51 | // Swaps two items, adjusts the index and left co-ord for all others
52 | if (oldIndex == newIndex) {
53 | return; // Nothing to swap;
54 | }
55 | var direction, lo, hi;
56 | if (newIndex > oldIndex) {
57 | lo = oldIndex;
58 | hi = newIndex;
59 | direction = -1;
60 | } else {
61 | direction = 1;
62 | hi = oldIndex;
63 | lo = newIndex;
64 | }
65 | var lis2 = new Array(); // We will build the new order in this array
66 | for (var i = 0; i < lis.length; i++) {
67 | if (i < lo || i > hi) {
68 | // Position of items not between the indexes is unaffected
69 | lis2[i] = lis[i];
70 | continue;
71 | } else if (i == newIndex) {
72 | lis2[i] = lis[oldIndex];
73 | continue;
74 | } else {
75 | // Item is between the two indexes - move it along 1
76 | lis2[i] = lis[i - direction];
77 | }
78 | }
79 | // Re-index everything
80 | reIndex(lis2);
81 | lis = lis2;
82 | draw();
83 | // document.getElementById('hiddenOrder').value = getOrder();
84 | document.getElementsBySelector('input[name=order_]')[0].value = getOrder();
85 | }
86 |
87 | function reIndex(lis) {
88 | for (var i = 0; i < lis.length; i++) {
89 | lis[i].index = i;
90 | }
91 | }
92 |
93 | function draw() {
94 | for (var i = 0; i < lis.length; i++) {
95 | var li = lis[i];
96 | li.index = i;
97 | li.style.position = 'absolute';
98 | li.style.left = (10 + left) + 'px';
99 | li.style.top = (10 + top + (i * height)) + 'px';
100 | }
101 | }
102 |
103 | function getOrder() {
104 | var order = new Array(lis.length);
105 | for (var i = 0; i < lis.length; i++) {
106 | order[i] = lis[i].id.substring(1, 100);
107 | }
108 | return order.join(',');
109 | }
110 |
111 | function setOrder(id_list) {
112 | /* Set the current order to match the lsit of IDs */
113 | var temp_lis = new Array();
114 | for (var i = 0; i < id_list.length; i++) {
115 | var id = 'p' + id_list[i];
116 | temp_lis[temp_lis.length] = document.getElementById(id);
117 | }
118 | reIndex(temp_lis);
119 | lis = temp_lis;
120 | draw();
121 | }
122 |
123 | function addEvent(elm, evType, fn, useCapture)
124 | // addEvent and removeEvent
125 | // cross-browser event handling for IE5+, NS6 and Mozilla
126 | // By Scott Andrew
127 | {
128 | if (elm.addEventListener){
129 | elm.addEventListener(evType, fn, useCapture);
130 | return true;
131 | } else if (elm.attachEvent){
132 | var r = elm.attachEvent("on"+evType, fn);
133 | return r;
134 | } else {
135 | elm['on'+evType] = fn;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/public/javascripts/SelectBox.js:
--------------------------------------------------------------------------------
1 | var SelectBox = {
2 | cache: new Object(),
3 | init: function(id) {
4 | var box = document.getElementById(id);
5 | var node;
6 | SelectBox.cache[id] = new Array();
7 | var cache = SelectBox.cache[id];
8 | for (var i = 0; (node = box.options[i]); i++) {
9 | cache.push({value: node.value, text: node.text, displayed: 1});
10 | }
11 | },
12 | redisplay: function(id) {
13 | // Repopulate HTML select box from cache
14 | var box = document.getElementById(id);
15 | box.options.length = 0; // clear all options
16 | for (var i = 0, j = SelectBox.cache[id].length; i < j; i++) {
17 | var node = SelectBox.cache[id][i];
18 | if (node.displayed) {
19 | box.options[box.options.length] = new Option(node.text, node.value, false, false);
20 | }
21 | }
22 | },
23 | filter: function(id, text) {
24 | // Redisplay the HTML select box, displaying only the choices containing ALL
25 | // the words in text. (It's an AND search.)
26 | var tokens = text.toLowerCase().split(/\s+/);
27 | var node, token;
28 | for (var i = 0; (node = SelectBox.cache[id][i]); i++) {
29 | node.displayed = 1;
30 | for (var j = 0; (token = tokens[j]); j++) {
31 | if (node.text.toLowerCase().indexOf(token) == -1) {
32 | node.displayed = 0;
33 | }
34 | }
35 | }
36 | SelectBox.redisplay(id);
37 | },
38 | delete_from_cache: function(id, value) {
39 | var node, delete_index = null;
40 | for (var i = 0; (node = SelectBox.cache[id][i]); i++) {
41 | if (node.value == value) {
42 | delete_index = i;
43 | break;
44 | }
45 | }
46 | var j = SelectBox.cache[id].length - 1;
47 | for (var i = delete_index; i < j; i++) {
48 | SelectBox.cache[id][i] = SelectBox.cache[id][i+1];
49 | }
50 | SelectBox.cache[id].length--;
51 | },
52 | add_to_cache: function(id, option) {
53 | SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1});
54 | },
55 | cache_contains: function(id, value) {
56 | // Check if an item is contained in the cache
57 | var node;
58 | for (var i = 0; (node = SelectBox.cache[id][i]); i++) {
59 | if (node.value == value) {
60 | return true;
61 | }
62 | }
63 | return false;
64 | },
65 | move: function(from, to) {
66 | var from_box = document.getElementById(from);
67 | var to_box = document.getElementById(to);
68 | var option;
69 | for (var i = 0; (option = from_box.options[i]); i++) {
70 | if (option.selected && SelectBox.cache_contains(from, option.value)) {
71 | SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1});
72 | SelectBox.delete_from_cache(from, option.value);
73 | }
74 | }
75 | SelectBox.redisplay(from);
76 | SelectBox.redisplay(to);
77 | },
78 | move_all: function(from, to) {
79 | var from_box = document.getElementById(from);
80 | var to_box = document.getElementById(to);
81 | var option;
82 | for (var i = 0; (option = from_box.options[i]); i++) {
83 | if (SelectBox.cache_contains(from, option.value)) {
84 | SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1});
85 | SelectBox.delete_from_cache(from, option.value);
86 | }
87 | }
88 | SelectBox.redisplay(from);
89 | SelectBox.redisplay(to);
90 | },
91 | sort: function(id) {
92 | SelectBox.cache[id].sort( function(a, b) {
93 | a = a.text.toLowerCase();
94 | b = b.text.toLowerCase();
95 | try {
96 | if (a > b) return 1;
97 | if (a < b) return -1;
98 | }
99 | catch (e) {
100 | // silently fail on IE 'unknown' exception
101 | }
102 | return 0;
103 | } );
104 | },
105 | select_all: function(id) {
106 | var box = document.getElementById(id);
107 | for (var i = 0; i < box.options.length; i++) {
108 | box.options[i].selected = 'selected';
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/lib/datamapper_support.rb:
--------------------------------------------------------------------------------
1 | require 'dm-core'
2 | require 'dm-aggregates'
3 | require 'dm-types'
4 | require 'dm-validations'
5 |
6 | module MerbAdmin
7 | class AbstractModel
8 | module DatamapperSupport
9 | def get(id)
10 | model.get(id).extend(InstanceMethods)
11 | end
12 |
13 | def count(options = {})
14 | model.count(options.reject{|key, value| [:sort, :sort_reverse].include?(key)})
15 | end
16 |
17 | def first(options = {})
18 | model.first(merge_order(options)).extend(InstanceMethods)
19 | end
20 |
21 | def last(options = {})
22 | model.last(merge_order(options)).extend(InstanceMethods)
23 | end
24 |
25 | def all(options = {})
26 | model.all(merge_order(options))
27 | end
28 |
29 | def paginated(options = {})
30 | page = options.delete(:page) || 1
31 | per_page = options.delete(:per_page) || MerbAdmin[:per_page]
32 |
33 | page_count = (count(options).to_f / per_page).ceil
34 |
35 | options.merge!({
36 | :limit => per_page,
37 | :offset => (page - 1) * per_page
38 | })
39 |
40 | [page_count, all(options)]
41 | end
42 |
43 | def create(params = {})
44 | model.create(params).extend(InstanceMethods)
45 | end
46 |
47 | def new(params = {})
48 | model.new(params).extend(InstanceMethods)
49 | end
50 |
51 | def destroy_all!
52 | model.all.destroy!
53 | end
54 |
55 | def has_many_associations
56 | associations.select do |association|
57 | association[:type] == :has_many
58 | end
59 | end
60 |
61 | def has_one_associations
62 | associations.select do |association|
63 | association[:type] == :has_one
64 | end
65 | end
66 |
67 | def belongs_to_associations
68 | associations.select do |association|
69 | association[:type] == :belongs_to
70 | end
71 | end
72 |
73 | def associations
74 | model.relationships.to_a.map do |name, association|
75 | {
76 | :name => name,
77 | :pretty_name => name.to_s.gsub("_", " ").capitalize,
78 | :type => association_type_lookup(association),
79 | :parent_model => association.parent_model,
80 | :parent_key => association.parent_key.map{|r| r.name},
81 | :child_model => association.child_model,
82 | :child_key => association.child_key.map{|r| r.name},
83 | }
84 | end
85 | end
86 |
87 | def properties
88 | model.properties.map do |property|
89 | {
90 | :name => property.name,
91 | :pretty_name => property.name.to_s.gsub(/_id$/, "").gsub("_", " ").capitalize,
92 | :type => type_lookup(property),
93 | :length => property.respond_to?(:length) ? property.length : nil,
94 | :nullable? => property.allow_nil?,
95 | :serial? => property.serial?,
96 | }
97 | end
98 | end
99 |
100 | private
101 |
102 | def merge_order(options)
103 | @sort ||= options.delete(:sort) || :id
104 | @sort_order ||= options.delete(:sort_reverse) ? :desc : :asc
105 | options.merge(:order => [@sort.to_sym.send(@sort_order)])
106 | end
107 |
108 | def association_type_lookup(association)
109 | if self.model == association.parent_model
110 | association.options[:max] > 1 ? :has_many : :has_one
111 | elsif self.model == association.child_model
112 | :belongs_to
113 | else
114 | raise "Unknown association type"
115 | end
116 | end
117 |
118 | def type_lookup(property)
119 | type = {
120 | BigDecimal => :big_decimal,
121 | DataMapper::Types::Boolean => :boolean,
122 | DataMapper::Types::Serial => :integer,
123 | DataMapper::Types::Text => :text,
124 | Date => :date,
125 | DateTime => :datetime,
126 | FalseClass => :boolean,
127 | Fixnum => :integer,
128 | Float => :float,
129 | Integer => :integer,
130 | String => :string,
131 | Time => :time,
132 | TrueClass => :boolean,
133 | }
134 | type[property.type] || type[property.primitive]
135 | end
136 |
137 | module InstanceMethods
138 | def update_attributes(attributes)
139 | update(attributes)
140 | end
141 | end
142 |
143 | end
144 | end
145 | end
146 |
--------------------------------------------------------------------------------
/lib/active_record_support.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 |
3 | module MerbAdmin
4 | class AbstractModel
5 | module ActiverecordSupport
6 | def get(id)
7 | model.find_by_id(id).extend(InstanceMethods)
8 | rescue ActiveRecord::RecordNotFound
9 | nil
10 | end
11 |
12 | def count(options = {})
13 | model.count(options.reject{|key, value| [:sort, :sort_reverse].include?(key)})
14 | end
15 |
16 | def first(options = {})
17 | model.first(merge_order(options)).extend(InstanceMethods)
18 | end
19 |
20 | def last(options = {})
21 | model.last(merge_order(options)).extend(InstanceMethods)
22 | end
23 |
24 | def all(options = {})
25 | model.all(merge_order(options))
26 | end
27 |
28 | def paginated(options = {})
29 | page = options.delete(:page) || 1
30 | per_page = options.delete(:per_page) || MerbAdmin[:per_page]
31 |
32 | page_count = (count(options).to_f / per_page).ceil
33 |
34 | options.merge!({
35 | :limit => per_page,
36 | :offset => (page - 1) * per_page
37 | })
38 |
39 | [page_count, all(options)]
40 | end
41 |
42 | def create(params = {})
43 | model.create(params).extend(InstanceMethods)
44 | end
45 |
46 | def new(params = {})
47 | model.new(params).extend(InstanceMethods)
48 | end
49 |
50 | def destroy_all!
51 | model.all.each do |object|
52 | object.destroy
53 | end
54 | end
55 |
56 | def has_many_associations
57 | associations.select do |association|
58 | association[:type] == :has_many
59 | end
60 | end
61 |
62 | def has_one_associations
63 | associations.select do |association|
64 | association[:type] == :has_one
65 | end
66 | end
67 |
68 | def belongs_to_associations
69 | associations.select do |association|
70 | association[:type] == :belongs_to
71 | end
72 | end
73 |
74 | def associations
75 | model.reflect_on_all_associations.map do |association|
76 | {
77 | :name => association.name,
78 | :pretty_name => association.name.to_s.gsub('_', ' ').capitalize,
79 | :type => association.macro,
80 | :parent_model => association_parent_model_lookup(association),
81 | :parent_key => association_parent_key_lookup(association),
82 | :child_model => association_child_model_lookup(association),
83 | :child_key => association_child_key_lookup(association),
84 | }
85 | end
86 | end
87 |
88 | def properties
89 | model.columns.map do |property|
90 | {
91 | :name => property.name.to_sym,
92 | :pretty_name => property.human_name,
93 | :type => property.type,
94 | :length => property.limit,
95 | :nullable? => property.null,
96 | :serial? => property.primary,
97 | }
98 | end
99 | end
100 |
101 | private
102 |
103 | def merge_order(options)
104 | @sort ||= options.delete(:sort) || "id"
105 | @sort_order ||= options.delete(:sort_reverse) ? "desc" : "asc"
106 | options.merge(:order => ["#{@sort} #{@sort_order}"])
107 | end
108 |
109 | def association_parent_model_lookup(association)
110 | case association.macro
111 | when :belongs_to
112 | association.klass
113 | when :has_one, :has_many
114 | association.active_record
115 | else
116 | raise "Unknown association type"
117 | end
118 | end
119 |
120 | def association_parent_key_lookup(association)
121 | [:id]
122 | end
123 |
124 | def association_child_model_lookup(association)
125 | case association.macro
126 | when :belongs_to
127 | association.active_record
128 | when :has_one, :has_many
129 | association.klass
130 | else
131 | raise "Unknown association type"
132 | end
133 | end
134 |
135 | def association_child_key_lookup(association)
136 | case association.macro
137 | when :belongs_to
138 | ["#{association.class_name.snake_case}_id".to_sym]
139 | when :has_one, :has_many
140 | [association.primary_key_name.to_sym]
141 | else
142 | raise "Unknown association type"
143 | end
144 | end
145 |
146 | module InstanceMethods
147 | end
148 |
149 | end
150 | end
151 | end
152 |
--------------------------------------------------------------------------------
/app/views/main/list.html.erb:
--------------------------------------------------------------------------------
1 | <%
2 | params = request.params.except(:action, :controller, :model_name)
3 | query = params[:query]
4 | filter = params[:filter]
5 | sort = params[:sort]
6 | sort_reverse = params[:sort_reverse]
7 | filters_exist = !@properties.select{|property| property[:type] == :boolean}.empty?
8 | %>
9 |
10 |
15 |
" id="changelist">
16 |
33 |
34 | <% if filters_exist %>
35 |
36 |
Filter
37 | <% @properties.each do |property| %>
38 | <% property_type = property[:type] %>
39 | <% property_name = property[:name] %>
40 | <% property_pretty_name = property[:pretty_name] %>
41 | <% if property_type == :boolean %>
42 |
By <%= property_pretty_name %>
43 |
44 | - ">
45 | <%= link_to("All", "?" + Merb::Parse.params_to_query_string(params.merge(:filter => (filter || {}).reject{|key, value| key.to_sym == property_name}))) %>
46 |
47 | - ">
48 | <%= link_to("Yes", "?" + Merb::Parse.params_to_query_string(params.merge(:filter => (filter || {}).merge({property_name => "true"})))) %>
49 |
50 | - ">
51 | <%= link_to("No", "?" + Merb::Parse.params_to_query_string(params.merge(:filter => (filter || {}).merge({property_name => "false"})))) %>
52 |
53 |
54 | <% end %>
55 | <% end %>
56 |
57 | <% end %>
58 |
59 |
60 |
61 | <% @properties.each do |property| %>
62 | <% property_name = property[:name] %>
63 | <% property_pretty_name = property[:pretty_name] %>
64 | | ">
65 | <%= link_to(property_pretty_name, "?" + Merb::Parse.params_to_query_string(params.merge(:sort => property_name).reject{|key, value| key.to_sym == :sort_reverse}.merge(sort == property_name.to_s && sort_reverse != "true" ? {:sort_reverse => "true"} : {}))) %>
66 | |
67 | <% end %>
68 |
69 |
70 |
71 | <% @objects.each_with_index do |object, index| %>
72 | ">
73 | <% @properties.each do |property| %>
74 | |
75 | <%= link_to(object_property(object, property), url(:merb_admin_edit, :model_name => @abstract_model.to_param, :id => object.id)) %>
76 | |
77 | <% end %>
78 |
79 | <% end %>
80 |
81 |
82 |
83 | <% if @page_count.to_i > 1 %>
84 | <%= paginate(@current_page, @page_count, :url => "?" + Merb::Parse.params_to_query_string(params)) %>
85 | <% end %>
86 | <%= @record_count %> <%= @record_count == 1 ? @abstract_model.pretty_name.downcase : @abstract_model.pretty_name.downcase.pluralize %>
87 | <% if @page_count.to_i == 2 %>
88 | <%= link_to("Show all", "?" + Merb::Parse.params_to_query_string(params.merge(:all => true)), :class => "showall") %>
89 | <% end %>
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/public/javascripts/calendar.js:
--------------------------------------------------------------------------------
1 | /*
2 | calendar.js - Calendar functions by Adrian Holovaty
3 | */
4 |
5 | function removeChildren(a) { // "a" is reference to an object
6 | while (a.hasChildNodes()) a.removeChild(a.lastChild);
7 | }
8 |
9 | // quickElement(tagType, parentReference, textInChildNode, [, attribute, attributeValue ...]);
10 | function quickElement() {
11 | var obj = document.createElement(arguments[0]);
12 | if (arguments[2] != '' && arguments[2] != null) {
13 | var textNode = document.createTextNode(arguments[2]);
14 | obj.appendChild(textNode);
15 | }
16 | var len = arguments.length;
17 | for (var i = 3; i < len; i += 2) {
18 | obj.setAttribute(arguments[i], arguments[i+1]);
19 | }
20 | arguments[1].appendChild(obj);
21 | return obj;
22 | }
23 |
24 | // CalendarNamespace -- Provides a collection of HTML calendar-related helper functions
25 | var CalendarNamespace = {
26 | monthsOfYear: gettext('January February March April May June July August September October November December').split(' '),
27 | daysOfWeek: gettext('S M T W T F S').split(' '),
28 | isLeapYear: function(year) {
29 | return (((year % 4)==0) && ((year % 100)!=0) || ((year % 400)==0));
30 | },
31 | getDaysInMonth: function(month,year) {
32 | var days;
33 | if (month==1 || month==3 || month==5 || month==7 || month==8 || month==10 || month==12) {
34 | days = 31;
35 | }
36 | else if (month==4 || month==6 || month==9 || month==11) {
37 | days = 30;
38 | }
39 | else if (month==2 && CalendarNamespace.isLeapYear(year)) {
40 | days = 29;
41 | }
42 | else {
43 | days = 28;
44 | }
45 | return days;
46 | },
47 | draw: function(month, year, div_id, callback) { // month = 1-12, year = 1-9999
48 | month = parseInt(month);
49 | year = parseInt(year);
50 | var calDiv = document.getElementById(div_id);
51 | removeChildren(calDiv);
52 | var calTable = document.createElement('table');
53 | quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month-1] + ' ' + year);
54 | var tableBody = quickElement('tbody', calTable);
55 |
56 | // Draw days-of-week header
57 | var tableRow = quickElement('tr', tableBody);
58 | for (var i = 0; i < 7; i++) {
59 | quickElement('th', tableRow, CalendarNamespace.daysOfWeek[i]);
60 | }
61 |
62 | var startingPos = new Date(year, month-1, 1).getDay();
63 | var days = CalendarNamespace.getDaysInMonth(month, year);
64 |
65 | // Draw blanks before first of month
66 | tableRow = quickElement('tr', tableBody);
67 | for (var i = 0; i < startingPos; i++) {
68 | var _cell = quickElement('td', tableRow, ' ');
69 | _cell.style.backgroundColor = '#f3f3f3';
70 | }
71 |
72 | // Draw days of month
73 | var currentDay = 1;
74 | for (var i = startingPos; currentDay <= days; i++) {
75 | if (i%7 == 0 && currentDay != 1) {
76 | tableRow = quickElement('tr', tableBody);
77 | }
78 | var cell = quickElement('td', tableRow, '');
79 | quickElement('a', cell, currentDay, 'href', 'javascript:void(' + callback + '('+year+','+month+','+currentDay+'));');
80 | currentDay++;
81 | }
82 |
83 | // Draw blanks after end of month (optional, but makes for valid code)
84 | while (tableRow.childNodes.length < 7) {
85 | var _cell = quickElement('td', tableRow, ' ');
86 | _cell.style.backgroundColor = '#f3f3f3';
87 | }
88 |
89 | calDiv.appendChild(calTable);
90 | }
91 | }
92 |
93 | // Calendar -- A calendar instance
94 | function Calendar(div_id, callback) {
95 | // div_id (string) is the ID of the element in which the calendar will
96 | // be displayed
97 | // callback (string) is the name of a JavaScript function that will be
98 | // called with the parameters (year, month, day) when a day in the
99 | // calendar is clicked
100 | this.div_id = div_id;
101 | this.callback = callback;
102 | this.today = new Date();
103 | this.currentMonth = this.today.getMonth() + 1;
104 | this.currentYear = this.today.getFullYear();
105 | }
106 | Calendar.prototype = {
107 | drawCurrent: function() {
108 | CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback);
109 | },
110 | drawDate: function(month, year) {
111 | this.currentMonth = month;
112 | this.currentYear = year;
113 | this.drawCurrent();
114 | },
115 | drawPreviousMonth: function() {
116 | if (this.currentMonth == 1) {
117 | this.currentMonth = 12;
118 | this.currentYear--;
119 | }
120 | else {
121 | this.currentMonth--;
122 | }
123 | this.drawCurrent();
124 | },
125 | drawNextMonth: function() {
126 | if (this.currentMonth == 12) {
127 | this.currentMonth = 1;
128 | this.currentYear++;
129 | }
130 | else {
131 | this.currentMonth++;
132 | }
133 | this.drawCurrent();
134 | },
135 | drawPreviousYear: function() {
136 | this.currentYear--;
137 | this.drawCurrent();
138 | },
139 | drawNextYear: function() {
140 | this.currentYear++;
141 | this.drawCurrent();
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/public/javascripts/urlify.js:
--------------------------------------------------------------------------------
1 | var LATIN_MAP = {
2 | 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç':
3 | 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I',
4 | 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö':
5 | 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U',
6 | 'Ý': 'Y', 'Þ': 'TH', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': 'a', 'ä':
7 | 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e',
8 | 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó':
9 | 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u',
10 | 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y'
11 | }
12 | var LATIN_SYMBOLS_MAP = {
13 | '©':'(c)'
14 | }
15 | var GREEK_MAP = {
16 | 'α':'a', 'β':'b', 'γ':'g', 'δ':'d', 'ε':'e', 'ζ':'z', 'η':'h', 'θ':'8',
17 | 'ι':'i', 'κ':'k', 'λ':'l', 'μ':'m', 'ν':'n', 'ξ':'3', 'ο':'o', 'π':'p',
18 | 'ρ':'r', 'σ':'s', 'τ':'t', 'υ':'y', 'φ':'f', 'χ':'x', 'ψ':'ps', 'ω':'w',
19 | 'ά':'a', 'έ':'e', 'ί':'i', 'ό':'o', 'ύ':'y', 'ή':'h', 'ώ':'w', 'ς':'s',
20 | 'ϊ':'i', 'ΰ':'y', 'ϋ':'y', 'ΐ':'i',
21 | 'Α':'A', 'Β':'B', 'Γ':'G', 'Δ':'D', 'Ε':'E', 'Ζ':'Z', 'Η':'H', 'Θ':'8',
22 | 'Ι':'I', 'Κ':'K', 'Λ':'L', 'Μ':'M', 'Ν':'N', 'Ξ':'3', 'Ο':'O', 'Π':'P',
23 | 'Ρ':'R', 'Σ':'S', 'Τ':'T', 'Υ':'Y', 'Φ':'F', 'Χ':'X', 'Ψ':'PS', 'Ω':'W',
24 | 'Ά':'A', 'Έ':'E', 'Ί':'I', 'Ό':'O', 'Ύ':'Y', 'Ή':'H', 'Ώ':'W', 'Ϊ':'I',
25 | 'Ϋ':'Y'
26 | }
27 | var TURKISH_MAP = {
28 | 'ş':'s', 'Ş':'S', 'ı':'i', 'İ':'I', 'ç':'c', 'Ç':'C', 'ü':'u', 'Ü':'U',
29 | 'ö':'o', 'Ö':'O', 'ğ':'g', 'Ğ':'G'
30 | }
31 | var RUSSIAN_MAP = {
32 | 'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh',
33 | 'з':'z', 'и':'i', 'й':'j', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o',
34 | 'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'c',
35 | 'ч':'ch', 'ш':'sh', 'щ':'sh', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu',
36 | 'я':'ya',
37 | 'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh',
38 | 'З':'Z', 'И':'I', 'Й':'J', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O',
39 | 'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'C',
40 | 'Ч':'Ch', 'Ш':'Sh', 'Щ':'Sh', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu',
41 | 'Я':'Ya'
42 | }
43 | var UKRAINIAN_MAP = {
44 | 'Є':'Ye', 'І':'I', 'Ї':'Yi', 'Ґ':'G', 'є':'ye', 'і':'i', 'ї':'yi', 'ґ':'g'
45 | }
46 | var CZECH_MAP = {
47 | 'č':'c', 'ď':'d', 'ě':'e', 'ň': 'n', 'ř':'r', 'š':'s', 'ť':'t', 'ů':'u',
48 | 'ž':'z', 'Č':'C', 'Ď':'D', 'Ě':'E', 'Ň': 'N', 'Ř':'R', 'Š':'S', 'Ť':'T',
49 | 'Ů':'U', 'Ž':'Z'
50 | }
51 |
52 | var POLISH_MAP = {
53 | 'ą':'a', 'ć':'c', 'ę':'e', 'ł':'l', 'ń':'n', 'ó':'o', 'ś':'s', 'ź':'z',
54 | 'ż':'z', 'Ą':'A', 'Ć':'C', 'Ę':'e', 'Ł':'L', 'Ń':'N', 'Ó':'o', 'Ś':'S',
55 | 'Ź':'Z', 'Ż':'Z'
56 | }
57 |
58 | var LATVIAN_MAP = {
59 | 'ā':'a', 'č':'c', 'ē':'e', 'ģ':'g', 'ī':'i', 'ķ':'k', 'ļ':'l', 'ņ':'n',
60 | 'š':'s', 'ū':'u', 'ž':'z', 'Ā':'A', 'Č':'C', 'Ē':'E', 'Ģ':'G', 'Ī':'i',
61 | 'Ķ':'k', 'Ļ':'L', 'Ņ':'N', 'Š':'S', 'Ū':'u', 'Ž':'Z'
62 | }
63 |
64 | var ALL_DOWNCODE_MAPS=new Array()
65 | ALL_DOWNCODE_MAPS[0]=LATIN_MAP
66 | ALL_DOWNCODE_MAPS[1]=LATIN_SYMBOLS_MAP
67 | ALL_DOWNCODE_MAPS[2]=GREEK_MAP
68 | ALL_DOWNCODE_MAPS[3]=TURKISH_MAP
69 | ALL_DOWNCODE_MAPS[4]=RUSSIAN_MAP
70 | ALL_DOWNCODE_MAPS[5]=UKRAINIAN_MAP
71 | ALL_DOWNCODE_MAPS[6]=CZECH_MAP
72 | ALL_DOWNCODE_MAPS[7]=POLISH_MAP
73 | ALL_DOWNCODE_MAPS[8]=LATVIAN_MAP
74 |
75 | var Downcoder = new Object();
76 | Downcoder.Initialize = function()
77 | {
78 | if (Downcoder.map) // already made
79 | return ;
80 | Downcoder.map ={}
81 | Downcoder.chars = '' ;
82 | for(var i in ALL_DOWNCODE_MAPS)
83 | {
84 | var lookup = ALL_DOWNCODE_MAPS[i]
85 | for (var c in lookup)
86 | {
87 | Downcoder.map[c] = lookup[c] ;
88 | Downcoder.chars += c ;
89 | }
90 | }
91 | Downcoder.regex = new RegExp('[' + Downcoder.chars + ']|[^' + Downcoder.chars + ']+','g') ;
92 | }
93 |
94 | downcode= function( slug )
95 | {
96 | Downcoder.Initialize() ;
97 | var downcoded =""
98 | var pieces = slug.match(Downcoder.regex);
99 | if(pieces)
100 | {
101 | for (var i = 0 ; i < pieces.length ; i++)
102 | {
103 | if (pieces[i].length == 1)
104 | {
105 | var mapped = Downcoder.map[pieces[i]] ;
106 | if (mapped != null)
107 | {
108 | downcoded+=mapped;
109 | continue ;
110 | }
111 | }
112 | downcoded+=pieces[i];
113 | }
114 | }
115 | else
116 | {
117 | downcoded = slug;
118 | }
119 | return downcoded;
120 | }
121 |
122 |
123 | function URLify(s, num_chars) {
124 | // changes, e.g., "Petty theft" to "petty_theft"
125 | // remove all these words from the string before urlifying
126 | s = downcode(s);
127 | removelist = ["a", "an", "as", "at", "before", "but", "by", "for", "from",
128 | "is", "in", "into", "like", "of", "off", "on", "onto", "per",
129 | "since", "than", "the", "this", "that", "to", "up", "via",
130 | "with"];
131 | r = new RegExp('\\b(' + removelist.join('|') + ')\\b', 'gi');
132 | s = s.replace(r, '');
133 | // if downcode doesn't hit, the char will be stripped here
134 | s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars
135 | s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces
136 | s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens
137 | s = s.toLowerCase(); // convert to lowercase
138 | return s.substring(0, num_chars);// trim to first num_chars chars
139 | }
140 |
141 |
--------------------------------------------------------------------------------
/public/stylesheets/changelists.css:
--------------------------------------------------------------------------------
1 | /* CHANGELISTS */
2 |
3 | #changelist {
4 | position: relative;
5 | width: 100%;
6 | }
7 |
8 | #changelist table {
9 | width: 100%;
10 | }
11 |
12 | .change-list .filtered table {
13 | border-right: 1px solid #ddd;
14 | }
15 |
16 | .change-list .filtered {
17 | min-height: 400px;
18 | }
19 |
20 | .change-list .filtered {
21 | background: white url(../images/changelist-bg.gif) top right repeat-y !important;
22 | }
23 |
24 | .change-list .filtered table, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {
25 | margin-right: 160px !important;
26 | width: auto !important;
27 | }
28 |
29 | .change-list .filtered table tbody th {
30 | padding-right: 1em;
31 | }
32 |
33 | #changelist .toplinks {
34 | border-bottom: 1px solid #ccc !important;
35 | }
36 |
37 | #changelist .paginator {
38 | color: #666;
39 | border-top: 1px solid #eee;
40 | border-bottom: 1px solid #eee;
41 | background: white url(../images/nav-bg.gif) 0 180% repeat-x;
42 | overflow: hidden;
43 | }
44 |
45 | .change-list .filtered .paginator {
46 | border-right: 1px solid #ddd;
47 | }
48 |
49 | /* CHANGELIST TABLES */
50 |
51 | #changelist table thead th {
52 | white-space: nowrap;
53 | vertical-align: middle;
54 | }
55 |
56 | #changelist table thead th:first-child {
57 | width: 1.5em;
58 | text-align: center;
59 | }
60 |
61 | #changelist table tbody td {
62 | border-left: 1px solid #ddd;
63 | }
64 |
65 | #changelist table tbody td:first-child {
66 | border-left: 0;
67 | border-right: 1px solid #ddd;
68 | text-align: center;
69 | }
70 |
71 | #changelist table tfoot {
72 | color: #666;
73 | }
74 |
75 | /* TOOLBAR */
76 |
77 | #changelist #toolbar {
78 | padding: 3px;
79 | border-bottom: 1px solid #ddd;
80 | background: #e1e1e1 url(../images/nav-bg.gif) top left repeat-x;
81 | color: #666;
82 | }
83 |
84 | #changelist #toolbar form input {
85 | font-size: 11px;
86 | padding: 1px 2px;
87 | }
88 |
89 | #changelist #toolbar form #searchbar {
90 | padding: 2px;
91 | }
92 |
93 | #changelist #changelist-search img {
94 | vertical-align: middle;
95 | }
96 |
97 | /* FILTER COLUMN */
98 |
99 | #changelist-filter {
100 | position: absolute;
101 | top: 0;
102 | right: 0;
103 | z-index: 1000;
104 | width: 160px;
105 | border-left: 1px solid #ddd;
106 | background: #efefef;
107 | margin: 0;
108 | }
109 |
110 | #changelist-filter h2 {
111 | font-size: 11px;
112 | padding: 2px 5px;
113 | border-bottom: 1px solid #ddd;
114 | }
115 |
116 | #changelist-filter h3 {
117 | font-size: 12px;
118 | margin-bottom: 0;
119 | }
120 |
121 | #changelist-filter ul {
122 | padding-left: 0;
123 | margin-left: 10px;
124 | }
125 |
126 | #changelist-filter li {
127 | list-style-type: none;
128 | margin-left: 0;
129 | padding-left: 0;
130 | }
131 |
132 | #changelist-filter a {
133 | color: #999;
134 | }
135 |
136 | #changelist-filter a:hover {
137 | color: #036;
138 | }
139 |
140 | #changelist-filter li.selected {
141 | border-left: 5px solid #ccc;
142 | padding-left: 5px;
143 | margin-left: -10px;
144 | }
145 |
146 | #changelist-filter li.selected a {
147 | color: #5b80b2 !important;
148 | }
149 |
150 | /* DATE DRILLDOWN */
151 |
152 | .change-list ul.toplinks {
153 | display: block;
154 | background: white url(../images/nav-bg-reverse.gif) 0 -10px repeat-x;
155 | border-top: 1px solid white;
156 | float: left;
157 | padding: 0 !important;
158 | margin: 0 !important;
159 | width: 100%;
160 | }
161 |
162 | .change-list ul.toplinks li {
163 | float: left;
164 | width: 9em;
165 | padding: 3px 6px;
166 | font-weight: bold;
167 | list-style-type: none;
168 | }
169 |
170 | .change-list ul.toplinks .date-back a {
171 | color: #999;
172 | }
173 |
174 | .change-list ul.toplinks .date-back a:hover {
175 | color: #036;
176 | }
177 |
178 | /* PAGINATOR */
179 |
180 | .paginator {
181 | font-size: 11px;
182 | padding-top: 10px;
183 | padding-bottom: 10px;
184 | line-height: 22px;
185 | margin: 0;
186 | border-top: 1px solid #ddd;
187 | }
188 |
189 | .paginator a:link, .paginator a:visited {
190 | padding: 2px 6px;
191 | border: solid 1px #ccc;
192 | background: white;
193 | text-decoration: none;
194 | }
195 |
196 | .paginator a.showall {
197 | padding: 0 !important;
198 | border: none !important;
199 | }
200 |
201 | .paginator a.showall:hover {
202 | color: #036 !important;
203 | background: transparent !important;
204 | }
205 |
206 | .paginator .end {
207 | border-width: 2px !important;
208 | margin-right: 6px;
209 | }
210 |
211 | .paginator .this-page {
212 | padding: 2px 6px;
213 | font-weight: bold;
214 | font-size: 13px;
215 | vertical-align: top;
216 | }
217 |
218 | .paginator a:hover {
219 | color: white;
220 | background: #5b80b2;
221 | border-color: #036;
222 | }
223 |
224 | /* ACTIONS */
225 |
226 | .filtered .actions {
227 | margin-right: 160px !important;
228 | border-right: 1px solid #ddd;
229 | }
230 |
231 | #changelist table input {
232 | margin: 0;
233 | }
234 |
235 | #changelist table tbody tr.selected {
236 | background-color: #FFFFCC;
237 | }
238 |
239 | #changelist .actions {
240 | color: #999;
241 | padding: 3px;
242 | border-top: 1px solid #fff;
243 | border-bottom: 1px solid #ddd;
244 | background: white url(../images/nav-bg-reverse.gif) 0 -10px repeat-x;
245 | }
246 |
247 | #changelist .actions:last-child {
248 | border-bottom: none;
249 | }
250 |
251 | #changelist .actions select {
252 | border: 1px solid #aaa;
253 | margin-left: 0.5em;
254 | padding: 1px 2px;
255 | }
256 |
257 | #changelist .actions label {
258 | font-size: 11px;
259 | margin-left: 0.5em;
260 | }
261 |
262 | #changelist #action-toggle {
263 | display: none;
264 | }
265 |
266 | #changelist .actions .button {
267 | font-size: 11px;
268 | padding: 1px 2px;
269 | }
270 |
--------------------------------------------------------------------------------
/app/controllers/main.rb:
--------------------------------------------------------------------------------
1 | require File.join( File.dirname(__FILE__), '..', '..', 'lib', 'abstract_model' )
2 |
3 | class MerbAdmin::Main < MerbAdmin::Application
4 | include Merb::MerbAdmin::MainHelper
5 |
6 | before :get_model, :exclude => ['index']
7 | before :get_object, :only => ['edit', 'update', 'delete', 'destroy']
8 | before :get_attributes, :only => ['create', 'update']
9 |
10 | def index
11 | @abstract_models = MerbAdmin::AbstractModel.all
12 | render(:layout => 'dashboard')
13 | end
14 |
15 | def list
16 | options = {}
17 | options.merge!(get_sort_hash)
18 | options.merge!(get_sort_reverse_hash)
19 | options.merge!(get_query_hash(options))
20 | options.merge!(get_filter_hash(options))
21 | per_page = MerbAdmin[:per_page]
22 | if params[:all]
23 | options.merge!(:limit => per_page * 2)
24 | @objects = @abstract_model.all(options).reverse
25 | else
26 | @current_page = (params[:page] || 1).to_i
27 | options.merge!(:page => @current_page, :per_page => per_page)
28 | @page_count, @objects = @abstract_model.paginated(options)
29 | options.delete(:page)
30 | options.delete(:per_page)
31 | options.delete(:offset)
32 | options.delete(:limit)
33 | end
34 | @record_count = @abstract_model.count(options)
35 | render(:layout => 'list')
36 | end
37 |
38 | def new
39 | @object = @abstract_model.new
40 | render(:layout => 'form')
41 | end
42 |
43 | def create
44 | @object = @abstract_model.new(@attributes)
45 | if @object.save && update_all_associations
46 | redirect_on_success
47 | else
48 | render_error(:new)
49 | end
50 | end
51 |
52 | def edit
53 | render(:layout => 'form')
54 | end
55 |
56 | def update
57 | if @object.update_attributes(@attributes) && update_all_associations
58 | redirect_on_success
59 | else
60 | render_error(:edit)
61 | end
62 | end
63 |
64 | def delete
65 | render(:layout => 'form')
66 | end
67 |
68 | def destroy
69 | if @object.destroy
70 | redirect(url(:merb_admin_list, :model_name => @abstract_model.to_param), :message => {:notice => "#{@abstract_model.pretty_name} was successfully destroyed"})
71 | else
72 | raise BadRequest
73 | end
74 | end
75 |
76 | private
77 |
78 | def get_model
79 | model_name = to_model_name(params[:model_name])
80 | @abstract_model = MerbAdmin::AbstractModel.new(model_name)
81 | @properties = @abstract_model.properties
82 | end
83 |
84 | def get_object
85 | @object = @abstract_model.get(params[:id])
86 | raise NotFound unless @object
87 | end
88 |
89 | def get_sort_hash
90 | sort = params[:sort]
91 | sort ? {:sort => sort} : {}
92 | end
93 |
94 | def get_sort_reverse_hash
95 | sort_reverse = params[:sort_reverse]
96 | sort_reverse ? {:sort_reverse => sort_reverse == "true"} : {}
97 | end
98 |
99 | def get_query_hash(options)
100 | query = params[:query]
101 | return {} unless query
102 | statements = []
103 | values = []
104 | conditions = options[:conditions] || [""]
105 |
106 | @properties.select{|property| property[:type] == :string}.each do |property|
107 | statements << "(#{property[:name]} LIKE ?)"
108 | values << "%#{query}%"
109 | end
110 |
111 | conditions[0] += " AND " unless conditions == [""]
112 | conditions[0] += statements.join(" OR ")
113 | conditions += values
114 | conditions != [""] ? {:conditions => conditions} : {}
115 | end
116 |
117 | def get_filter_hash(options)
118 | filter = params[:filter]
119 | return {} unless filter
120 | statements = []
121 | values = []
122 | conditions = options[:conditions] || [""]
123 |
124 | filter.each_pair do |key, value|
125 | @properties.select{|property| property[:type] == :boolean && property[:name] == key.to_sym}.each do |property|
126 | statements << "(#{key} = ?)"
127 | values << (value == "true")
128 | end
129 | end
130 |
131 | conditions[0] += " AND " unless conditions == [""]
132 | conditions[0] += statements.join(" AND ")
133 | conditions += values
134 | conditions != [""] ? {:conditions => conditions} : {}
135 | end
136 |
137 | def get_attributes
138 | @attributes = params[@abstract_model.to_param] || {}
139 | # Delete fields that are blank
140 | @attributes.each do |key, value|
141 | @attributes[key] = nil if value.blank?
142 | end
143 | end
144 |
145 | def update_all_associations
146 | @abstract_model.associations.each do |association|
147 | ids = (params[:associations] || {}).delete(association[:name])
148 | case association[:type]
149 | when :has_one
150 | update_association(association, ids)
151 | when :has_many
152 | update_associations(association, ids.to_a)
153 | end
154 | end
155 | end
156 |
157 | def update_association(association, id = nil)
158 | associated_model = MerbAdmin::AbstractModel.new(association[:child_model])
159 | if object = associated_model.get(id)
160 | object.update_attributes(association[:child_key].first => @object.id)
161 | end
162 | end
163 |
164 | def update_associations(association, ids = [])
165 | @object.send(association[:name]).clear
166 | ids.each do |id|
167 | update_association(association, id)
168 | end
169 | @object.save
170 | end
171 |
172 | def redirect_on_success
173 | param = @abstract_model.to_param
174 | pretty_name = @abstract_model.pretty_name
175 | action = params[:action]
176 | if params[:_continue]
177 | redirect(url(:merb_admin_edit, :model_name => param, :id => @object.id), :message => {:notice => "#{pretty_name} was successfully #{action}d"})
178 | elsif params[:_add_another]
179 | redirect(url(:merb_admin_new, :model_name => param), :message => {:notice => "#{pretty_name} was successfully #{action}d"})
180 | else
181 | redirect(url(:merb_admin_list, :model_name => param), :message => {:notice => "#{pretty_name} was successfully #{action}d"})
182 | end
183 | end
184 |
185 | def render_error(template)
186 | action = params[:action]
187 | message[:error] = "#{@abstract_model.pretty_name} failed to be #{action}d"
188 | render(template, :layout => 'form')
189 | end
190 |
191 | end
192 |
--------------------------------------------------------------------------------
/public/javascripts/core.js:
--------------------------------------------------------------------------------
1 | // Core javascript helper functions
2 |
3 | // basic browser identification & version
4 | var isOpera = (navigator.userAgent.indexOf("Opera")>=0) && parseFloat(navigator.appVersion);
5 | var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]);
6 |
7 | // Cross-browser event handlers.
8 | function addEvent(obj, evType, fn) {
9 | if (obj.addEventListener) {
10 | obj.addEventListener(evType, fn, false);
11 | return true;
12 | } else if (obj.attachEvent) {
13 | var r = obj.attachEvent("on" + evType, fn);
14 | return r;
15 | } else {
16 | return false;
17 | }
18 | }
19 |
20 | function removeEvent(obj, evType, fn) {
21 | if (obj.removeEventListener) {
22 | obj.removeEventListener(evType, fn, false);
23 | return true;
24 | } else if (obj.detachEvent) {
25 | obj.detachEvent("on" + evType, fn);
26 | return true;
27 | } else {
28 | return false;
29 | }
30 | }
31 |
32 | // quickElement(tagType, parentReference, textInChildNode, [, attribute, attributeValue ...]);
33 | function quickElement() {
34 | var obj = document.createElement(arguments[0]);
35 | if (arguments[2] != '' && arguments[2] != null) {
36 | var textNode = document.createTextNode(arguments[2]);
37 | obj.appendChild(textNode);
38 | }
39 | var len = arguments.length;
40 | for (var i = 3; i < len; i += 2) {
41 | obj.setAttribute(arguments[i], arguments[i+1]);
42 | }
43 | arguments[1].appendChild(obj);
44 | return obj;
45 | }
46 |
47 | // ----------------------------------------------------------------------------
48 | // Cross-browser xmlhttp object
49 | // from http://jibbering.com/2002/4/httprequest.html
50 | // ----------------------------------------------------------------------------
51 | var xmlhttp;
52 | /*@cc_on @*/
53 | /*@if (@_jscript_version >= 5)
54 | try {
55 | xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
56 | } catch (e) {
57 | try {
58 | xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
59 | } catch (E) {
60 | xmlhttp = false;
61 | }
62 | }
63 | @else
64 | xmlhttp = false;
65 | @end @*/
66 | if (!xmlhttp && typeof XMLHttpRequest != 'undefined') {
67 | xmlhttp = new XMLHttpRequest();
68 | }
69 |
70 | // ----------------------------------------------------------------------------
71 | // Find-position functions by PPK
72 | // See http://www.quirksmode.org/js/findpos.html
73 | // ----------------------------------------------------------------------------
74 | function findPosX(obj) {
75 | var curleft = 0;
76 | if (obj.offsetParent) {
77 | while (obj.offsetParent) {
78 | curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft);
79 | obj = obj.offsetParent;
80 | }
81 | // IE offsetParent does not include the top-level
82 | if (isIE && obj.parentElement){
83 | curleft += obj.offsetLeft - obj.scrollLeft;
84 | }
85 | } else if (obj.x) {
86 | curleft += obj.x;
87 | }
88 | return curleft;
89 | }
90 |
91 | function findPosY(obj) {
92 | var curtop = 0;
93 | if (obj.offsetParent) {
94 | while (obj.offsetParent) {
95 | curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop);
96 | obj = obj.offsetParent;
97 | }
98 | // IE offsetParent does not include the top-level
99 | if (isIE && obj.parentElement){
100 | curtop += obj.offsetTop - obj.scrollTop;
101 | }
102 | } else if (obj.y) {
103 | curtop += obj.y;
104 | }
105 | return curtop;
106 | }
107 |
108 | //-----------------------------------------------------------------------------
109 | // Date object extensions
110 | // ----------------------------------------------------------------------------
111 | Date.prototype.getCorrectYear = function() {
112 | // Date.getYear() is unreliable --
113 | // see http://www.quirksmode.org/js/introdate.html#year
114 | var y = this.getYear() % 100;
115 | return (y < 38) ? y + 2000 : y + 1900;
116 | }
117 |
118 | Date.prototype.getTwoDigitMonth = function() {
119 | return (this.getMonth() < 9) ? '0' + (this.getMonth()+1) : (this.getMonth()+1);
120 | }
121 |
122 | Date.prototype.getTwoDigitDate = function() {
123 | return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate();
124 | }
125 |
126 | Date.prototype.getTwoDigitHour = function() {
127 | return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours();
128 | }
129 |
130 | Date.prototype.getTwoDigitMinute = function() {
131 | return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes();
132 | }
133 |
134 | Date.prototype.getTwoDigitSecond = function() {
135 | return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds();
136 | }
137 |
138 | Date.prototype.getISODate = function() {
139 | return this.getCorrectYear() + '-' + this.getTwoDigitMonth() + '-' + this.getTwoDigitDate();
140 | }
141 |
142 | Date.prototype.getHourMinute = function() {
143 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute();
144 | }
145 |
146 | Date.prototype.getHourMinuteSecond = function() {
147 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond();
148 | }
149 |
150 | // ----------------------------------------------------------------------------
151 | // String object extensions
152 | // ----------------------------------------------------------------------------
153 | String.prototype.pad_left = function(pad_length, pad_string) {
154 | var new_string = this;
155 | for (var i = 0; new_string.length < pad_length; i++) {
156 | new_string = pad_string + new_string;
157 | }
158 | return new_string;
159 | }
160 |
161 | // ----------------------------------------------------------------------------
162 | // Get the computed style for and element
163 | // ----------------------------------------------------------------------------
164 | function getStyle(oElm, strCssRule){
165 | var strValue = "";
166 | if(document.defaultView && document.defaultView.getComputedStyle){
167 | strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
168 | }
169 | else if(oElm.currentStyle){
170 | strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
171 | return p1.toUpperCase();
172 | });
173 | strValue = oElm.currentStyle[strCssRule];
174 | }
175 | return strValue;
176 | }
177 |
--------------------------------------------------------------------------------
/public/javascripts/SelectFilter2.js:
--------------------------------------------------------------------------------
1 | /*
2 | SelectFilter2 - Turns a multiple-select box into a filter interface.
3 |
4 | Different than SelectFilter because this is coupled to the admin framework.
5 |
6 | Requires core.js, SelectBox.js and addevent.js.
7 | */
8 |
9 | function findForm(node) {
10 | // returns the node of the form containing the given node
11 | if (node.tagName.toLowerCase() != 'form') {
12 | return findForm(node.parentNode);
13 | }
14 | return node;
15 | }
16 |
17 | var SelectFilter = {
18 | init: function(field_id, field_name, is_stacked, admin_media_prefix) {
19 | var from_box = document.getElementById(field_id);
20 | from_box.id += '_from'; // change its ID
21 | from_box.className = 'filtered';
22 |
23 | // Remove , because it just gets in the way.
24 | var ps = from_box.parentNode.getElementsByTagName('p');
25 | for (var i=0; i or
30 | var selector_div = quickElement('div', from_box.parentNode);
31 | selector_div.className = is_stacked ? 'selector stacked' : 'selector';
32 |
33 | //
34 | var selector_available = quickElement('div', selector_div, '');
35 | selector_available.className = 'selector-available';
36 | quickElement('h2', selector_available, interpolate(gettext('Available %s'), [field_name]));
37 | var filter_p = quickElement('p', selector_available, '');
38 | filter_p.className = 'selector-filter';
39 | quickElement('img', filter_p, '', 'src', admin_media_prefix + '/selector-search.gif');
40 | filter_p.appendChild(document.createTextNode(' '));
41 | var filter_input = quickElement('input', filter_p, '', 'type', 'text');
42 | filter_input.id = field_id + '_input';
43 | selector_available.appendChild(from_box);
44 | var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); })()');
45 | choose_all.className = 'selector-chooseall';
46 |
47 | //
48 | var selector_chooser = quickElement('ul', selector_div, '');
49 | selector_chooser.className = 'selector-chooser';
50 | var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Add'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to");})()');
51 | add_link.className = 'selector-add';
52 | var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from");})()');
53 | remove_link.className = 'selector-remove';
54 |
55 | //
56 | var selector_chosen = quickElement('div', selector_div, '');
57 | selector_chosen.className = 'selector-chosen';
58 | quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s'), [field_name]));
59 | var selector_filter = quickElement('p', selector_chosen, gettext('Select your choice(s) and click '));
60 | selector_filter.className = 'selector-filter';
61 | quickElement('img', selector_filter, '', 'src', admin_media_prefix + (is_stacked ? '/selector_stacked-add.gif':'/selector-add.gif'), 'alt', 'Add');
62 | var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
63 | to_box.className = 'filtered';
64 | var clear_all = quickElement('a', selector_chosen, gettext('Clear all'), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from");})()');
65 | clear_all.className = 'selector-clearall';
66 |
67 | from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
68 |
69 | // Set up the JavaScript event handlers for the select box filter interface
70 | addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
71 | addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
72 | addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); });
73 | addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); });
74 | addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
75 | SelectBox.init(field_id + '_from');
76 | SelectBox.init(field_id + '_to');
77 | // Move selected from_box options to to_box
78 | SelectBox.move(field_id + '_from', field_id + '_to');
79 | },
80 | filter_key_up: function(event, field_id) {
81 | from = document.getElementById(field_id + '_from');
82 | // don't submit form if user pressed Enter
83 | if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
84 | from.selectedIndex = 0;
85 | SelectBox.move(field_id + '_from', field_id + '_to');
86 | from.selectedIndex = 0;
87 | return false;
88 | }
89 | var temp = from.selectedIndex;
90 | SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
91 | from.selectedIndex = temp;
92 | return true;
93 | },
94 | filter_key_down: function(event, field_id) {
95 | from = document.getElementById(field_id + '_from');
96 | // right arrow -- move across
97 | if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
98 | var old_index = from.selectedIndex;
99 | SelectBox.move(field_id + '_from', field_id + '_to');
100 | from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index;
101 | return false;
102 | }
103 | // down arrow -- wrap around
104 | if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) {
105 | from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
106 | }
107 | // up arrow -- wrap around
108 | if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) {
109 | from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1;
110 | }
111 | return true;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/public/stylesheets/forms.css:
--------------------------------------------------------------------------------
1 | @import url('widgets.css');
2 |
3 | /* FORM ROWS */
4 |
5 | .form-row {
6 | overflow: hidden;
7 | padding: 8px 12px;
8 | font-size: 11px;
9 | border-bottom: 1px solid #eee;
10 | }
11 |
12 | .form-row img, .form-row input {
13 | vertical-align: middle;
14 | }
15 |
16 | form .form-row p {
17 | padding-left: 0;
18 | font-size: 11px;
19 | }
20 |
21 | /* FORM LABELS */
22 |
23 | form h4 {
24 | margin: 0 !important;
25 | padding: 0 !important;
26 | border: none !important;
27 | }
28 |
29 | label {
30 | font-weight: normal !important;
31 | color: #666;
32 | font-size: 12px;
33 | }
34 |
35 | .required label, label.required {
36 | font-weight: bold !important;
37 | color: #333 !important;
38 | }
39 |
40 | /* RADIO BUTTONS */
41 |
42 | form ul.radiolist li {
43 | list-style-type: none;
44 | }
45 |
46 | form ul.radiolist label {
47 | float: none;
48 | display: inline;
49 | }
50 |
51 | form ul.inline {
52 | margin-left: 0;
53 | padding: 0;
54 | }
55 |
56 | form ul.inline li {
57 | float: left;
58 | padding-right: 7px;
59 | }
60 |
61 | /* ALIGNED FIELDSETS */
62 |
63 | .aligned label {
64 | display: block;
65 | padding: 3px 10px 0 0;
66 | float: left;
67 | width: 8em;
68 | }
69 |
70 | .colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
71 | width: 350px;
72 | }
73 |
74 | form .aligned p, form .aligned ul {
75 | margin-left: 7em;
76 | padding-left: 30px;
77 | }
78 |
79 | form .aligned table p {
80 | margin-left: 0;
81 | padding-left: 0;
82 | }
83 |
84 | form .aligned p.help {
85 | padding-left: 38px;
86 | }
87 |
88 | .aligned .vCheckboxLabel {
89 | float: none !important;
90 | display: inline;
91 | padding-left: 4px;
92 | }
93 |
94 | .colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
95 | width: 610px;
96 | }
97 |
98 | .checkbox-row p.help {
99 | margin-left: 0;
100 | padding-left: 0 !important;
101 | }
102 |
103 | fieldset .field-box {
104 | float: left;
105 | margin-right: 20px;
106 | }
107 |
108 | /* WIDE FIELDSETS */
109 |
110 | .wide label {
111 | width: 15em !important;
112 | }
113 |
114 | form .wide p {
115 | margin-left: 15em;
116 | }
117 |
118 | form .wide p.help {
119 | padding-left: 38px;
120 | }
121 |
122 | .colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
123 | width: 450px;
124 | }
125 |
126 | /* COLLAPSED FIELDSETS */
127 |
128 | fieldset.collapsed * {
129 | display: none;
130 | }
131 |
132 | fieldset.collapsed h2, fieldset.collapsed {
133 | display: block !important;
134 | }
135 |
136 | fieldset.collapsed h2 {
137 | background-image: url(../images/nav-bg.gif);
138 | background-position: bottom left;
139 | color: #999;
140 | }
141 |
142 | fieldset.collapsed .collapse-toggle {
143 | padding: 3px 5px !important;
144 | background: transparent;
145 | display: inline !important;
146 | }
147 |
148 | /* MONOSPACE TEXTAREAS */
149 |
150 | fieldset.monospace textarea {
151 | font-family: "Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace;
152 | }
153 |
154 | /* SUBMIT ROW */
155 |
156 | .submit-row {
157 | padding: 5px 7px;
158 | text-align: right;
159 | background: white url(../images/nav-bg.gif) 0 100% repeat-x;
160 | border: 1px solid #ccc;
161 | margin: 5px 0;
162 | overflow: hidden;
163 | }
164 |
165 | .submit-row input {
166 | margin: 0 0 0 5px;
167 | }
168 |
169 | .submit-row p {
170 | margin: 0.3em;
171 | }
172 |
173 | .submit-row p.deletelink-box {
174 | float: left;
175 | }
176 |
177 | .submit-row .deletelink {
178 | background: url(../images/icon_deletelink.gif) 0 50% no-repeat;
179 | padding-left: 14px;
180 | }
181 |
182 | /* CUSTOM FORM FIELDS */
183 |
184 | .vSelectMultipleField {
185 | vertical-align: top !important;
186 | }
187 |
188 | .vCheckboxField {
189 | border: none;
190 | }
191 |
192 | .vDateField, .vTimeField {
193 | margin-right: 2px;
194 | }
195 |
196 | .vURLField {
197 | width: 30em;
198 | }
199 |
200 | .vLargeTextField, .vXMLLargeTextField {
201 | width: 48em;
202 | }
203 |
204 | .flatpages-flatpage #id_content {
205 | height: 40.2em;
206 | }
207 |
208 | .module table .vPositiveSmallIntegerField {
209 | width: 2.2em;
210 | }
211 |
212 | .vTextField {
213 | width: 20em;
214 | }
215 |
216 | .vIntegerField {
217 | width: 5em;
218 | }
219 |
220 | .vForeignKeyRawIdAdminField {
221 | width: 5em;
222 | }
223 |
224 | /* INLINES */
225 |
226 | .inline-group {
227 | padding: 0;
228 | border: 1px solid #ccc;
229 | margin: 10px 0;
230 | }
231 |
232 | .inline-group .aligned label {
233 | width: 8em;
234 | }
235 |
236 | .inline-related {
237 | position: relative;
238 | }
239 |
240 | .inline-related h3 {
241 | margin: 0;
242 | color: #666;
243 | padding: 3px 5px;
244 | font-size: 11px;
245 | background: #e1e1e1 url(../images/nav-bg.gif) top left repeat-x;
246 | border-bottom: 1px solid #ddd;
247 | }
248 |
249 | .inline-related h3 span.delete {
250 | padding-left: 20px;
251 | position: absolute;
252 | top: 2px;
253 | right: 10px;
254 | }
255 |
256 | .inline-related h3 span.delete label {
257 | margin-left: 2px;
258 | font-size: 11px;
259 | }
260 |
261 | .inline-related fieldset {
262 | margin: 0;
263 | background: #fff;
264 | border: none;
265 | }
266 |
267 | .inline-related fieldset.module h3 {
268 | margin: 0;
269 | padding: 2px 5px 3px 5px;
270 | font-size: 11px;
271 | text-align: left;
272 | font-weight: bold;
273 | background: #bcd;
274 | color: #fff;
275 | }
276 |
277 | .inline-related.tabular fieldset.module table {
278 | width: 100%;
279 | }
280 |
281 | .last-related fieldset {
282 | border: none;
283 | }
284 |
285 | .inline-group .tabular tr.has_original td {
286 | padding-top: 2em;
287 | }
288 |
289 | .inline-group .tabular tr td.original {
290 | padding: 2px 0 0 0;
291 | width: 0;
292 | _position: relative;
293 | }
294 |
295 | .inline-group .tabular th.original {
296 | width: 0px;
297 | padding: 0;
298 | }
299 |
300 | .inline-group .tabular td.original p {
301 | position: absolute;
302 | left: 0;
303 | height: 1.1em;
304 | padding: 2px 7px;
305 | overflow: hidden;
306 | font-size: 9px;
307 | font-weight: bold;
308 | color: #666;
309 | _width: 700px;
310 | }
311 |
312 | .inline-group ul.tools {
313 | padding: 0;
314 | margin: 0;
315 | list-style: none;
316 | }
317 |
318 | .inline-group ul.tools li {
319 | display: inline;
320 | padding: 0 5px;
321 | }
322 |
323 | .inline-group ul.tools a.add {
324 | background: url(../images/icon_addlink.gif) 0 50% no-repeat;
325 | padding-left: 14px;
326 | }
327 |
328 |
--------------------------------------------------------------------------------
/app/helpers/main_helper.rb:
--------------------------------------------------------------------------------
1 | require 'builder'
2 | module Merb
3 | module MerbAdmin
4 | module MainHelper
5 | def to_model_name(param)
6 | param.split("::").map{|x| x.camel_case}.join("::")
7 | end
8 |
9 | def object_label(object)
10 | if object.nil?
11 | nil
12 | elsif object.respond_to?(:name) && object.name
13 | object.name
14 | elsif object.respond_to?(:title) && object.title
15 | object.title
16 | else
17 | "#{object.class.to_s} ##{object.id}"
18 | end
19 | end
20 |
21 | def object_property(object, property)
22 | property_type = property[:type]
23 | property_name = property[:name]
24 | case property_type
25 | when :boolean
26 | if object.send(property_name) == true
27 | Builder::XmlMarkup.new.img(:src => image_path("icon-yes.gif"), :alt => "True")
28 | else
29 | Builder::XmlMarkup.new.img(:src => image_path("icon-no.gif"), :alt => "False")
30 | end
31 | when :datetime
32 | value = object.send(property_name)
33 | value.respond_to?(:strftime) ? value.strftime("%b. %d, %Y, %I:%M%p") : nil
34 | when :date
35 | value = object.send(property_name)
36 | value.respond_to?(:strftime) ? value.strftime("%b. %d, %Y") : nil
37 | when :time
38 | value = object.send(property_name)
39 | value.respond_to?(:strftime) ? value.strftime("%I:%M%p") : nil
40 | when :string
41 | if property_name.to_s =~ /(image|logo|photo|photograph|picture|thumb|thumbnail)_ur(i|l)/i
42 | Builder::XmlMarkup.new.img(:src => object.send(property_name), :width => 10, :height => 10)
43 | else
44 | object.send(property_name).to_s.truncate(50)
45 | end
46 | when :text
47 | object.send(property_name).to_s.truncate(50)
48 | when :integer
49 | association = @abstract_model.belongs_to_associations.select{|a| a[:child_key].first == property_name}.first
50 | if association
51 | object_label(object.send(association[:name]))
52 | else
53 | object.send(property_name)
54 | end
55 | else
56 | object.send(property_name)
57 | end
58 | end
59 |
60 | # Given a page count and the current page, we generate a set of pagination
61 | # links.
62 | #
63 | # * We use an inner and outer window into a list of links. For a set of
64 | # 20 pages with the current page being 10:
65 | # outer_window:
66 | # 1 2 ..... 19 20
67 | # inner_window
68 | # 5 6 7 8 9 10 11 12 13 14
69 | #
70 | # This is totally adjustable, or can be turned off by giving the
71 | # :inner_window setting a value of nil.
72 | #
73 | # * Options
74 | # :left_cut_label => text_for_cut::
75 | # Used when the page numbers need to be cut off to prevent the set of
76 | # pagination links from being too long.
77 | # Defaults to '…'
78 | # :right_cut_label => text_for_cut::
79 | # Same as :left_cut_label but for the right side of numbers.
80 | # Defaults to '…'
81 | # :outer_window => number_of_pages::
82 | # Sets the number of pages to include in the outer 'window'
83 | # Defaults to 2
84 | # :inner_window => number_of_pages::
85 | # Sets the number of pags to include in the inner 'window'
86 | # Defaults to 7
87 | # :page_param => name_of_page_paramiter
88 | # Sets the name of the paramiter the paginator uses to return what
89 | # page is being requested.
90 | # Defaults to 'page'
91 | # :url => url_for_links
92 | # Provides the base url to use in the page navigation links.
93 | # Defaults to ''
94 | def paginate(current_page, page_count, options = {})
95 | options[:left_cut_label] ||= '…'
96 | options[:right_cut_label] ||= '…'
97 | options[:outer_window] ||= 2
98 | options[:inner_window] ||= 7
99 | options[:page_param] ||= 'page'
100 | options[:url] ||= ''
101 |
102 | url = options.delete(:url)
103 | url << (url.include?('?') ? '&' : '?') << options[:page_param]
104 |
105 | pages = {
106 | :all => (1..page_count).to_a,
107 | :left => [],
108 | :center => [],
109 | :right => []
110 | }
111 |
112 | # Only worry about using our 'windows' if the page count is less then
113 | # our windows combined.
114 | if options[:inner_window].nil? || ((options[:outer_window] * 2) + options[:inner_window] + 2) >= page_count
115 | pages[:center] = pages[:all]
116 | else
117 | pages[:left] = pages[:all][0, options[:outer_window]]
118 | pages[:right] = pages[:all][page_count - options[:outer_window], options[:outer_window]]
119 | pages[:center] = case current_page
120 | # allow the inner 'window' to shift to right when close to the left edge
121 | # Ex: 1 2 [3] 4 5 6 7 8 9 ... 20
122 | when -infinity .. (options[:inner_window] / 2) + 3
123 | pages[:all][options[:outer_window], options[:inner_window]] +
124 | [options[:right_cut_label]]
125 | # allow the inner 'window' to shift left when close to the right edge
126 | # Ex: 1 2 ... 12 13 14 15 16 [17] 18 19 20
127 | when (page_count - (options[:inner_window] / 2.0).ceil) - 1 .. infinity
128 | [options[:left_cut_label]] +
129 | pages[:all][page_count - options[:inner_window] - options[:outer_window], options[:inner_window]]
130 | # Display the unshifed window
131 | # ex: 1 2 ... 5 6 7 [8] 9 10 11 ... 19 20
132 | else
133 | [options[:left_cut_label]] +
134 | pages[:all][current_page - (options[:inner_window] / 2) - 1, options[:inner_window]] +
135 | [options[:right_cut_label]]
136 | end
137 | end
138 |
139 | b = []
140 |
141 | [pages[:left], pages[:center], pages[:right]].each do |p|
142 | p.each do |page_number|
143 | case page_number
144 | when String
145 | b << page_number
146 | when current_page
147 | b << Builder::XmlMarkup.new.span(page_number, :class => "this-page")
148 | when page_count
149 | b << Builder::XmlMarkup.new.a(page_number, :class => "end", :href => "#{url}=#{page_number}")
150 | else
151 | b << Builder::XmlMarkup.new.a(page_number, :href => "#{url}=#{page_number}")
152 | end
153 | end
154 | end
155 |
156 | b.join(" ")
157 | end
158 |
159 | private
160 |
161 | def infinity
162 | 1.0 / 0
163 | end
164 |
165 | end
166 | end
167 | end
168 |
--------------------------------------------------------------------------------
/public/javascripts/getElementsBySelector.js:
--------------------------------------------------------------------------------
1 | /* document.getElementsBySelector(selector)
2 | - returns an array of element objects from the current document
3 | matching the CSS selector. Selectors can contain element names,
4 | class names and ids and can be nested. For example:
5 |
6 | elements = document.getElementsBySelect('div#main p a.external')
7 |
8 | Will return an array of all 'a' elements with 'external' in their
9 | class attribute that are contained inside 'p' elements that are
10 | contained inside the 'div' element which has id="main"
11 |
12 | New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
13 | See http://www.w3.org/TR/css3-selectors/#attribute-selectors
14 |
15 | Version 0.4 - Simon Willison, March 25th 2003
16 | -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
17 | -- Opera 7 fails
18 | */
19 |
20 | function getAllChildren(e) {
21 | // Returns all children of element. Workaround required for IE5/Windows. Ugh.
22 | return e.all ? e.all : e.getElementsByTagName('*');
23 | }
24 |
25 | document.getElementsBySelector = function(selector) {
26 | // Attempt to fail gracefully in lesser browsers
27 | if (!document.getElementsByTagName) {
28 | return new Array();
29 | }
30 | // Split selector in to tokens
31 | var tokens = selector.split(' ');
32 | var currentContext = new Array(document);
33 | for (var i = 0; i < tokens.length; i++) {
34 | token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');;
35 | if (token.indexOf('#') > -1) {
36 | // Token is an ID selector
37 | var bits = token.split('#');
38 | var tagName = bits[0];
39 | var id = bits[1];
40 | var element = document.getElementById(id);
41 | if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) {
42 | // ID not found or tag with that ID not found, return false.
43 | return new Array();
44 | }
45 | // Set currentContext to contain just this element
46 | currentContext = new Array(element);
47 | continue; // Skip to next token
48 | }
49 | if (token.indexOf('.') > -1) {
50 | // Token contains a class selector
51 | var bits = token.split('.');
52 | var tagName = bits[0];
53 | var className = bits[1];
54 | if (!tagName) {
55 | tagName = '*';
56 | }
57 | // Get elements matching tag, filter them for class selector
58 | var found = new Array;
59 | var foundCount = 0;
60 | for (var h = 0; h < currentContext.length; h++) {
61 | var elements;
62 | if (tagName == '*') {
63 | elements = getAllChildren(currentContext[h]);
64 | } else {
65 | try {
66 | elements = currentContext[h].getElementsByTagName(tagName);
67 | }
68 | catch(e) {
69 | elements = [];
70 | }
71 | }
72 | for (var j = 0; j < elements.length; j++) {
73 | found[foundCount++] = elements[j];
74 | }
75 | }
76 | currentContext = new Array;
77 | var currentContextIndex = 0;
78 | for (var k = 0; k < found.length; k++) {
79 | if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) {
80 | currentContext[currentContextIndex++] = found[k];
81 | }
82 | }
83 | continue; // Skip to next token
84 | }
85 | // Code to deal with attribute selectors
86 | if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
87 | var tagName = RegExp.$1;
88 | var attrName = RegExp.$2;
89 | var attrOperator = RegExp.$3;
90 | var attrValue = RegExp.$4;
91 | if (!tagName) {
92 | tagName = '*';
93 | }
94 | // Grab all of the tagName elements within current context
95 | var found = new Array;
96 | var foundCount = 0;
97 | for (var h = 0; h < currentContext.length; h++) {
98 | var elements;
99 | if (tagName == '*') {
100 | elements = getAllChildren(currentContext[h]);
101 | } else {
102 | elements = currentContext[h].getElementsByTagName(tagName);
103 | }
104 | for (var j = 0; j < elements.length; j++) {
105 | found[foundCount++] = elements[j];
106 | }
107 | }
108 | currentContext = new Array;
109 | var currentContextIndex = 0;
110 | var checkFunction; // This function will be used to filter the elements
111 | switch (attrOperator) {
112 | case '=': // Equality
113 | checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
114 | break;
115 | case '~': // Match one of space seperated words
116 | checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
117 | break;
118 | case '|': // Match start with value followed by optional hyphen
119 | checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
120 | break;
121 | case '^': // Match starts with value
122 | checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
123 | break;
124 | case '$': // Match ends with value - fails with "Warning" in Opera 7
125 | checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
126 | break;
127 | case '*': // Match ends with value
128 | checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
129 | break;
130 | default :
131 | // Just test for existence of attribute
132 | checkFunction = function(e) { return e.getAttribute(attrName); };
133 | }
134 | currentContext = new Array;
135 | var currentContextIndex = 0;
136 | for (var k = 0; k < found.length; k++) {
137 | if (checkFunction(found[k])) {
138 | currentContext[currentContextIndex++] = found[k];
139 | }
140 | }
141 | // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
142 | continue; // Skip to next token
143 | }
144 | // If we get here, token is JUST an element (not a class or ID selector)
145 | tagName = token;
146 | var found = new Array;
147 | var foundCount = 0;
148 | for (var h = 0; h < currentContext.length; h++) {
149 | var elements = currentContext[h].getElementsByTagName(tagName);
150 | for (var j = 0; j < elements.length; j++) {
151 | found[foundCount++] = elements[j];
152 | }
153 | }
154 | currentContext = found;
155 | }
156 | return currentContext;
157 | }
158 |
159 | /* That revolting regular expression explained
160 | /^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
161 | \---/ \---/\-------------/ \-------/
162 | | | | |
163 | | | | The value
164 | | | ~,|,^,$,* or =
165 | | Attribute
166 | Tag
167 | */
168 |
--------------------------------------------------------------------------------
/lib/merb-admin/slicetasks.rb:
--------------------------------------------------------------------------------
1 | require 'abstract_model'
2 |
3 | namespace :slices do
4 | namespace :"merb-admin" do
5 |
6 | # # Uncomment the following lines and edit the pre defined tasks
7 | #
8 | # # implement this to test for structural/code dependencies
9 | # # like certain directories or availability of other files
10 | # desc "Test for any dependencies"
11 | # task :preflight do
12 | # end
13 | #
14 | # # implement this to perform any database related setup steps
15 | # desc "Migrate the database"
16 | # task :migrate do
17 | # end
18 |
19 | desc "Copies sample models, copies and runs sample migrations, and loads sample data into your app"
20 | task :activerecord => ["activerecord:copy_sample_models", "activerecord:copy_sample_migrations", "activerecord:migrate", "load_sample_data"]
21 | namespace :activerecord do
22 | desc "Copies sample models into your app"
23 | task :copy_sample_models do
24 | copy_models(:activerecord)
25 | end
26 |
27 | desc "Copies sample migrations into your app"
28 | task :copy_sample_migrations do
29 | copy_migrations(:activerecord)
30 | end
31 |
32 | desc "Migrate the database to the latest version"
33 | task :migrate do
34 | Rake::Task["db:migrate"].reenable
35 | Rake::Task["db:migrate"].invoke
36 | end
37 | end
38 |
39 | desc "Copies sample models, runs sample migrations, and loads sample data into your app"
40 | task :datamapper => ["datamapper:copy_sample_models", "datamapper:migrate", "load_sample_data"]
41 | namespace :datamapper do
42 | desc "Copies sample models into your app"
43 | task :copy_sample_models do
44 | copy_models(:datamapper)
45 | end
46 |
47 | desc "Perform non destructive automigration"
48 | task :migrate do
49 | Rake::Task["db:automigrate"].reenable
50 | Rake::Task["db:automigrate"].invoke
51 | end
52 | end
53 |
54 | desc "Copies sample models, copies and runs sample migrations, and loads sample data"
55 | task :sequel => ["sequel:copy_sample_models", "sequel:copy_sample_migrations", "sequel:migrate", "load_sample_data"]
56 | namespace :sequel do
57 | desc "Copies sample models into your app"
58 | task :copy_sample_models do
59 | copy_models(:sequel)
60 | end
61 |
62 | desc "Copies sample migrations into your app"
63 | task :copy_sample_migrations do
64 | copy_migrations(:sequel)
65 | end
66 |
67 | desc "Perform migration using migrations in schema/migrations"
68 | task :migrate do
69 | require 'sequel/extensions/migration'
70 | Rake::Task["sequel:db:migrate"].reenable
71 | Rake::Task["sequel:db:migrate"].invoke
72 | end
73 | end
74 |
75 | desc "Loads sample data into your app"
76 | task :load_sample_data do
77 | load_data
78 | end
79 |
80 | end
81 | end
82 |
83 | private
84 |
85 | def load_data
86 | require "mlb"
87 |
88 | require_models
89 |
90 | puts "Loading current MLB leagues, divisions, teams, and players"
91 | MLB::Team.all.each do |mlb_team|
92 | unless league = MerbAdmin::AbstractModel.new("League").first(:conditions => ["name = ?", mlb_team.league])
93 | league = MerbAdmin::AbstractModel.new("League").create(:name => mlb_team.league)
94 | end
95 | unless division = MerbAdmin::AbstractModel.new("Division").first(:conditions => ["name = ?", mlb_team.division])
96 | division = MerbAdmin::AbstractModel.new("Division").create(:name => mlb_team.division, :league => league)
97 | end
98 | unless team = MerbAdmin::AbstractModel.new("Team").first(:conditions => ["name = ?", mlb_team.name])
99 | team = MerbAdmin::AbstractModel.new("Team").create(:name => mlb_team.name, :logo_url => mlb_team.logo_url, :manager => mlb_team.manager, :ballpark => mlb_team.ballpark, :mascot => mlb_team.mascot, :founded => mlb_team.founded, :wins => mlb_team.wins, :losses => mlb_team.losses, :win_percentage => ("%.3f" % (mlb_team.wins.to_f / (mlb_team.wins + mlb_team.losses))).to_f, :division => division, :league => league)
100 | end
101 | mlb_team.players.reject{|player| player.number.nil?}.each do |player|
102 | MerbAdmin::AbstractModel.new("Player").create(:name => player.name, :number => player.number, :position => player.position, :team => team)
103 | end
104 | end
105 | end
106 |
107 | def copy_models(orm = nil)
108 | orm ||= set_orm
109 | puts "Copying sample #{orm} models into host application - resolves any collisions"
110 | seen, copied, duplicated = [], [], []
111 | Dir.glob(File.dirname(__FILE__) / ".." / ".." / "spec" / "models" / orm.to_s.downcase / MerbAdmin.glob_for(:model)).each do |source_filename|
112 | next if seen.include?(source_filename)
113 | destination_filename = Merb.dir_for(:model) / File.basename(source_filename)
114 | mirror_file(source_filename, destination_filename, copied, duplicated)
115 | seen << source_filename
116 | end
117 | copied.each { |f| puts "- copied #{f}" }
118 | duplicated.each { |f| puts "! duplicated override as #{f}" }
119 | end
120 |
121 | def copy_migrations(orm = nil)
122 | orm ||= set_orm
123 | puts "Copying sample #{orm} migrations into host application - resolves any collisions"
124 | seen, copied, duplicated = [], [], []
125 | Dir.glob(File.dirname(__FILE__) / ".." / ".." / "spec" / "migrations" / orm.to_s.downcase / "*.rb").each do |source_filename|
126 | next if seen.include?(source_filename)
127 | destination_filename = Merb.root / "schema" / "migrations" / File.basename(source_filename)
128 | mirror_file(source_filename, destination_filename, copied, duplicated)
129 | seen << source_filename
130 | end
131 | copied.each { |f| puts "- copied #{f}" }
132 | duplicated.each { |f| puts "! duplicated override as #{f}" }
133 | end
134 |
135 | def require_models
136 | Dir.glob(Merb.dir_for(:model) / Merb.glob_for(:model)).each do |model_filename|
137 | require model_filename
138 | end
139 | end
140 |
141 | def set_orm(orm = nil)
142 | orm || ENV['MERB_ORM'] || (Merb.orm != :none ? Merb.orm : nil) || :activerecord
143 | end
144 |
145 | def mirror_file(source, dest, copied = [], duplicated = [], postfix = '_override')
146 | base, rest = split_name(source)
147 | dst_dir = File.dirname(dest)
148 | dup_path = dst_dir / "#{base}#{postfix}.#{rest}"
149 | if File.file?(source)
150 | FileUtils.mkdir_p(dst_dir) unless File.directory?(dst_dir)
151 | if File.exists?(dest) && !File.exists?(dup_path) && !FileUtils.identical?(source, dest)
152 | # copy app-level override to *_override.ext
153 | FileUtils.copy_entry(dest, dup_path, false, false, true)
154 | duplicated << dup_path.relative_path_from(Merb.root)
155 | end
156 | # copy gem-level original to location
157 | if !File.exists?(dest) || (File.exists?(dest) && !FileUtils.identical?(source, dest))
158 | FileUtils.copy_entry(source, dest, false, false, true)
159 | copied << dest.relative_path_from(Merb.root)
160 | end
161 | end
162 | end
163 |
164 | def split_name(name)
165 | file_name = File.basename(name)
166 | mres = /^([^\/\.]+)\.(.+)$/i.match(file_name)
167 | mres.nil? ? [file_name, ''] : [mres[1], mres[2]]
168 | end
169 |
--------------------------------------------------------------------------------
/public/javascripts/dateparse.js:
--------------------------------------------------------------------------------
1 | /* 'Magic' date parsing, by Simon Willison (6th October 2003)
2 | http://simon.incutio.com/archive/2003/10/06/betterDateInput
3 | Adapted for 6newslawrence.com, 28th January 2004
4 | */
5 |
6 | /* Finds the index of the first occurence of item in the array, or -1 if not found */
7 | if (typeof Array.prototype.indexOf == 'undefined') {
8 | Array.prototype.indexOf = function(item) {
9 | var len = this.length;
10 | for (var i = 0; i < len; i++) {
11 | if (this[i] == item) {
12 | return i;
13 | }
14 | }
15 | return -1;
16 | };
17 | }
18 | /* Returns an array of items judged 'true' by the passed in test function */
19 | if (typeof Array.prototype.filter == 'undefined') {
20 | Array.prototype.filter = function(test) {
21 | var matches = [];
22 | var len = this.length;
23 | for (var i = 0; i < len; i++) {
24 | if (test(this[i])) {
25 | matches[matches.length] = this[i];
26 | }
27 | }
28 | return matches;
29 | };
30 | }
31 |
32 | var monthNames = gettext("January February March April May June July August September October November December").split(" ");
33 | var weekdayNames = gettext("Sunday Monday Tuesday Wednesday Thursday Friday Saturday").split(" ");
34 |
35 | /* Takes a string, returns the index of the month matching that string, throws
36 | an error if 0 or more than 1 matches
37 | */
38 | function parseMonth(month) {
39 | var matches = monthNames.filter(function(item) {
40 | return new RegExp("^" + month, "i").test(item);
41 | });
42 | if (matches.length == 0) {
43 | throw new Error("Invalid month string");
44 | }
45 | if (matches.length > 1) {
46 | throw new Error("Ambiguous month");
47 | }
48 | return monthNames.indexOf(matches[0]);
49 | }
50 | /* Same as parseMonth but for days of the week */
51 | function parseWeekday(weekday) {
52 | var matches = weekdayNames.filter(function(item) {
53 | return new RegExp("^" + weekday, "i").test(item);
54 | });
55 | if (matches.length == 0) {
56 | throw new Error("Invalid day string");
57 | }
58 | if (matches.length > 1) {
59 | throw new Error("Ambiguous weekday");
60 | }
61 | return weekdayNames.indexOf(matches[0]);
62 | }
63 |
64 | /* Array of objects, each has 're', a regular expression and 'handler', a
65 | function for creating a date from something that matches the regular
66 | expression. Handlers may throw errors if string is unparseable.
67 | */
68 | var dateParsePatterns = [
69 | // Today
70 | { re: /^tod/i,
71 | handler: function() {
72 | return new Date();
73 | }
74 | },
75 | // Tomorrow
76 | { re: /^tom/i,
77 | handler: function() {
78 | var d = new Date();
79 | d.setDate(d.getDate() + 1);
80 | return d;
81 | }
82 | },
83 | // Yesterday
84 | { re: /^yes/i,
85 | handler: function() {
86 | var d = new Date();
87 | d.setDate(d.getDate() - 1);
88 | return d;
89 | }
90 | },
91 | // 4th
92 | { re: /^(\d{1,2})(st|nd|rd|th)?$/i,
93 | handler: function(bits) {
94 | var d = new Date();
95 | d.setDate(parseInt(bits[1], 10));
96 | return d;
97 | }
98 | },
99 | // 4th Jan
100 | { re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+)$/i,
101 | handler: function(bits) {
102 | var d = new Date();
103 | d.setDate(parseInt(bits[1], 10));
104 | d.setMonth(parseMonth(bits[2]));
105 | return d;
106 | }
107 | },
108 | // 4th Jan 2003
109 | { re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+),? (\d{4})$/i,
110 | handler: function(bits) {
111 | var d = new Date();
112 | d.setDate(parseInt(bits[1], 10));
113 | d.setMonth(parseMonth(bits[2]));
114 | d.setYear(bits[3]);
115 | return d;
116 | }
117 | },
118 | // Jan 4th
119 | { re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?$/i,
120 | handler: function(bits) {
121 | var d = new Date();
122 | d.setDate(parseInt(bits[2], 10));
123 | d.setMonth(parseMonth(bits[1]));
124 | return d;
125 | }
126 | },
127 | // Jan 4th 2003
128 | { re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,? (\d{4})$/i,
129 | handler: function(bits) {
130 | var d = new Date();
131 | d.setDate(parseInt(bits[2], 10));
132 | d.setMonth(parseMonth(bits[1]));
133 | d.setYear(bits[3]);
134 | return d;
135 | }
136 | },
137 | // next Tuesday - this is suspect due to weird meaning of "next"
138 | { re: /^next (\w+)$/i,
139 | handler: function(bits) {
140 | var d = new Date();
141 | var day = d.getDay();
142 | var newDay = parseWeekday(bits[1]);
143 | var addDays = newDay - day;
144 | if (newDay <= day) {
145 | addDays += 7;
146 | }
147 | d.setDate(d.getDate() + addDays);
148 | return d;
149 | }
150 | },
151 | // last Tuesday
152 | { re: /^last (\w+)$/i,
153 | handler: function(bits) {
154 | throw new Error("Not yet implemented");
155 | }
156 | },
157 | // mm/dd/yyyy (American style)
158 | { re: /(\d{1,2})\/(\d{1,2})\/(\d{4})/,
159 | handler: function(bits) {
160 | var d = new Date();
161 | d.setYear(bits[3]);
162 | d.setDate(parseInt(bits[2], 10));
163 | d.setMonth(parseInt(bits[1], 10) - 1); // Because months indexed from 0
164 | return d;
165 | }
166 | },
167 | // yyyy-mm-dd (ISO style)
168 | { re: /(\d{4})-(\d{1,2})-(\d{1,2})/,
169 | handler: function(bits) {
170 | var d = new Date();
171 | d.setYear(parseInt(bits[1]));
172 | d.setMonth(parseInt(bits[2], 10) - 1);
173 | d.setDate(parseInt(bits[3], 10));
174 | return d;
175 | }
176 | },
177 | ];
178 |
179 | function parseDateString(s) {
180 | for (var i = 0; i < dateParsePatterns.length; i++) {
181 | var re = dateParsePatterns[i].re;
182 | var handler = dateParsePatterns[i].handler;
183 | var bits = re.exec(s);
184 | if (bits) {
185 | return handler(bits);
186 | }
187 | }
188 | throw new Error("Invalid date string");
189 | }
190 |
191 | function fmt00(x) {
192 | // fmt00: Tags leading zero onto numbers 0 - 9.
193 | // Particularly useful for displaying results from Date methods.
194 | //
195 | if (Math.abs(parseInt(x)) < 10){
196 | x = "0"+ Math.abs(x);
197 | }
198 | return x;
199 | }
200 |
201 | function parseDateStringISO(s) {
202 | try {
203 | var d = parseDateString(s);
204 | return d.getFullYear() + '-' + (fmt00(d.getMonth() + 1)) + '-' + fmt00(d.getDate())
205 | }
206 | catch (e) { return s; }
207 | }
208 | function magicDate(input) {
209 | var messagespan = input.id + 'Msg';
210 | try {
211 | var d = parseDateString(input.value);
212 | input.value = d.getFullYear() + '-' + (fmt00(d.getMonth() + 1)) + '-' +
213 | fmt00(d.getDate());
214 | input.className = '';
215 | // Human readable date
216 | if (document.getElementById(messagespan)) {
217 | document.getElementById(messagespan).firstChild.nodeValue = d.toDateString();
218 | document.getElementById(messagespan).className = 'normal';
219 | }
220 | }
221 | catch (e) {
222 | input.className = 'error';
223 | var message = e.message;
224 | // Fix for IE6 bug
225 | if (message.indexOf('is null or not an object') > -1) {
226 | message = 'Invalid date string';
227 | }
228 | if (document.getElementById(messagespan)) {
229 | document.getElementById(messagespan).firstChild.nodeValue = message;
230 | document.getElementById(messagespan).className = 'error';
231 | }
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/public/stylesheets/global.css:
--------------------------------------------------------------------------------
1 | body { margin:0; padding:0; font-size:12px; font-family:"Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif; color:#333; background:#fff; }
2 |
3 | /* LINKS */
4 | a:link, a:visited { color: #5b80b2; text-decoration:none; }
5 | a:hover { color: #036; }
6 | a img { border:none; }
7 | a.section:link, a.section:visited { color: white; text-decoration:none; }
8 |
9 | /* GLOBAL DEFAULTS */
10 | p, ol, ul, dl { margin:.2em 0 .8em 0; }
11 | p { padding:0; line-height:140%; }
12 |
13 | h1,h2,h3,h4,h5 { font-weight:bold; }
14 | h1 { font-size:18px; color:#666; padding:0 6px 0 0; margin:0 0 .2em 0; }
15 | h2 { font-size:16px; margin:1em 0 .5em 0; }
16 | h2.subhead { font-weight:normal;margin-top:0; }
17 | h3 { font-size:14px; margin:.8em 0 .3em 0; color:#666; font-weight:bold; }
18 | h4 { font-size:12px; margin:1em 0 .8em 0; padding-bottom:3px; }
19 | h5 { font-size:10px; margin:1.5em 0 .5em 0; color:#666; text-transform:uppercase; letter-spacing:1px; }
20 |
21 | ul li { list-style-type:square; padding:1px 0; }
22 | ul.plainlist { margin-left:0 !important; }
23 | ul.plainlist li { list-style-type:none; }
24 | li ul { margin-bottom:0; }
25 | li, dt, dd { font-size:11px; line-height:14px; }
26 | dt { font-weight:bold; margin-top:4px; }
27 | dd { margin-left:0; }
28 |
29 | form { margin:0; padding:0; }
30 | fieldset { margin:0; padding:0; }
31 |
32 | blockquote { font-size:11px; color:#777; margin-left:2px; padding-left:10px; border-left:5px solid #ddd; }
33 | code, pre { font-family:"Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; background:inherit; color:#666; font-size:11px; }
34 | pre.literal-block { margin:10px; background:#eee; padding:6px 8px; }
35 | code strong { color:#930; }
36 | hr { clear:both; color:#eee; background-color:#eee; height:1px; border:none; margin:0; padding:0; font-size:1px; line-height:1px; }
37 |
38 | /* TEXT STYLES & MODIFIERS */
39 | .small { font-size:11px; }
40 | .tiny { font-size:10px; }
41 | p.tiny { margin-top:-2px; }
42 | .mini { font-size:9px; }
43 | p.mini { margin-top:-3px; }
44 | .help, p.help { font-size:10px !important; color:#999; }
45 | p img, h1 img, h2 img, h3 img, h4 img, td img { vertical-align:middle; }
46 | .quiet, a.quiet:link, a.quiet:visited { color:#999 !important;font-weight:normal !important; }
47 | .quiet strong { font-weight:bold !important; }
48 | .float-right { float:right; }
49 | .float-left { float:left; }
50 | .clear { clear:both; }
51 | .align-left { text-align:left; }
52 | .align-right { text-align:right; }
53 | .example { margin:10px 0; padding:5px 10px; background:#efefef; }
54 | .nowrap { white-space:nowrap; }
55 |
56 | /* TABLES */
57 | table { border-collapse:collapse; border-color:#ccc; }
58 | td, th { font-size:11px; line-height:13px; border-bottom:1px solid #eee; vertical-align:top; padding:5px; font-family:"Lucida Grande", Verdana, Arial, sans-serif; }
59 | th { text-align:left; font-size:12px; font-weight:bold; }
60 | thead th,
61 | tfoot td { color:#666; padding:2px 5px; font-size:11px; background:#e1e1e1 url(../images/nav-bg.gif) top left repeat-x; border-left:1px solid #ddd; border-bottom:1px solid #ddd; }
62 | tfoot td { border-bottom:none; border-top:1px solid #ddd; }
63 | thead th:first-child,
64 | tfoot td:first-child { border-left:none !important; }
65 | thead th.optional { font-weight:normal !important; }
66 | fieldset table { border-right:1px solid #eee; }
67 | tr.row-label td { font-size:9px; padding-top:2px; padding-bottom:0; border-bottom:none; color:#666; margin-top:-1px; }
68 | tr.alt { background:#f6f6f6; }
69 | .row1 { background:#EDF3FE; }
70 | .row2 { background:white; }
71 |
72 | /* SORTABLE TABLES */
73 | thead th a:link, thead th a:visited { color:#666; display:block; }
74 | table thead th.sorted { background-position:bottom left !important; }
75 | table thead th.sorted a { padding-right:13px; }
76 | table thead th.ascending a { background:url(../images/arrow-down.gif) right .4em no-repeat; }
77 | table thead th.descending a { background:url(../images/arrow-up.gif) right .4em no-repeat; }
78 |
79 | /* ORDERABLE TABLES */
80 | table.orderable tbody tr td:hover { cursor:move; }
81 | table.orderable tbody tr td:first-child { padding-left:14px; background-image:url(../images/nav-bg-grabber.gif); background-repeat:repeat-y; }
82 | table.orderable-initalized .order-cell, body>tr>td.order-cell { display:none; }
83 |
84 | /* FORM DEFAULTS */
85 | input, textarea, select { margin:2px 0; padding:2px 3px; vertical-align:middle; font-family:"Lucida Grande", Verdana, Arial, sans-serif; font-weight:normal; font-size:11px; }
86 | textarea { vertical-align:top !important; }
87 | input[type=text], input[type=password], textarea, select, .vTextField { border:1px solid #ccc; }
88 |
89 | /* FORM BUTTONS */
90 | .button, input[type=submit], input[type=button], .submit-row input { background:white url(../images/nav-bg.gif) bottom repeat-x; padding:3px; color:black; border:1px solid #bbb; border-color:#ddd #aaa #aaa #ddd; }
91 | .button:active, input[type=submit]:active, input[type=button]:active { background-image:url(../images/nav-bg-reverse.gif); background-position:top; }
92 | .button.default, input[type=submit].default, .submit-row input.default { border:2px solid #5b80b2; background:#7CA0C7 url(../images/default-bg.gif) bottom repeat-x; font-weight:bold; color:white; float:right; }
93 | .button.default:active, input[type=submit].default:active { background-image:url(../images/default-bg-reverse.gif); background-position:top; }
94 |
95 | /* MODULES */
96 | .module { border:1px solid #ccc; margin-bottom:5px; background:white; }
97 | .module p, .module ul, .module h3, .module h4, .module dl, .module pre { padding-left:10px; padding-right:10px; }
98 | .module blockquote { margin-left:12px; }
99 | .module ul, .module ol { margin-left:1.5em; }
100 | .module h3 { margin-top:.6em; }
101 | .module h2, .module caption, .inline-group h2 { margin:0; padding:2px 5px 3px 5px; font-size:11px; text-align:left; font-weight:bold; background:#7CA0C7 url(../images/default-bg.gif) top left repeat-x; color:white; }
102 | .module table { border-collapse: collapse; }
103 |
104 | /* MESSAGES & ERRORS */
105 | ul.messagelist { padding:0 0 5px 0; margin:0; }
106 | ul.messagelist li { font-size:12px; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border-bottom:1px solid #ddd; color:#666; background:#ffc url(../images/icon_success.gif) 5px .3em no-repeat; }
107 | .errornote { font-size:12px !important; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border:1px solid red; color:red;background:#ffc url(../images/icon_error.gif) 5px .3em no-repeat; }
108 | ul.errorlist { margin:0 !important; padding:0 !important; }
109 | .errorlist li { font-size:12px !important; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border:1px solid red; color:white; background:red url(../images/icon_alert.gif) 5px .3em no-repeat; }
110 | td ul.errorlist { margin:0 !important; padding:0 !important; }
111 | td ul.errorlist li { margin:0 !important; }
112 | .errors { background:#ffc; }
113 | .errors input, .errors select { border:1px solid red; }
114 | div.system-message { background: #ffc; margin: 10px; padding: 6px 8px; font-size: .8em; }
115 | div.system-message p.system-message-title { padding:4px 5px 4px 25px; margin:0; color:red; background:#ffc url(../images/icon_error.gif) 5px .3em no-repeat; }
116 | .description { font-size:12px; padding:5px 0 0 12px; }
117 |
118 | /* BREADCRUMBS */
119 | div.breadcrumbs { background:white url(../images/nav-bg-reverse.gif) 0 -10px repeat-x; padding:2px 8px 3px 8px; font-size:11px; color:#999; border-top:1px solid white; border-bottom:1px solid #ccc; text-align:left; }
120 |
121 | /* ACTION ICONS */
122 | .addlink { padding-left:12px; background:url(../images/icon_addlink.gif) 0 .2em no-repeat; }
123 | .changelink { padding-left:12px; background:url(../images/icon_changelink.gif) 0 .2em no-repeat; }
124 | .deletelink { padding-left:12px; background:url(../images/icon_deletelink.gif) 0 .25em no-repeat; }
125 | a.deletelink:link, a.deletelink:visited { color:#CC3434; }
126 | a.deletelink:hover { color:#993333; }
127 |
128 | /* OBJECT TOOLS */
129 | .object-tools { font-size:10px; font-weight:bold; font-family:Arial,Helvetica,sans-serif; padding-left:0; float:right; position:relative; margin-top:-2.4em; margin-bottom:-2em; }
130 | .form-row .object-tools { margin-top:5px; margin-bottom:5px; float:none; height:2em; padding-left:3.5em; }
131 | .object-tools li { display:block; float:left; background:url(../images/tool-left.gif) 0 0 no-repeat; padding:0 0 0 8px; margin-left:2px; height:16px; }
132 | .object-tools li:hover { background:url(../images/tool-left_over.gif) 0 0 no-repeat; }
133 | .object-tools a:link, .object-tools a:visited { display:block; float:left; color:white; padding:.1em 14px .1em 8px; height:14px; background:#999 url(../images/tool-right.gif) 100% 0 no-repeat; }
134 | .object-tools a:hover, .object-tools li:hover a { background:#5b80b2 url(../images/tool-right_over.gif) 100% 0 no-repeat; }
135 | .object-tools a.viewsitelink, .object-tools a.golink { background:#999 url(../images/tooltag-arrowright.gif) top right no-repeat; padding-right:28px; }
136 | .object-tools a.viewsitelink:hover, .object-tools a.golink:hover { background:#5b80b2 url(../images/tooltag-arrowright_over.gif) top right no-repeat; }
137 | .object-tools a.addlink { background:#999 url(../images/tooltag-add.gif) top right no-repeat; padding-right:28px; }
138 | .object-tools a.addlink:hover { background:#5b80b2 url(../images/tooltag-add_over.gif) top right no-repeat; }
139 |
140 | /* OBJECT HISTORY */
141 | table#change-history { width:100%; }
142 | table#change-history tbody th { width:16em; }
143 |
--------------------------------------------------------------------------------
/lib/sequel_support.rb:
--------------------------------------------------------------------------------
1 | require 'sequel'
2 | require 'sequel/extensions/pagination'
3 |
4 | class Sequel::Model
5 | =begin
6 | # Intialize each column to the default value for new model objects
7 | def after_initialize
8 | super
9 | model.columns.each do |x|
10 | if !@values.include?(x) && db_schema[x][:allow_null]
11 | send("#{x}=", db_schema[x][:ruby_default])
12 | end
13 | end
14 | end
15 | =end
16 |
17 | # Return an empty array for *_to_many association methods for new model objects
18 | def _load_associated_objects(opts)
19 | opts.returns_array? && new? ? [] : super
20 | end
21 | end
22 |
23 | module MerbAdmin
24 | class AbstractModel
25 | module SequelSupport
26 | def get(id)
27 | model.first(:id => id).extend(InstanceMethods)
28 | end
29 |
30 | def count(options = {})
31 | if options[:conditions] && !options[:conditions].empty?
32 | model.where(options[:conditions]).count
33 | else
34 | model.count
35 | end
36 | end
37 |
38 | def first(options = {})
39 | sort = options.delete(:sort) || :id
40 | sort_order = options.delete(:sort_reverse) ? :desc : :asc
41 |
42 | if options[:conditions] && !options[:conditions].empty?
43 | model.order(sort.to_sym.send(sort_order)).first(options[:conditions]).extend(InstanceMethods)
44 | else
45 | model.order(sort.to_sym.send(sort_order)).first.extend(InstanceMethods)
46 | end
47 | end
48 |
49 | def last(options = {})
50 | sort = options.delete(:sort) || :id
51 | sort_order = options.delete(:sort_reverse) ? :desc : :asc
52 |
53 | if options[:conditions] && !options[:conditions].empty?
54 | model.order(sort.to_sym.send(sort_order)).last(options[:conditions]).extend(InstanceMethods)
55 | else
56 | model.order(sort.to_sym.send(sort_order)).last.extend(InstanceMethods)
57 | end
58 | end
59 |
60 | def all(options = {})
61 | offset = options.delete(:offset)
62 | limit = options.delete(:limit)
63 |
64 | sort = options.delete(:sort) || :id
65 | sort_order = options.delete(:sort_reverse) ? :desc : :asc
66 |
67 | if options[:conditions] && !options[:conditions].empty?
68 | model.where(options[:conditions]).order(sort.to_sym.send(sort_order))
69 | else
70 | model.order(sort.to_sym.send(sort_order))
71 | end
72 | end
73 |
74 | def paginated(options = {})
75 | page = options.delete(:page) || 1
76 | per_page = options.delete(:per_page) || MerbAdmin[:per_page]
77 | page_count = (count(options).to_f / per_page).ceil
78 |
79 | sort = options.delete(:sort) || :id
80 | sort_order = options.delete(:sort_reverse) ? :desc : :asc
81 |
82 | if options[:conditions] && !options[:conditions].empty?
83 | [page_count, model.paginate(page.to_i, per_page).where(options[:conditions]).order(sort.to_sym.send(sort_order))]
84 | else
85 | [page_count, model.paginate(page.to_i, per_page).order(sort.to_sym.send(sort_order))]
86 | end
87 | end
88 |
89 | def create(params = {})
90 | model.create(params).extend(InstanceMethods)
91 | end
92 |
93 | def new(params = {})
94 | model.new(params).extend(InstanceMethods)
95 | end
96 |
97 | def destroy_all!
98 | model.destroy
99 | end
100 |
101 | def has_many_associations
102 | associations.select do |association|
103 | association[:type] == :has_many
104 | end
105 | end
106 |
107 | def has_one_associations
108 | associations.select do |association|
109 | association[:type] == :has_one
110 | end
111 | end
112 |
113 | def belongs_to_associations
114 | associations.select do |association|
115 | association[:type] == :belongs_to
116 | end
117 | end
118 |
119 | def associations
120 | model.all_association_reflections.map do |association|
121 | {
122 | :name => association_name_lookup(association),
123 | :pretty_name => association_pretty_name_lookup(association),
124 | :type => association_type_lookup(association),
125 | :parent_model => association_parent_model_lookup(association),
126 | :parent_key => association_parent_key_lookup(association),
127 | :child_model => association_child_model_lookup(association),
128 | :child_key => association_child_key_lookup(association),
129 | }
130 | end
131 | end
132 |
133 | def properties
134 | model.columns.map do |property|
135 | {
136 | :name => property,
137 | :pretty_name => property.to_s.gsub(/_id$/, "").gsub("_", " ").capitalize,
138 | :type => property_type_lookup(property),
139 | :length => property_length_lookup(property),
140 | :nullable? => model.db_schema[property][:allow_null],
141 | :serial? => model.db_schema[property][:primary_key],
142 | }
143 | end
144 | end
145 |
146 | private
147 |
148 | def property_type_lookup(property)
149 | case model.db_schema[property][:db_type]
150 | when /\A(?:medium|small)?int(?:eger)?(?:\((?:\d+)\))?\z/io
151 | :integer
152 | when /\Atinyint(?:\((\d+)\))?\z/io
153 | :boolean
154 | when /\Abigint(?:\((?:\d+)\))?\z/io
155 | :integer
156 | when /\A(?:real|float|double(?: precision)?)\z/io
157 | :float
158 | when 'boolean'
159 | :boolean
160 | when /\A(?:(?:tiny|medium|long|n)?text|clob)\z/io
161 | :text
162 | when 'date'
163 | :date
164 | when /\A(?:small)?datetime\z/io
165 | :datetime
166 | when /\Atimestamp(?: with(?:out)? time zone)?\z/io
167 | :datetime
168 | when /\Atime(?: with(?:out)? time zone)?\z/io
169 | :time
170 | when /\An?char(?:acter)?(?:\((\d+)\))?\z/io
171 | :string
172 | when /\A(?:n?varchar|character varying|bpchar|string)(?:\((\d+)\))?\z/io
173 | :string
174 | when /\A(?:small)?money\z/io
175 | :big_decimal
176 | when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?\z/io
177 | :big_decimal
178 | when 'year'
179 | :integer
180 | else
181 | :string
182 | end
183 | end
184 |
185 | def property_length_lookup(property)
186 | case model.db_schema[property][:db_type]
187 | when /\An?char(?:acter)?(?:\((\d+)\))?\z/io
188 | $1 ? $1.to_i : 255
189 | when /\A(?:n?varchar|character varying|bpchar|string)(?:\((\d+)\))?\z/io
190 | $1 ? $1.to_i : 255
191 | else
192 | nil
193 | end
194 | end
195 |
196 | def association_name_lookup(association)
197 | case association[:type]
198 | when :one_to_many
199 | association[:name]
200 | when :one_to_one
201 | # association[:name].to_s.singularize.to_sym
202 | association[:name].to_s.to_sym
203 | when :many_to_one
204 | association[:name]
205 | else
206 | raise "Unknown association type"
207 | end
208 | end
209 |
210 | def association_pretty_name_lookup(association)
211 | case association[:type]
212 | when :one_to_many
213 | association[:name].to_s.gsub('_', ' ').capitalize
214 | when :one_to_one
215 | association[:name].to_s.singularize.gsub('_', ' ').capitalize
216 | when :many_to_one
217 | association[:name].to_s.gsub('_', ' ').capitalize
218 | else
219 | raise "Unknown association type"
220 | end
221 | end
222 |
223 | def association_type_lookup(association)
224 | case association[:type]
225 | when :one_to_many
226 | :has_many
227 | when :one_to_one
228 | :has_one
229 | when :many_to_one
230 | :belongs_to
231 | else
232 | raise "Unknown association type"
233 | end
234 | end
235 |
236 | def association_parent_model_lookup(association)
237 | case association[:type]
238 | when :one_to_many, :one_to_one
239 | association[:model]
240 | when :many_to_one
241 | Object.const_get(association[:class_name])
242 | else
243 | raise "Unknown association type"
244 | end
245 | end
246 |
247 | def association_parent_key_lookup(association)
248 | [:id]
249 | end
250 |
251 | def association_child_model_lookup(association)
252 | case association[:type]
253 | when :one_to_many, :one_to_one
254 | Object.const_get(association[:class_name])
255 | when :many_to_one
256 | association[:model]
257 | else
258 | raise "Unknown association type"
259 | end
260 | end
261 |
262 | def association_child_key_lookup(association)
263 | case association[:type]
264 | when :one_to_many, :one_to_one
265 | association[:keys]
266 | when :many_to_one
267 | ["#{association[:class_name].snake_case}_id".to_sym]
268 | else
269 | raise "Unknown association type"
270 | end
271 | end
272 |
273 | module InstanceMethods
274 | def update_attributes(attributes)
275 | update(attributes)
276 | true
277 | end
278 | end
279 |
280 | end
281 | end
282 | end
283 |
--------------------------------------------------------------------------------