admin_data 1.1.12 → 1.1.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile.lock +1 -1
  3. data/README.md +1 -1
  4. data/app/controllers/admin_data/analytics_controller.rb +38 -0
  5. data/app/controllers/admin_data/public_controller.rb +2 -0
  6. data/app/controllers/admin_data/search_controller.rb +10 -5
  7. data/app/helpers/admin_data/application_helper.rb +21 -0
  8. data/app/views/admin_data/analytics/_bar_chart.html.erb +39 -0
  9. data/app/views/admin_data/analytics/index.html.erb +41 -0
  10. data/app/views/admin_data/search/search/_advance_search_form.html.erb +1 -7
  11. data/app/views/admin_data/search/search/_listing.html.erb +4 -2
  12. data/app/views/admin_data/search/search/_search_form.html.erb +0 -1
  13. data/app/views/admin_data/shared/_secondary_navigation.html.erb +5 -0
  14. data/app/views/layouts/admin_data.html.erb +4 -1
  15. data/config/routes.rb +8 -5
  16. data/lib/admin_data/analytics.rb +176 -0
  17. data/lib/admin_data/exceptions.rb +5 -0
  18. data/lib/admin_data/search.rb +139 -137
  19. data/lib/admin_data/version.rb +1 -1
  20. data/lib/admin_data.rb +3 -0
  21. data/lib/public/images/sort_by_asc.jpg +0 -0
  22. data/lib/public/images/sort_by_desc.jpg +0 -0
  23. data/lib/public/images/sort_by_nothing.jpg +0 -0
  24. data/lib/public/javascripts/advance_search/sortby.js +14 -0
  25. data/lib/public/javascripts/analytics/report.js +7 -0
  26. data/lib/public/stylesheets/base.css +25 -1
  27. data/test/rails_root/Gemfile +15 -12
  28. data/test/rails_root/Gemfile.lock +8 -2
  29. data/test/rails_root/README.md +16 -8
  30. data/test/rails_root/app/models/car.rb +3 -0
  31. data/test/rails_root/config/application.rb +0 -27
  32. data/test/rails_root/config/cucumber.yml +0 -2
  33. data/test/rails_root/config/database.yml.mysql +22 -0
  34. data/test/rails_root/config/database.yml.pg +15 -0
  35. data/test/rails_root/config/{database.yml → database.yml.sqlite3} +1 -1
  36. data/test/rails_root/db/development.sqlite3 +0 -0
  37. data/test/rails_root/db/migrate/{20091030202259_create_users.rb → 20091030202259_create_tables.rb} +7 -1
  38. data/test/rails_root/db/schema.rb +6 -0
  39. data/test/rails_root/features/quick_search.feature +0 -11
  40. data/test/rails_root/lib/tasks/dbs.rake +30 -0
  41. data/test/rails_root/lib/tasks/sample_cars.rake +18 -0
  42. data/test/rails_root/test/unit/car_test.rb +100 -0
  43. metadata +30 -13
  44. data/app/views/admin_data/search/search/_sortby.html.erb +0 -19
  45. data/test/rails_root/features/advance_search/sort.feature +0 -16
  46. data/test/rails_root/test/performance/browsing_test.rb +0 -9
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ test/rails_root/db/*.sqlite3
15
15
  test/rails_root/.rvmrc
16
16
  test/rails_root/.bundle
17
17
  test/rails_root/.bundle/*
18
+ test/rails_root/config/database.yml
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- admin_data (1.1.12)
4
+ admin_data (1.1.13)
5
5
  will_paginate (= 3.0.pre2)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  #Test#
12
12
 
13
- <tt>cd _test/rails_root</tt> and read the instructions mentioned at README.md there.
13
+ <tt>cd test/rails_root</tt> and read the instructions mentioned at README.md there.
14
14
 
15
15
 
16
16
 
@@ -0,0 +1,38 @@
1
+ module AdminData
2
+ class AnalyticsController < ApplicationController
3
+
4
+ before_filter :get_class_from_params
5
+ before_filter :set_ivars
6
+
7
+ rescue_from AdminData::NoCreatedAtColumnException, :with => :render_no_created_at
8
+
9
+ def render_no_created_at
10
+ render :text => "Model #{@klass} does not have created_at column"
11
+ end
12
+
13
+ def daily
14
+ @chart_title = "#{@klass.name} records created in the last 30 days"
15
+
16
+ a = AdminData::Analytics.daily_report(@klass, Time.now)
17
+ @chart_data_s = a.map {|e| e.last }.join(', ')
18
+ @chart_data_x_axis = a.map {|e| e.first}.join(', ')
19
+ render :action => 'index'
20
+ end
21
+
22
+ def monthly
23
+ @chart_title = "#{@klass.name} rercords created last year"
24
+ a = AdminData::Analytics.monthly_report(@klass, Time.now)
25
+ @chart_data_s = a.map {|e| e.last }.join(', ')
26
+ @chart_data_x_axis = a.map {|e| e.first}.join(', ')
27
+ render :action => 'index'
28
+ end
29
+
30
+ def set_ivars
31
+ @chart_width = 950
32
+ @chart_height = 400
33
+ @chart_h_axis_title = ''
34
+ @chart_legend_name = 'Created'
35
+ end
36
+
37
+ end
38
+ end
@@ -15,6 +15,8 @@ module AdminData
15
15
  content_type = "text/javascript"
16
16
  when /\.png$/i
17
17
  content_type = "image/png"
18
+ when /\.jpg$/i
19
+ content_type = "image/jpg"
18
20
  else
19
21
  render :nothing => true, :status => 404 and return
20
22
  end
@@ -1,5 +1,3 @@
1
- require File.join(AdminData::LIBPATH, 'admin_data', 'search')
2
-
3
1
  class SearchAction
4
2
  attr_accessor :relation, :success_message
5
3
 
@@ -27,12 +25,13 @@ module AdminData
27
25
 
28
26
  layout 'search'
29
27
 
30
- include Search
28
+ include AdminData::Search
31
29
 
32
30
  before_filter :get_class_from_params
33
31
  before_filter :ensure_valid_children_klass, :only => [:quick_search]
34
32
  before_filter :ensure_is_authorized_for_update_opration, :only => [:advance_search]
35
33
  before_filter :set_column_type_info, :only => [:advance_search]
34
+ before_filter :handle_sorting
36
35
 
37
36
 
38
37
  def quick_search
@@ -48,7 +47,7 @@ module AdminData
48
47
  @records = has_many_proxy.send(:paginate, h)
49
48
  else
50
49
  params[:query] = params[:query].strip unless params[:query].blank?
51
- cond = build_quick_search_conditions(@klass, params[:query])
50
+ cond = build_quick_search_condition(@klass, params[:query])
52
51
  h = { :page => params[:page], :per_page => per_page, :order => order, :conditions => cond }
53
52
  @records = @klass.unscoped.paginate(h)
54
53
  end
@@ -58,7 +57,7 @@ module AdminData
58
57
 
59
58
  def advance_search
60
59
  @page_title = "Advance search #{@klass.name.underscore}"
61
- hash = build_advance_search_conditions(@klass, params[:adv_search])
60
+ hash = build_advance_search_condition(@klass, params[:adv_search])
62
61
  relation = hash[:cond]
63
62
  errors = hash[:errors]
64
63
  order = default_order
@@ -134,5 +133,11 @@ module AdminData
134
133
  @column_type_info = "{#{column_type_info}}"
135
134
  end
136
135
 
136
+ def handle_sorting
137
+ sort_order = params[:sortby] || 'id desc'
138
+ @sort_by_column_name, @sort_order = sort_order.split
139
+ @sort_css = @sort_order == 'asc' ? 'sort_by_asc' : 'sort_by_desc'
140
+ end
141
+
137
142
  end
138
143
  end
@@ -1,6 +1,27 @@
1
1
  module AdminData
2
2
  module ApplicationHelper
3
3
 
4
+ def get_sort_order(column)
5
+ if column == @sort_by_column_name && @sort_order == 'desc'
6
+ "#{column} asc"
7
+ else
8
+ "#{column} desc"
9
+ end
10
+ end
11
+
12
+ def get_sort_title_with_url(column, klass)
13
+ order = get_sort_order(column)
14
+ link_to column_title(klass, column), admin_data_search_path(:klass => klass, :query => params[:query], :sortby => order)
15
+ end
16
+
17
+ def get_sort_class(column)
18
+ sort_class = 'sortable'
19
+ if column == @sort_by_column_name
20
+ sort_class << ' ' + @sort_css
21
+ end
22
+ sort_class
23
+ end
24
+
4
25
  def parent_layout(layout)
5
26
  @_content_for[:layout] = self.output_buffer
6
27
  self.output_buffer = render(:file => "layouts/#{layout}")
@@ -0,0 +1,39 @@
1
+
2
+ <%= javascript_include_tag "https://www.google.com/jsapi" %>
3
+ <script>
4
+ google.load('visualization', '1', {'packages':['corechart']});
5
+ google.setOnLoadCallback(drawChart);
6
+
7
+ function drawChart() {
8
+
9
+ var data = new google.visualization.DataTable();
10
+ var raw_data = [['<%= chart_legend_name %>', <%= chart_data_s %>]];
11
+
12
+ var years = [<%= chart_data_x_axis %>];
13
+
14
+ data.addColumn('string', 'Year');
15
+ for (var i = 0; i < raw_data.length; ++i) {
16
+ data.addColumn('number', raw_data[i][0]);
17
+ }
18
+
19
+ data.addRows(years.length);
20
+
21
+ for (var j = 0; j < years.length; ++j) {
22
+ data.setValue(j, 0, years[j].toString());
23
+ }
24
+
25
+ for (var i = 0; i < raw_data.length; ++i) {
26
+ for (var j = 1; j < raw_data[i].length; ++j) {
27
+ data.setValue(j-1, i+1, raw_data[i][j]);
28
+ }
29
+ }
30
+
31
+ // Create and draw the visualization.
32
+ var chart = new google.visualization.ColumnChart(document.getElementById('chart_div'));
33
+ chart.draw(data,
34
+ { title: '<%= chart_title %>',
35
+ width: <%= chart_width %>,
36
+ height: <%= chart_height %>,
37
+ hAxis: {title: '<%= chart_h_axis_title %>'}});
38
+ }
39
+ </script>
@@ -0,0 +1,41 @@
1
+ <%= render 'bar_chart', :chart_title => @chart_title,
2
+ :chart_h_axis_title => @chart_h_axis_title,
3
+ :chart_legend_name => @chart_legend_name,
4
+ :chart_data_s => @chart_data_s,
5
+ :chart_data_x_axis => @chart_data_x_axis,
6
+ :chart_width => @chart_width,
7
+ :chart_height => @chart_height %>
8
+
9
+ <div id="main">
10
+
11
+ <%= breadcrum do %>
12
+ <%= link_to @klass, admin_data_search_path(:klass => @klass.name) %>
13
+ <% end %>
14
+
15
+ <%= render 'admin_data/shared/flash_message', :model => @model %>
16
+
17
+ <div class="block" id="block-tables">
18
+ <div class="secondary-navigation">
19
+ <%= render 'admin_data/shared/secondary_navigation', :klass => @klass %>
20
+ <div class="clear"></div>
21
+ </div>
22
+ <div class="content rounded">
23
+ <div>
24
+ <h2 class="title" style='float:left;'>Analytics</h2>
25
+
26
+ <div style='float:left;margin-top:18px;margin-left:20px;'>
27
+ <%= radio_button_tag :report_type, admin_data_daily_analytics_path(:klass => @klass), params[:action] == 'daily' %> Daily
28
+ &nbsp;
29
+ <%= radio_button_tag :report_type, admin_data_monthly_analytics_path(:klass => @klass), params[:action] == 'monthly' %> Monthly
30
+ </div>
31
+ <div style='clear:both;'></div>
32
+ </div>
33
+
34
+
35
+ <div class="inner">
36
+ <div id="chart_div"></div>
37
+
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
@@ -6,15 +6,9 @@
6
6
  <div id='advance_search' class='search_box'>
7
7
  <table id='advance_search_table' class='advtable'>
8
8
  </table>
9
+ <input type="hidden" id="advance_search_sortby" name="sortby" value="<%=params[:sortby]%>" />
9
10
 
10
11
  <div class='sortby_umbrella'>
11
- <div class='group'>
12
- <div class='sortby_text'>Sort by</div>
13
- <select name='sortby' id='sortby'>
14
- <%= AdminData::Util.build_sort_options(klass,params[:sortby]).html_safe %>
15
- </select>
16
- <br />
17
- </div>
18
12
  <div class='clear'></div>
19
13
  <input type="submit" value="Search" class='submit_search' />
20
14
  </div>
@@ -10,9 +10,11 @@
10
10
  <table cellspacing="3" cellpadding="3" id="view_table" class='table colorize'>
11
11
 
12
12
  <thead>
13
- <tr>
13
+ <tr class='thead'>
14
14
  <% columns_order(klass).each do |column| %>
15
- <th> <%= column_title(klass, column) %> </th>
15
+ <th class='<%=get_sort_class(column)%>' data-sortby='<%=get_sort_order(column)%>'>
16
+ <%= get_sort_title_with_url(column, klass) %>
17
+ </th>
16
18
  <% end %>
17
19
  </tr>
18
20
  </thead>
@@ -4,7 +4,6 @@
4
4
  <div id='quick_search'>
5
5
  <label class='label keyword_label'>Search keyword</label>
6
6
  <input type="text" id="quick_search_input" name="query" value="<%=params[:query]%>" />
7
- <%= render :partial => 'admin_data/search/search/sortby', :locals => {:klass => klass} %>
8
7
  <div class='clear'></div>
9
8
  <%= submit_tag 'Search', :name => nil, :disable_with => 'searching ...', :class => 'submit_search' %>
10
9
  </div>
@@ -1,6 +1,7 @@
1
1
  <%
2
2
  quick_search_tab_active = params[:action] == 'quick_search' ? 'active' : ''
3
3
  advance_search_tab_active = params[:action] == 'advance_search' ? 'active' : ''
4
+ analytics_tab_active = params[:controller] == 'admin_data/analytics' ? 'active' : ''
4
5
  table_structure_tab_active = params[:controller] == 'admin_data/table_structure' ? 'active' : ''
5
6
  add_new_record_tab_active = %w(new create).include?(params[:action]) ? 'active' : ''
6
7
  %>
@@ -14,6 +15,10 @@
14
15
  <%= link_to 'Advance Search', admin_data_advance_search_path(:klass => klass.name.underscore) %>
15
16
  </li>
16
17
 
18
+ <li class="<%=analytics_tab_active%>">
19
+ <%= link_to 'Analytics', admin_data_daily_analytics_path(:klass => klass.name.underscore) %>
20
+ </li>
21
+
17
22
  <li class="<%=table_structure_tab_active%>">
18
23
  <%= link_to 'Table Structure', admin_data_table_structure_path(:klass => klass.name.underscore) %>
19
24
  </li>
@@ -29,8 +29,11 @@
29
29
  'advance_search/act_on_result',
30
30
  'advance_search/build_first_row',
31
31
  'advance_search/event_bindings',
32
- 'advance_search/trigger_submit_on_domready'
32
+ 'advance_search/trigger_submit_on_domready',
33
+ 'advance_search/sortby'
33
34
  ) %>
35
+ <% elsif params[:controller] == 'admin_data/analytics' %>
36
+ <%= javascript_include_tag('analytics/report') %>
34
37
  <% end %>
35
38
 
36
39
  <%= csrf_meta_tag %>
data/config/routes.rb CHANGED
@@ -19,15 +19,18 @@ Rails.application.routes.draw do
19
19
  match '/jstest', :to => :jstest, :as => :jstest
20
20
  end
21
21
 
22
- match '/table_structure/:klass' => "table_structure#index", :as => :table_structure
22
+ match '/table_structure/:klass' => "table_structure#index", :as => :table_structure
23
23
 
24
- match '/quick_search/:klass' => "search#quick_search", :as => :search
24
+ match '/quick_search/:klass' => "search#quick_search", :as => :search
25
25
 
26
- match '/advance_search/:klass' => "search#advance_search", :as => :advance_search
26
+ match '/advance_search/:klass' => "search#advance_search", :as => :advance_search
27
27
 
28
- match '/feed/:klasss' => "feed#index", :defaults => { :format =>'rss' }, :as => :feed
28
+ match '/analytics/daily/:klass' => "analytics#daily", :as => :daily_analytics
29
+ match '/analytics/monthly/:klass' => "analytics#monthly", :as => :monthly_analytics
29
30
 
30
- match '/public/*file' => "public#serve", :as => :public
31
+ match '/feed/:klasss' => "feed#index", :defaults => { :format =>'rss' }, :as => :feed
32
+
33
+ match '/public/*file' => "public#serve", :as => :public
31
34
 
32
35
  root :to => "home#index"
33
36
  end
@@ -0,0 +1,176 @@
1
+ require "active_support/all"
2
+
3
+ module AdminData
4
+ module Analytics
5
+
6
+ # a utility class to handle date interpolation for different databases
7
+ class Dater
8
+ attr_accessor :adapter, :type
9
+
10
+ def initialize(adapter, type = 'daily')
11
+ @adapter = adapter
12
+ @type = type
13
+ end
14
+
15
+ def date_select_key
16
+ "date_data"
17
+ end
18
+
19
+ def group_by_key
20
+ if adapter =~ /postgresql/i
21
+ self.type == 'monthly' ? "date_part('year', created_at), date_part('month', created_at)" : "date_data"
22
+ elsif adapter =~ /mysql/i
23
+ self.type == 'monthly' ? "YEAR(created_at), MONTH(created_at)" : "date_data"
24
+ else
25
+ self.type == 'monthly' ? "strftime('%Y', created_at), strftime('%m', created_at)" : "date_data"
26
+ end
27
+ end
28
+
29
+ def count_select_key
30
+ "count_data"
31
+ end
32
+
33
+ def count_function
34
+ "count(*) as count_data"
35
+ end
36
+
37
+ def date_select_function
38
+ self.type == 'monthly' ? date_select_function_monthly : date_select_function_daily
39
+ end
40
+
41
+ private
42
+
43
+ def date_select_function_monthly
44
+ if adapter =~ /mysql/i
45
+ "MONTH(created_at) as date_data"
46
+ elsif adapter =~ /postgresql/i
47
+ "date_part('month', created_at) as date_data"
48
+ else
49
+ "strftime('%m', created_at) as date_data"
50
+ end
51
+ end
52
+
53
+ def date_select_function_daily
54
+ if adapter =~ /mysql/i
55
+ "date_format(created_at, '%Y-%m-%d') as date_data"
56
+ else
57
+ "date(created_at) as date_data"
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ def self.monthly_report(klass, end_date)
64
+ begin_date = end_date.ago(1.year)
65
+ raise "begin_date should not be after end_date" if begin_date > end_date
66
+ raise AdminData::NoCreatedAtColumnException unless klass.columns.find {|r| r.name == 'created_at'}
67
+
68
+ begin_date = begin_date.beginning_of_day
69
+ end_date = end_date.end_of_day
70
+
71
+ dater = Dater.new(ActiveRecord::Base.connection.adapter_name, 'monthly')
72
+
73
+ query = klass.unscoped
74
+ query = query.where(["created_at >= ?", begin_date])
75
+ query = query.where(["created_at <= ?", end_date])
76
+ query = query.group(dater.group_by_key)
77
+ query = query.select(dater.date_select_function)
78
+ query = query.select(dater.count_function)
79
+ debug "sql: " + query.to_sql
80
+ result = query.all
81
+ debug "sql result: #{result.inspect}"
82
+
83
+ extract_data_from_result_set_monthly(result, dater, begin_date, end_date)
84
+ end
85
+
86
+ def self.extract_data_from_result_set_monthly(result, dater, begin_date, end_date)
87
+ debug "extracting from result set now"
88
+ result_hash = {}
89
+ result.each do |record|
90
+ result_hash.merge!(record[dater.date_select_key].to_i => record[dater.count_select_key])
91
+ end
92
+
93
+ debug "result_hash.inspect is #{result_hash.inspect}"
94
+
95
+ current_month = begin_date.strftime('%m').to_i
96
+ months_order = (1..current_month).to_a.reverse + (current_month..12).to_a.reverse
97
+ months_order.uniq!.reverse!
98
+
99
+ debug "months order is #{months_order.inspect}"
100
+
101
+ final_hash = ActiveSupport::OrderedHash.new
102
+ months_order.each do |month|
103
+ month = month.to_i #sqlite3 has months as 03 instead of 3
104
+ m = Time.now.change(:month => month)
105
+ m = m.ago(1.year) if month > current_month
106
+
107
+ key = m.strftime('%b-%Y')
108
+ key = "'#{key}'"
109
+ if dater.adapter =~ /postgresql/i
110
+ value = result_hash[month]
111
+ elsif dater.adapter =~ /mysql/i
112
+ value = result_hash[month]
113
+ else
114
+ value = result_hash[month]
115
+ end
116
+ value = value.to_i
117
+ debug "month: #{month} key: #{key} value: #{value}"
118
+ final_hash.merge!(key => value)
119
+ end
120
+
121
+ final_hash.to_a.tap {|e| debug e.inspect }
122
+ end
123
+
124
+ def self.daily_report(klass, end_date)
125
+ begin_date = end_date.ago(1.month)
126
+ raise "begin_date should not be after end_date" if begin_date > end_date
127
+ raise AdminData::NoCreatedAtColumnException unless klass.columns.find {|r| r.name == 'created_at'}
128
+
129
+ begin_date = begin_date.beginning_of_day
130
+ end_date = end_date.end_of_day
131
+
132
+ dater = Dater.new(ActiveRecord::Base.connection.adapter_name)
133
+
134
+ query = klass.unscoped
135
+ query = query.where(["created_at >= ?", begin_date])
136
+ query = query.where(["created_at <= ?", end_date])
137
+ query = query.group(dater.group_by_key)
138
+ query = query.select(dater.date_select_function)
139
+ query = query.select(dater.count_function)
140
+ debug "sql: " + query.to_sql
141
+ result = query.all
142
+ debug "sql result: #{result.inspect}"
143
+
144
+ extract_data_from_result_set_daily(result, dater, begin_date, end_date).tap {|e| debug "formatted output: "+e.inspect}
145
+ end
146
+
147
+ def self.extract_data_from_result_set_daily(result, dater, begin_date, end_date)
148
+ count = result.map {|r| r[dater.count_select_key] }
149
+ dates = result.map {|r| r[dater.date_select_key] }
150
+
151
+ debug "count is: #{count.inspect}"
152
+ debug "dates is: #{dates.inspect}"
153
+
154
+ final_output= []
155
+
156
+ while(begin_date) do
157
+ s = begin_date.strftime('%Y-%m-%d')
158
+ final_count = if index = dates.index(s)
159
+ count[index].to_i
160
+ else
161
+ 0
162
+ end
163
+ final_output << ["'#{s}'", final_count]
164
+ begin_date = begin_date.tomorrow
165
+ break if begin_date > end_date
166
+ end
167
+ final_output
168
+ end
169
+
170
+ def self.debug(msg)
171
+ puts msg
172
+ end
173
+
174
+ end
175
+
176
+ end
@@ -0,0 +1,5 @@
1
+ module AdminData
2
+
3
+ class NoCreatedAtColumnException < Exception; end
4
+
5
+ end