rear 0.2.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 (121) hide show
  1. checksums.yaml +15 -0
  2. data/.travis.yml +7 -0
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile +20 -0
  5. data/LICENSE +19 -0
  6. data/README.md +101 -0
  7. data/Rakefile +79 -0
  8. data/assets/api.js +307 -0
  9. data/assets/bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css +8 -0
  10. data/assets/bootstrap-datetimepicker/js/bootstrap-datetimepicker.min.js +26 -0
  11. data/assets/bootstrap/css/bootstrap-responsive.min.css +9 -0
  12. data/assets/bootstrap/css/bootstrap.min.css +9 -0
  13. data/assets/bootstrap/img/glyphicons-halflings-white.png +0 -0
  14. data/assets/bootstrap/img/glyphicons-halflings.png +0 -0
  15. data/assets/bootstrap/js/bootstrap.min.js +6 -0
  16. data/assets/jquery.js +5 -0
  17. data/assets/noty/jquery.noty.js +520 -0
  18. data/assets/noty/layouts/top.js +34 -0
  19. data/assets/noty/layouts/topRight.js +43 -0
  20. data/assets/noty/promise.js +432 -0
  21. data/assets/noty/themes/default.js +156 -0
  22. data/assets/select2-bootstrap.css +86 -0
  23. data/assets/select2/select2-spinner.gif +0 -0
  24. data/assets/select2/select2.css +652 -0
  25. data/assets/select2/select2.min.js +22 -0
  26. data/assets/select2/select2.png +0 -0
  27. data/assets/select2/select2x2.png +0 -0
  28. data/assets/ui.css +75 -0
  29. data/assets/xhr.js +4 -0
  30. data/bin/rear +65 -0
  31. data/docs/Assocs.md +100 -0
  32. data/docs/Columns.md +404 -0
  33. data/docs/Deploy.md +62 -0
  34. data/docs/FileManager.md +75 -0
  35. data/docs/Filters.md +341 -0
  36. data/docs/Setup.md +201 -0
  37. data/lib/rear.rb +13 -0
  38. data/lib/rear/actions.rb +98 -0
  39. data/lib/rear/constants.rb +61 -0
  40. data/lib/rear/controller_setup.rb +249 -0
  41. data/lib/rear/helpers.rb +17 -0
  42. data/lib/rear/helpers/class.rb +46 -0
  43. data/lib/rear/helpers/columns.rb +68 -0
  44. data/lib/rear/helpers/filters.rb +147 -0
  45. data/lib/rear/helpers/generic.rb +73 -0
  46. data/lib/rear/helpers/order.rb +47 -0
  47. data/lib/rear/helpers/pager.rb +35 -0
  48. data/lib/rear/helpers/render.rb +37 -0
  49. data/lib/rear/home_controller.rb +10 -0
  50. data/lib/rear/input.rb +341 -0
  51. data/lib/rear/orm.rb +73 -0
  52. data/lib/rear/rear.rb +74 -0
  53. data/lib/rear/setup.rb +9 -0
  54. data/lib/rear/setup/associations.rb +33 -0
  55. data/lib/rear/setup/columns.rb +109 -0
  56. data/lib/rear/setup/filters.rb +314 -0
  57. data/lib/rear/setup/generic.rb +59 -0
  58. data/lib/rear/setup/menu.rb +39 -0
  59. data/lib/rear/templates/editor/ace.slim +7 -0
  60. data/lib/rear/templates/editor/assocs.slim +10 -0
  61. data/lib/rear/templates/editor/boolean.slim +5 -0
  62. data/lib/rear/templates/editor/bulk_edit.slim +75 -0
  63. data/lib/rear/templates/editor/checkbox.slim +5 -0
  64. data/lib/rear/templates/editor/ckeditor.slim +7 -0
  65. data/lib/rear/templates/editor/date.slim +9 -0
  66. data/lib/rear/templates/editor/datetime.slim +9 -0
  67. data/lib/rear/templates/editor/layout.slim +103 -0
  68. data/lib/rear/templates/editor/password.slim +1 -0
  69. data/lib/rear/templates/editor/radio.slim +5 -0
  70. data/lib/rear/templates/editor/select.slim +3 -0
  71. data/lib/rear/templates/editor/string.slim +1 -0
  72. data/lib/rear/templates/editor/text.slim +1 -0
  73. data/lib/rear/templates/editor/time.slim +9 -0
  74. data/lib/rear/templates/error.slim +36 -0
  75. data/lib/rear/templates/filters/boolean.slim +10 -0
  76. data/lib/rear/templates/filters/checkbox.slim +15 -0
  77. data/lib/rear/templates/filters/date.slim +10 -0
  78. data/lib/rear/templates/filters/datetime.slim +10 -0
  79. data/lib/rear/templates/filters/layout.slim +26 -0
  80. data/lib/rear/templates/filters/radio.slim +14 -0
  81. data/lib/rear/templates/filters/select.slim +9 -0
  82. data/lib/rear/templates/filters/string.slim +3 -0
  83. data/lib/rear/templates/filters/text.slim +3 -0
  84. data/lib/rear/templates/filters/time.slim +10 -0
  85. data/lib/rear/templates/home.slim +0 -0
  86. data/lib/rear/templates/layout.slim +78 -0
  87. data/lib/rear/templates/pager.slim +22 -0
  88. data/lib/rear/templates/pane/ace.slim +2 -0
  89. data/lib/rear/templates/pane/assocs.slim +62 -0
  90. data/lib/rear/templates/pane/boolean.slim +2 -0
  91. data/lib/rear/templates/pane/checkbox.slim +5 -0
  92. data/lib/rear/templates/pane/ckeditor.slim +2 -0
  93. data/lib/rear/templates/pane/date.slim +2 -0
  94. data/lib/rear/templates/pane/datetime.slim +2 -0
  95. data/lib/rear/templates/pane/layout.slim +111 -0
  96. data/lib/rear/templates/pane/password.slim +2 -0
  97. data/lib/rear/templates/pane/quickview.slim +21 -0
  98. data/lib/rear/templates/pane/radio.slim +5 -0
  99. data/lib/rear/templates/pane/select.slim +5 -0
  100. data/lib/rear/templates/pane/string.slim +2 -0
  101. data/lib/rear/templates/pane/text.slim +2 -0
  102. data/lib/rear/templates/pane/time.slim +2 -0
  103. data/lib/rear/utils.rb +288 -0
  104. data/rear.gemspec +27 -0
  105. data/test/helpers.rb +33 -0
  106. data/test/models/ar.rb +52 -0
  107. data/test/models/dm.rb +53 -0
  108. data/test/models/sq.rb +58 -0
  109. data/test/setup.rb +4 -0
  110. data/test/templates/adhoc/book/editor/name.slim +1 -0
  111. data/test/templates/adhoc/book/editor/string.slim +1 -0
  112. data/test/templates/adhoc/book/pane/name.slim +1 -0
  113. data/test/templates/adhoc/book/pane/string.slim +1 -0
  114. data/test/templates/shared/shared-templates/editor/string.slim +1 -0
  115. data/test/templates/shared/shared-templates/pane/string.slim +1 -0
  116. data/test/test__assocs.rb +249 -0
  117. data/test/test__columns.rb +269 -0
  118. data/test/test__crud.rb +81 -0
  119. data/test/test__custom_templates.rb +147 -0
  120. data/test/test__filters.rb +228 -0
  121. metadata +220 -0
@@ -0,0 +1,17 @@
1
+ module RearHelpers
2
+ module ClassMixin
3
+ include RearConstants
4
+ end
5
+
6
+ module InstanceMixin
7
+ include RearConstants
8
+ end
9
+ end
10
+
11
+ require 'rear/helpers/class'
12
+ require 'rear/helpers/columns'
13
+ require 'rear/helpers/filters'
14
+ require 'rear/helpers/generic'
15
+ require 'rear/helpers/order'
16
+ require 'rear/helpers/pager'
17
+ require 'rear/helpers/render'
@@ -0,0 +1,46 @@
1
+ module RearHelpers
2
+ module ClassMixin
3
+
4
+ def default_label
5
+ @__rear__default_label ||= self.name.gsub(/\W/, '_').freeze
6
+ end
7
+
8
+ def menu_group?; @__rear__menu_group end
9
+ def readonly?; @__rear__readonly end
10
+
11
+ def orm
12
+ @__rear__orm
13
+ end
14
+
15
+ def assocs
16
+ @__rear__managed_assocs ||= (@__rear__assocs || {}).inject({}) do |map,(type,assocs)|
17
+ map.merge type => assocs.reject {|assoc,*| ignored_assocs.include? assoc}
18
+ end
19
+ end
20
+
21
+ # keeps the list of "real" columns as well as "virtual" ones.
22
+ # virtual refers to columns displayed on pane/editor pages
23
+ # but does exists in db
24
+ def columns
25
+ @__rear__columns ||= []
26
+ end
27
+
28
+ # keeps the list of columns that "physically" exists in db
29
+ def real_columns
30
+ @__rear__real_columns || []
31
+ end
32
+
33
+ def filters
34
+ mounted? ? @__rear__filters || {} : @__rear__filters ||= {}
35
+ end
36
+
37
+ def quick_filters
38
+ mounted? ? @__rear__quick_filters || {} : @__rear__quick_filters ||= {}
39
+ end
40
+
41
+ def internal_filters
42
+ mounted? ? @__rear__internal_filters || [] : @__rear__internal_filters ||= []
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,68 @@
1
+ module RearHelpers
2
+ module InstanceMixin
3
+
4
+ def pane_columns
5
+ @__rear__pane_columns ||= begin
6
+ pane_columns = columns.select {|c| c.pane?}
7
+ [RearInput.new(pkey)] + case action_name
8
+ when :reverse_assoc
9
+ (assoc_columns = __rear__.assoc_columns) ?
10
+ pane_columns.select {|c| assoc_columns.include? c.name} :
11
+ pane_columns[0..1]
12
+ when :quickview
13
+ pane_columns[0..0]
14
+ else
15
+ pane_columns
16
+ end
17
+ end
18
+ end
19
+
20
+ def editor_columns
21
+ @__rear__editor_columns ||= columns.select {|c| c.editor?}
22
+ end
23
+
24
+ def columns
25
+ @__rear__columns ||= __rear__.columns.inject([]) do |columns,(name, type, attrs, proc)|
26
+ columns << RearInput.new(name, type, attrs, @brand_new_item, &proc)
27
+ end
28
+ end
29
+
30
+ def attrs column, scope
31
+ meth = '%s_attrs' % scope
32
+ column_attrs = column.send(meth)
33
+ return column_attrs if column_attrs.any?
34
+ __rear__.send(meth)
35
+ end
36
+
37
+ def render_pane_column column
38
+ render_column column, :pane
39
+ end
40
+
41
+ def render_editor_column column
42
+ render_column column, :editor
43
+ end
44
+
45
+ def render_column column, scope
46
+ template, value = %w[template value].map {|m| column.send('%s_%s' % [scope, m])}
47
+ locals = if column.optioned?
48
+ options = column.options
49
+ options = Hash[options.zip(options)] if options.is_a?(Array)
50
+ active_options = self.instance_exec(&column.active_options)
51
+ active_options = [active_options] unless active_options.is_a?(Array)
52
+ {options: options, active_options: active_options}
53
+ else
54
+ {value: self.instance_exec(&value)}
55
+ end
56
+ template.is_a?(Proc) ?
57
+ self.instance_exec(&template) :
58
+ render_slim_p(locals) { template_cache(path_to_rear_templates template) }
59
+ end
60
+
61
+ def sortable_column? column
62
+ if column.name == pkey || column.order_by? ||
63
+ __rear__.real_columns.any? {|(n,t)| n == column.name}
64
+ sortable_vector column
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,147 @@
1
+ module RearHelpers
2
+ module InstanceMixin
3
+
4
+ def filters
5
+ __rear__.filters
6
+ end
7
+
8
+ def quick_filters
9
+ __rear__.quick_filters
10
+ end
11
+
12
+ def opted_quick_filters
13
+ @__rear__opted_quick_filters ||= begin
14
+ given_filters = params[:quick_filters] || {}
15
+ quick_filters.inject({}) do |map,(column,*)|
16
+ (v = given_filters[column.to_s]) && v.size > 0 ? map.merge(column => v) : map
17
+ end
18
+ end
19
+ end
20
+
21
+ def quick_filter? column
22
+ opted_quick_filters[column]
23
+ end
24
+
25
+ # turn
26
+ # {'date' => {'gte' => 'foo', 'lte' => 'bar'}}
27
+ # into
28
+ # {
29
+ # :date => {
30
+ # :gte => ['foo', ['%s >= ?', '%s'] ],
31
+ # :lte => ['bar', ['%s <= ?', '%s'] ],
32
+ # }
33
+ # }
34
+ def opted_filters
35
+ @__rear__opted_filters ||= (params[:filters]||{}).inject({}) do |map,s|
36
+ (column = s.first) && (filter = filters[(column = column.to_sym)]).is_a?(Hash) &&
37
+ (setup = s.last).is_a?(Hash) && setup.each_pair do |cmp,v|
38
+ v && v.size > 0 && (query_formats = filters_query_map[(cmp=cmp.to_sym)]) &&
39
+ (filter_setup = filter[cmp]) && (map[column] ||= {})[cmp] = [
40
+ v, # do not typecast to Fixnum cause this will break Hash to query_string conversion
41
+ query_formats,
42
+ filter_setup,
43
+ ]
44
+ end
45
+ map
46
+ end
47
+ end
48
+
49
+ def filters_query_map
50
+ @@__rear__filters__query_map ||= FILTERS__QUERY_MAP.call(__rear__.orm)
51
+ end
52
+
53
+ def filter name, comparison_function = FILTERS__DECORATIVE_CMP
54
+ ((opted_filters[name] || {})[comparison_function] || []).first
55
+ end
56
+ alias filter? filter
57
+ alias decorative_filter? filter
58
+
59
+ def filters_to_sql
60
+ conditions, sql_chunks, values = {}, [], []
61
+
62
+ quick_filters.each_pair do |column,filters|
63
+ next unless label = opted_quick_filters[column]
64
+ filters.each_pair do |filter,(query_formats,value)|
65
+ next unless label == filter
66
+ sql_chunks << query_formats.first % RearUtils.quote_column(model, column)
67
+ values << ([TrueClass, FalseClass, Regexp].include?(value.class) ? value : query_formats.last % value)
68
+ end
69
+ end
70
+
71
+ opted_filters.each_pair do |column, setups|
72
+ setups.each_pair do |cmp, (value, query_formats, filter_setup)|
73
+ next if filter_setup[:decorative?]
74
+
75
+ sql_chunk = query_formats.first % RearUtils.quote_column(model, column)
76
+ custom_sql_chunk = nil
77
+
78
+ if filter_setup[:type] == :boolean && FILTERS__STR_TO_BOOLEAN.has_key?(value)
79
+ values << FILTERS__STR_TO_BOOLEAN[value]
80
+ else
81
+ case cmp
82
+ when :in
83
+ values << value
84
+ when :csl # comma separated list
85
+ values << value.to_s.split(',')
86
+ else
87
+ if value.is_a?(Array)
88
+ custom_sql_chunk = []
89
+ value.each do |v|
90
+ custom_sql_chunk << sql_chunk
91
+ values << query_formats.last % v
92
+ end
93
+ custom_sql_chunk = '(' + custom_sql_chunk.join(' OR ') + ')'
94
+ else
95
+ values << query_formats.last % value
96
+ end
97
+ end
98
+ end
99
+ sql_chunks << (custom_sql_chunk || sql_chunk)
100
+ end
101
+ end
102
+
103
+ __rear__.internal_filters.each do |m|
104
+ next unless items = self.send(m)
105
+ sql_chunks << filters_query_map[:in].first % pkey
106
+ values << items.map {|i| i[pkey]}
107
+ end
108
+
109
+ sql_chunks.any? ?
110
+ conditions.merge(conditions: [sql_chunks.join(' AND '), *values]) :
111
+ conditions
112
+ end
113
+
114
+ def render_filters
115
+ html = assets_mapper(route(:assets), suffix: '-rear').js_tag('xhr')
116
+ main_filters = filters.inject(html) do |html,(column,setups)|
117
+ setups.each_pair do |comparison_function, setup|
118
+ context = {
119
+ name: 'filters[%s][%s]' % [column, comparison_function],
120
+ column: column,
121
+ value: filter(column, comparison_function),
122
+ setup: setup,
123
+ attrs: Hash[setup[:attrs]]
124
+ }
125
+ template = template_cache(path_to_rear_templates(setup[:template]))
126
+ html << render_slim_p(context) { template }
127
+ end
128
+ html << '&nbsp;'
129
+ end
130
+ end
131
+
132
+ def filter_setup_to_options(setup)
133
+ options = setup[:proc] ? self.instance_exec(&setup[:proc]) : {}
134
+ if options.is_a?(Array)
135
+ options.flatten!
136
+ options = Hash[options.zip(options)]
137
+ end
138
+ return options if options.is_a?(Hash)
139
+ warn "
140
+ %s#%s filter expects options to be provided as a Hash or Array or via a block.
141
+ If block given, it should return a Hash or Array
142
+ " % [self.class, setup[:label]]
143
+ {}
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,73 @@
1
+ module RearHelpers
2
+ module InstanceMixin
3
+
4
+ attr_reader :items
5
+ attr_accessor :item, :item_id, :column
6
+
7
+ def orm
8
+ @__rear__orm ||= RearORM.new(__rear__.model, __rear__.pkey)
9
+ end
10
+
11
+ def dom_id
12
+ @__rear__dom_id ||= (params[:dom_id] || # params[:dom_id] are used by decorative filters
13
+ (@reverse_assoc ? @reverse_assoc.dom_id : 'rear_element_%s' % self.__id__)).freeze
14
+ end
15
+
16
+ def model
17
+ __rear__.model
18
+ end
19
+
20
+ def sequel?
21
+ @@__rear__is_sequel ||= RearUtils.orm(model) == :sq
22
+ end
23
+
24
+ def pkey
25
+ __rear__.pkey
26
+ end
27
+
28
+ def assocs *types
29
+ return __rear__.assocs if types.empty?
30
+ types.inject({}) do |assocs,type|
31
+ assocs.merge(__rear__.assocs[type.to_sym] || {})
32
+ end
33
+ end
34
+
35
+ def template_cache template
36
+ ((@__rear__template_cache ||= {})[template] ||= {})[File.mtime(template)] ||= File.read(template)
37
+ end
38
+
39
+ # used for cosmetic compatibility between filters and columns.
40
+ # columns block are executed inside Column instance,
41
+ # and using `options` method to define options for :select/:radio/:checkbox columns.
42
+ # filters of these types also uses a block and it is executed in controller's context.
43
+ # so adding this method here will allow to use `options` method inside filter's block.
44
+ #
45
+ # @example
46
+ # filter :colors do
47
+ # options 'Red', 'Green', 'Blue'
48
+ # end
49
+ # # this is equivalent to
50
+ # filter :colors do
51
+ # ['Red', 'Green', 'Blue']
52
+ # end
53
+ # # but looks cosmetically better cause uses same syntax as columns
54
+ #
55
+ def options *args
56
+ args
57
+ end
58
+
59
+ def associated_model_controller model
60
+ (@__rear__associated_model_controllers ||= {})[model] ||=
61
+ RearUtils.associated_model_controller(model, :ensure_mounted)
62
+ end
63
+
64
+ def __rear__
65
+ self.class
66
+ end
67
+
68
+ def readonly?
69
+ self.class.readonly?
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,47 @@
1
+ module RearHelpers
2
+ module InstanceMixin
3
+
4
+ # flip-flopping vectors, that's it, if current vector set to asc
5
+ # this method will return desc, and vice-versa.
6
+ # also it will return UI arrow direction - when vector is asc
7
+ # arrow is down and arrow is up when vector is desc
8
+ def sortable_vector column
9
+ vector = ['asc', nil].include?(order_params[column.string_name]) ? 'desc' : 'asc'
10
+ [vector, vector == 'asc' ? 'down' : 'up']
11
+ end
12
+
13
+ # checks whether ordering is happening by given column
14
+ # and if it is, check whether a valid vector used
15
+ def sortable_vector? column
16
+ if vector = (params[:order] || {})[column.string_name]
17
+ valid_sortable_vector? vector
18
+ end
19
+ end
20
+
21
+ def valid_sortable_vector? vector
22
+ vector if vector == 'asc' || vector == 'desc'
23
+ end
24
+
25
+ def order_params
26
+ @__rear__order_params ||= pane_columns.inject({}) do |map,column|
27
+ (vector = sortable_vector?(column)) ?
28
+ map.update(column.string_name => vector) : map
29
+ end.freeze
30
+ end
31
+
32
+ def order_params_to_sql
33
+ order = []
34
+ order_params.each_pair do |column_name, vector|
35
+ next unless column = columns.find {|c| c.string_name == column_name}
36
+ case __rear__.orm
37
+ when :dm
38
+ column.order_by.each {|c| order << c.send(vector)}
39
+ when :ar
40
+ columns = column.order_by.map {|c| RearUtils.quote_ar_column(model, c)}
41
+ order << [columns.join(', '), vector].join(' ')
42
+ end
43
+ end
44
+ order.any? ? order : nil
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ module RearHelpers
2
+ module InstanceMixin
3
+
4
+ def pager_linker page, label = nil
5
+ label ||= page
6
+ url = route(action, *action_params__array, pager_params(page))
7
+ return link_to(url, label) unless xhr?
8
+
9
+ onclick = "Rear.switch_page('#%s', '%s');" % [dom_id, url]
10
+ link_to nil, label, onclick: onclick
11
+ end
12
+
13
+ def pager_params page = nil, filters = nil
14
+ page.is_a?(Hash) && (filters = page) && (page = nil)
15
+ page ||= params[:page]
16
+ {
17
+ page: page.to_s,
18
+ filters: filters || pager_filters,
19
+ quick_filters: opted_quick_filters,
20
+ order: order_params
21
+ }.reject {|k,v| v.nil? || v.empty?}
22
+ end
23
+
24
+ def pager_filters
25
+ @__rear__pager_filters ||= filters.inject({}) do |map,(column, setups)|
26
+ setups.each do |comparison_function, setup|
27
+ (value = filter?(column, comparison_function)) &&
28
+ (map[column] ||= {})[comparison_function] = value
29
+ end
30
+ map
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ module RearHelpers
2
+ module InstanceMixin
3
+
4
+ def reander *args, &proc
5
+ reander_layout(:layout) { render_slim args, &proc }
6
+ end
7
+
8
+ def reander_partial path, *rest
9
+ render_slim_p path_to_rear_templates('%s.slim' % path), *rest
10
+ end
11
+ alias reander_p reander_partial
12
+
13
+ def reander_layout path, *rest, &proc
14
+ render_slim_l path_to_rear_templates('%s.slim' % path), *rest, &proc
15
+ end
16
+ alias reander_l reander_layout
17
+
18
+ def path_to_rear_templates *chunks
19
+ template = File.join *chunks.map(&:to_s)
20
+ locations = []
21
+ {
22
+ @__rear__templates_path => File.join(app.root, @__rear__templates_path.to_s),
23
+ @__rear__templates_fullpath => @__rear__templates_fullpath,
24
+ }.select {|k,v| k}.each_value do |prefix|
25
+ locations << File.join(prefix, EUtils.class_to_route(self.class.model)) if self.class.respond_to?(:model)
26
+ locations << File.join(prefix, 'shared-templates')
27
+ end
28
+ locations << PATH__TEMPLATES
29
+ locations.each do |p|
30
+ fp = File.join(p, template)
31
+ return explicit_view_path(fp) if File.file?(fp)
32
+ end
33
+ raise ArgumentError, '%s template not found in any of %s paths' % [template,locations*', ']
34
+ end
35
+
36
+ end
37
+ end