slash_admin 0.1.0 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
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