carload 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8cd7d8e37f5576e78d936449c1e5037208db1789
4
- data.tar.gz: feed52ae809a23d8cdc1f591e8ea76760e387945
3
+ metadata.gz: 9dc8244c3cd30b96538af399c5d325a32c7b46f2
4
+ data.tar.gz: 038e2a71da51255577b35a15fc21a8b6b66fb7b8
5
5
  SHA512:
6
- metadata.gz: cbc8dcf229e5789e2f7517b8b8c7e1b717abfc63372fd76bc537571969072a0f260d8cd5b90b56aba35ea5d5780f20a6b3a2cb7222552eb1828548ba528100d2
7
- data.tar.gz: 4faa773c891477b8f1b72c9b54e33ecebdd55d8c9e333ced189a8acec895c24309d54bf4ae4ca14fd4133ef4075c5d74e92cf4d1aa78b52ce68ead454f862df8
6
+ metadata.gz: 7a24677546621e1a187042e2e27ea5b603495590543b624133ab06151ddb423e2d7ad07695241277f52f3b9efc212bee433b87583140749be9e91a9693818d23
7
+ data.tar.gz: 99b19227d0f62ad5ae1081e5fa1deb04becb1373ec97f88f40514d9493e265b3e4bc84907d829afa908bad798287e7c62da828f22c9798fa3f60d18ccf42e770
data/README.md CHANGED
@@ -26,9 +26,24 @@ You can edit the initializer `config/initializers/carload.rb` for example:
26
26
 
27
27
  ```ruby
28
28
  Carload.setup do |config|
29
+ # Set the title that will be displayed on the browser tab area.
30
+ config.page.title = nil
31
+
32
+ # Set the footer text that will be displayed on each page.
33
+ config.page.footer = nil
34
+
35
+ # Set the colors of page elements.
36
+ config.page.main_color = nil
37
+ config.page.text_color = 'black'
38
+ config.page.button_color = nil
39
+ config.page.button_text_color = nil
40
+
29
41
  # Specify which authentication solution is used. Currently, we only support Devise.
30
42
  config.auth_solution = :devise
31
43
 
44
+ # Set which file upload solution is used. Currently, we only support Carrierwave.
45
+ config.upload_solution = :carrierwave
46
+
32
47
  # Set the actions used to discern user's permission to access dashboard.
33
48
  #
34
49
  # config.dashboard.permits_user.<method> = '...'
@@ -87,6 +102,8 @@ class Dashboard < Carload::Dashboard
87
102
  end
88
103
  ```
89
104
 
105
+ - You can run another generator `rails g carload:dash <model_name>` to generate the above content automatically (it may ask you some question).
106
+
90
107
  - Make sure you have the necessary I18n translation files, for example:
91
108
 
92
109
  ```yaml
@@ -15,3 +15,18 @@
15
15
  .select2-container {
16
16
  min-width: 174px;
17
17
  }
18
+
19
+ .select2-search__field {
20
+ line-height: 22px;
21
+ }
22
+ .select2-search__field:focus {
23
+ box-shadow: none !important;
24
+ }
25
+ .select2-selection__choice {
26
+ line-height: 22px;
27
+ background-color: $main-color !important;
28
+ color: white;
29
+ }
30
+ .select2-selection__choice__remove {
31
+ color: white !important;
32
+ }
@@ -0,0 +1,5 @@
1
+ $main-color: <%= Carload.page.main_color || '#8C7DA6' %>;
2
+ $text-color: <%= Carload.page.text_color || '#3F3F44' %>;
3
+ $button-color-1: <%= Carload.page.button_color_1 || Carload.page.button_color || '#60b044' %>;
4
+ $button-color-2: <%= Carload.page.button_color_2 || Carload.page.button_color || '#8add6d' %>;
5
+ $button-text-color: <%= Carload.page.button_text_color || 'white' %>;
@@ -51,19 +51,21 @@ th {
51
51
  }
52
52
 
53
53
  .btn-primary {
54
- color: #fff;
54
+ color: $button-text-color;
55
55
  text-shadow: 0 -1px 0 rgba(0,0,0,0.15);
56
- background-color: #60b044;
57
- background-image: -webkit-linear-gradient(#8add6d, #60b044);
58
- background-image: linear-gradient(#8add6d, #60b044);
59
- border-color: #5ca941;
56
+ background-color: $button-color-1;
57
+ background-image: -webkit-linear-gradient($button-color-2, $button-color-1);
58
+ background-image: linear-gradient($button-color-2, $button-color-1);
59
+ border: none;
60
+ height: 34px;
60
61
  }
61
62
  .btn-primary:hover {
62
- color: #fff;
63
- background-color: #569e3d;
64
- background-image: -webkit-linear-gradient(#79d858, #569e3d);
65
- background-image: linear-gradient(#79d858, #569e3d);
66
- border-color: #4a993e;
63
+ color: lighten($button-text-color, 20%);
64
+ background-color: lighten($button-color-1, 20%);
65
+ background-image: -webkit-linear-gradient(lighten($button-color-2, 20%), lighten($button-color-1, 20%));
66
+ background-image: linear-gradient(lighten($button-color-2, 20%), lighten($button-color-1, 20%));
67
+ border: none;
68
+ height: 34px;
67
69
  }
68
70
 
69
71
  .form-control:hover {
@@ -75,3 +77,7 @@ th {
75
77
  border-color: $main-color !important;
76
78
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px $main-color !important;
77
79
  }
80
+
81
+ .label-primary {
82
+ background-color: $main-color;
83
+ }
@@ -25,11 +25,13 @@ $half-spacing: 8px;
25
25
  border-top-right-radius: 0;
26
26
  }
27
27
  a {
28
+ color: $text-color;
28
29
  border-color: $main-color;
29
30
  }
30
31
  a:hover {
31
32
  background-color: lighten($main-color, 20%);
32
33
  border-color: lighten($main-color, 20%);
34
+ color: white;
33
35
  }
34
36
  a.active {
35
37
  background-color: $main-color;
@@ -64,6 +66,9 @@ $half-spacing: 8px;
64
66
  td {
65
67
  overflow: scroll;
66
68
  }
69
+ th:first-child, td:first-child {
70
+ padding-left: $spacing;
71
+ }
67
72
  th:last-child {
68
73
  width: 50px;
69
74
  }
@@ -78,22 +83,19 @@ $half-spacing: 8px;
78
83
  #dashboard-index-buttons {
79
84
  display: inline-block;
80
85
  margin-right: $spacing;
81
- }
82
- #new_q {
83
- display: inline-block;
86
+ line-height: 34px;
84
87
  }
85
88
 
86
- .search-fields {
87
- display: inline-flex;
88
- .form-group {
89
- display: inline-flex;
90
- margin-right: 5px;
91
- margin-bottom: 0;
89
+ #search-input {
90
+ position: relative;
91
+ .icon {
92
+ position: absolute;
93
+ bottom: 8px;
94
+ left: 8px;
95
+ color: #ccc;
92
96
  }
93
- .form-group:last-child {
94
- margin-right: 0;
97
+ input {
98
+ width: 100%;
99
+ padding-left: 30px;
95
100
  }
96
101
  }
97
- .search-buttons {
98
- display: inline-flex;
99
- }
@@ -8,14 +8,20 @@ module Carload
8
8
 
9
9
  before_action :set_model
10
10
  before_action :set_object, only: [:edit, :update, :destroy]
11
+ before_action :transform_polymorphic_params, only: [:create, :update]
12
+
11
13
  include Croppable
14
+ include ApplicationHelper
12
15
 
13
16
  def index
14
17
  authorize :carload_dashboard, :index? unless Carload.auth_solution == :none
15
- @search = @model_class.search(params[:q])
16
- @objects = @search.result.page(params[:page])
18
+ if params[:search].present?
19
+ @query = params[:search][:query]
20
+ @objects = @model_class.search(params[:search][:query]).page(params[:page])
21
+ else
22
+ @objects = @model_class.page(params[:page])
23
+ end
17
24
  @show_attributes = Dashboard.model(@model_name).index_page[:shows][:attributes] + [:created_at, :updated_at]
18
- @search_attributes = Dashboard.model(@model_name).index_page[:searches][:attributes]
19
25
  render "dashboard/#{@model_names}/index.html.erb"
20
26
  end
21
27
 
@@ -82,5 +88,15 @@ module Carload
82
88
  def rescue_unmanaged_model_error exception
83
89
  redirect_to dashboard_error_path(message: exception.message)
84
90
  end
91
+
92
+ def transform_polymorphic_params
93
+ polymorphic_params = {}
94
+ params[@model_name].each do |key, value|
95
+ if polymorphic? key
96
+ polymorphic_params["#{key}_id"], polymorphic_params["#{key}_type"] = value.split(',')
97
+ end
98
+ end
99
+ params[@model_name].merge! polymorphic_params
100
+ end
85
101
  end
86
102
  end
@@ -8,8 +8,24 @@ module Carload
8
8
  end
9
9
  end
10
10
 
11
+ def polymorphic? attribute_name
12
+ Dashboard.model(@model_name).associated_models.each_value do |associated_model|
13
+ return associated_model[:name] if attribute_name =~ /#{associated_model[:name]}/ and associated_model[:polymorphic]
14
+ end
15
+ false
16
+ end
17
+
11
18
  def image? attribute_name
12
19
  attribute_name.to_s =~ /image|logo|img/
13
20
  end
21
+
22
+ def id_or_ids associated_model
23
+ case associated_model[:association_type]
24
+ when :has_many
25
+ "#{associated_model[:name]}_ids"
26
+ else
27
+ "#{associated_model[:name]}_id"
28
+ end
29
+ end
14
30
  end
15
31
  end
@@ -1,59 +1,45 @@
1
1
  module Carload
2
2
  module DashboardHelper
3
- def generate_input form, model_name, attribute_name, column
4
- if Dashboard::ModelSpec.foreign_key? attribute_name
3
+ def generate_input form, model_name, attribute_name, options = {}
4
+ if options[:polymorphic]
5
+ form.input attribute_name,
6
+ collection: @model_class.send(attribute_name.to_s.pluralize),
7
+ selected: options[:value],
8
+ input_html: { class: 'use-select2' }
9
+ elsif attribute_name =~ /_id$/
5
10
  associated_model = attribute_name.gsub(/_id$/, '').to_sym
6
- options = Dashboard.model(model_name).associated_models[associated_model]
7
- label_attribute = options[:choose_by]
8
- if options[:polymorphic]
9
- forms = ''
10
- options[:available_models].each_with_index do |real_model, i|
11
- forms << form.input(attribute_name,
12
- label: t("activerecord.attributes.#{model_name}.#{real_model}.#{label_attribute}"),
13
- collection: real_model.camelize.constantize.all,
14
- label_method: label_attribute,
15
- value_method: :id,
16
- input_html: {
17
- class: 'use-select2',
18
- data: {
19
- placeholder: t('carload.placeholder.select', thing: t("activerecord.attributes.#{real_model}.#{label_attribute}"))
20
- }
21
- },
22
- wrapper_html: {
23
- id: "#{real_model.camelize}-#{label_attribute}"
24
- }
25
- )
26
- end
27
- # Add JavaScript to select which real model to work on.
28
- forms << <<-EOT
29
- <script>
30
- $('.#{model_name}_#{associated_model}_id').hide()
31
- if ($('##{model_name}_#{associated_model}_type').val() != '') {
32
- $('#' + $('##{model_name}_#{associated_model}_type').val() + '-#{label_attribute}').show()
33
- }
34
- $('##{model_name}_#{associated_model}_type').change(function() {
35
- $('.#{model_name}_#{associated_model}_id').hide()
36
- $('#' + $(this).val() + '-#{label_attribute}').show()
37
- $('.package_packagable_id > .select2-container').css('width', '100%')
38
- })
39
- </script>
40
- EOT
41
- raw forms.html_safe
42
- else
43
- form.association associated_model,
44
- label_method: label_attribute,
45
- label: t("activerecord.models.#{associated_model}"),
46
- input_html: {
47
- class: 'use-select2',
48
- data: {
49
- placeholder: t('carload.placeholder.select', thing: t("activerecord.attributes.#{associated_model}.#{label_attribute}"))
50
- }
11
+ association_specs = Dashboard.model(model_name).associated_models[associated_model]
12
+ label_attribute = association_specs[:choose_by]
13
+ form.association associated_model,
14
+ label_method: label_attribute,
15
+ label: t("activerecord.models.#{associated_model}"),
16
+ input_html: {
17
+ class: 'use-select2',
18
+ data: {
19
+ placeholder: t('carload.placeholder.select', thing: t("activerecord.attributes.#{associated_model}.#{label_attribute}"))
51
20
  }
52
- end
21
+ }
22
+ elsif attribute_name =~ /_ids$/
23
+ # Mandle many-to-many association.
24
+ associated_model = attribute_name.gsub(/_ids$/, '').to_sym
25
+ association_specs = Dashboard.model(model_name).associated_models[associated_model]
26
+ label_attribute = association_specs[:choose_by]
27
+ form.input attribute_name,
28
+ label: t("activerecord.attributes.#{associated_model}.#{label_attribute}") + " (#{t("activerecord.models.#{associated_model}")})",
29
+ collection: associated_model.to_s.camelize.constantize.all,
30
+ label_method: label_attribute,
31
+ value_method: :id,
32
+ input_html: {
33
+ class: 'use-select2',
34
+ multiple: true,
35
+ data: {
36
+ placeholder: t('carload.placeholder.select', thing: t("activerecord.attributes.#{associated_model}.#{label_attribute}"))
37
+ }
38
+ }
53
39
  elsif attribute_name =~ /_type$/
54
40
  associated_model = attribute_name.gsub(/_type$/, '').to_sym
55
- options = Dashboard.model(model_name).associated_models[associated_model]
56
- form.input attribute_name, collection: options[:available_models].map(&:camelize),
41
+ association_specs = Dashboard.model(model_name).associated_models[associated_model]
42
+ form.input attribute_name, collection: association_specs[:instance_models].map{ |x| x.to_s.camelize },
57
43
  input_html: {
58
44
  class: 'use-select2',
59
45
  data: {
@@ -67,20 +53,29 @@ module Carload
67
53
  end
68
54
  end
69
55
 
70
- def generate_search_input form, model_name, attribute
71
- if attribute[:options]
72
- form.input "#{attribute[:name].to_s.gsub('.', '_')}_#{attribute[:term]}",
73
- required: false, label: false, collection: attribute[:options],
74
- input_html: {
75
- class: 'use-select2',
76
- data: {
77
- placeholder: t('carload.placeholder.select', thing: t("activerecord.attributes.#{model_name}.#{attribute[:name]}"))
78
- }
79
- }
80
- else
81
- form.input "#{attribute[:name].to_s.gsub('.', '_')}_#{attribute[:term]}",
82
- placeholder: t("activerecord.attributes.#{@model_name}.#{attribute[:name]}"),
83
- required: false, label: false
56
+ def generate_show_title attribute
57
+ case attribute
58
+ when Symbol
59
+ begin
60
+ t("activerecord.attributes.#{@model_name}.#{attribute}", raise: true)
61
+ rescue
62
+ t("carload.activerecord.#{attribute}", raise: true)
63
+ end
64
+ when String
65
+ begin
66
+ t("activerecord.attributes.#{@model_name}.#{attribute}", raise: true)
67
+ rescue
68
+ "#{t("activerecord.attributes.#{attribute}", raise: true)} (#{t("activerecord.models.#{attribute.split('.').first.to_s.singularize}", raise: true)})"
69
+ end
70
+ when Array
71
+ if attribute.first == :pluck
72
+ raise UnsupportedError.new("attribute #{attribute}") if attribute.size != 3
73
+ model_name = attribute[1].to_s.singularize
74
+ attribute_name = attribute[2]
75
+ "#{t("activerecord.attributes.#{model_name}.#{attribute_name}", raise: true)} (#{t("activerecord.models.#{model_name}", raise: true)})"
76
+ else
77
+ "#{t("activerecord.attributes.#{attribute.join('.')}", raise: true)} (#{t("activerecord.models.#{attribute[0].to_s.singularize}", raise: true)})"
78
+ end
84
79
  end
85
80
  end
86
81
 
@@ -89,7 +84,20 @@ module Carload
89
84
  when Symbol
90
85
  object.send attribute
91
86
  when String
92
- eval "object.#{attribute.gsub('.', '&.')}"
87
+ res = eval "object.#{attribute.gsub('.', '&.')}"
88
+ case res
89
+ when String
90
+ res
91
+ when Array
92
+ raw res.map { |x| "<span class='label label-primary'>#{x}</span>" }.join(' ')
93
+ end
94
+ when Array
95
+ if attribute.first == :pluck
96
+ raise UnsupportedError.new("attribute #{attribute}") if attribute.size != 3
97
+ generate_show object, "#{attribute[1].to_s.pluralize}.pluck(:#{attribute[2]})"
98
+ else
99
+ generate_show object, attribute.join('.')
100
+ end
93
101
  end
94
102
  end
95
103
  end
@@ -1,7 +1,19 @@
1
1
  <%= simple_form_for @object, url: "/carload/dashboard/#{@model_names}/#{@object.id}" do |f| %>
2
+ <!-- Normal attributes -->
2
3
  <% @model_class.columns_hash.each do |name, column| %>
3
- <% next if Dashboard::ModelSpec::SkippedAttributes.include? name %>
4
- <%= generate_input(f, @model_name, name, column) rescue nil %>
4
+ <% next if Carload::ModelSpec::SkippedAttributes.include? name or polymorphic? name %>
5
+ <%= generate_input f, @model_name, name %>
6
+ <% end %>
7
+ <!-- Polymorphics -->
8
+ <% Dashboard.model(@model_name).associated_models.each_value do |associated_model| %>
9
+ <% next if associated_model[:polymorphic] != true %>
10
+ <%= generate_input f, @model_name, associated_model[:name], polymorphic: true,
11
+ value: "#{@object.send("#{associated_model[:name]}_id")},#{@object.send("#{associated_model[:name]}_type")}" %>
12
+ <% end %>
13
+ <!-- Join tables -->
14
+ <% Dashboard.model(@model_name).associated_models.each_value do |associated_model| %>
15
+ <% next if not associated_model[:join_table] %>
16
+ <%= generate_input f, @model_name, id_or_ids(associated_model) %>
5
17
  <% end %>
6
18
  <%= f.button :submit, t('carload.action.submit'), class: 'btn btn-primary' %>
7
19
  <% end %>
@@ -9,13 +9,7 @@
9
9
  <tr>
10
10
  <th width='20'>ID</th>
11
11
  <% @show_attributes.each do |attribute| %>
12
- <th width='100'>
13
- <% begin %>
14
- <%= t("activerecord.attributes.#{@model_name}.#{attribute}", raise: true) %>
15
- <% rescue %>
16
- <%= t("carload.activerecord.#{attribute}") %>
17
- <% end %>
18
- </th>
12
+ <th width='100'><%= generate_show_title attribute rescue attribute %></th>
19
13
  <% end %>
20
14
  <th width='20'><%= t('carload.actions') %></th>
21
15
  </tr>
@@ -1,10 +1,6 @@
1
- <%= simple_form_for @search, url: dashboard_search_path(@model_names), method: :post do |f| %>
2
- <div class='search-fields'>
3
- <% @search_attributes.each do |attribute| %>
4
- <%= generate_search_input f, @model_name, attribute %>
5
- <% end %>
6
- </div>
7
- <div class='search-buttons'>
8
- <%= f.submit t('carload.action.search'), class: 'btn btn-primary' %>
9
- </div>
10
- <% end %>
1
+ <div id='search-input'>
2
+ <%= form_for :search, url: dashboard_search_path(@model_names), method: :get do |f| %>
3
+ <span class='icon'><%= fa_icon('search') %></span>
4
+ <%= f.text_field :query, class: 'form-control', placeholder: 'Search ...', value: @query %>
5
+ <% end %>
6
+ </div>
@@ -1,7 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title>Carload</title>
4
+ <title><%= Carload.page.title || 'Carload' %></title>
5
5
  <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'/>
6
6
  <%= stylesheet_link_tag 'carload/dashboard', media: 'all' %>
7
7
  <%= javascript_include_tag 'carload/dashboard' %>
@@ -25,7 +25,7 @@
25
25
  </div>
26
26
 
27
27
  <footer class='center'>
28
- <%= raw t('carload.footer.message') %>
28
+ <%= raw Carload.page.footer || t('carload.footer.message') %>
29
29
  </footer>
30
30
  </div>
31
31
 
@@ -0,0 +1,7 @@
1
+ class CarloadEnableZhparserExtension < ActiveRecord::Migration[5.0]
2
+ def change
3
+ enable_extension :zhparser
4
+ execute 'create text search configuration zhparser (parser = zhparser);'
5
+ execute 'alter text search configuration zhparser add mapping for n,v,a,i,e,l with simple;'
6
+ end
7
+ end
@@ -0,0 +1,52 @@
1
+ module Carload
2
+ module AssociationPipelines
3
+ def association_pipelines
4
+ [ :pipeline_1, :pipeline_2, :pipeline_3 ]
5
+ end
6
+
7
+ # Find polymorphic instance models.
8
+ def pipeline_1 association
9
+ return unless association.options[:polymorphic]
10
+ associated_model = @associated_models[association.name]
11
+ ActiveRecord::Base.descendants.each do |model|
12
+ next if model.name == 'ApplicationRecord' or model.name.underscore == @name.to_s
13
+ model.reflect_on_all_associations.each do |model_association|
14
+ next unless model_association.options[:as] == association.name
15
+ associated_model[:instance_models] ||= []
16
+ associated_model[:instance_models] << model.name.underscore.to_sym
17
+ if not associated_model[:attributes]
18
+ associated_model[:attributes] = model.column_names - ModelSpec::SkippedAttributes
19
+ else
20
+ associated_model[:attributes] = associated_model[:attributes] & model.column_names
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ # Add possible attributes to let user choose.
27
+ def pipeline_2 association
28
+ return unless associated_model = @associated_models[association.name.to_s.singularize.to_sym]
29
+ model = association.name.to_s.singularize.camelize.constantize rescue return
30
+ associated_model[:attributes] ||= []
31
+ associated_model[:attributes] = model.column_names - ModelSpec::SkippedAttributes
32
+ associated_model[:attributes] = associated_model[:attributes] - [
33
+ "#{@name}_id",
34
+ "#{associated_model[:polymorphic]}_id",
35
+ "#{associated_model[:polymorphic]}_type"
36
+ ]
37
+ end
38
+
39
+ # Add has-many permitted attribute.
40
+ def pipeline_3 association
41
+ _association = (association.delegate_reflection rescue nil) || association
42
+ return unless _association.class == ActiveRecord::Reflection::HasManyReflection
43
+ # Exclude join table.
44
+ return unless @klass.reflect_on_all_associations.select { |x| x.options[:through] == _association.name }.empty?
45
+ @attributes[:permitted].each do |permitted|
46
+ next unless permitted.class == Hash
47
+ return if permitted.keys.first == :"#{_association.class_name.underscore}_ids"
48
+ end
49
+ @attributes[:permitted] << { :"#{_association.class_name.underscore}_ids" => [] }
50
+ end
51
+ end
52
+ end
@@ -1,112 +1,12 @@
1
1
  module Carload
2
2
  class Dashboard
3
- class ModelSpec
4
- attr_accessor :default, :attributes, :index_page, :associated_models
5
-
6
- SkippedAttributes = [
7
- 'id', 'created_at', 'updated_at',
8
- 'encrypted_password', 'reset_password_token',
9
- 'reset_password_sent_at', 'remember_created_at',
10
- 'sign_in_count', 'current_sign_in_at',
11
- 'last_sign_in_at', 'current_sign_in_ip',
12
- 'last_sign_in_ip'
13
- ].freeze
14
-
15
- def self.foreign_key? attribute
16
- attribute =~ /_id$/
17
- end
18
-
19
- def foreign_key? attribute
20
- ModelSpec.foreign_key? attribute
21
- end
22
-
23
- def self.polymorphic? model_class, attribute
24
- return false unless foreign_key? attribute
25
- model_class.column_names.include? "#{attribute.to_s.gsub('_id', '')}_type"
26
- end
27
-
28
- def polymorphic? model_class, attribute
29
- ModelSpec.polymorphic? model_class, attribute
30
- end
31
-
32
- def initialize model_class = nil
33
- @default = false
34
- @attributes = ExtendedHash.new
35
- @index_page = ExtendedHash.new
36
- @index_page[:shows] = ExtendedHash.new
37
- @index_page[:searches] = ExtendedHash.new
38
- @associated_models = {}
39
- if model_class
40
- @attributes[:permitted] = model_class.column_names - SkippedAttributes
41
- @attributes[:permitted].each do |attribute|
42
- @index_page[:shows][:attributes] ||= []
43
- @index_page[:searches][:attributes] ||= []
44
- if foreign_key? attribute
45
- associated_model = attribute.gsub('_id', '')
46
- @associated_models[associated_model] = {
47
- choose_by: nil, # Wait for setting.
48
- polymorphic: polymorphic?(model_class, attribute),
49
- model: model_class.name.underscore.to_sym
50
- }
51
- else
52
- @index_page[:shows][:attributes] << attribute
53
- @index_page[:searches][:attributes] << { name: attribute.to_sym, term: :cont }
54
- end
55
- end
56
- end
57
- end
58
-
59
- def changed? spec
60
- not @attributes[:permitted] == spec.attributes[:permitted] or
61
- not @index_page[:searches][:attributes] == spec.index_page[:searches][:attributes]
62
- end
63
-
64
- def revise_stage_1!
65
- # Handle polymorphic associated models if necessary.
66
- @associated_models.each do |associated_model, options|
67
- next if not options or not options[:polymorphic]
68
- ActiveRecord::Base.descendants.each do |model|
69
- next if model.name == 'ApplicationRecord'
70
- if not model.reflect_on_all_associations.map(&:name).select { |x| x.to_s =~ /\b(#{options[:model]}|#{options[:model].to_s.pluralize})\b/ }.empty?
71
- options[:available_models] ||= []
72
- options[:available_models] << model.name.underscore
73
- if not options[:common_attributes]
74
- options[:common_attributes] = model.column_names - SkippedAttributes
75
- else
76
- options[:common_attributes] = options[:common_attributes] & model.column_names
77
- end
78
- end
79
- end
80
- end
81
- end
82
-
83
- def revise_stage_2!
84
- # Handle associated models if necessary.
85
- @associated_models.each do |associated_model, options|
86
- next if not options
87
- if options[:polymorphic]
88
- # There should be a <associated_model>_type already.
89
- @index_page[:shows][:attributes] << "#{associated_model}.#{options[:choose_by]}"
90
- @index_page[:searches][:attributes].each do |attribute|
91
- next unless attribute[:name] == :"#{associated_model}_type"
92
- attribute[:options] = options[:available_models]
93
- end
94
- else
95
- @index_page[:searches][:attributes] << {
96
- name: "#{associated_model}.#{options[:choose_by]}",
97
- term: :cont
98
- }
99
- end
100
- end
101
- end
102
- end
103
-
104
3
  class << self
105
4
  def model name, &block
106
5
  name = name.to_sym
107
6
  if block_given?
108
7
  @@models ||= {}
109
8
  spec = @@models[name] || ModelSpec.new
9
+ spec.name = name
110
10
  yield spec
111
11
  @@models[name] = spec
112
12
  else
@@ -118,12 +18,9 @@ module Carload
118
18
  model_a = options.keys.first
119
19
  model_b = options.values.first
120
20
  options.shift
21
+ options[:name] = model_b
121
22
  @@models[model_a] ||= ModelSpec.new
122
23
  @@models[model_a].associated_models[model_b] = options
123
- unless options[:polymorphic]
124
- @@models[model_b] ||= ModelSpec.new
125
- @@models[model_b].associated_models[model_a] = nil
126
- end
127
24
  end
128
25
 
129
26
  def models
@@ -153,10 +50,10 @@ module Carload
153
50
  RUBY
154
51
  default = false
155
52
  next if spec.associated_models.empty?
156
- spec.associated_models.each do |associated_model, options|
157
- next if not options.class == Hash or not options[:choose_by]
53
+ spec.associated_models.each_value do |associated_model|
54
+ next unless associated_model[:choose_by]
158
55
  content << <<-RUBY
159
- associate(#{{ name.to_sym => associated_model.to_sym }.merge options})
56
+ associate(#{{ name.to_sym => associated_model[:name], choose_by: associated_model[:choose_by].to_sym }})
160
57
  RUBY
161
58
  end
162
59
  end
@@ -8,5 +8,54 @@ module Carload
8
8
  require_dependency file
9
9
  end
10
10
  end
11
+
12
+ config.after_initialize do
13
+ # Fill up associations of models.
14
+ Dashboard.models.each do |name, spec|
15
+ spec.klass = name.to_s.camelize.constantize
16
+ spec.klass.reflect_on_all_associations.each do |association|
17
+ spec.handle_association association, rescue: true
18
+ end
19
+ end
20
+ # Reopen model classes to add pg text search.
21
+ Dictionaries = {
22
+ en: 'english',
23
+ :'zh-CN' => 'zhparser'
24
+ }.freeze
25
+ Dashboard.models.each do |name, spec|
26
+ # NOTE: Only direct columns are included.
27
+ attributes = spec.index_page.searches.attributes.select { |x| x[:name].class == Symbol }.map { |x| x[:name] }
28
+ spec.klass.class_eval do
29
+ include PgSearch
30
+ pg_search_scope :search,
31
+ against: attributes,
32
+ using: {
33
+ tsearch: {
34
+ prefix: true,
35
+ negation: true,
36
+ dictionary: Dictionaries[I18n.locale]
37
+ }
38
+ }
39
+ end
40
+ end
41
+ # Reopen model classes to handle polymorphic association.
42
+ Dashboard.models.each do |name, spec|
43
+ spec.associated_models.values.select { |x| x[:polymorphic] == true }.each do |associated_model|
44
+ polymorphic_objects = []
45
+ associated_model[:instance_models].each do |instance_model|
46
+ Dashboard.model(instance_model).klass.all.each do |object|
47
+ polymorphic_objects << ["#{object.class} - #{object.send(associated_model[:choose_by])}", "#{object.id},#{object.class}"]
48
+ end
49
+ end
50
+ spec.klass.class_eval do
51
+ class_eval <<-RUBY
52
+ def self.#{associated_model[:name].to_s.pluralize}
53
+ #{polymorphic_objects}
54
+ end
55
+ RUBY
56
+ end
57
+ end
58
+ end
59
+ end
11
60
  end
12
61
  end
@@ -0,0 +1,98 @@
1
+ module Carload
2
+ class ModelSpec
3
+ include AssociationPipelines
4
+
5
+ attr_accessor :name, :klass, :default, :attributes, :index_page, :associated_models
6
+
7
+ SkippedAttributes = [
8
+ 'id', 'created_at', 'updated_at',
9
+ 'encrypted_password', 'reset_password_token',
10
+ 'reset_password_sent_at', 'remember_created_at',
11
+ 'sign_in_count', 'current_sign_in_at',
12
+ 'last_sign_in_at', 'current_sign_in_ip',
13
+ 'last_sign_in_ip'
14
+ ].freeze
15
+
16
+ AssociationTypes = {
17
+ ActiveRecord::Reflection::BelongsToReflection => :belongs_to,
18
+ ActiveRecord::Reflection::HasOneReflection => :has_one,
19
+ ActiveRecord::Reflection::HasManyReflection => :has_many,
20
+ ActiveRecord::Reflection::ThroughReflection => :through
21
+ }
22
+
23
+ def initialize model_class = nil
24
+ @default = false
25
+ @attributes = ExtendedHash.new
26
+ @index_page = ExtendedHash.new
27
+ @index_page[:shows] = ExtendedHash.new
28
+ @index_page[:shows][:attributes] ||= []
29
+ @index_page[:searches] = ExtendedHash.new
30
+ @index_page[:searches][:attributes] ||= []
31
+ @associated_models = {}
32
+ if model_class
33
+ @name = model_class.name.underscore
34
+ @klass = model_class
35
+ @attributes[:permitted] = (model_class.column_names - SkippedAttributes).map(&:to_sym)
36
+ # Handle model associations.
37
+ model_class.reflect_on_all_associations.each do |association|
38
+ handle_association association
39
+ end
40
+ @attributes[:permitted].each do |attribute|
41
+ next if attribute.class == Hash
42
+ @index_page[:shows][:attributes] << attribute
43
+ @index_page[:searches][:attributes] << { name: attribute.to_sym, term: :cont }
44
+ end
45
+ end
46
+ end
47
+
48
+ def changed? spec
49
+ not @default == spec.default or
50
+ not @attributes[:permitted] == spec.attributes[:permitted] or
51
+ not @index_page[:shows][:attributes] == spec.index_page[:shows][:attributes] or
52
+ not @index_page[:searches][:attributes] == spec.index_page[:searches][:attributes]
53
+ end
54
+
55
+ def revise!
56
+ # Handle associated models if necessary.
57
+ @associated_models.each_value do |associated_model|
58
+ next unless associated_model[:choose_by]
59
+ if associated_model[:association_type] == :has_many
60
+ show_name = [:pluck, associated_model[:name].to_s.pluralize.to_sym, associated_model[:choose_by]]
61
+ else
62
+ show_name = "#{associated_model[:name]}.#{associated_model[:choose_by]}"
63
+ end
64
+ @index_page[:shows][:attributes].delete_if { |x| x == :"#{associated_model[:name]}_id" }
65
+ @index_page[:shows][:attributes] << show_name
66
+ end
67
+ end
68
+
69
+ def handle_association association, options = {}
70
+ begin
71
+ _association = (association.delegate_reflection rescue nil) || association
72
+ name = (_association.klass.name.underscore.to_sym rescue nil) || _association.name
73
+ association_type = AssociationTypes[_association.class]
74
+ polymorphic = association.options[:polymorphic] || association.options[:as]
75
+ foreign_key = @klass.column_names.include?("#{(_association.klass.name.underscore rescue nil) || _association.name}_id")
76
+ join_table = association.options[:through].to_s.singularize.to_sym if association.options[:through]
77
+ @associated_models[name] = {
78
+ name: name,
79
+ association_type: association_type,
80
+ polymorphic: polymorphic,
81
+ foreign_key: foreign_key,
82
+ join_table: join_table,
83
+ choose_by: nil
84
+ }.merge @associated_models[name] || {}
85
+ association_pipelines.each { |pipeline| send pipeline, association }
86
+ # Delete join-table model!
87
+ if association.options[:through]
88
+ @associated_models.delete association.options[:through]
89
+ @associated_models.delete association.options[:through].to_s.singularize.to_sym
90
+ end
91
+ rescue => e
92
+ raise e unless options[:rescue]
93
+ raise e if not e&.original_exception&.class == PG::UndefinedTable and
94
+ not e.class == ActiveRecord::NoDatabaseError
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,3 +1,3 @@
1
1
  module Carload
2
- VERSION = '0.3.0'
2
+ VERSION = '0.4.0'
3
3
  end
data/lib/carload.rb CHANGED
@@ -4,13 +4,17 @@ Gem.loaded_specs['carload'].dependencies.each do |dependency|
4
4
  end
5
5
 
6
6
  require 'carload/extended_hash'
7
+ require 'carload/association_pipelines'
8
+ require 'carload/model_spec'
7
9
  require 'carload/dashboard'
8
10
  require 'carload/engine'
9
11
  require 'carload/exceptions'
10
12
 
11
13
  module Carload
12
14
  def self.setup &block
15
+ # Read in configuration.
13
16
  @@config = ExtendedHash.new
17
+ @@config[:page] = ExtendedHash.new
14
18
  @@config[:dashboard] = ExtendedHash.new
15
19
  @@config[:dashboard][:permits_user] = ExtendedHash.new
16
20
  yield @@config
@@ -20,7 +24,7 @@ module Carload
20
24
  if not [:carrierwave].include? @@config[:upload_solution]
21
25
  raise UnsupportedError.new("upload solution #{@@config[:upload_solution]}")
22
26
  end
23
-
27
+ # Define configuation helpers.
24
28
  @@config.each do |key, value|
25
29
  define_singleton_method key do
26
30
  value
@@ -8,34 +8,35 @@ module Carload
8
8
  model_specs = {}
9
9
  Rails.application.eager_load! # It is necessary to load models manually.
10
10
  ActiveRecord::Base.descendants.each do |model| # Rails 5 can use ApplicationRecord.
11
- next if model.name == 'ApplicationRecord'
11
+ next if model.name == 'ApplicationRecord' or model.name == 'PgSearch::Document'
12
12
  name = model.name.underscore
13
- model_specs[name] = Dashboard::ModelSpec.new model
13
+ model_specs[name] = ModelSpec.new model
14
14
  end
15
15
  spec = model_specs[model]
16
16
  if not spec.associated_models.empty?
17
- spec.revise_stage_1!
18
17
  cli = HighLine.new
19
- cli.say "\nModel #{model} has associated with other models."
20
- spec.associated_models.each do |associated_model, options|
21
- spec.associated_models[associated_model][:choose_by] = cli.choose do |menu|
22
- menu.prompt = "Choose the attribute of model #{associated_model} for choosing in #{model}? "
23
- attributes = options[:polymorphic] ? options[:common_attributes] : model_specs[associated_model].attributes.permitted
24
- attributes.each do |attribute|
25
- next if attribute =~ /_id$/
26
- menu.choice attribute.to_sym
18
+ spec.associated_models.each_value do |associated_model|
19
+ next if associated_model[:attributes].empty?
20
+ if associated_model[:polymorphic]
21
+ attributes = associated_model[:attributes]
22
+ else
23
+ attributes = model_specs[associated_model[:name].to_s].attributes.permitted.select { |x| x.class != Hash }
24
+ end
25
+ if attributes.size == 1
26
+ associated_model[:choose_by] = attributes.first
27
+ else
28
+ associated_model[:choose_by] = cli.choose do |menu|
29
+ menu.prompt = "Choose the attribute of model #{associated_model[:name]} for choosing in #{model}? "
30
+ attributes.each do |attribute|
31
+ next if attribute.to_s =~ /_id$/
32
+ menu.choice attribute
33
+ end
27
34
  end
28
35
  end
29
36
  end
30
- spec.revise_stage_2!
37
+ spec.revise!
31
38
  end
32
39
  # Check if model exists in dashboard file, but it may be changed.
33
- begin
34
- load 'app/carload/dashboard.rb'
35
- rescue LoadError
36
- Dashboard.models[model.to_sym] = spec
37
- Dashboard.write 'app/carload/dashboard.rb'
38
- end
39
40
  if not Dashboard.models.keys.include? model.to_sym or Dashboard.model(model).changed? spec
40
41
  Dashboard.models[model.to_sym] = spec
41
42
  Dashboard.write 'app/carload/dashboard.rb'
@@ -26,5 +26,15 @@ require 'carload'
26
26
  def copy_dashboard_file
27
27
  copy_file 'dashboard.rb', 'app/carload/dashboard.rb'
28
28
  end
29
+
30
+ def copy_migration_files
31
+ # Copy migrations if necessary.
32
+ case I18n.locale
33
+ when :'zh-CN'
34
+ ['20161030074822_carload_enable_zhparser_extension.rb'].each do |file|
35
+ copy_file "#{Carload::Engine.root}/db/migrate/#{file}", "db/migrate/#{file}"
36
+ end
37
+ end
38
+ end
29
39
  end
30
40
  end
@@ -1,8 +1,20 @@
1
1
  Carload.setup do |config|
2
- # Specify which authentication solution is used. Currently, we only support Devise.
2
+ # Set the title that will be displayed on the browser tab area.
3
+ config.page.title = nil
4
+
5
+ # Set the footer text that will be displayed on each page.
6
+ config.page.footer = nil
7
+
8
+ # Set the colors of page elements.
9
+ config.page.main_color = nil
10
+ config.page.text_color = 'black'
11
+ config.page.button_color = nil
12
+ config.page.button_text_color = nil
13
+
14
+ # Set which authentication solution is used. Currently, we only support Devise.
3
15
  config.auth_solution = :devise
4
16
 
5
- # Specify which file upload solution is used. Currently, we only support Carrierwave.
17
+ # Set which file upload solution is used. Currently, we only support Carrierwave.
6
18
  config.upload_solution = :carrierwave
7
19
 
8
20
  # Set the actions used to discern user's permission to access dashboard.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carload
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Li Dong
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-05 00:00:00.000000000 Z
11
+ date: 2016-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -129,19 +129,19 @@ dependencies:
129
129
  - !ruby/object:Gem::Version
130
130
  version: '3.3'
131
131
  - !ruby/object:Gem::Dependency
132
- name: ransack
132
+ name: pg_search
133
133
  requirement: !ruby/object:Gem::Requirement
134
134
  requirements:
135
135
  - - "~>"
136
136
  - !ruby/object:Gem::Version
137
- version: '1.8'
137
+ version: '1.0'
138
138
  type: :runtime
139
139
  prerelease: false
140
140
  version_requirements: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - "~>"
143
143
  - !ruby/object:Gem::Version
144
- version: '1.8'
144
+ version: '1.0'
145
145
  - !ruby/object:Gem::Dependency
146
146
  name: kaminari
147
147
  requirement: !ruby/object:Gem::Requirement
@@ -227,7 +227,7 @@ files:
227
227
  - app/assets/javascripts/carload/dashboard.js
228
228
  - app/assets/stylesheets/carload/_select2.scss
229
229
  - app/assets/stylesheets/carload/application.scss
230
- - app/assets/stylesheets/carload/colors.scss
230
+ - app/assets/stylesheets/carload/colors.scss.erb
231
231
  - app/assets/stylesheets/carload/common.scss
232
232
  - app/assets/stylesheets/carload/dashboard.scss
233
233
  - app/assets/stylesheets/carload/error.scss
@@ -264,11 +264,14 @@ files:
264
264
  - config/locales/simple_form.en.yml
265
265
  - config/locales/zh-CN.yml
266
266
  - config/routes.rb
267
+ - db/migrate/20161030074822_carload_enable_zhparser_extension.rb
267
268
  - lib/carload.rb
269
+ - lib/carload/association_pipelines.rb
268
270
  - lib/carload/dashboard.rb
269
271
  - lib/carload/engine.rb
270
272
  - lib/carload/exceptions.rb
271
273
  - lib/carload/extended_hash.rb
274
+ - lib/carload/model_spec.rb
272
275
  - lib/carload/version.rb
273
276
  - lib/generators/carload/USAGE
274
277
  - lib/generators/carload/dash_generator.rb
@@ -1,2 +0,0 @@
1
- $main-color: #8C7DA6;
2
- $text-color: #3F3F44;