nemo 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/INSTALL +5 -0
- data/README +25 -0
- data/lib/nemo.rb +47 -0
- data/lib/nemo/components.rb +4 -0
- data/lib/nemo/components/calendar.rb +191 -0
- data/lib/nemo/components/confirm.rb +30 -0
- data/lib/nemo/components/multiattr.rb +93 -0
- data/lib/nemo/examples.rb +5 -0
- data/lib/nemo/examples/date_picker.rb +74 -0
- data/lib/nemo/examples/person_editor.rb +293 -0
- data/lib/nemo/metaobject.rb +5 -0
- data/lib/nemo/metaobject/acollector.rb +79 -0
- data/lib/nemo/metaobject/attributes.rb +282 -0
- data/lib/nemo/metaobject/metaobject.rb +86 -0
- data/lib/nemo/metaobject/validation.rb +32 -0
- data/lib/nemo/util.rb +31 -0
- data/lib/nemo/util/accessors.rb +85 -0
- data/lib/nemo/util/blankslate.rb +6 -0
- data/lib/nemo/util/collector.rb +54 -0
- data/lib/nemo/util/date.rb +41 -0
- data/lib/nemo/visitors.rb +4 -0
- data/lib/nemo/visitors/editor.rb +186 -0
- data/lib/nemo/visitors/viewer.rb +63 -0
- data/lib/nemo/visitors/visitor.rb +14 -0
- data/nemo.gemspec +23 -0
- data/nemo_rdoc.bat +1 -0
- data/test/date_picker.rb +14 -0
- data/test/nemo/calendar.css +26 -0
- data/test/nemo/calendar.gif +0 -0
- data/test/nemo/person_editor.css +40 -0
- data/test/person_editor.rb +14 -0
- data/test/unit_test.rb +128 -0
- metadata +86 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
class Nemo::MetaObject::MetaObject
|
2
|
+
|
3
|
+
attr_accessor :attributes, :baseobject
|
4
|
+
|
5
|
+
def initialize(baseobject, options=Hash.new, &block)
|
6
|
+
@attributes = Array.new
|
7
|
+
@baseobject = baseobject
|
8
|
+
options.each { |k,v| send(k.to_s+'=', v) }
|
9
|
+
collect(&block)
|
10
|
+
refresh_cache
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(attribute)
|
14
|
+
attribute.metaobject = self
|
15
|
+
@attributes << attribute
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](*symbols)
|
19
|
+
return attribute_of(symbols.first) if symbols.size == 1
|
20
|
+
symbols.collect { |s| attribute_of(s) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def collector_class
|
24
|
+
Nemo::MetaObject::AttributeCollector
|
25
|
+
end
|
26
|
+
|
27
|
+
def collect(&block)
|
28
|
+
collector_class.new(self).collect(&block)
|
29
|
+
@attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
def accept(visitor, &block)
|
33
|
+
visible_attributes.collect{ |a| a.accept(visitor) }.join
|
34
|
+
end
|
35
|
+
|
36
|
+
def attribute_of(symbol)
|
37
|
+
@attributes.find { |a| a.symbol == symbol }
|
38
|
+
end
|
39
|
+
|
40
|
+
def hide_attribute_of(symbol)
|
41
|
+
a = attribute_of(symbol)
|
42
|
+
a.hidden = true if a
|
43
|
+
end
|
44
|
+
|
45
|
+
def hide_attributes_of(*symbols)
|
46
|
+
symbols.each { |s| hide_attribute_of(s) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def hide!(*symbols)
|
50
|
+
hide_attributes_of(*symbols)
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def multiple_attributes
|
55
|
+
@attributes.find_all { |a| a.multiple_attribute? }
|
56
|
+
end
|
57
|
+
|
58
|
+
def remove_attribute_of(symbol)
|
59
|
+
@attributes.delete(attribute_of(symbol))
|
60
|
+
end
|
61
|
+
|
62
|
+
def remove_attributes_of(*symbols)
|
63
|
+
symbols.each { |s| remove_attribute(s) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def symbols
|
67
|
+
@attributes.collect { |a| a.symbol }
|
68
|
+
end
|
69
|
+
|
70
|
+
def visible_attributes
|
71
|
+
@attributes.reject { |a| a.hidden? }
|
72
|
+
end
|
73
|
+
|
74
|
+
def commit_cache
|
75
|
+
@attributes.each { |a| a.commit_cache }
|
76
|
+
end
|
77
|
+
|
78
|
+
def refresh_cache
|
79
|
+
@attributes.each { |a| a.refresh_cache }
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate
|
83
|
+
@attributes.collect{ |a| a.validate_cache }.compact
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Nemo::MetaObject::ValidationError
|
2
|
+
|
3
|
+
attr_accessor :attribute, :error_string
|
4
|
+
|
5
|
+
def initialize(a, error_string)
|
6
|
+
@attribute = a
|
7
|
+
@error_string = error_string
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
sprintf('%s: %s', @attribute.label, @error_string)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
class Nemo::MetaObject::ValidationRule
|
17
|
+
|
18
|
+
extend Nemo::Util::Accessors
|
19
|
+
|
20
|
+
call_accessor :error_string
|
21
|
+
proc_accessor :rule
|
22
|
+
|
23
|
+
def initialize(&block)
|
24
|
+
@error_string = 'not valid'
|
25
|
+
@rule = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid?(value)
|
29
|
+
begin Nemo::Util.to_bool(@rule.call(value)) if @rule rescue false end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
data/lib/nemo/util.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Utility methods and objects
|
2
|
+
#
|
3
|
+
module Nemo::Util
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Format string as PascalCase (my_var, myVar become MyVar)
|
8
|
+
#
|
9
|
+
def pascal_case(value)
|
10
|
+
value.to_s.split('_').collect{ |w| w[0,1].upcase+w[1..-1] }.join
|
11
|
+
end
|
12
|
+
|
13
|
+
# Converts a value to true or false
|
14
|
+
#
|
15
|
+
# True:: <tt>true, :true, "true", :yes, "yes"</tt>
|
16
|
+
# False:: <tt>false, :false, "false", :no, "no", nil</tt>
|
17
|
+
#
|
18
|
+
# Otherwise (value && true) is applied
|
19
|
+
#
|
20
|
+
def to_bool(value)
|
21
|
+
value = false if [:false, 'false', :no, 'no', nil].include?(value)
|
22
|
+
value = true if [:true, 'true', :yes, 'yes'].include?(value)
|
23
|
+
value && true
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'nemo/util/accessors'
|
29
|
+
require 'nemo/util/blankslate'
|
30
|
+
require 'nemo/util/collector'
|
31
|
+
require 'nemo/util/date'
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Alternate attr_accessor methods
|
2
|
+
#
|
3
|
+
module Nemo::Util::Accessors
|
4
|
+
|
5
|
+
# Call syntax
|
6
|
+
#
|
7
|
+
# class Foo
|
8
|
+
# extend Nemo::Util::Accessors
|
9
|
+
# call_accessor :bar
|
10
|
+
# end
|
11
|
+
# foo = Foo.new
|
12
|
+
# foo.bar = 1 # @bar = 1 (standard writer)
|
13
|
+
# foo.bar(1) # @bar = 1 (call writer)
|
14
|
+
# foo.bar # => 1 (standard reader)
|
15
|
+
#
|
16
|
+
def call_accessor(*symbols)
|
17
|
+
str = String.new
|
18
|
+
class_eval { attr_accessor(*symbols) }
|
19
|
+
symbols.each do |symbol|
|
20
|
+
str << "
|
21
|
+
def #{symbol}(*args)
|
22
|
+
@#{symbol} = args.first if args.size > 0
|
23
|
+
@#{symbol}
|
24
|
+
end
|
25
|
+
"
|
26
|
+
class_eval(str)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Boolean syntax
|
31
|
+
#
|
32
|
+
# class Foo
|
33
|
+
# extend Nemo::Util::Accessors
|
34
|
+
# bool_accessor :bar
|
35
|
+
# end
|
36
|
+
# foo = Foo.new
|
37
|
+
# foo.bar = false # @bar = false (standard writer)
|
38
|
+
# foo.bar # @bar = true (always sets to true)
|
39
|
+
# foo.bar(false) # @bar = false (call writer)
|
40
|
+
# foo.bar? # => true (true or false)
|
41
|
+
#
|
42
|
+
def bool_accessor(*symbols)
|
43
|
+
str = String.new
|
44
|
+
class_eval { attr_accessor(*symbols) }
|
45
|
+
symbols.each do |symbol|
|
46
|
+
str << "
|
47
|
+
def #{symbol}(*args)
|
48
|
+
@#{symbol} = (args.size > 0) ? Nemo::Util.to_bool(args.first) : true
|
49
|
+
end
|
50
|
+
def #{symbol}?
|
51
|
+
Nemo::Util.to_bool(@#{symbol})
|
52
|
+
end
|
53
|
+
"
|
54
|
+
class_eval(str)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Procedure syntax
|
59
|
+
#
|
60
|
+
# class Foo
|
61
|
+
# extend Nemo::Util::Accessors
|
62
|
+
# proc_accessor :bar
|
63
|
+
# end
|
64
|
+
# foo = Foo.new
|
65
|
+
# block = Proc.new { 1 }
|
66
|
+
# foo.bar = block # @bar = Proc.new { 1 } (standard writer)
|
67
|
+
# foo.bar { 1 } # @bar = Proc.new { 1 } (&block writer)
|
68
|
+
# foo.bar # @bar => <Proc:x> (standard reader)
|
69
|
+
# foo.bar.call # => 1
|
70
|
+
#
|
71
|
+
def proc_accessor(*symbols)
|
72
|
+
str = String.new
|
73
|
+
class_eval { attr_accessor(*symbols) }
|
74
|
+
symbols.each do |symbol|
|
75
|
+
str << "
|
76
|
+
def #{symbol}(&block)
|
77
|
+
@#{symbol} = block if block_given?
|
78
|
+
@#{symbol}
|
79
|
+
end
|
80
|
+
"
|
81
|
+
class_eval(str)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Collect a list of objects using minimal syntax,
|
2
|
+
# used in conjunction with Nemo::Util::Accessors.
|
3
|
+
# Use the << or o methods to add an object.
|
4
|
+
# All other messages are forwarded to the most recently added object.
|
5
|
+
#
|
6
|
+
# class Person
|
7
|
+
# extend Nemo::Util::Accessors
|
8
|
+
# call_accessor :name, :age
|
9
|
+
# end
|
10
|
+
# collector = Nemo::Util::ObjectCollector.new
|
11
|
+
# people = collector.collect do
|
12
|
+
# o Person.new
|
13
|
+
# name 'Kevin'
|
14
|
+
# age 28
|
15
|
+
# o Person.new
|
16
|
+
# name 'Sara'
|
17
|
+
# age 22
|
18
|
+
# end
|
19
|
+
# people.inspect
|
20
|
+
# # => [<Person @age=28, @name="Kevin">, <Person @age=22, @name="Sara">]
|
21
|
+
#
|
22
|
+
class Nemo::Util::ObjectCollector < Nemo::Util::BlankSlate
|
23
|
+
|
24
|
+
attr_accessor :index_error
|
25
|
+
attr_accessor :objects
|
26
|
+
|
27
|
+
def self.collect(&block)
|
28
|
+
self.new.collect(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@objects = Array.new
|
33
|
+
@index_error = 'no objects defined'
|
34
|
+
end
|
35
|
+
|
36
|
+
def <<(object)
|
37
|
+
@objects << object
|
38
|
+
end
|
39
|
+
|
40
|
+
def o(object)
|
41
|
+
self << object
|
42
|
+
end
|
43
|
+
|
44
|
+
def collect(&block)
|
45
|
+
instance_eval(&block) if block_given?
|
46
|
+
@objects
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_missing(symbol, *args, &block)
|
50
|
+
raise IndexError, @index_error if @objects.size == 0
|
51
|
+
@objects.last.send(symbol, *args, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Date
|
2
|
+
|
3
|
+
# Calendar representation of a month. Consists of an
|
4
|
+
# array of weeks, each week an array of 7 days, each day a Date object.
|
5
|
+
# Padded with days showing for previous and next month.
|
6
|
+
#
|
7
|
+
def calendar
|
8
|
+
# months
|
9
|
+
curr_month = Date.new(self.year, self.month, 1)
|
10
|
+
prev_month = (curr_month << 1)
|
11
|
+
next_month = (curr_month >> 1)
|
12
|
+
|
13
|
+
# previous month days
|
14
|
+
prev_days = Array.new
|
15
|
+
prev_in_curr = curr_month.wday
|
16
|
+
((curr_month-1)-(prev_in_curr-1)).upto(curr_month-1) { |d| prev_days << d }
|
17
|
+
|
18
|
+
# current month days
|
19
|
+
curr_days = Array.new
|
20
|
+
curr_month.upto(next_month-1) { |d| curr_days << d }
|
21
|
+
|
22
|
+
# next month days
|
23
|
+
next_days = Array.new
|
24
|
+
days = prev_days+curr_days
|
25
|
+
weeks = (days.size.to_f/7).ceil
|
26
|
+
cdays_size = weeks*7
|
27
|
+
next_in_curr = (cdays_size-days.size)
|
28
|
+
next_month.upto(next_month+(next_in_curr-1)) { |d| next_days << d }
|
29
|
+
days += next_days
|
30
|
+
|
31
|
+
# split into weeks
|
32
|
+
table = Array.new
|
33
|
+
days.each do |day|
|
34
|
+
table << Array.new if table.size == 0 or table.last.size == 7
|
35
|
+
table.last << day
|
36
|
+
end
|
37
|
+
|
38
|
+
table
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
class Nemo::Visitors::Editor < Nemo::Visitors::Visitor
|
2
|
+
|
3
|
+
attr_accessor :validation_errors, :multiple_attribute_editors
|
4
|
+
bool_accessor :subform
|
5
|
+
|
6
|
+
def initialize(metaobject)
|
7
|
+
super
|
8
|
+
@subform = false
|
9
|
+
@validation_errors = Array.new
|
10
|
+
initialize_subforms
|
11
|
+
end
|
12
|
+
|
13
|
+
def multiattr_class
|
14
|
+
Nemo::Components::MultipleAttributeContainer
|
15
|
+
end
|
16
|
+
|
17
|
+
def calendar_class
|
18
|
+
Nemo::Components::MiniCalendar
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize_subforms
|
22
|
+
@multiple_attribute_editors = Hash.new
|
23
|
+
@metaobject.multiple_attributes.each do |a|
|
24
|
+
@multiple_attribute_editors[a] = multiattr_class.new(a)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid?
|
29
|
+
@validation_errors = @metaobject.validate
|
30
|
+
@validation_errors.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def validation_errors?
|
34
|
+
@validation_errors.size > 0
|
35
|
+
end
|
36
|
+
|
37
|
+
def children
|
38
|
+
@multiple_attribute_editors.values
|
39
|
+
end
|
40
|
+
|
41
|
+
def backtrack_state(snapshot)
|
42
|
+
super
|
43
|
+
snapshot.add(self.children)
|
44
|
+
end
|
45
|
+
|
46
|
+
def commit
|
47
|
+
@metaobject.commit_cache
|
48
|
+
end
|
49
|
+
|
50
|
+
def cancel
|
51
|
+
answer nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def save
|
55
|
+
if valid?
|
56
|
+
commit
|
57
|
+
answer @metaobject.baseobject
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def remove_errors(a)
|
62
|
+
@validation_errors.delete_if { |e| e.attribute == a }
|
63
|
+
end
|
64
|
+
|
65
|
+
def call_date_selector(a)
|
66
|
+
current = (a.date_from_cache || Date.today)
|
67
|
+
if date = call( calendar_class.new(current) )
|
68
|
+
a.date_to_cache(date)
|
69
|
+
remove_errors(a)
|
70
|
+
if error = a.validate_cache
|
71
|
+
@validation_errors << error
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_buttons
|
77
|
+
r.table_row do
|
78
|
+
r.table_data.colspan(2).with do
|
79
|
+
r.submit_button.id(@metaobject.object_id.to_s+'_save').value('save').callback{ save }
|
80
|
+
r.space
|
81
|
+
r.submit_button.id(@metaobject.object_id.to_s+'_cancel').value('cancel').callback{ cancel }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def render
|
87
|
+
if subform?
|
88
|
+
render_main
|
89
|
+
else
|
90
|
+
r.form.id(@metaobject.object_id).with { render_main }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def render_label(a, &block)
|
95
|
+
r.table_row do
|
96
|
+
r.table_header(a.label)
|
97
|
+
r.table_data { block ? block.call : r.space }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def render_main
|
102
|
+
r.table do
|
103
|
+
render_validation_errors
|
104
|
+
render_fields
|
105
|
+
render_buttons
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def render_fields
|
110
|
+
@metaobject.accept(self)
|
111
|
+
end
|
112
|
+
|
113
|
+
def render_validation_errors
|
114
|
+
if validation_errors?
|
115
|
+
r.table_row { r.table_header.colspan(2).with('The following fields are not valid. Please correct.') }
|
116
|
+
r.table_row { r.table_data.colspan(2).with { @validation_errors.each { |e| r.text('• '); r.encode_text(e.to_s); r.break } } }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def visit_text_attribute(a)
|
121
|
+
return if a.read_only?
|
122
|
+
render_label(a) do
|
123
|
+
if a.multiline?
|
124
|
+
r.textarea.id(a.object_id).callback{ |input| @metaobject[a.symbol].cache = input }.with { r.encode_text(a.cache) }
|
125
|
+
else
|
126
|
+
r.text_input.id(a.object_id).maxlength(a.maxlength).value(a.cache).callback { |input| @metaobject[a.symbol].cache = input }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def visit_password_attribute(a)
|
132
|
+
return if a.read_only?
|
133
|
+
render_label(a) do
|
134
|
+
r.text_input.id(a.object_id).type('password').maxlength(a.maxlength).value(a.cache).callback { |input| @metaobject[a.symbol].cache = input }
|
135
|
+
r.break
|
136
|
+
r.text_input.id(a.object_id.to_s+'_confirm').type('password').maxlength(a.maxlength).value(a.cache_confirm).callback { |input| @metaobject[a.symbol].cache_confirm = input }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def visit_date_attribute(a)
|
141
|
+
return if a.read_only?
|
142
|
+
date = a.cache
|
143
|
+
render_label(a) do
|
144
|
+
r.text_input.id(a.object_id).maxlength(a.maxlength).value(a.cache).callback { |input| @metaobject[a.symbol].cache = input }
|
145
|
+
r.space
|
146
|
+
r.nemo_calendar_button.callback { call_date_selector(a) }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def visit_boolean_attribute(a)
|
151
|
+
return if a.read_only?
|
152
|
+
render_label(a) do
|
153
|
+
r.select_list(a.items).id(a.object_id).labels(a.formatted_items).selected([a.cache]).callback do |input|
|
154
|
+
@metaobject[a.symbol].cache = Nemo::Util.to_bool(input.first)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def visit_multiple_attribute(a)
|
160
|
+
return if a.read_only?
|
161
|
+
render_label(a) do
|
162
|
+
r.render( @multiple_attribute_editors[a] )
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def visit_single_relationship_attribute(a)
|
167
|
+
return if a.read_only?
|
168
|
+
render_label(a) do
|
169
|
+
r.select_list(a.items).id(a.object_id).labels(a.formatted_items).selected([a.cache]).callback do |input|
|
170
|
+
@metaobject[a.symbol].cache = input.first
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def visit_multiple_relationship_attribute(a)
|
176
|
+
return if a.read_only?
|
177
|
+
items = a.items
|
178
|
+
size = (items.size < 8) ? items.size : 8
|
179
|
+
render_label(a) do
|
180
|
+
r.select_list(a.items).id(a.object_id).size(size).multiple.labels(a.formatted_items).selected(a.cache).callback do |input|
|
181
|
+
@metaobject[a.symbol].cache = input
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|