motion-prime 0.1.7 → 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 (46) hide show
  1. checksums.yaml +8 -8
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile.lock +1 -1
  4. data/files/app/config/base.rb +7 -4
  5. data/files/app/styles/sidebar.rb +4 -4
  6. data/lib/motion-prime.rb +1 -0
  7. data/motion-prime/config/base.rb +5 -0
  8. data/motion-prime/elements/_text_height_mixin.rb +4 -4
  9. data/motion-prime/elements/base.rb +11 -5
  10. data/motion-prime/elements/draw.rb +105 -26
  11. data/motion-prime/elements/draw/image.rb +2 -2
  12. data/motion-prime/elements/draw/label.rb +20 -3
  13. data/motion-prime/elements/error_message.rb +23 -0
  14. data/motion-prime/elements/label.rb +8 -0
  15. data/motion-prime/models/association.rb +0 -1
  16. data/motion-prime/models/base.rb +8 -192
  17. data/motion-prime/models/errors.rb +62 -1
  18. data/motion-prime/models/exceptions.rb +3 -0
  19. data/motion-prime/models/model.rb +33 -1
  20. data/motion-prime/models/sync.rb +233 -0
  21. data/motion-prime/screens/_base_mixin.rb +4 -1
  22. data/motion-prime/screens/_navigation_mixin.rb +5 -5
  23. data/motion-prime/sections/base.rb +18 -5
  24. data/motion-prime/sections/form.rb +75 -16
  25. data/motion-prime/sections/form/base_field_section.rb +52 -1
  26. data/motion-prime/sections/form/select_field_section.rb +14 -1
  27. data/motion-prime/sections/form/string_field_section.rb +20 -5
  28. data/motion-prime/sections/form/switch_field_section.rb +33 -0
  29. data/motion-prime/sections/form/table_field_section.rb +40 -0
  30. data/motion-prime/sections/form/text_field_section.rb +23 -6
  31. data/motion-prime/sections/table.rb +25 -8
  32. data/motion-prime/styles/base.rb +31 -4
  33. data/motion-prime/support/dm_button.rb +3 -2
  34. data/motion-prime/version.rb +1 -1
  35. data/motion-prime/views/layout.rb +1 -1
  36. data/motion-prime/views/view_builder.rb +82 -66
  37. data/motion-prime/views/view_styler.rb +2 -0
  38. data/resources/fonts/ubuntu.ttf +0 -0
  39. data/spec/helpers/models.rb +1 -1
  40. data/spec/models/association_spec.rb +1 -1
  41. data/spec/models/errors_spec.rb +29 -0
  42. data/spec/models/finder_spec.rb +1 -1
  43. data/spec/models/{base_model_spec.rb → model_spec.rb} +30 -1
  44. data/spec/models/store_extension_spec.rb +1 -1
  45. data/spec/models/store_spec.rb +1 -1
  46. metadata +12 -4
@@ -1,3 +1,64 @@
1
1
  module MotionPrime
2
- class StoreError < StandardError; end
2
+ class Errors
3
+ attr_accessor :keys
4
+ attr_accessor :errors
5
+
6
+ def initialize(model)
7
+ @keys = []
8
+ @errors = {}
9
+ model.class.attributes.map(&:to_sym).each do |key|
10
+ initialize_for_key key
11
+ end
12
+ end
13
+
14
+ def initialize_for_key(key)
15
+ @keys << key.to_sym unless @keys.include?(key.to_sym)
16
+ @errors[key.to_sym] ||= []
17
+ end
18
+
19
+ def get(key)
20
+ initialize_for_key(key)
21
+ @errors[key.to_sym]
22
+ end
23
+
24
+ def set(key, errors)
25
+ initialize_for_key(key)
26
+ @errors[key.to_sym] = Array.wrap(errors)
27
+ end
28
+
29
+ def add(key, error)
30
+ initialize_for_key(key)
31
+ @errors[key.to_sym] << error
32
+ end
33
+
34
+ def [](key)
35
+ get(key)
36
+ end
37
+
38
+ def []=(key, errors)
39
+ set(key, errors)
40
+ end
41
+
42
+ def reset
43
+ @keys.each do |key|
44
+ set(key, [])
45
+ end
46
+ end
47
+
48
+ def messages
49
+ errors.values.compact.flatten
50
+ end
51
+
52
+ def blank?
53
+ messages.blank?
54
+ end
55
+
56
+ def present?
57
+ !blank?
58
+ end
59
+
60
+ def to_s
61
+ messages.join(';')
62
+ end
63
+ end
3
64
  end
@@ -0,0 +1,3 @@
1
+ module MotionPrime
2
+ class StoreError < StandardError; end
3
+ end
@@ -2,7 +2,6 @@ module MotionPrime
2
2
  module ModelMethods
3
3
  def save
4
4
  raise StoreError, 'No store provided' unless self.store
5
-
6
5
  error_ptr = Pointer.new(:id)
7
6
  self.store.addObject(self, error: error_ptr)
8
7
  raise StoreError, error_ptr[0].description if error_ptr[0]
@@ -21,6 +20,39 @@ module MotionPrime
21
20
  def store
22
21
  super || self.class.store
23
22
  end
23
+
24
+ def assign_attributes(new_attributes, options = {})
25
+ attributes = new_attributes.symbolize_keys
26
+ attributes.each do |k, v|
27
+ if respond_to?("#{k}=")
28
+ send("#{k}=", v) unless options[:skip_nil_values] && v.nil?
29
+ elsif options[:check_attribute_presence]
30
+ puts "unknown attribute: #{k}"
31
+ else
32
+ raise(NoMethodError, "unknown attribute: #{k}")
33
+ end
34
+ end
35
+ end
36
+
37
+ def attributes_hash
38
+ self.info.to_hash.symbolize_keys
39
+ end
40
+
41
+ def new_record?
42
+ id.blank?
43
+ end
44
+
45
+ def persisted?
46
+ !new_record?
47
+ end
48
+
49
+ def model_name
50
+ self.class.name.underscore
51
+ end
52
+
53
+ def inspect
54
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}> " + MotionPrime::JSON.generate(info)
55
+ end
24
56
  end
25
57
 
26
58
  module ModelClassMethods
@@ -0,0 +1,233 @@
1
+ module MotionPrime
2
+ module ModelSyncMethods
3
+ def self.included(base)
4
+ base.class_attribute :_sync_url
5
+ base.class_attribute :_updatable_attributes
6
+ base.class_attribute :_associations
7
+ end
8
+
9
+ def sync_url(method = :get)
10
+ url = self.class.sync_url
11
+ url = url.call(method) if url.is_a?(Proc)
12
+ normalize_sync_url(url)
13
+ end
14
+
15
+ # destroy on server and delete on local
16
+ def destroy(&block)
17
+ use_callback = block_given?
18
+ api_client.delete(sync_url(:delete)) do
19
+ block.call() if use_callback
20
+ end
21
+ delete
22
+ end
23
+
24
+ # sync with server and save on local
25
+ def sync!(sync_options = {}, &block)
26
+ sync(sync_options.merge(save: true), &block)
27
+ end
28
+
29
+ # sync with with server
30
+ # TODO: order of fetch/update should be based on updated time?
31
+ def sync(sync_options = {}, &block)
32
+ use_callback = block_given?
33
+ should_fetch = sync_options[:fetch]
34
+ should_update = sync_options[:update]
35
+ should_fetch_associations = if sync_options.has_key?(:fetch_associations)
36
+ sync_options[:fetch_associations]
37
+ else # do not need to fetch unless this is a GET request
38
+ should_fetch
39
+ end
40
+
41
+ method = if should_update
42
+ persisted? ? :put : :post
43
+ else
44
+ :get
45
+ end
46
+ url = sync_url(method)
47
+
48
+ if url.blank?
49
+ should_fetch = false
50
+ should_update = false
51
+ end
52
+
53
+ should_fetch = !new_record? if should_fetch.nil?
54
+ should_update = new_record? if should_update.nil?
55
+
56
+ fetch_with_url url do
57
+ save if sync_options[:save]
58
+ block.call if use_callback
59
+ end if should_fetch
60
+
61
+ update_with_url url, sync_options do |data, status_code|
62
+ save if sync_options[:save] && status_code.to_s =~ /20\d/
63
+ # run callback only if it wasn't run on fetch
64
+ block.call(data, status_code) if use_callback && !should_fetch
65
+ end if should_update
66
+
67
+ fetch_associations(sync_options) do
68
+ # run callback only if it wasn't run on fetch or update
69
+ block.call if use_callback && !should_fetch && !should_update
70
+ end if should_fetch_associations
71
+ end
72
+
73
+ # fetch from server using url
74
+ def fetch_with_url(url, &block)
75
+ api_client.get(url) do |data|
76
+ if data.present?
77
+ fetch_with_attributes(data, &block)
78
+ end
79
+ end
80
+ end
81
+
82
+ # update on server using url
83
+ def update_with_url(url, sync_options = nil, &block)
84
+ use_callback = block_given?
85
+ post_data = { model_name => filtered_updatable_attributes(sync_options)}
86
+ api_client.send(id ? :put : :post, url, post_data) do |data, status_code|
87
+ if status_code.to_s =~ /20\d/ && data.is_a?(Hash)
88
+ self.id ||= data['id']
89
+ accessible_attributes = self.class.attributes.map(&:to_sym) - [:id]
90
+ attrs = data.symbolize_keys.slice(*accessible_attributes)
91
+ fetch_with_attributes(attrs)
92
+ end
93
+ block.call(data, status_code) if use_callback
94
+ end
95
+ end
96
+
97
+ # set attributes, using fetch
98
+ def fetch_with_attributes(attrs, &block)
99
+ attrs.each do |key, value|
100
+ if respond_to?(:"fetch_#{key}")
101
+ self.send(:"fetch_#{key}", value)
102
+ elsif respond_to?(:"#{key}=")
103
+ self.send(:"#{key}=", value)
104
+ end
105
+ end
106
+ block.call(self) if block_given?
107
+ end
108
+
109
+ def fetch_associations(sync_options = {}, &block)
110
+ use_callback = block_given?
111
+ associations = self.class._associations || {}
112
+
113
+ associations.keys.each_with_index do |key, index|
114
+ if use_callback && associations.count - 1 == index
115
+ fetch_association(key, sync_options, &block)
116
+ else
117
+ fetch_association(key, sync_options)
118
+ end
119
+ end
120
+ end
121
+
122
+ def fetch_association(key, sync_options = {}, &block)
123
+ options = self.class._associations[key]
124
+ return unless options[:sync_url]
125
+ options[:type] == :many ?
126
+ fetch_has_many(key, options, sync_options, &block) :
127
+ fetch_has_one(key, options, sync_options, &block)
128
+ end
129
+
130
+ def fetch_has_many(key, options = {}, sync_options = {}, &block)
131
+ old_collection = self.send(key)
132
+ use_callback = block_given?
133
+ puts "SYNC: started sync for #{key} in #{self.class.name}"
134
+ api_client.get normalize_sync_url(options[:sync_url]) do |data|
135
+ data = data[options[:sync_key]] if options[:sync_key]
136
+ if data.present?
137
+ # Update/Create existing records
138
+ data.each do |attributes|
139
+ model = old_collection.detect{ |model| model.id == attributes[:id]}
140
+ unless model
141
+ model = key.singularize.to_s.classify.constantize.new
142
+ self.send(:"#{key}_bag") << model
143
+ end
144
+ model.fetch_with_attributes(attributes)
145
+ model.save if sync_options[:save]
146
+ end
147
+ old_collection.each do |old_model|
148
+ model = data.detect{ |model| model[:id] == old_model.id}
149
+ unless model
150
+ old_model.delete
151
+ end
152
+ end
153
+ save if sync_options[:save]
154
+ puts "SYNC: finished sync for #{key} in #{self.class.name}"
155
+ block.call if use_callback
156
+ else
157
+ puts "SYNC ERROR: failed sync for #{key} in #{self.class.name}"
158
+ block.call if use_callback
159
+ end
160
+ end
161
+ end
162
+
163
+ def fetch_has_one(key, options = {}, &block)
164
+ use_callback = block_given?
165
+ puts "SYNC: started sync for #{key} in #{self.class.name}"
166
+ api_client.get normalize_sync_url(options[:sync_url]) do |data|
167
+ data = data[options[:sync_key]] if options[:sync_key]
168
+ if data.present?
169
+ model = self.send(key)
170
+ unless model
171
+ model = key.singularize.to_s.classify.constantize.new
172
+ self.send(:"#{key}_bag") << model
173
+ end
174
+ model.fetch_with_attributes(data)
175
+ model.save if sync_options[:save]
176
+ block.call if use_callback
177
+ else
178
+ puts "SYNC ERROR: failed sync for #{key} in #{self.class.name}"
179
+ block.call if use_callback
180
+ end
181
+ end
182
+ end
183
+
184
+ def filtered_updatable_attributes(options = {})
185
+ slice_attributes = options[:updatable_attributes].map(&:to_sym) if options.has_key?(:updatable_attributes)
186
+ updatable_attributes = self.class.updatable_attributes
187
+
188
+ if updatable_attributes.blank?
189
+ attrs = attributes_hash.slice(*slice_attributes) if slice_attributes
190
+ return attrs
191
+ end
192
+
193
+ updatable_attributes = updatable_attributes.slice(*slice_attributes) if slice_attributes
194
+ updatable_attributes.to_a.inject({}) do |hash, attribute|
195
+ key, options = *attribute
196
+ return hash if options[:if] && !send(options[:if])
197
+ value = if block = options[:block]
198
+ block.call(self)
199
+ else
200
+ info[key]
201
+ end
202
+ hash.merge(key => value)
203
+ end
204
+ end
205
+
206
+ def normalize_sync_url(url)
207
+ url.to_s.gsub(':id', id.to_s)
208
+ end
209
+ end
210
+
211
+ module ModelSyncClassMethods
212
+ def sync_url(url = nil, &block)
213
+ if url || block_given?
214
+ self._sync_url = url || block
215
+ else
216
+ self._sync_url
217
+ end
218
+ end
219
+
220
+ def updatable_attributes(*attrs)
221
+ return self._updatable_attributes if attrs.blank?
222
+ attrs.each do |attribute|
223
+ updatable_attribute attribute
224
+ end
225
+ end
226
+
227
+ def updatable_attribute(attribute, options = {}, &block)
228
+ options[:block] = block if block_given?
229
+ self._updatable_attributes ||= {}
230
+ self._updatable_attributes[attribute] = options
231
+ end
232
+ end
233
+ end
@@ -37,7 +37,10 @@ module MotionPrime
37
37
  self.send("#{k}=", v) if self.respond_to?("#{k}=")
38
38
  end
39
39
 
40
- self.add_navigation_controller if args[:navigation]
40
+ if @wrap_in_navigation = args[:navigation]
41
+ self.add_navigation_controller
42
+ end
43
+
41
44
  self.on_init if respond_to?(:on_init)
42
45
  self
43
46
  end
@@ -10,9 +10,9 @@ module MotionPrime
10
10
  ensure_wrapper_controller_in_place(screen, args)
11
11
  screen.send(:on_screen_load) if screen.respond_to?(:on_screen_load)
12
12
  if args[:modal]
13
- present_modal_view_controller screen, (args[:animated] || true)
13
+ present_modal_view_controller screen, (args.has_key?(:animated) ? args[:animated] : true)
14
14
  elsif has_navigation?
15
- push_view_controller screen
15
+ push_view_controller screen, args
16
16
  else
17
17
  app_delegate.open_screen(screen.main_controller)
18
18
  end
@@ -32,7 +32,7 @@ module MotionPrime
32
32
 
33
33
  def close_screen(args = {})
34
34
  args ||= {}
35
- args[:animated] ||= true
35
+ args[:animated] = args.has_key?(:animated) ? args[:animated] : true
36
36
  # Pop current view, maybe with arguments, if in navigation controller
37
37
  if modal?
38
38
  close_modal_screen args
@@ -52,8 +52,8 @@ module MotionPrime
52
52
  end
53
53
  end
54
54
 
55
- def push_view_controller(vc)
56
- navigation_controller.pushViewController(vc, animated: true)
55
+ def push_view_controller(vc, args = {})
56
+ navigation_controller.pushViewController(vc, animated: (args.has_key?(:animated) ? args[:animated] : true))
57
57
  end
58
58
 
59
59
  protected
@@ -23,11 +23,15 @@ module MotionPrime
23
23
  def initialize(options = {})
24
24
  @options = options
25
25
  @model = options[:model]
26
- @name = options[:name] ||= self.class.name.demodulize.underscore.gsub(/\_section$/, '')
26
+ @name = options[:name] ||= default_name
27
27
  create_elements
28
28
  self.hide if container_options[:hidden]
29
29
  end
30
30
 
31
+ def default_name
32
+ self.class.name.demodulize.underscore.gsub(/\_section$/, '')
33
+ end
34
+
31
35
  def elements_options
32
36
  self.class.elements_options || {}
33
37
  end
@@ -35,14 +39,23 @@ module MotionPrime
35
39
  def create_elements
36
40
  self.elements = {}
37
41
  elements_options.each do |key, opts|
38
- # we should clone options to prevent overriding options
39
- # in next element with same name in another class
40
- options = opts.clone
41
- options[:section] = self
42
+ next unless render_element?(key)
43
+ options = build_options_for_element(opts)
42
44
  self.elements[key] = MotionPrime::BaseElement.factory(options.delete(:type), options)
43
45
  end
44
46
  end
45
47
 
48
+ def render_element?(element_name)
49
+ true
50
+ end
51
+
52
+ def build_options_for_element(opts)
53
+ # we should clone options to prevent overriding options
54
+ # in next element with same name in another class
55
+ options = opts.clone
56
+ options.merge(section: self)
57
+ end
58
+
46
59
  def render(container_options = {})
47
60
  self.container_options.merge!(container_options)
48
61
  self.screen = container_options.delete(:to)
@@ -20,6 +20,7 @@ module MotionPrime
20
20
  KEYBOARD_HEIGHT_PORTRAIT = 216
21
21
  KEYBOARD_HEIGHT_LANDSCAPE = 162
22
22
 
23
+ class_attribute :text_field_limits, :text_view_limits
23
24
  class_attribute :fields_options
24
25
  attr_accessor :fields, :field_indexes, :keyboard_visible
25
26
 
@@ -30,8 +31,8 @@ module MotionPrime
30
31
  end
31
32
 
32
33
  def render_table
33
- @data_stamp = Time.now.to_i
34
34
  init_form_fields
35
+ set_data_stamp(self.field_indexes.values)
35
36
  self.table_view = screen.table_view(
36
37
  styles: [:base_form, name.to_sym], delegate: self, dataSource: self
37
38
  ).view
@@ -39,12 +40,29 @@ module MotionPrime
39
40
 
40
41
  def render_cell(index, table)
41
42
  item = data[index.row]
42
-
43
- screen.table_view_cell styles: [:base_form_field, :"#{name}_field"], reuse_identifier: cell_name(table, index) do
43
+ styles = [:base_form_field, :"#{name}_field"]
44
+ if item.respond_to?(:container_styles) && item.container_styles.present?
45
+ styles += Array.wrap(item.container_styles)
46
+ end
47
+ screen.table_view_cell styles: styles, reuse_identifier: cell_name(table, index) do |cell_element|
48
+ item.cell_element = cell_element if item.respond_to?(:cell_element)
44
49
  item.render(to: screen)
45
50
  end
46
51
  end
47
52
 
53
+ def reload_cell(section)
54
+ field = section.name.to_sym
55
+ path = table_view.indexPathForRowAtPoint(section.cell.center) # do not use indexPathForCell here as field may be invisibe
56
+ table_view.beginUpdates
57
+ section.cell.removeFromSuperview
58
+
59
+ fields[field] = load_field(self.class.fields_options[field])
60
+ @data = nil
61
+ set_data_stamp([field_indexes[field]])
62
+ table_view.reloadRowsAtIndexPaths([path], withRowAnimation: UITableViewRowAnimationNone)
63
+ table_view.endUpdates
64
+ end
65
+
48
66
  # Returns element based on field name and element name
49
67
  #
50
68
  # Examples:
@@ -88,16 +106,6 @@ module MotionPrime
88
106
  field(field_name).focus
89
107
  end
90
108
 
91
- class << self
92
- def field(name, options = {})
93
- options[:name] = name
94
- options[:type] ||= :string
95
- self.fields_options ||= {}
96
- self.fields_options[name] = options
97
- self.fields_options[name]
98
- end
99
- end
100
-
101
109
  def set_height_with_keyboard
102
110
  return if keyboard_visible
103
111
  self.table_view.height -= KEYBOARD_HEIGHT_PORTRAIT
@@ -148,15 +156,66 @@ module MotionPrime
148
156
  on_input_edit(text_field)
149
157
  end
150
158
 
151
- private
159
+ def textView(text_view, shouldChangeTextInRange:range, replacementText:string)
160
+ limit = (self.class.text_view_limits || {}).find do |field_name, limit|
161
+ view("#{field_name}:input")
162
+ end.try(:last)
163
+ return true unless limit
164
+ allow_string_replacement?(text_view, limit, range, string)
165
+ end
166
+
167
+ def textField(text_field, shouldChangeCharactersInRange:range, replacementString:string)
168
+ limit = (self.class.text_field_limits || {}).find do |field_name, limit|
169
+ view("#{field_name}:input")
170
+ end.try(:last)
171
+ return true unless limit
172
+ allow_string_replacement?(text_field, limit, range, string)
173
+ end
174
+
175
+ def allow_string_replacement?(target, limit, range, string)
176
+ if string.length.zero? || (range.length + limit - target.text.length) >= string.length
177
+ true
178
+ else
179
+ target.text.length < limit
180
+ end
181
+ end
182
+
183
+ def load_field(field)
184
+ klass = "MotionPrime::#{field[:type].classify}FieldSection".constantize
185
+ klass.new(field.merge(form: self))
186
+ end
187
+
188
+ def render_field?(name)
189
+ true
190
+ end
191
+
192
+ class << self
193
+ def field(name, options = {})
194
+ options[:name] = name
195
+ options[:type] ||= :string
196
+ self.fields_options ||= {}
197
+ self.fields_options[name] = options
198
+ self.fields_options[name]
199
+ end
152
200
 
201
+ def limit_text_field_length(name, limit)
202
+ self.text_field_limits ||= {}
203
+ self.text_field_limits[name] = limit
204
+ end
205
+ def limit_text_view_length(name, limit)
206
+ self.text_view_limits ||= {}
207
+ self.text_view_limits[name] = limit
208
+ end
209
+ end
210
+
211
+ private
153
212
  def init_form_fields
154
213
  self.fields = {}
155
214
  self.field_indexes = {}
156
215
  index = 0
157
216
  (self.class.fields_options || []).each do |key, field|
158
- klass = "MotionPrime::#{field[:type].classify}FieldSection".constantize
159
- self.fields[key] = klass.new(field.merge(form: self))
217
+ next unless render_field?(key)
218
+ self.fields[key] = load_field(field)
160
219
  self.field_indexes[key] = index
161
220
  index += 1
162
221
  end