motion-prime 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.
- data/.gitignore +17 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +83 -0
- data/README.md +67 -0
- data/Rakefile +23 -0
- data/app/app_delegate.rb +5 -0
- data/doc/SECTION.md +7 -0
- data/doc/STYLE.md +39 -0
- data/files/Gemfile +3 -0
- data/files/Rakefile +16 -0
- data/files/app/app_delegate.rb +4 -0
- data/files/app/screens/application_screen.rb +3 -0
- data/lib/motion-prime.rb +14 -0
- data/lib/view_styler.rb +141 -0
- data/motion-prime.gemspec +27 -0
- data/motion-prime/app_delegate.rb +56 -0
- data/motion-prime/elements/base.rb +94 -0
- data/motion-prime/elements/button.rb +7 -0
- data/motion-prime/elements/draw.rb +56 -0
- data/motion-prime/elements/draw/image.rb +43 -0
- data/motion-prime/elements/draw/label.rb +13 -0
- data/motion-prime/elements/image.rb +14 -0
- data/motion-prime/elements/label.rb +20 -0
- data/motion-prime/elements/text_field.rb +7 -0
- data/motion-prime/elements/text_view.rb +7 -0
- data/motion-prime/helpers/has_authorization.rb +10 -0
- data/motion-prime/helpers/has_search_bar.rb +25 -0
- data/motion-prime/models/base.rb +220 -0
- data/motion-prime/screens/_aliases_mixin.rb +32 -0
- data/motion-prime/screens/_base_mixin.rb +119 -0
- data/motion-prime/screens/_navigation_bar_mixin.rb +57 -0
- data/motion-prime/screens/_navigation_mixin.rb +118 -0
- data/motion-prime/screens/_orientations_mixin.rb +39 -0
- data/motion-prime/screens/base_screen.rb +22 -0
- data/motion-prime/screens/sidebar_container_screen.rb +58 -0
- data/motion-prime/sections/base.rb +101 -0
- data/motion-prime/sections/draw.rb +62 -0
- data/motion-prime/sections/form.rb +103 -0
- data/motion-prime/sections/form/base_field_section.rb +26 -0
- data/motion-prime/sections/form/password_field_section.rb +33 -0
- data/motion-prime/sections/form/select_field_section.rb +40 -0
- data/motion-prime/sections/form/string_field_section.rb +32 -0
- data/motion-prime/sections/form/submit_field_section.rb +20 -0
- data/motion-prime/sections/form/text_field_section.rb +33 -0
- data/motion-prime/sections/table.rb +97 -0
- data/motion-prime/sections/table/refresh_mixin.rb +13 -0
- data/motion-prime/styles/forms.rb +93 -0
- data/motion-prime/support/_key_value_store.rb +10 -0
- data/motion-prime/support/dm_button.rb +22 -0
- data/motion-prime/support/dm_cell_with_section.rb +12 -0
- data/motion-prime/support/dm_text_field.rb +30 -0
- data/motion-prime/support/dm_text_view.rb +93 -0
- data/motion-prime/support/dm_view_controller.rb +50 -0
- data/motion-prime/support/dm_view_with_section.rb +11 -0
- data/motion-prime/support/navigation_controller.rb +4 -0
- data/motion-prime/support/ui_search_bar_custom.rb +10 -0
- data/motion-prime/support/ui_view.rb +59 -0
- data/motion-prime/version.rb +3 -0
- data/motion-prime/views/layout.rb +45 -0
- data/motion-prime/views/styles.rb +44 -0
- data/motion-prime/views/view_builder.rb +80 -0
- data/motion-prime/views/view_styler.rb +141 -0
- data/resources/Default-568h@2x.png +0 -0
- data/spec/main_spec.rb +9 -0
- metadata +245 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../motion-prime/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "motion-prime"
|
6
|
+
spec.version = MotionPrime::VERSION
|
7
|
+
spec.authors = ["Iskander Haziev"]
|
8
|
+
spec.email = ["gvalmon@gmail.com"]
|
9
|
+
spec.description = %q{RubyMotion apps development framework}
|
10
|
+
spec.summary = %q{RubyMotion apps development framework}
|
11
|
+
spec.homepage = ""
|
12
|
+
spec.license = ""
|
13
|
+
|
14
|
+
spec.files = `git ls-files`.split($\)
|
15
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
16
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
|
19
|
+
spec.add_development_dependency "rake"
|
20
|
+
spec.add_dependency "cocoapods"
|
21
|
+
spec.add_dependency "motion-cocoapods"
|
22
|
+
spec.add_dependency "motion-require"
|
23
|
+
spec.add_dependency "motion-support"
|
24
|
+
spec.add_dependency 'bubble-wrap'
|
25
|
+
spec.add_dependency 'sugarcube'
|
26
|
+
spec.add_dependency 'nano-store'
|
27
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
motion_require './helpers/has_authorization'
|
2
|
+
module MotionPrime
|
3
|
+
class BaseAppDelegate
|
4
|
+
include MotionPrime::HasAuthorization
|
5
|
+
|
6
|
+
attr_accessor :window, :sidebar_container
|
7
|
+
|
8
|
+
def application(application, didFinishLaunchingWithOptions:launch_options)
|
9
|
+
on_load(application, launch_options)
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def app_delegate
|
14
|
+
UIApplication.sharedApplication.delegate
|
15
|
+
end
|
16
|
+
|
17
|
+
def app_window
|
18
|
+
self.app_delegate.window
|
19
|
+
end
|
20
|
+
|
21
|
+
def open_root_screen(screen)
|
22
|
+
screen.send(:on_screen_load) if screen.respond_to?(:on_screen_load)
|
23
|
+
screen = screen.main_controller if screen.respond_to?(:main_controller)
|
24
|
+
|
25
|
+
self.window ||= UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
|
26
|
+
self.window.rootViewController = screen
|
27
|
+
self.window.makeKeyAndVisible
|
28
|
+
screen
|
29
|
+
end
|
30
|
+
|
31
|
+
def open_screen(screen)
|
32
|
+
if sidebar?
|
33
|
+
sidebar_container.content_controller = screen
|
34
|
+
else
|
35
|
+
open_root_screen(screen)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def sidebar?
|
40
|
+
!sidebar_container.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
def open_with_sidebar(content, menu, options={})
|
44
|
+
self.sidebar_container = SidebarContainerScreen.new(menu, content, options)
|
45
|
+
open_root_screen(sidebar_container)
|
46
|
+
end
|
47
|
+
|
48
|
+
def show_sidebar
|
49
|
+
sidebar_container.show_sidebar
|
50
|
+
end
|
51
|
+
|
52
|
+
def hide_sidebar
|
53
|
+
sidebar_container.hide_sidebar
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module MotionPrime
|
2
|
+
class BaseElement
|
3
|
+
# MotionPrime::BaseElement is container for UIView class elements with options.
|
4
|
+
# Elements are located inside Sections
|
5
|
+
|
6
|
+
include ::MotionSupport::Callbacks
|
7
|
+
attr_accessor :options, :section, :name,
|
8
|
+
:view_class, :view, :styles, :screen
|
9
|
+
|
10
|
+
define_callbacks :render
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@options = options
|
14
|
+
@section = WeakRef.new(options.delete(:section))
|
15
|
+
@name = options[:name]
|
16
|
+
@block = options.delete(:block)
|
17
|
+
@view_class = options.delete(:view_class) || "UIView"
|
18
|
+
@view_name = self.class.name.demodulize.underscore.gsub('_element', '')
|
19
|
+
end
|
20
|
+
|
21
|
+
def render(options = {}, &block)
|
22
|
+
self.screen = options[:to]
|
23
|
+
run_callbacks :render do
|
24
|
+
render!(&block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def render!(&block)
|
29
|
+
@view = screen.add_view view_class.constantize, computed_options, &block
|
30
|
+
end
|
31
|
+
|
32
|
+
# Lazy-computing options
|
33
|
+
def computed_options
|
34
|
+
compute_options! if @computed_options.blank?
|
35
|
+
@computed_options
|
36
|
+
end
|
37
|
+
|
38
|
+
def compute_options!
|
39
|
+
@computed_options = options
|
40
|
+
compute_block_options
|
41
|
+
compute_style_options
|
42
|
+
@computed_options = normalize_options(@computed_options)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Compute options sent inside block, e.g.
|
46
|
+
# element :button do
|
47
|
+
# {name: model.name}
|
48
|
+
# end
|
49
|
+
def compute_block_options
|
50
|
+
if block = @block
|
51
|
+
@computed_options.merge!(section.send :instance_eval, &block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def compute_style_options
|
56
|
+
@styles = [:"base_#{@view_name}"]
|
57
|
+
@styles += Array.wrap(@computed_options.delete(:styles))
|
58
|
+
@styles += [:"#{section.name}_#{name}"] if section.present?
|
59
|
+
@computed_options.merge!(style_options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def style_options
|
63
|
+
Styles.for(styles)
|
64
|
+
end
|
65
|
+
|
66
|
+
def normalize_options(options)
|
67
|
+
options.each do |key, option|
|
68
|
+
options[key] = if option.is_a?(Proc) && key != :block
|
69
|
+
section.send :instance_eval, &option
|
70
|
+
else
|
71
|
+
option
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class << self
|
77
|
+
def factory(type, options = {})
|
78
|
+
class_name = "#{type.classify}Element"
|
79
|
+
options.merge!({view_class: "UI#{type.classify}"})
|
80
|
+
if MotionPrime.const_defined?(class_name)
|
81
|
+
"MotionPrime::#{class_name}".constantize.new(options)
|
82
|
+
else
|
83
|
+
self.new(options)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
def before_render(method_name)
|
87
|
+
set_callback :render, :before, method_name
|
88
|
+
end
|
89
|
+
def after_render(method_name)
|
90
|
+
set_callback :render, :after, method_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module MotionPrime
|
2
|
+
class DrawElement < BaseElement
|
3
|
+
# MotionPrime::DrawElement is container for drawRect method options.
|
4
|
+
# Elements are located inside Sections
|
5
|
+
|
6
|
+
def render!
|
7
|
+
end
|
8
|
+
|
9
|
+
def view
|
10
|
+
@view ||= section.container_view
|
11
|
+
end
|
12
|
+
|
13
|
+
def computed_left
|
14
|
+
width = computed_options[:width]
|
15
|
+
left = computed_options[:left]
|
16
|
+
right = computed_options[:right]
|
17
|
+
return left if left
|
18
|
+
return 0 if right.nil?
|
19
|
+
|
20
|
+
width = 0.0 if width.nil?
|
21
|
+
max_width = view.bounds.size.width
|
22
|
+
|
23
|
+
# calculate left if width is relative, e.g 0.7
|
24
|
+
if width > 0 && width <= 1
|
25
|
+
max_width - (max_width * width) - right
|
26
|
+
else
|
27
|
+
max_width - width - right
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def computed_top
|
32
|
+
height = computed_options[:height]
|
33
|
+
top = computed_options[:top]
|
34
|
+
bottom = computed_options[:bottom]
|
35
|
+
return top if top
|
36
|
+
return 0 if bottom.nil?
|
37
|
+
|
38
|
+
height = 0.0 if height.nil?
|
39
|
+
max_height = view.bounds.size.height
|
40
|
+
|
41
|
+
# calculate top if height is relative, e.g 0.7
|
42
|
+
if height > 0 && height <= 1
|
43
|
+
max_height - (max_height * height) - bottom
|
44
|
+
else
|
45
|
+
max_height - height - bottom
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class << self
|
50
|
+
def factory(type, options = {})
|
51
|
+
class_name = "#{type.classify}DrawElement"
|
52
|
+
"MotionPrime::#{class_name}".constantize.new(options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
motion_require '../draw.rb'
|
2
|
+
module MotionPrime
|
3
|
+
class ImageDrawElement < DrawElement
|
4
|
+
attr_accessor :image_data
|
5
|
+
def draw_in(rect)
|
6
|
+
image_rect = CGRectMake(
|
7
|
+
computed_left,
|
8
|
+
computed_top,
|
9
|
+
computed_options[:width],
|
10
|
+
computed_options[:height]
|
11
|
+
)
|
12
|
+
# draw already initialized image
|
13
|
+
if image_data
|
14
|
+
image_data.drawInRect(image_rect)
|
15
|
+
# draw image from resources
|
16
|
+
elsif computed_options[:image]
|
17
|
+
self.image_data = computed_options[:image].uiimage
|
18
|
+
image_data.drawInRect(image_rect)
|
19
|
+
# show default image and download image from url
|
20
|
+
elsif computed_options[:url]
|
21
|
+
if computed_options[:default]
|
22
|
+
computed_options[:default].uiimage.drawInRect(image_rect)
|
23
|
+
end
|
24
|
+
manager = SDWebImageManager.sharedManager
|
25
|
+
manager.downloadWithURL(computed_options[:url],
|
26
|
+
options: 0,
|
27
|
+
progress: lambda{ |r_size, e_size| },
|
28
|
+
completed: lambda{ |image, error, type, finished|
|
29
|
+
if image
|
30
|
+
self.image_data = image
|
31
|
+
if type == SDImageCacheTypeNone || type == SDImageCacheTypeDisk
|
32
|
+
# if it's first call, we should redraw view, because it's async
|
33
|
+
section.container_view.setNeedsDisplay
|
34
|
+
else
|
35
|
+
# if it's second call, we should just draw image
|
36
|
+
self.image_data.drawInRect(image_rect)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
} )
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
motion_require '../draw.rb'
|
2
|
+
module MotionPrime
|
3
|
+
class LabelDrawElement < DrawElement
|
4
|
+
def draw_in(rect)
|
5
|
+
color = computed_options[:color] || computed_options[:text_color]
|
6
|
+
color.uicolor.set if color
|
7
|
+
computed_options[:text].to_s.drawAtPoint(
|
8
|
+
CGPointMake(computed_left, computed_top),
|
9
|
+
withFont: computed_options[:font]
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module MotionPrime
|
2
|
+
class ImageElement < BaseElement
|
3
|
+
after_render :fetch_image
|
4
|
+
def view_class
|
5
|
+
"UIImageView"
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch_image
|
9
|
+
return unless computed_options[:url]
|
10
|
+
view.setImageWithURL NSURL.URLWithString(computed_options[:url]),
|
11
|
+
placeholderImage: computed_options[:default].uiimage
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module MotionPrime
|
2
|
+
class LabelElement < BaseElement
|
3
|
+
after_render :size_to_fit
|
4
|
+
|
5
|
+
def size_to_fit
|
6
|
+
if computed_options[:size_to_fit] || style_options[:size_to_fit]
|
7
|
+
view.sizeToFit
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def height
|
12
|
+
width = computed_options[:width]
|
13
|
+
font = computed_options[:font] || :system.uifont
|
14
|
+
raise "Please set element width for height calculation" unless width
|
15
|
+
computed_options[:text].sizeWithFont(font,
|
16
|
+
constrainedToSize: [width, Float::MAX],
|
17
|
+
lineBreakMode:UILineBreakModeWordWrap).height
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# This module adds search functionality, to Screen or TableSection
|
2
|
+
module MotionPrime
|
3
|
+
module HasSearchBar
|
4
|
+
def add_search_bar(&block)
|
5
|
+
search_bar = create_search_bar
|
6
|
+
search_bar.delegate = self
|
7
|
+
self.table_view.tableHeaderView = search_bar if is_a?(TableSection)
|
8
|
+
@search_callback = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_search_bar
|
12
|
+
name = is_a?(TableSection) ? name : self.class.name.underscore
|
13
|
+
screen = is_a?(TableSection) ? self.screen : self
|
14
|
+
screen.search_bar(styles: [:"base_search_bar", :"#{name}_search_bar"]).view
|
15
|
+
end
|
16
|
+
|
17
|
+
def searchBar(search_bar, textDidChange: text)
|
18
|
+
@search_callback.call(text)
|
19
|
+
end
|
20
|
+
|
21
|
+
def searchBarSearchButtonClicked(search_bar)
|
22
|
+
search_bar.resignFirstResponder
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
motion_require '../helpers/has_authorization'
|
2
|
+
module MotionPrime
|
3
|
+
class BaseModel < NanoStore::Model
|
4
|
+
class_attribute :sync_url
|
5
|
+
class_attribute :sync_attributes
|
6
|
+
class_attribute :_associations
|
7
|
+
alias_method :attributes, :info
|
8
|
+
include MotionPrime::HasAuthorization
|
9
|
+
|
10
|
+
def sync_url
|
11
|
+
self.class.sync_url.to_s.gsub(':id', id.to_s)
|
12
|
+
end
|
13
|
+
|
14
|
+
def model_name
|
15
|
+
self.class.name.underscore
|
16
|
+
end
|
17
|
+
|
18
|
+
def new_record?
|
19
|
+
id.blank?
|
20
|
+
end
|
21
|
+
|
22
|
+
def destroy(&block)
|
23
|
+
use_callback = block_given?
|
24
|
+
api_client.delete(sync_url) do
|
25
|
+
block.call() if use_callback
|
26
|
+
end
|
27
|
+
delete
|
28
|
+
end
|
29
|
+
|
30
|
+
# fetch attributes from url
|
31
|
+
def sync_with_url(url, &block)
|
32
|
+
api_client.get(url) do |data|
|
33
|
+
if data.present?
|
34
|
+
sync_with_attributes(data, &block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def update_with_url(url, &block)
|
40
|
+
use_callback = block_given?
|
41
|
+
post_data = { model_name => filtered_sync_attributes}
|
42
|
+
api_client.send(id ? :put : :post, url, post_data) do |data|
|
43
|
+
self.id ||= data['id']
|
44
|
+
block.call() if use_callback
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# set attributes
|
49
|
+
def sync_with_attributes(attrs, &block)
|
50
|
+
attrs.each do |key, value|
|
51
|
+
if respond_to?(:"sync_#{key}")
|
52
|
+
self.send(:"sync_#{key}", value)
|
53
|
+
elsif respond_to?(:"#{key}=")
|
54
|
+
self.send(:"#{key}=", value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
block.call(self) if block_given?
|
58
|
+
end
|
59
|
+
|
60
|
+
def sync!(sync_options = {}, &block)
|
61
|
+
sync(sync_options.merge(save: true), &block)
|
62
|
+
end
|
63
|
+
|
64
|
+
# sync with url and
|
65
|
+
# TODO: order of fetch/update should be based on updated time
|
66
|
+
def sync(sync_options = {}, &block)
|
67
|
+
use_callback = block_given?
|
68
|
+
should_fetch = sync_options[:fetch]
|
69
|
+
should_update = sync_options[:update]
|
70
|
+
|
71
|
+
should_fetch = !new_record? if should_fetch.nil?
|
72
|
+
should_update = new_record? if should_update.nil?
|
73
|
+
|
74
|
+
sync_with_url self.sync_url do
|
75
|
+
save if sync_options[:save]
|
76
|
+
block.call if use_callback
|
77
|
+
end if should_fetch
|
78
|
+
update_with_url self.sync_url do
|
79
|
+
save if sync_options[:save]
|
80
|
+
block.call if use_callback
|
81
|
+
end if should_update
|
82
|
+
|
83
|
+
sync_associations(sync_options)
|
84
|
+
end
|
85
|
+
|
86
|
+
def sync_associations(sync_options = {})
|
87
|
+
(self.class._associations || []).each do |key, options|
|
88
|
+
sync_association(key, sync_options)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def sync_association(key, sync_options = {}, &block)
|
93
|
+
options = self.class._associations[key]
|
94
|
+
return unless options[:sync_url]
|
95
|
+
options[:type] == :many ?
|
96
|
+
sync_has_many(key, options, sync_options, &block) :
|
97
|
+
sync_has_one(key, options, sync_options, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def sync_has_many(key, options = {}, sync_options = {}, &block)
|
101
|
+
old_collection = self.send(key)
|
102
|
+
use_callback = block_given?
|
103
|
+
puts "SYNC: started sync for #{key} in #{self.class.name}"
|
104
|
+
api_client.get(options[:sync_url]) do |data|
|
105
|
+
if data.present?
|
106
|
+
# Update/Create existing records
|
107
|
+
data.each do |attributes|
|
108
|
+
model = old_collection.detect{ |model| model.id == attributes[:id]}
|
109
|
+
unless model
|
110
|
+
model = key.singularize.to_s.classify.constantize.new
|
111
|
+
self.send(:"#{key}_bag") << model
|
112
|
+
end
|
113
|
+
model.sync_with_attributes(attributes)
|
114
|
+
model.save if sync_options[:save]
|
115
|
+
end
|
116
|
+
old_collection.each do |old_model|
|
117
|
+
model = data.detect{ |model| model[:id] == old_model.id}
|
118
|
+
unless model
|
119
|
+
old_model.delete
|
120
|
+
end
|
121
|
+
end
|
122
|
+
save if sync_options[:save]
|
123
|
+
puts "SYNC: finished sync for #{key} in #{self.class.name}"
|
124
|
+
block.call if use_callback
|
125
|
+
else
|
126
|
+
puts "SYNC ERROR: failed sync for #{key} in #{self.class.name}"
|
127
|
+
block.call if use_callback
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def sync_has_one(key, options = {}, &block)
|
133
|
+
# TODO: add implementation
|
134
|
+
end
|
135
|
+
|
136
|
+
def inspect
|
137
|
+
"#<#{self.class}:0x#{self.object_id.to_s(16)}> " + BW::JSON.generate(attributes)
|
138
|
+
end
|
139
|
+
|
140
|
+
# NOTE: .clear method doesn't work, using removeArray hack for now
|
141
|
+
def _clear_bag(bag_name)
|
142
|
+
bag = self.send(bag_name.to_sym)
|
143
|
+
bag_copy = bag.to_a.clone
|
144
|
+
bag - bag.to_a # this removes association from model
|
145
|
+
bag_copy.each(&:delete) # this removes collection from db
|
146
|
+
end
|
147
|
+
|
148
|
+
def filtered_sync_attributes
|
149
|
+
return attributes if self.class.sync_attributes.blank?
|
150
|
+
attributes.reject do |key, value|
|
151
|
+
self.class.sync_attributes.exclude?(key.to_sym)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class << self
|
156
|
+
def sync_url(url = nil)
|
157
|
+
url ? self.sync_url = url : super
|
158
|
+
end
|
159
|
+
|
160
|
+
def sync_attributes(*attrs)
|
161
|
+
attrs ? self.sync_attributes = attrs : super
|
162
|
+
end
|
163
|
+
|
164
|
+
def has_one(association_name, options = {})
|
165
|
+
bag_name = "#{association_name.pluralize}_bag"
|
166
|
+
self.bag bag_name.to_sym
|
167
|
+
|
168
|
+
self._associations ||= {}
|
169
|
+
self._associations[association_name] = options.merge(type: :one)
|
170
|
+
|
171
|
+
define_method("#{association_name}=") do |value|
|
172
|
+
self._clear_bag(bag_name)
|
173
|
+
|
174
|
+
self.send(:"#{bag_name}") << value
|
175
|
+
value
|
176
|
+
end
|
177
|
+
define_method("#{association_name}_attributes=") do |value|
|
178
|
+
self._clear_bag(bag_name)
|
179
|
+
|
180
|
+
association = association_name.classify.constantize.new
|
181
|
+
association.sync_with_attributes(value)
|
182
|
+
association.save
|
183
|
+
self.send(:"#{bag_name}") << association
|
184
|
+
association
|
185
|
+
end
|
186
|
+
define_method("#{association_name}") do
|
187
|
+
self.send(:"#{bag_name}").to_a.first
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def has_many(association_name, options = {})
|
192
|
+
bag_name = "#{association_name}_bag"
|
193
|
+
self.bag bag_name.to_sym
|
194
|
+
|
195
|
+
self._associations ||= {}
|
196
|
+
self._associations[association_name] = options.merge(type: :many)
|
197
|
+
|
198
|
+
define_method("#{association_name}_attributes=") do |value|
|
199
|
+
self._clear_bag(bag_name)
|
200
|
+
|
201
|
+
association = []
|
202
|
+
value.each do |attrs|
|
203
|
+
model = association_name.classify.constantize.new
|
204
|
+
model.sync_with_attributes(attrs)
|
205
|
+
association << model
|
206
|
+
end
|
207
|
+
self.send(:"#{bag_name}=", association)
|
208
|
+
association
|
209
|
+
end
|
210
|
+
define_method("#{association_name}=") do |value|
|
211
|
+
self._clear_bag(bag_name)
|
212
|
+
self.send(:"#{bag_name}=", value)
|
213
|
+
end
|
214
|
+
define_method("#{association_name}") do
|
215
|
+
self.send(:"#{bag_name}").to_a
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|