slash_admin 0.1.0 → 1.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -12
  3. data/app/assets/images/slash_admin/favicon.png +0 -0
  4. data/app/assets/images/slash_admin/logo.png +0 -0
  5. data/app/assets/javascripts/slash_admin/application.js +33 -25
  6. data/app/assets/stylesheets/slash_admin/application.scss +271 -146
  7. data/app/assets/stylesheets/slash_admin/colors.scss +16 -13
  8. data/app/controllers/slash_admin/models_controller.rb +166 -32
  9. data/app/controllers/slash_admin/security/sessions_controller.rb +1 -1
  10. data/app/controllers/slash_admin/selectize_controller.rb +21 -1
  11. data/app/helpers/slash_admin/application_helper.rb +45 -4
  12. data/app/helpers/slash_admin/widgets_helper.rb +1 -0
  13. data/app/views/layouts/slash_admin/application.html.erb +1 -1
  14. data/app/views/slash_admin/base/_data_belongs_to.html.erb +14 -16
  15. data/app/views/slash_admin/base/_data_list.html.erb +118 -121
  16. data/app/views/slash_admin/base/_data_nestable.html.erb +2 -2
  17. data/app/views/slash_admin/base/_data_new.html.erb +4 -1
  18. data/app/views/slash_admin/base/_filters.html.erb +63 -55
  19. data/app/views/slash_admin/base/index.html.erb +3 -3
  20. data/app/views/slash_admin/custom_fields/_google_map.html.erb +14 -11
  21. data/app/views/slash_admin/custom_fields/_select.html.erb +5 -1
  22. data/app/views/slash_admin/custom_fields/_timezone.html.erb +2 -0
  23. data/app/views/slash_admin/dashboard/widgets/_statistic_progress_tile.html.erb +1 -1
  24. data/app/views/slash_admin/fields/_belongs_to.html.erb +2 -2
  25. data/app/views/slash_admin/fields/_carrierwave.html.erb +107 -10
  26. data/app/views/slash_admin/fields/_has_many.html.erb +2 -2
  27. data/app/views/slash_admin/fields/_has_one.html.erb +9 -24
  28. data/app/views/slash_admin/fields/_json.html.erb +1 -1
  29. data/app/views/slash_admin/fields/_jsonb.html.erb +14 -0
  30. data/app/views/slash_admin/{custom_fields → fields}/_nested_belongs_to.html.erb +11 -10
  31. data/app/views/slash_admin/fields/_nested_has_one.html.erb +23 -0
  32. data/app/views/slash_admin/security/sessions/new.html.erb +6 -3
  33. data/app/views/slash_admin/shared/_batch_actions.html.erb +1 -1
  34. data/app/views/slash_admin/shared/_header.html.erb +2 -3
  35. data/app/views/slash_admin/shared/_menu.html.erb +33 -30
  36. data/app/views/slash_admin/shared/_new_form_buttons.html.erb +2 -2
  37. data/config/initializers/validators.rb +4 -0
  38. data/config/locales/{slash_admin.en.yml → en.yml} +3 -0
  39. data/config/locales/{slash_admin.fr.yml → fr.yml} +4 -1
  40. data/lib/batch_translation.rb +1 -1
  41. data/lib/generators/slash_admin/override_admin/override_admin_generator.rb +1 -0
  42. data/lib/generators/slash_admin/override_session/templates/session.erb +2 -2
  43. data/lib/slash_admin.rb +1 -3
  44. data/lib/slash_admin/version.rb +1 -1
  45. data/vendor/assets/javascripts/bootstrap-datepicker.fr.min.js +1 -0
  46. data/vendor/assets/javascripts/bootstrap-datepicker.min.js +8 -0
  47. data/vendor/assets/stylesheets/bootstrap-datepicker.min.css +7 -0
  48. metadata +19 -69
@@ -1,17 +1,20 @@
1
1
  // Bootstrap override
2
- $blue: #659be0 !default;
3
- $yellow: #f1c40f !default;
4
- $green: #36c6d3 !default;
5
- $red: #ed6b75 !default;
2
+ $blue: #6798ff !default;
3
+ $yellow: #fad961 !default;
4
+ $green: #57ca85 !default;
5
+ $red: #F36265 !default;
6
+ $pink: #ff3466 !default;
6
7
 
7
- $success: #26c281;
8
- $info: #4b77be;
9
- $error: #e7505a;
10
- $notice: #f7ca18;
8
+ $info: #6798ff;
9
+ $notice: #fad961;
10
+ $success: #57ca85;
11
+ $error: #F36265;
11
12
 
12
- // Dasboard
13
- $primary: #18cdc4;
14
- $secondary: #26344b;
15
- $tertiary: #eef1f5;
16
- $grey: #666;
13
+ $primary: #2e5bff;
14
+ $blueIcon: #9ddbff;
15
+ $secondary: #120731;
16
+ $sidebar: #33375b;
17
+ $textColor: #c5a6ee;
18
+ $tertiary: #f8f8fd;
19
+ $grey: #7985A6;
17
20
  $lightgrey: #e1e5ec;
@@ -2,6 +2,7 @@ require 'csv'
2
2
 
3
3
  module SlashAdmin
4
4
  class ModelsController < SlashAdmin::BaseController
5
+ skip_before_action :verify_authenticity_token, only: :nestable
5
6
  before_action :handle_internal_default
6
7
  before_action :handle_default
7
8
  before_action :nestable_config
@@ -21,7 +22,16 @@ module SlashAdmin
21
22
  column = @model_class.arel_table[params[:order_field].to_sym]
22
23
  order = params[:order].downcase
23
24
  if %w(asc desc).include?(order)
24
- @models = @models_export.order(column.send(params[:order].downcase)).page(params[:page]).per(params[:per])
25
+ if @models_export.is_a? Array
26
+ if order == 'asc'
27
+ @models = @models_export.sort { |m1, m2| m1.send(params[:order_field]) <=> m2.send(params[:order_field]) }
28
+ else
29
+ @models = @models_export.sort { |m1, m2| m2.send(params[:order_field]) <=> m1.send(params[:order_field]) }
30
+ end
31
+ @models = Kaminari.paginate_array(@models).page(params[:page]).per(params[:per])
32
+ else
33
+ @models = @models_export.order(column.send(params[:order].downcase)).page(params[:page]).per(params[:per])
34
+ end
25
35
  end
26
36
 
27
37
  @fields = if @use_export_params
@@ -32,8 +42,8 @@ module SlashAdmin
32
42
 
33
43
  respond_to do |format|
34
44
  format.html
35
- format.csv { send_data export_csv.encode('iso-8859-1'), filename: "#{@model_name.pluralize.upcase}_#{Date.today}.csv", type: 'text/csv; charset=iso-8859-1; header=present' }
36
- format.xls { send_data render_to_string, filename: "#{@model_name.pluralize.upcase}_#{Date.today}.xls" }
45
+ format.csv { stream_csv_report }
46
+ format.xls { send_data render_to_string, filename: "#{@model_class.model_name.plural.upcase}_#{Date.today}.xls" }
37
47
  format.js { @models }
38
48
  end
39
49
  end
@@ -44,25 +54,31 @@ module SlashAdmin
44
54
  end
45
55
 
46
56
  def before_validate_on_create; end
57
+ def after_save_on_create; end
47
58
  def create
48
59
  authorize! :new, @model_class
60
+ handle_has_one
61
+
49
62
  @model = @model_class.new(permit_params)
50
63
 
51
64
  before_validate_on_create
65
+ handle_specific_fields
52
66
 
53
67
  if @model.valid?
54
68
  if @model.save!
69
+ after_save_on_create
55
70
  respond_to do |format|
56
71
  format.html do
57
72
  flash[:success] = t('slash_admin.controller.create.success', model_name: @model_name)
58
73
  redirect_to handle_redirect_after_submit and return
59
74
  end
60
- format.js { render json: @model and return }
75
+ format.js { render json: { id: @model.id, name: helpers.show_object(@model) } and return }
61
76
  end
62
77
  end
63
78
  else
64
79
  flash[:error] = t('slash_admin.controller.create.error', model_name: @model_name)
65
80
  end
81
+
66
82
  respond_to do |format|
67
83
  format.html { render :new }
68
84
  format.js { render json: { errors: @model.errors.full_messages } }
@@ -74,23 +90,30 @@ module SlashAdmin
74
90
  @model = @model_class.find(params[:id])
75
91
  end
76
92
 
77
- def before_validate_on_update; end
78
93
  def update
79
94
  authorize! :edit, @model_class
80
95
  @model = @model_class.find(params[:id])
81
96
 
97
+ handle_has_one
98
+
99
+ @model.assign_attributes(permit_params)
100
+
82
101
  before_validate_on_update
102
+ handle_specific_fields
83
103
 
84
- if @model.update(permit_params)
85
- flash[:success] = t('slash_admin.controller.update.success', model_name: @model_name)
86
- respond_to do |format|
87
- format.html { redirect_to handle_redirect_after_submit and return }
88
- format.js
104
+ if @model.valid?
105
+ if @model.save!
106
+ after_save_on_update
107
+ flash[:success] = t('slash_admin.controller.update.success', model_name: @model_name)
108
+ respond_to do |format|
109
+ format.html { redirect_to handle_redirect_after_submit and return }
110
+ format.js
111
+ end
89
112
  end
90
113
  else
91
114
  flash[:error] = t('slash_admin.controller.update.error', model_name: @model_name)
92
115
  end
93
- render :edit and return
116
+ render :edit
94
117
  end
95
118
 
96
119
  def show
@@ -112,6 +135,49 @@ module SlashAdmin
112
135
  end
113
136
  end
114
137
 
138
+ def before_validate_on_update; end
139
+ def after_save_on_update; end
140
+
141
+ def handle_has_one
142
+ @has_one = {}
143
+ Array.wrap(update_params + create_params).uniq.each do |p|
144
+ if helpers.guess_field_type(@model_class.new, p) == 'has_one' && !@model_class.nested_attributes_options.key?(p.to_sym)
145
+ @has_one[p] = permit_params[p]
146
+ permit_params.delete(p)
147
+ end
148
+ end
149
+ end
150
+
151
+ def handle_specific_fields
152
+ # has_one
153
+ if @has_one.present?
154
+ @has_one.each do |k, v|
155
+ if v.present?
156
+ @model.send("#{k}=", helpers.class_name_from_association(@model, k).constantize.find(v))
157
+ else
158
+ @model.send("#{k}=", nil)
159
+ end
160
+ end
161
+ end
162
+
163
+ # JSON
164
+ @model_class.columns_hash.each do |k, v|
165
+ if permit_params[k].is_a? String
166
+ if v.type == :json || v.type == :jsonb || helpers.serialized_json_field?(@model_class, k.to_s)
167
+ begin
168
+ @model.send("#{k}=", JSON.parse(permit_params[k]))
169
+ rescue
170
+ # Handle case when single string passed, we transform it into array to have a valid json
171
+ json = permit_params[k].split(',').to_json
172
+ @model.send("#{k}=", JSON.parse(json))
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ # Other
179
+ end
180
+
115
181
  def nestable
116
182
  unless @is_nestable
117
183
  flash[:error] = t('slash_admin.controller.nestable.error', model_name: @model_name)
@@ -135,16 +201,39 @@ module SlashAdmin
135
201
  end
136
202
 
137
203
  def handle_filtered_search
138
- search = @model_class.all
204
+ if @model_class.respond_to? :translated_attribute_names
205
+ search = @model_class.with_translations(I18n.locale).all
206
+ else
207
+ search = @model_class.all
208
+ end
209
+
210
+ virtual_fields = []
139
211
 
140
212
  params[:filters].each do |attr, query|
141
213
  unless query.blank?
142
- # column = @model_class.arel_table[attr.to_sym]
143
- case helpers.guess_field_type(@model_class, attr)
214
+ attr_type = helpers.guess_field_type(@model_class, attr)
215
+ if @model_class.respond_to?(:translated_attribute_names) && @model_class.translated_attribute_names.include?(attr.to_sym)
216
+ attr = "#{@model_class.name.singularize.underscore}_translations.#{attr}"
217
+ end
218
+ case attr_type
219
+ when 'belongs_to'
220
+ search = search.eager_load(attr.to_s)
221
+ search = search.where(attr.to_s + '_id IN (' + query.join(',') + ')')
222
+ when 'has_one'
223
+ search = search.eager_load(attr.to_s)
224
+ search = search.where(attr.to_s.pluralize + '.id IN (' + query.join(',') + ')')
144
225
  when 'string', 'text'
145
- # TODO: handle unnaccent if postgres and extensions installed
146
- # search = search.where("unaccent(lower(#{attr})) LIKE unaccent(lower(:query))", query: "%#{query}%")
147
- search = search.where("lower(#{attr}) LIKE lower(:query)", query: "%#{query}%")
226
+ query.strip! unless query.strip!.nil?
227
+ attributes = @model_class.new.attributes.keys
228
+ if !attributes.include?(attr.to_s) && @model_class.method_defined?(attr.to_s)
229
+ virtual_fields << attr.to_s
230
+ else
231
+ begin
232
+ search = search.where("unaccent(lower(#{attr})) LIKE unaccent(lower(:query))", query: "%#{query}%")
233
+ rescue
234
+ search = search.where("lower(#{attr}) LIKE lower(:query)", query: "%#{query}%")
235
+ end
236
+ end
148
237
  when 'date', 'datetime'
149
238
  if query.is_a?(String)
150
239
  search = search.where("#{attr} = :query", query: query)
@@ -163,12 +252,31 @@ module SlashAdmin
163
252
  end
164
253
  end
165
254
  when 'decimal', 'number', 'integer'
166
- search = search.where("#{attr} >= :query", query: query['from']) if query['from'].present?
167
- search = search.where("#{attr} <= :query", query: query['to']) if query['to'].present?
255
+ if query.instance_of?(ActionController::Parameters)
256
+ if query['from'].present? || query['to'].present?
257
+ search = search.where("#{attr} >= :query", query: query['from']) if query['from'].present?
258
+ search = search.where("#{attr} <= :query", query: query['to']) if query['to'].present?
259
+ end
260
+ else
261
+ if attr_type == 'decimal' || attr_type == 'number'
262
+ query = query.to_f
263
+ elsif attr_type == 'integer'
264
+ query = query.to_i
265
+ end
266
+ search = search.where("#{attr} = :query", query: query)
267
+ end
168
268
  when 'boolean'
169
269
  search = search.where("#{attr} = :query", query: to_boolean(query))
170
- when 'belongs_to', 'has_one'
171
- search = search.where(attr.to_s + '_id IN (' + query.join(',') + ')')
270
+ else
271
+ raise Exception.new("Unable to query for attribute_type : #{attr_type}")
272
+ end
273
+ end
274
+ end
275
+
276
+ params[:filters].each do |attr, query|
277
+ unless query.blank?
278
+ if virtual_fields.present? && virtual_fields.include?(attr.to_s)
279
+ search = search.select { |s| s.send(attr).present? ? s.send(attr).downcase.include?(query.downcase) : nil }
172
280
  end
173
281
  end
174
282
  end
@@ -178,14 +286,7 @@ module SlashAdmin
178
286
 
179
287
  # Export CSV
180
288
  def export_csv(options = {})
181
- CSV.generate(options) do |csv|
182
- header = @fields.map { |f| @model_class.human_attribute_name(f) }
183
- csv << header
184
- @models_export.each do |m|
185
- csv << m.attributes.values_at(*@fields)
186
- end
187
- csv
188
- end
289
+ @models_export.to_sql
189
290
  end
190
291
 
191
292
  def update_params(options = {})
@@ -228,9 +329,9 @@ module SlashAdmin
228
329
  def handle_default
229
330
  @title = @model_name.present? ? @model_class.model_name.human(count: 2) : nil
230
331
  @sub_title = nil
231
- @per = 10
332
+ @per = 20
232
333
  @page = 1
233
- @per_values = [10, 20, 50, 100, 150]
334
+ @per_values = [20, 30, 50, 100, 150]
234
335
  @use_export_params = false
235
336
  @order_field = :id
236
337
  @order = 'DESC'
@@ -312,7 +413,31 @@ module SlashAdmin
312
413
 
313
414
  # Exclude default params for edit and create
314
415
  def exclude_default_params(params)
315
- params - %w(id created_at updated_at slug position)
416
+ params = params - %w(id created_at updated_at slug position)
417
+ if @model_class.try(:translated_attribute_names).present?
418
+ params = params - @model_class.translated_attribute_names.map(&:to_s)
419
+ end
420
+ params
421
+ end
422
+
423
+ def stream_file(filename, extension)
424
+ response.headers['Content-Type'] = 'application/octet-stream'
425
+ response.headers['Content-Disposition'] = "attachment; filename=#{filename}.#{extension}"
426
+
427
+ yield response.stream
428
+ ensure
429
+ response.stream.close
430
+ end
431
+
432
+ def stream_csv_report
433
+ query = @models_export.limit(5000).to_sql
434
+ query_options = 'WITH CSV HEADER'
435
+
436
+ stream_file("#{@model_class.model_name.plural.upcase}_#{Date.today}", 'csv') do |stream|
437
+ stream_query_rows(query, query_options) do |row_from_db|
438
+ stream.write row_from_db
439
+ end
440
+ end
316
441
  end
317
442
 
318
443
  private
@@ -321,5 +446,14 @@ module SlashAdmin
321
446
  def export_params
322
447
  list_params
323
448
  end
449
+
450
+ def stream_query_rows(sql_query, options = 'WITH CSV HEADER')
451
+ conn = ActiveRecord::Base.connection.raw_connection
452
+ conn.copy_data "COPY (#{sql_query}) TO STDOUT #{options};" do
453
+ while row = conn.get_copy_data
454
+ yield row
455
+ end
456
+ end
457
+ end
324
458
  end
325
459
  end
@@ -12,7 +12,7 @@ module SlashAdmin
12
12
  admin = Admin.where('username = :value OR lower(email) = lower(:value)', value: params[:admin][:login]).first
13
13
  if admin&.authenticate(params[:admin][:password])
14
14
  session[:admin_id] = admin.id
15
- flash[:notice] = 'Vous êtes à présent connecté.'
15
+ flash[:success] = 'Vous êtes à présent connecté.'
16
16
  redirect_to slash_admin.dashboard_path
17
17
  else
18
18
  @error_messages = 'Merci de vérifier vos identifiants'
@@ -16,20 +16,40 @@ module SlashAdmin
16
16
 
17
17
  duplicate_for_orwhere = results
18
18
 
19
+ virtual_fields = []
20
+
19
21
  params[:fields].each_with_index do |f, index|
20
22
  if model_class.respond_to? :translated_attribute_names
21
23
  if model_class.translated_attribute_names.include?(f.to_sym)
22
24
  f = "#{params[:model_class].singularize.underscore}_translations.#{f}"
23
25
  end
26
+ else
27
+ unless model_class.column_names.include?(f) || model_class.respond_to?(f)
28
+ raise Exception.new("Unable to find attribute: #{f} in model_column: #{model_class}, you may need to override autocomplete_params in you target's model controller")
29
+ end
24
30
  end
25
31
 
26
32
  if index == 0
27
- results = results.where("lower(#{f}) LIKE lower(:query)", query: "%#{params[:q]}%")
33
+ if model_class.column_names.include?(f)
34
+ results = results.where("lower(#{f}) LIKE lower(:query)", query: "%#{params[:q]}%")
35
+ else
36
+ virtual_fields << f
37
+ end
28
38
  else
29
39
  results = results.or(duplicate_for_orwhere.where("lower(#{f}) LIKE lower(:query)", query: "%#{params[:q]}%"))
30
40
  end
31
41
  end
32
42
 
43
+ params[:fields].each do |f|
44
+ unless params[:q].blank?
45
+ if virtual_fields.present? && virtual_fields.include?(f)
46
+ results = results.select { |s| s.send(f).present? ? s.send(f).downcase.include?(params[:q].downcase) : nil }
47
+ end
48
+ end
49
+ end
50
+
51
+
52
+
33
53
  formatted_result = []
34
54
  results.each do |r|
35
55
  formatted_result << { id: r.id, name: helpers.show_object(r) }
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module SlashAdmin
3
4
  module ApplicationHelper
4
5
  def page_title(content)
@@ -18,10 +19,17 @@ module SlashAdmin
18
19
  end
19
20
  end
20
21
 
21
- # Direct Link temp TODO
22
22
  if s[:path].present?
23
- access = true
23
+ if can? s[:role], s[:role]
24
+ access = true
25
+ end
26
+ if s[:role].present?
27
+ if can? s[:role], s[:role]
28
+ access = true
29
+ end
30
+ end
24
31
  end
32
+
25
33
  access
26
34
  end
27
35
 
@@ -79,6 +87,7 @@ module SlashAdmin
79
87
  return true unless object.errors.messages[field_name].blank?
80
88
  end
81
89
 
90
+ # Type must be 'warning' or 'success' or 'error'
82
91
  def toastr_bootstrap
83
92
  flash_messages = []
84
93
  flash.each do |type, message|
@@ -150,6 +159,8 @@ module SlashAdmin
150
159
  has_many_fields = object_class.reflect_on_all_associations(:has_many).map(&:name)
151
160
  has_one_fields = object_class.reflect_on_all_associations(:has_one).map(&:name)
152
161
 
162
+ return if attr.is_a? Hash
163
+
153
164
  type = if object_class&.uploaders&.key?(attr.to_sym)
154
165
  'image'
155
166
  elsif belongs_to_fields.include?(attr.to_sym)
@@ -170,6 +181,28 @@ module SlashAdmin
170
181
  type
171
182
  end
172
183
 
184
+ # From shoulda-matchers https://github.com/thoughtbot/shoulda-matchers/blob/da4e6ddd06de54016e7c2afd953120f0f6529c70/lib/shoulda/matchers/rails_shim.rb
185
+ # @param model @model_class
186
+ def serialized_attributes_for(model)
187
+ serialized_columns = model.columns.select do |column|
188
+ model.type_for_attribute(column.name).is_a?(
189
+ ::ActiveRecord::Type::Serialized,
190
+ )
191
+ end
192
+
193
+ serialized_columns.inject({}) do |hash, column|
194
+ hash[column.name.to_s] = model.type_for_attribute(column.name).coder
195
+ hash
196
+ end
197
+ end
198
+
199
+ # @param model @model_class
200
+ # @param attribute String
201
+ def serialized_json_field?(model, attribute)
202
+ hash = serialized_attributes_for(model)
203
+ hash.key?(attribute) && hash[attribute] == ::ActiveRecord::Coders::JSON
204
+ end
205
+
173
206
  def admin_custom_field(form, attribute)
174
207
  type = attribute[attribute.keys.first][:type].to_s
175
208
  render partial: "slash_admin/custom_fields/#{type}", locals: { f: form, a: attribute }
@@ -186,7 +219,11 @@ module SlashAdmin
186
219
  if attribute.is_a?(Hash)
187
220
  admin_custom_field(form, attribute)
188
221
  elsif belongs_to_fields.include?(attribute.to_sym)
189
- render partial: 'slash_admin/fields/belongs_to', locals: { f: form, a: attribute }
222
+ if form.object.class.nested_attributes_options.key?(attribute.to_sym)
223
+ render partial: 'slash_admin/fields/nested_belongs_to', locals: { f: form, a: attribute }
224
+ else
225
+ render partial: 'slash_admin/fields/belongs_to', locals: { f: form, a: attribute }
226
+ end
190
227
  elsif has_many_fields.include?(attribute.to_sym)
191
228
  # if has nested_attributes_options for has_many field
192
229
  if form.object.class.nested_attributes_options.key?(attribute.to_sym)
@@ -195,7 +232,11 @@ module SlashAdmin
195
232
  render partial: 'slash_admin/fields/has_many', locals: { f: form, a: attribute }
196
233
  end
197
234
  elsif has_one_fields.include?(attribute.to_sym)
198
- render partial: 'slash_admin/fields/has_one', locals: { f: form, a: attribute }
235
+ if form.object.class.nested_attributes_options.key?(attribute.to_sym)
236
+ render partial: 'slash_admin/fields/nested_has_one', locals: { f: form, a: attribute }
237
+ else
238
+ render partial: 'slash_admin/fields/has_one', locals: { f: form, a: attribute }
239
+ end
199
240
  elsif form.object.class&.uploaders&.key?(attribute.to_sym)
200
241
  render partial: 'slash_admin/fields/carrierwave', locals: { f: form, a: attribute }
201
242
  else