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.
@@ -0,0 +1,293 @@
1
+ # Web interface for maintaining a list of Person objects
2
+ #
3
+ module Nemo::Examples::PersonEditor; end
4
+
5
+ # Wrap standard editor in a page
6
+ #
7
+ class Nemo::Examples::PersonEditor::Editor < Nemo::Visitors::Editor
8
+
9
+ def render
10
+ r.page do
11
+ r.head do
12
+ r.title('Person Editor')
13
+ r.link.type('text/css').rel('stylesheet').href('/nemo/person_editor.css')
14
+ r.style('body { text-align: center }')
15
+ end
16
+ super
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ # Used by render to display model as a single table
23
+ # with one row per record, versus one table per record.
24
+ #
25
+ class Nemo::Examples::PersonEditor::ListItemViewer < Nemo::Visitors::Viewer
26
+
27
+ def render
28
+ render_fields
29
+ end
30
+
31
+ def render_label(a, &block)
32
+ r.table_data { block ? block.call : r.space }
33
+ end
34
+
35
+ end
36
+
37
+ # Maintain a list of Person objects
38
+ #
39
+ class Nemo::Examples::PersonEditor::DomainModel
40
+
41
+ attr_accessor :persons
42
+
43
+ def initialize
44
+ @persons = Array.new
45
+ end
46
+
47
+ def add_person(person)
48
+ @persons << person
49
+ end
50
+
51
+ def remove_person(person)
52
+ @persons.delete(person)
53
+ end
54
+
55
+ def male_persons
56
+ @persons.find_all { |p| p.sex == true }
57
+ end
58
+
59
+ def female_persons
60
+ @persons.find_all { |p| p.sex == false }
61
+ end
62
+
63
+ end
64
+
65
+ class Nemo::Examples::PersonEditor::Session < Wee::Session
66
+
67
+ # Initialize the Wee session
68
+ #
69
+ def initialize
70
+ super do
71
+ self.root_component = Nemo::Examples::PersonEditor::Root.new
72
+ self.page_store = Wee::Utils::LRUCache.new(25) # backtrack up to 25 pages
73
+ end
74
+ end
75
+
76
+ # Create a new Person
77
+ #
78
+ # person(:name=>'Joe', birthday=Date.new(1950,1,1))
79
+ #
80
+ def person(hash=Hash.new)
81
+ obj = Nemo::Examples::PersonEditor::Person.new
82
+ hash.each { |k,v| obj.send(k.to_s+'=', v) }
83
+ obj
84
+ end
85
+
86
+ # Create a new PhoneNumber
87
+ #
88
+ # phone_number(:type=>'home', :number=>'281-328-1328')
89
+ #
90
+ def phone_number(hash=Hash.new)
91
+ obj = Nemo::Examples::PersonEditor::PhoneNumber.new
92
+ hash.each { |k,v| obj.send(k.to_s+'=', v) }
93
+ obj
94
+ end
95
+
96
+ # Create a new DomainModel and populate with data.
97
+ # Define and store in a constant on creation, return existing if already defined.
98
+ #
99
+ def domainmodel
100
+ unless Nemo::Examples::PersonEditor.const_defined?(:Model)
101
+ Nemo::Examples::PersonEditor.const_set(:Model, Nemo::Examples::PersonEditor::DomainModel.new)
102
+ father = person(:name=>'Joe', :sex=>true, :birthday=>Date.new(1950,1,1))
103
+ mother = person(:name=>'Jane', :sex=>false, :birthday=>Date.new(1960,1,1))
104
+ child1 = person(:name=>'Lisa', :sex=>false, :birthday=>Date.new(1982,1,1), :father=>father, :mother=>mother)
105
+ child2 = person(:name=>'Liza', :sex=>false, :birthday=>Date.new(1978,1,1), :father=>father, :mother=>mother)
106
+ father.phone_numbers << phone_number(:number=>'281-328-1328', :type=>'home')
107
+ child1.phone_numbers << phone_number(:number=>'132-813-2813', :type=>'work')
108
+ [father, mother, child1, child2].each { |p| Nemo::Examples::PersonEditor::Model.add_person(p) }
109
+ end
110
+ Nemo::Examples::PersonEditor::Model
111
+ end
112
+
113
+ end
114
+
115
+ # Display a list of persons for editing or deletion
116
+ #
117
+ class Nemo::Examples::PersonEditor::Root < Wee::Component
118
+
119
+ attr_accessor :views
120
+
121
+ # Create a ListItemViewer for each record
122
+ #
123
+ def initialize
124
+ super
125
+ @views = Wee::Session.current.domainmodel.persons.collect { |p| viewer_for(p) }
126
+ @views.each { |view| @children << view }
127
+ end
128
+
129
+ # Enable Wee backtracking for views
130
+ #
131
+ def backtrack_state(snapshot)
132
+ super
133
+ snapshot.add(self.children)
134
+ end
135
+
136
+ # Create a new ListItemViewer for a given metaobject
137
+ #
138
+ def viewer_for(obj)
139
+ Nemo::Examples::PersonEditor::ListItemViewer.new(obj.metaobject.hide!(:password, :interests))
140
+ end
141
+
142
+ # Render the page
143
+ #
144
+ def render
145
+ r.html do
146
+ r.head { r.title('Person Editor') }
147
+ r.link.type('text/css').rel('stylesheet').href('/nemo/person_editor.css')
148
+ r.body do
149
+ r.h1('Person Editor')
150
+ r.table.width('100%').with do
151
+ if @views.empty?
152
+ r.table_row { r.table_header('No persons found.') }
153
+ else
154
+ labels = @views.first.metaobject.visible_attributes.collect { |a| a.label }
155
+ r.table_row { labels.each { |label| r.table_header(label) }; r.table_header { r.space } }
156
+ @views.each do |view|
157
+ r.table_row do
158
+ r.render(view)
159
+ r.table_header do
160
+ r.anchor.callback{ edit(view.metaobject.baseobject) }.with('edit')
161
+ r.space; r.text('|'); r.space
162
+ r.anchor.callback{ delete(view.metaobject.baseobject) }.with('delete')
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ r.break
169
+ r.anchor.callback{ add_new }.with('create new person')
170
+ end
171
+ end
172
+ end
173
+
174
+ # Add a new Person
175
+ #
176
+ def add_new
177
+ if obj = call( Nemo::Examples::PersonEditor::Editor.new(Nemo::Examples::PersonEditor::Person.new.metaobject) )
178
+ session.domainmodel.add_person(obj)
179
+ @views << viewer_for(obj)
180
+ end
181
+ end
182
+
183
+ # Edit a Person
184
+ #
185
+ def edit(obj)
186
+ call( Nemo::Examples::PersonEditor::Editor.new(obj.metaobject) ) # will auto-commit
187
+ end
188
+
189
+ # Delete a Person.
190
+ # Requires confirmation: Are you sure you want to delete? [OK] [Cancel]
191
+ #
192
+ def delete(obj)
193
+ if call( Nemo::Components::ConfirmDialog.new("Are you sure you want to delete #{obj.name}?") )
194
+ session.domainmodel.remove_person(obj)
195
+ session.domainmodel.persons.each do |person|
196
+ person.father = nil if person.father == obj
197
+ person.mother = nil if person.mother == obj
198
+ end
199
+ @views.delete_if { |view| view.metaobject.baseobject == obj }
200
+ end
201
+ end
202
+
203
+ end
204
+
205
+ class Nemo::Examples::PersonEditor::PhoneNumber
206
+
207
+ attr_accessor :number, :type
208
+
209
+ def metaobject
210
+ Nemo.metaobject_for(self) {
211
+ text :type
212
+ label 'Type'
213
+ maxlength 10
214
+ required
215
+
216
+ text :number
217
+ label 'Number'
218
+ maxlength 20
219
+ required
220
+ }
221
+ end
222
+
223
+ end
224
+
225
+ class Nemo::Examples::PersonEditor::Person
226
+
227
+ INTERESTS = ['Art', 'Music', 'Coffee', 'Ruby']
228
+
229
+ attr_accessor :birthday, :father, :interests, :name, :mother, :password, :phone_numbers, :sex
230
+
231
+ def initialize
232
+ @interests = ['Art', 'Music']
233
+ @password = 'abc123'
234
+ @phone_numbers = Array.new
235
+ end
236
+
237
+ def age
238
+ date = (@birthday || Date.today-1)
239
+ (Date.today-date).to_i/365
240
+ end
241
+
242
+ def metaobject
243
+ Nemo.metaobject_for(self) {
244
+ text :name
245
+ label 'Full Name'
246
+ maxlength 30
247
+ required
248
+
249
+ password :password
250
+ label 'Password'
251
+ required
252
+
253
+ date :birthday
254
+ label 'Birthday'
255
+ format_with { |date| date.strftime('%b %d, %Y') }
256
+ required
257
+
258
+ text :age
259
+ label 'Age'
260
+ read_only
261
+
262
+ boolean :sex
263
+ label 'Sex'
264
+ true_item_string 'male'
265
+ false_item_string 'female'
266
+
267
+ single_relationship :father
268
+ label 'Father'
269
+ relationship_to { Wee::Session.current.domainmodel.male_persons }
270
+ format_with { |each| sprintf('%s (%s)', each.name, each.metaobject[:sex].formatted_value) }
271
+ nil_item_string 'unknown'
272
+
273
+ single_relationship :mother
274
+ label 'Mother'
275
+ relationship_to { Wee::Session.current.domainmodel.female_persons }
276
+ format_with { |each| sprintf('%s (%s)', each.name, each.metaobject[:sex].formatted_value) }
277
+ nil_item_string 'unknown'
278
+ link_action { |item| puts "==== Action was taken ====" }
279
+
280
+ multiple_relationship :interests
281
+ label 'Interests'
282
+ relationship_to { INTERESTS }
283
+ add_validation_rule { |value| value.size > 1 }.
284
+ error_string('Select at least two items of interest')
285
+
286
+ multiple :phone_numbers
287
+ label 'Phone#s'
288
+ base_class Nemo::Examples::PersonEditor::PhoneNumber
289
+ format_with { |each| sprintf('%s: %s', each.type, each.number) }
290
+ }
291
+ end
292
+
293
+ end
@@ -0,0 +1,5 @@
1
+ module Nemo::MetaObject; end
2
+ require 'nemo/metaobject/acollector'
3
+ require 'nemo/metaobject/attributes'
4
+ require 'nemo/metaobject/metaobject'
5
+ require 'nemo/metaobject/validation'
@@ -0,0 +1,79 @@
1
+ # Collect a list of Attribute objects using minimal syntax,
2
+ # Use the attribute methods (#text, #date, #boolean, etc) to add new attributes objects.
3
+ # All other messages will be sent to the most recently added attribute.
4
+ #
5
+ # collector = Nemo::MetaObject::AttributeCollector
6
+ # attributes = collector.new(metaobject).collect do
7
+ # text :name
8
+ # label 'Full Name'
9
+ # maxlength 30
10
+ # date :birthday
11
+ # label 'Birthday'
12
+ # read_only
13
+ # end
14
+ # attributes.inspect
15
+ # # => [<TextAttribute @label="Kevin", @maxlength=30>,
16
+ # # <DateAttribute @label="Birthday", @read_only=true>]
17
+ #
18
+ class Nemo::MetaObject::AttributeCollector < Nemo::Util::ObjectCollector
19
+
20
+ attr_accessor :metaobject
21
+
22
+ def initialize(metaobject)
23
+ super()
24
+ @metaobject = metaobject
25
+ @index_error = 'no attributes defined'
26
+ end
27
+
28
+ # Collects to the metaobject
29
+ #
30
+ def <<(attr)
31
+ @metaobject << attr
32
+ super
33
+ end
34
+
35
+ # Shortcut for creating attribute class instances
36
+ #
37
+ # self[:text, symbol] => Nemo::MetaObject::TextAttribute.new(symbol)
38
+ #
39
+ def [](type, symbol)
40
+ attr = Nemo::Util.pascal_case(type)+'Attribute'
41
+ Nemo::MetaObject.const_get(attr).new(symbol)
42
+ end
43
+
44
+ def text(symbol)
45
+ self << self[:text, symbol]
46
+ end
47
+
48
+ def password(symbol)
49
+ self << self[:password, symbol]
50
+ end
51
+
52
+ def date(symbol)
53
+ self << self[:date, symbol]
54
+ end
55
+
56
+ def boolean(symbol)
57
+ self << self[:boolean, symbol]
58
+ end
59
+
60
+ def multiple(symbol)
61
+ self << self[:multiple, symbol]
62
+ end
63
+
64
+ # SingleAttribute is simply a MultipleAttribute with an item_limit of 1
65
+ #
66
+ def single(symbol)
67
+ multiple(symbol)
68
+ item_limit 1
69
+ end
70
+
71
+ def single_relationship(symbol)
72
+ self << self[:single_relationship, symbol]
73
+ end
74
+
75
+ def multiple_relationship(symbol)
76
+ self << self[:multiple_relationship, symbol]
77
+ end
78
+
79
+ end
@@ -0,0 +1,282 @@
1
+ class Nemo::MetaObject::Attribute
2
+
3
+ extend Nemo::Util::Accessors
4
+
5
+ attr_accessor :cache, :metaobject, :symbol, :validation_rules
6
+ call_accessor :label
7
+ bool_accessor :hidden, :multiple_attribute, :read_only
8
+ proc_accessor :format_with
9
+
10
+ def initialize(symbol)
11
+ @format_with = Proc.new { |value| value.nil? ? value : value.to_s }
12
+ @hidden = false
13
+ @label = String.new
14
+ @multiple_attribute = false
15
+ @read_only = false
16
+ @symbol= symbol
17
+ @validation_rules = Array.new
18
+ end
19
+
20
+ def error_class
21
+ Nemo::MetaObject::ValidationError
22
+ end
23
+
24
+ def rule_class
25
+ Nemo::MetaObject::ValidationRule
26
+ end
27
+
28
+ def accept(visitor)
29
+ raise NotImplementedError, 'subclass responsibility'
30
+ end
31
+
32
+ def commit_cache
33
+ @metaobject.baseobject.send(@symbol.to_s+'=', @cache) unless read_only?
34
+ end
35
+
36
+ def refresh_cache
37
+ @cache = value
38
+ end
39
+
40
+ def validate_cache
41
+ error = @validation_rules.find_all { |rule| ! rule.valid?(@cache) }.first
42
+ error_class.new(self, error.error_string) if error
43
+ end
44
+
45
+ def value
46
+ @metaobject.baseobject.send(@symbol)
47
+ end
48
+
49
+ def format(value)
50
+ @format_with ? @format_with.call(value).to_s : value.to_s
51
+ end
52
+
53
+ def formatted_value
54
+ format(value)
55
+ end
56
+
57
+ def add_required_rule
58
+ add_validation_rule { |value| ! value.nil? }.error_string('required')
59
+ end
60
+
61
+ def required
62
+ add_required_rule
63
+ end
64
+
65
+ def required?
66
+ @validation_rules.size > 0
67
+ end
68
+
69
+ def add_validation_rule(&block)
70
+ @validation_rules << rule_class.new(&block)
71
+ @validation_rules.last
72
+ end
73
+
74
+ end
75
+
76
+ class Nemo::MetaObject::TextAttribute < Nemo::MetaObject::Attribute
77
+
78
+ call_accessor :maxlength
79
+ bool_accessor :multiline
80
+
81
+ def initialize(symbol)
82
+ super
83
+ @maxlength = 255
84
+ @multiline = false
85
+ end
86
+
87
+ def accept(visitor)
88
+ visitor.visit_text_attribute(self)
89
+ end
90
+
91
+ def add_required_rule
92
+ add_validation_rule { |value| ! value.to_s.strip.empty? }.error_string('required')
93
+ end
94
+
95
+ def cache
96
+ @cache.to_s
97
+ end
98
+
99
+ end
100
+
101
+ class Nemo::MetaObject::PasswordAttribute < Nemo::MetaObject::TextAttribute
102
+
103
+ attr_accessor :cache_confirm
104
+
105
+ def accept(visitor)
106
+ visitor.visit_password_attribute(self)
107
+ end
108
+
109
+ def refresh_cache
110
+ super
111
+ @cache_confirm = @cache
112
+ end
113
+
114
+ def validate_cache
115
+ return error_class.new(self, 'Confirmation of password is not correct.') if @cache != @cache_confirm
116
+ super
117
+ end
118
+
119
+ end
120
+
121
+ class Nemo::MetaObject::DateAttribute < Nemo::MetaObject::TextAttribute
122
+
123
+ def initialize(symbol)
124
+ super
125
+ @format_with = Proc.new { |value| value.to_s }
126
+ end
127
+
128
+ def accept(visitor)
129
+ visitor.visit_date_attribute(self)
130
+ end
131
+
132
+ def add_required_rule
133
+ add_validation_rule { |value| date_from_cached(value) && true }
134
+ end
135
+
136
+ def date_from_cache
137
+ date_from_cached(@cache)
138
+ end
139
+
140
+ def date_from_cached(value)
141
+ begin Date.parse(value.to_s) rescue nil end
142
+ end
143
+
144
+ def date_to_cache(date)
145
+ @cache = begin @format_with.call(date) rescue date.to_s end
146
+ end
147
+
148
+ def refresh_cache
149
+ date_to_cache(value)
150
+ end
151
+
152
+ def commit_cache
153
+ @metaobject.baseobject.send(@symbol.to_s+'=', date_from_cache) unless read_only?
154
+ end
155
+
156
+ end
157
+
158
+ class Nemo::MetaObject::MultipleAttribute < Nemo::MetaObject::Attribute
159
+
160
+ call_accessor :base_class, :item_limit
161
+ bool_accessor :item_delete
162
+ proc_accessor :link_action
163
+
164
+ def initialize(symbol)
165
+ super
166
+ @multiple_attribute = true
167
+ @item_limit = 1_000_000
168
+ @item_delete = true
169
+ end
170
+
171
+ def add_required_rule
172
+ add_validation_rule { |value| ! value.to_s.empty? }.error_string('required')
173
+ end
174
+
175
+ def accept(visitor)
176
+ visitor.visit_multiple_attribute(self)
177
+ end
178
+
179
+ def formatted_value
180
+ value.collect { |item| format(item) }
181
+ end
182
+
183
+ def add_to_cache(item)
184
+ @cache ||= Array.new
185
+ @cache << item
186
+ end
187
+
188
+ def refresh_cache
189
+ @cache = value.collect
190
+ @cache = @cache[0,@item_limit] if @cache.size > @item_limit
191
+ end
192
+
193
+ def remove_from_cache(item)
194
+ cache.delete(item)
195
+ end
196
+
197
+ end
198
+
199
+ class Nemo::MetaObject::RelationshipAttribute < Nemo::MetaObject::Attribute
200
+
201
+ proc_accessor :link_action, :relationship_to
202
+
203
+ def items
204
+ return @relationship_to.call if @relationship_to
205
+ Array.new
206
+ end
207
+
208
+ def formatted_items
209
+ return @relationship_to.call.collect { |i| format(i) } if @relationship_to
210
+ Array.new
211
+ end
212
+
213
+ end
214
+
215
+ class Nemo::MetaObject::BooleanAttribute < Nemo::MetaObject::RelationshipAttribute
216
+
217
+ call_accessor :true_item_string, :false_item_string
218
+
219
+ def initialize(symbol)
220
+ super
221
+ @true_item_string = 'yes'
222
+ @false_item_string = 'no'
223
+ @relationship_to = Proc.new { [true,false] }
224
+ @format_with = Proc.new { |value| value ? @true_item_string : @false_item_string }
225
+ end
226
+
227
+ def accept(visitor)
228
+ visitor.visit_boolean_attribute(self)
229
+ end
230
+
231
+ end
232
+
233
+ class Nemo::MetaObject::SingleRelationshipAttribute < Nemo::MetaObject::RelationshipAttribute
234
+
235
+ call_accessor :nil_item_string
236
+
237
+ def accept(visitor)
238
+ visitor.visit_single_relationship_attribute(self)
239
+ end
240
+
241
+ def has_nil_item?
242
+ Nemo::Util.to_bool(@nil_item_string)
243
+ end
244
+
245
+ def items
246
+ @nil_item_string ? [nil]+super : super
247
+ end
248
+
249
+ def formatted_items
250
+ @nil_item_string ? [@nil_item_string]+super : super
251
+ end
252
+
253
+ end
254
+
255
+ class Nemo::MetaObject::MultipleRelationshipAttribute < Nemo::MetaObject::RelationshipAttribute
256
+
257
+ def accept(visitor)
258
+ visitor.visit_multiple_relationship_attribute(self)
259
+ end
260
+
261
+ def add_required_rule
262
+ add_validation_rule { |value| ! value.to_s.empty? }.error_string('required')
263
+ end
264
+
265
+ def add_to_cache(item)
266
+ @cache ||= Array.new
267
+ @cache << item
268
+ end
269
+
270
+ def refresh_cache
271
+ @cache = value.collect
272
+ end
273
+
274
+ def remove_from_cache(item)
275
+ cache.delete(item)
276
+ end
277
+
278
+ def formatted_value
279
+ value.collect { |item| format(item) }
280
+ end
281
+
282
+ end