simple_admin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +6 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +12 -0
  4. data/README.rdoc +318 -0
  5. data/Rakefile +67 -0
  6. data/TODO.rdoc +4 -0
  7. data/app/assets/images/simple_admin/active_admin/admin_notes_icon.png +0 -0
  8. data/app/assets/images/simple_admin/active_admin/loading.gif +0 -0
  9. data/app/assets/images/simple_admin/active_admin/nested_menu_arrow.gif +0 -0
  10. data/app/assets/images/simple_admin/active_admin/nested_menu_arrow_dark.gif +0 -0
  11. data/app/assets/images/simple_admin/active_admin/orderable.png +0 -0
  12. data/app/assets/javascripts/simple_admin/active_admin.js +434 -0
  13. data/app/assets/stylesheets/simple_admin/active_admin.css +1445 -0
  14. data/app/controllers/simple_admin/admin_controller.rb +117 -0
  15. data/app/helpers/simple_admin/admin_helper.rb +11 -0
  16. data/app/helpers/simple_admin/display_helper.rb +38 -0
  17. data/app/helpers/simple_admin/filter_helper.rb +147 -0
  18. data/app/helpers/simple_admin/header_helper.rb +60 -0
  19. data/app/helpers/simple_admin/path_helper.rb +12 -0
  20. data/app/helpers/simple_admin/sidebar_helper.rb +9 -0
  21. data/app/helpers/simple_admin/table_helper.rb +39 -0
  22. data/app/helpers/simple_admin/title_helper.rb +35 -0
  23. data/app/views/layouts/simple_admin.html.erb +41 -0
  24. data/app/views/simple_admin/admin/_form.html.erb +16 -0
  25. data/app/views/simple_admin/admin/edit.html.erb +7 -0
  26. data/app/views/simple_admin/admin/index.csv.erb +19 -0
  27. data/app/views/simple_admin/admin/index.html.erb +82 -0
  28. data/app/views/simple_admin/admin/new.html.erb +7 -0
  29. data/app/views/simple_admin/admin/show.html.erb +22 -0
  30. data/config/routes.rb +8 -0
  31. data/lib/rails/generators/simple_admin/simple_admin_generator.rb +33 -0
  32. data/lib/rails/generators/simple_admin/templates/initializer.rb +76 -0
  33. data/lib/simple_admin.rb +100 -0
  34. data/lib/simple_admin/attributes.rb +80 -0
  35. data/lib/simple_admin/breadcrumbs.rb +24 -0
  36. data/lib/simple_admin/builder.rb +35 -0
  37. data/lib/simple_admin/engine.rb +8 -0
  38. data/lib/simple_admin/filters.rb +5 -0
  39. data/lib/simple_admin/interface.rb +55 -0
  40. data/lib/simple_admin/section.rb +30 -0
  41. data/lib/simple_admin/version.rb +3 -0
  42. data/rails/init.rb +2 -0
  43. data/simple_admin.gemspec +34 -0
  44. data/spec/acceptance/admin_thing_spec.rb +13 -0
  45. data/spec/controllers/simple_admin/admin_controller_spec.rb +95 -0
  46. data/spec/factories.rb +14 -0
  47. data/spec/simple_admin/attributes_spec.rb +106 -0
  48. data/spec/simple_admin/breadcrumbs_spec.rb +18 -0
  49. data/spec/simple_admin/builder_spec.rb +57 -0
  50. data/spec/simple_admin/engine_spec.rb +9 -0
  51. data/spec/simple_admin/filters_spec.rb +16 -0
  52. data/spec/simple_admin/interface_spec.rb +98 -0
  53. data/spec/simple_admin/section_spec.rb +63 -0
  54. data/spec/simple_admin/simple_admin_spec.rb +68 -0
  55. data/spec/spec_helper.rb +32 -0
  56. metadata +285 -0
@@ -0,0 +1,117 @@
1
+ require 'kaminari'
2
+ require 'meta_search'
3
+
4
+ module SimpleAdmin
5
+ class AdminController < ::ApplicationController
6
+ before_filter :require_user
7
+ before_filter :lookup_interface
8
+ before_filter :lookup_resource, :only => [:show, :edit, :update, :destroy]
9
+ before_filter :handle_before
10
+
11
+ unloadable
12
+
13
+ respond_to :csv, :json, :xml, :html
14
+
15
+ helper SimpleAdmin::AdminHelper
16
+
17
+ layout 'simple_admin'
18
+
19
+ def index
20
+ @collection = @interface.constant
21
+ @collection = @collection.order("#{@interface.constant.table_name}.#{$1} #{$2}") if params[:order] && params[:order] =~ /^([\w\_\.]+)_(desc|asc)$/
22
+ @collection = @collection.metasearch(clean_search_params(params))
23
+ @collection = @collection.page(params[:page]).per(@per_page || SimpleAdmin.default_per_page) if params[:format].blank? || params[:format] == 'html'
24
+ respond_with(@collection)
25
+ end
26
+
27
+ def show
28
+ @resource = @interface.constant.find(params[:id])
29
+ respond_with(@resource)
30
+ end
31
+
32
+ def new
33
+ @resource = @interface.constant.new
34
+ respond_with(@resource)
35
+ end
36
+
37
+ def edit
38
+ respond_with(@resource)
39
+ end
40
+
41
+ def create
42
+ @resource = @interface.constant.new(params[@interface.member.to_sym])
43
+ # respond_with will fail without explicit urls
44
+ respond_to do |format|
45
+ if @resource.save
46
+ format.html { redirect_to send("simple_admin_#{@interface.member}_path", @resource), :notice => "#{@interface.member.titleize} was successfully created." }
47
+ format.json { render :json => @resource, :status => :created, :location => send("simple_admin_#{@interface.member}_path", @resource) }
48
+ format.xml { render :xml => @resource, :status => :created, :location => send("simple_admin_#{@interface.member}_path", @resource) }
49
+ else
50
+ format.html { render :action => "new" }
51
+ format.json { render :json => @resource.errors, :status => :unprocessable_entity }
52
+ format.xml { render :xml => @resource.errors, :status => :unprocessable_entity }
53
+ end
54
+ end
55
+ end
56
+
57
+ def update
58
+ # respond_with will fail without explicit urls
59
+ respond_to do |format|
60
+ if @resource.update_attributes(params[@interface.member.to_sym])
61
+ format.html { redirect_to send("simple_admin_#{@interface.member}_path", @resource), :notice => "#{@interface.member.titleize} was successfully updated." }
62
+ format.json { head :ok }
63
+ format.xml { head :ok }
64
+ else
65
+ format.html { render :action => "edit" }
66
+ format.json { render :json => @resource.errors, :status => :unprocessable_entity }
67
+ format.xml { render :xml => @resource.errors, :status => :unprocessable_entity }
68
+ end
69
+ end
70
+ end
71
+
72
+ def destroy
73
+ @resource.destroy
74
+ # respond_with will fail without explicit urls
75
+ respond_to do |format|
76
+ format.html { redirect_to send("simple_admin_#{@interface.collection}_path") }
77
+ format.json { head :ok }
78
+ format.xml { head :ok }
79
+ end
80
+ end
81
+
82
+ protected
83
+
84
+ def require_user
85
+ send(SimpleAdmin.require_user_method) if SimpleAdmin.require_user_method
86
+ end
87
+
88
+ def lookup_interface
89
+ SimpleAdmin.registered.each do |interface|
90
+ @interface = interface if interface.collection == params[:interface]
91
+ end
92
+ # This should not be reached, routing should catch errors before this point
93
+ raise UnknownAdminInterface.new("Could not find the interface for simple admin") unless @interface
94
+ end
95
+
96
+ def lookup_resource
97
+ @resource = @interface.constant.find(params[:id])
98
+ end
99
+
100
+ def handle_before
101
+ @interface.before.each do |before|
102
+ next unless before[:actions].include?(params[:action].to_sym)
103
+ instance_eval(&before[:data])
104
+ end
105
+ end
106
+
107
+ def clean_search_params(search_params)
108
+ return {} unless search_params.is_a?(Hash)
109
+ search_params = search_params.dup
110
+ search_params.delete_if do |key, value|
111
+ value == "" ||
112
+ ["utf8", "scope", "commit", "action", "order", "interface", "controller", "format"].include?(key)
113
+ end
114
+ search_params
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,11 @@
1
+ module SimpleAdmin
2
+ module AdminHelper
3
+ include SimpleAdmin::TitleHelper
4
+ include SimpleAdmin::HeaderHelper
5
+ include SimpleAdmin::TableHelper
6
+ include SimpleAdmin::DisplayHelper
7
+ include SimpleAdmin::FilterHelper
8
+ include SimpleAdmin::SidebarHelper
9
+ include SimpleAdmin::PathHelper
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ module SimpleAdmin
2
+ module DisplayHelper
3
+
4
+ # Return a pretty string for any object
5
+ # Date Time are formatted via #localize with :format => :long
6
+ # ActiveRecord objects are formatted via #auto_link
7
+ # We attempt to #display_name of any other objects
8
+ def pretty_format(object)
9
+ case object
10
+ when String
11
+ object
12
+ when Date, Time
13
+ localize(object, :format => :long)
14
+ else
15
+ (object.respond_to?(:display_name) && object.send(:display_name)) ||
16
+ (object.respond_to?(:full_name) && object.send(:full_name)) ||
17
+ (object.respond_to?(:name) && object.send(:name)) ||
18
+ (object.respond_to?(:username) && object.send(:username)) ||
19
+ (object.respond_to?(:login) && object.send(:login)) ||
20
+ (object.respond_to?(:title) && object.send(:title)) ||
21
+ (object.respond_to?(:email) && object.send(:email)) ||
22
+ (object.respond_to?(:to_s) && object.send(:to_s)) ||
23
+ "#{object}"
24
+ end
25
+ end
26
+
27
+ def data_for(col)
28
+ value = if col.data
29
+ col.data.call(@resource, col)
30
+ elsif col.attribute.to_s =~ /^([\w]+)_id$/ && @resource.respond_to?($1.to_sym)
31
+ pretty_format(@resource.send($1))
32
+ else
33
+ pretty_format(@resource.send(col.attribute))
34
+ end
35
+ value ||= content_tag(:span, 'Empty', :class => 'empty')
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,147 @@
1
+ module SimpleAdmin
2
+ module FilterHelper
3
+ def filter_for(method, klass, options={})
4
+ options ||= {}
5
+ options[:as] ||= default_filter_type(klass, method)
6
+ return "" unless options[:as]
7
+ field_type = options.delete(:as)
8
+ content_tag :div, :class => "filter_form_field filter_#{field_type}" do
9
+ send("filter_#{field_type}_input", klass, method, options)
10
+ end
11
+ end
12
+
13
+ def filter_string_input(klass, method, options = {})
14
+ field_name = "#{method}_contains"
15
+
16
+ [ label(field_name, "Search #{method.to_s.titlecase}"),
17
+ text_field_tag(field_name, params[field_name] || '')
18
+ ].join("\n").html_safe
19
+ end
20
+
21
+ def filter_date_range_input(klass, method, options = {})
22
+ gt_field_name = "#{method}_gte"
23
+ lt_field_name = "#{method}_lte"
24
+
25
+ [ label(gt_field_name, method.to_s.titlecase),
26
+ filter_date_text_field(klass, gt_field_name),
27
+ " - ",
28
+ filter_date_text_field(klass, lt_field_name)
29
+ ].join("\n").html_safe
30
+ end
31
+
32
+ def filter_date_text_field(klass, method)
33
+ current_value = params[method] || ''
34
+ text_field_tag(method, current_value.respond_to?(:strftime) ? current_value.strftime("%Y-%m-%d") : current_value, :size => 12, :class => "datepicker", :max => 10)
35
+ end
36
+
37
+ def filter_numeric_input(klass, method, options = {})
38
+ filters = numeric_filters_for_method(method, options.delete(:filters) || default_numeric_filters)
39
+ current_filter = current_numeric_scope(klass, filters)
40
+ filter_select = select_tag '', options_for_select(filters, current_filter), :onchange => "document.getElementById('#{method}_numeric').name = '' + this.value + '';"
41
+ filter_input = text_field_tag(current_filter, params[current_filter] || '', :size => 10, :id => "#{method}_numeric")
42
+
43
+ [ label_tag(method), filter_select, " ", filter_input].join("\n").html_safe
44
+ end
45
+
46
+ def numeric_filters_for_method(method, filters)
47
+ filters.collect{|scope| [scope[0], [method,scope[1]].join("_") ] }
48
+ end
49
+
50
+ # Returns the scope for which we are currently searching. If no search is available
51
+ # it returns the first scope
52
+ def current_numeric_scope(klass, filters)
53
+ filters[1..-1].inject(filters.first){|a,b| params[b[1].to_sym] ? b : a }[1]
54
+ end
55
+
56
+ def default_numeric_filters
57
+ [['Equal To', 'eq'], ['Greater Than', 'gt'], ['Less Than', 'lt']]
58
+ end
59
+
60
+ def filter_select_input(klass, method, options = {})
61
+ association_name = method.to_s.gsub(/_id$/, '').to_sym
62
+ input_name = if reflection = reflection_for(klass, association_name)
63
+ if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
64
+ "#{association_name.to_s.singularize}_ids"
65
+ else
66
+ reflection.options[:foreign_key] || "#{association_name}_id"
67
+ end
68
+ else
69
+ association_name
70
+ end
71
+ input_name = (input_name + "_eq").to_sym
72
+ collection = find_collection_for_column(klass, association_name, options)
73
+ [ label(input_name, method.to_s.titlecase),
74
+ select_tag(input_name, options_for_select(collection, params[input_name]), :include_blank => options[:include_blank] || 'Any')
75
+ ].join("\n").html_safe
76
+ end
77
+
78
+ def find_collection_for_column(klass, column, options) #:nodoc:
79
+ collection = if options[:collection]
80
+ options.delete(:collection)
81
+ elsif reflection = reflection_for(klass, column)
82
+ options[:find_options] ||= {}
83
+ if conditions = reflection.options[:conditions]
84
+ options[:find_options][:conditions] = reflection.klass.merge_conditions(conditions, options[:find_options][:conditions])
85
+ end
86
+ reflection.klass.find(:all, options[:find_options])
87
+ else
88
+ boolean_collection(klass, column, options)
89
+ end
90
+ collection = collection.to_a if collection.is_a?(Hash)
91
+ collection.map { |o| [pretty_format(o), o.id] }
92
+ end
93
+
94
+ def boolean_collection(klass, column, options)
95
+ [['Yes', true], ['No', false]]
96
+ end
97
+
98
+ def filter_check_boxes_input(klass, method, options = {})
99
+ input_name = (generate_association_input_name(method).to_s + "_in").to_sym
100
+ collection = find_collection_for_column(method, options)
101
+ selected_values = klass.send(input_name) || []
102
+ checkboxes = template.content_tag :div, :class => "check_boxes_wrapper" do
103
+ collection.map do |c|
104
+ label = c.is_a?(Array) ? c.first : c
105
+ value = c.is_a?(Array) ? c.last : c
106
+ "<label><input type=\"checkbox\" name=\"#{input_name}[]\" value=\"#{value}\" #{selected_values.include?(value) ? "checked" : ""}/> #{label}</label>"
107
+ end.join("\n").html_safe
108
+ end
109
+
110
+ [ label(input_name, method.to_s.titlecase),
111
+ checkboxes
112
+ ].join("\n").html_safe
113
+ end
114
+
115
+ # Returns the default filter type for a given attribute
116
+ def default_filter_type(klass, method)
117
+ if column = column_for(klass, method)
118
+ case column.type
119
+ when :date, :datetime
120
+ return :date_range
121
+ when :string, :text
122
+ return :string
123
+ when :integer
124
+ return :select if reflection_for(klass, method.to_s.gsub('_id','').to_sym)
125
+ return :numeric
126
+ when :float, :decimal
127
+ return :numeric
128
+ end
129
+ end
130
+
131
+ if reflection = reflection_for(klass, method)
132
+ return :select if reflection.macro == :belongs_to && !reflection.options[:polymorphic]
133
+ end
134
+ end
135
+
136
+ # Returns the column for an attribute on the object being searched
137
+ # if it exists. Otherwise returns nil
138
+ def column_for(klass, method)
139
+ klass.columns_hash[method.to_s] if klass.respond_to?(:columns_hash)
140
+ end
141
+
142
+ # Returns the association reflection for the method if it exists
143
+ def reflection_for(klass, method)
144
+ klass.reflect_on_association(method) if klass.respond_to?(:reflect_on_association)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,60 @@
1
+ module SimpleAdmin
2
+ module HeaderHelper
3
+ def tabs
4
+ content_tag :ul, :id => 'tabs' do
5
+ SimpleAdmin.registered.collect do |interface|
6
+ content_tag :li, :id => interface.collection, :class => "#{'current' if @interface == interface}" do
7
+ link_to interface.collection.titlecase, send("simple_admin_#{interface.collection}_path".to_sym)
8
+ end
9
+ end.join.html_safe
10
+ end
11
+ end
12
+
13
+ def utility_nav
14
+ content_tag :p, :id => 'utility_nav' do
15
+ if SimpleAdmin.current_user_method && send(SimpleAdmin.current_user_method)
16
+ content = "".html_safe
17
+ content << content_tag(:span, send(SimpleAdmin.current_user_name_method), :class => "current_user") if SimpleAdmin.current_user_name_method
18
+ content << link_to("Logout", Rails.application.routes.url_helpers.logout_path) if Rails.application.routes.url_helpers.respond_to?(:logout_path)
19
+ content
20
+ end
21
+ end
22
+ end
23
+
24
+ def breadcrumbs
25
+ content_tag :span, :class => 'breadcrumb' do
26
+ SimpleAdmin::Breadcrumbs.parse(request.fullpath, params[:action]).collect do |crumb|
27
+ link_to(crumb.first, crumb.last) +
28
+ content_tag(:span, ' / ', :class => 'breadcrumb_sep')
29
+ end.join.html_safe
30
+ end
31
+ end
32
+
33
+ def action_items
34
+ content_tag :div, :class => "action_items" do
35
+ content = ""
36
+ # If we are currently showing, then check for edit and destroy action items
37
+ if params[:action].to_sym == :show
38
+ if controller.action_methods.include?('edit')
39
+ content << link_to("Edit #{@interface.member.titlecase}",
40
+ send("edit_simple_admin_#{@interface.member}_path", @object))
41
+ end
42
+ content << "&nbsp;"
43
+ if controller.action_methods.include?("destroy")
44
+ content << link_to("Delete #{@interface.member.titlecase}",
45
+ send("simple_admin_#{@interface.member}_path", @object),
46
+ :method => :delete, :confirm => "Are you sure you want to delete this?")
47
+ end
48
+ end
49
+ # If we are not showing an item or creating a new one, then check for new action items
50
+ unless [:new, :show].include?(params[:action].to_sym)
51
+ if controller.action_methods.include?('new')
52
+ content << link_to("New #{@interface.member.titlecase}",
53
+ send("new_simple_admin_#{@interface.member}_path"))
54
+ end
55
+ end
56
+ content.html_safe
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,12 @@
1
+ module SimpleAdmin
2
+ module PathHelper
3
+ def resource_path(res)
4
+ if res.new_record?
5
+ send("simple_admin_#{@interface.collection}_path")
6
+ else
7
+ send("simple_admin_#{@interface.member}_path", res)
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,9 @@
1
+ module SimpleAdmin
2
+ module SidebarHelper
3
+ def sidebars
4
+ @interface.sidebars_for(:index).each do |sidebar|
5
+ instance_exec(sidebar, &sidebar[:data])
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,39 @@
1
+ module SimpleAdmin
2
+ module TableHelper
3
+ def sortable_header_classes_for(col)
4
+ sort = current_sort
5
+ classes = []
6
+ classes << "sortable" if col.sortable
7
+ classes << "sorted-#{sort[1]}" if sort[0] == col.sort_key
8
+ classes.join(' ')
9
+ end
10
+
11
+ # Returns an array for the current sort order
12
+ # current_sort[0] #=> sort_key
13
+ # current_sort[1] #=> asc | desc
14
+ def current_sort
15
+ if params[:order] && params[:order] =~ /^([\w\_\.]+)_(desc|asc)$/
16
+ [$1,$2]
17
+ else
18
+ []
19
+ end
20
+ end
21
+
22
+ # Returns the order to use for a given sort key
23
+ #
24
+ # Default is to use 'desc'. If the current sort key is
25
+ # 'desc' it will return 'asc'
26
+ def order_for_sort_key(sort_key)
27
+ current_key, current_order = current_sort
28
+ return 'desc' unless current_key == sort_key
29
+ current_order == 'desc' ? 'asc' : 'desc'
30
+ end
31
+
32
+ def resource_actions(object)
33
+ links = link_to "View", send("simple_admin_#{@interface.member}_path", object), :class => "member_link view_link"
34
+ links += link_to "Edit", send("edit_simple_admin_#{@interface.member}_path", object), :class => "member_link edit_link"
35
+ links += link_to "Delete", send("simple_admin_#{@interface.member}_path", object), :method => :delete, :confirm => "Are you sure you want to delete this?", :class => "member_link delete_link"
36
+ links
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ module SimpleAdmin
2
+ module TitleHelper
3
+ def title
4
+ "#{page_title} | #{site_title}"
5
+ end
6
+
7
+ def site_title
8
+ SimpleAdmin::site_title
9
+ end
10
+
11
+ def page_title
12
+ options = @interface.options_for(params[:action].to_sym)
13
+ case options[:title]
14
+ when Proc
15
+ options[:title].call(@resource)
16
+ when Symbol
17
+ if @resource
18
+ @resource.send(optons[:title])
19
+ else
20
+ options[:title].to_s
21
+ end
22
+ when String
23
+ options[:title]
24
+ else
25
+ if @resource && @resource.new_record?
26
+ "New #{@interface.member.titleize}"
27
+ elsif @resource
28
+ "#{@interface.member.titleize}#{@resource.to_param.match(/\d+/) ? ' #' : ': '}#{@resource.to_param}"
29
+ else
30
+ @interface.collection.titlecase
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end