yodel 0.0.1

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 (200) hide show
  1. data/.document +5 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +63 -0
  5. data/LICENSE +1 -0
  6. data/README.rdoc +20 -0
  7. data/Rakefile +1 -0
  8. data/bin/yodel +4 -0
  9. data/lib/yodel.rb +22 -0
  10. data/lib/yodel/application/application.rb +44 -0
  11. data/lib/yodel/application/extension.rb +59 -0
  12. data/lib/yodel/application/request_handler.rb +48 -0
  13. data/lib/yodel/application/yodel.rb +25 -0
  14. data/lib/yodel/command/command.rb +94 -0
  15. data/lib/yodel/command/deploy.rb +67 -0
  16. data/lib/yodel/command/dns_server.rb +16 -0
  17. data/lib/yodel/command/installer.rb +229 -0
  18. data/lib/yodel/config/config.rb +30 -0
  19. data/lib/yodel/config/environment.rb +16 -0
  20. data/lib/yodel/config/yodel.rb +21 -0
  21. data/lib/yodel/exceptions/destroyed_record.rb +2 -0
  22. data/lib/yodel/exceptions/domain_not_found.rb +16 -0
  23. data/lib/yodel/exceptions/duplicate_layout.rb +2 -0
  24. data/lib/yodel/exceptions/exceptions.rb +3 -0
  25. data/lib/yodel/exceptions/inconsistent_lock_state.rb +2 -0
  26. data/lib/yodel/exceptions/invalid_field.rb +2 -0
  27. data/lib/yodel/exceptions/invalid_index.rb +2 -0
  28. data/lib/yodel/exceptions/invalid_mixin.rb +2 -0
  29. data/lib/yodel/exceptions/invalid_model_field.rb +2 -0
  30. data/lib/yodel/exceptions/layout_not_found.rb +2 -0
  31. data/lib/yodel/exceptions/mass_assignment.rb +2 -0
  32. data/lib/yodel/exceptions/missing_migration.rb +2 -0
  33. data/lib/yodel/exceptions/missing_root_directory.rb +15 -0
  34. data/lib/yodel/exceptions/unable_to_acquire_lock.rb +2 -0
  35. data/lib/yodel/exceptions/unauthorised.rb +2 -0
  36. data/lib/yodel/exceptions/unknown_field.rb +2 -0
  37. data/lib/yodel/middleware/development_server.rb +180 -0
  38. data/lib/yodel/middleware/error_pages.rb +72 -0
  39. data/lib/yodel/middleware/public_assets.rb +78 -0
  40. data/lib/yodel/middleware/request.rb +16 -0
  41. data/lib/yodel/middleware/site_detector.rb +22 -0
  42. data/lib/yodel/mime_types/default_mime_set.rb +28 -0
  43. data/lib/yodel/mime_types/mime_type.rb +68 -0
  44. data/lib/yodel/mime_types/mime_type_set.rb +41 -0
  45. data/lib/yodel/mime_types/mime_types.rb +6 -0
  46. data/lib/yodel/mime_types/yodel.rb +15 -0
  47. data/lib/yodel/models/api/api.rb +1 -0
  48. data/lib/yodel/models/api/api_call.rb +87 -0
  49. data/lib/yodel/models/core/associations/association.rb +37 -0
  50. data/lib/yodel/models/core/associations/associations.rb +22 -0
  51. data/lib/yodel/models/core/associations/counts/many_association.rb +18 -0
  52. data/lib/yodel/models/core/associations/counts/one_association.rb +22 -0
  53. data/lib/yodel/models/core/associations/embedded/embedded_association.rb +47 -0
  54. data/lib/yodel/models/core/associations/embedded/embedded_record_array.rb +12 -0
  55. data/lib/yodel/models/core/associations/embedded/many_embedded_association.rb +62 -0
  56. data/lib/yodel/models/core/associations/embedded/one_embedded_association.rb +49 -0
  57. data/lib/yodel/models/core/associations/query/many_query_association.rb +10 -0
  58. data/lib/yodel/models/core/associations/query/one_query_association.rb +10 -0
  59. data/lib/yodel/models/core/associations/query/query_association.rb +64 -0
  60. data/lib/yodel/models/core/associations/record_association.rb +38 -0
  61. data/lib/yodel/models/core/associations/store/many_store_association.rb +32 -0
  62. data/lib/yodel/models/core/associations/store/one_store_association.rb +14 -0
  63. data/lib/yodel/models/core/associations/store/store_association.rb +51 -0
  64. data/lib/yodel/models/core/attachments/attachment.rb +73 -0
  65. data/lib/yodel/models/core/attachments/image.rb +38 -0
  66. data/lib/yodel/models/core/core.rb +15 -0
  67. data/lib/yodel/models/core/fields/alias_field.rb +32 -0
  68. data/lib/yodel/models/core/fields/array_field.rb +64 -0
  69. data/lib/yodel/models/core/fields/attachment_field.rb +42 -0
  70. data/lib/yodel/models/core/fields/boolean_field.rb +28 -0
  71. data/lib/yodel/models/core/fields/change_sensitive_array.rb +96 -0
  72. data/lib/yodel/models/core/fields/change_sensitive_hash.rb +53 -0
  73. data/lib/yodel/models/core/fields/color_field.rb +4 -0
  74. data/lib/yodel/models/core/fields/date_field.rb +35 -0
  75. data/lib/yodel/models/core/fields/decimal_field.rb +19 -0
  76. data/lib/yodel/models/core/fields/email_field.rb +10 -0
  77. data/lib/yodel/models/core/fields/enum_field.rb +33 -0
  78. data/lib/yodel/models/core/fields/field.rb +154 -0
  79. data/lib/yodel/models/core/fields/fields.rb +29 -0
  80. data/lib/yodel/models/core/fields/fields_field.rb +31 -0
  81. data/lib/yodel/models/core/fields/filter_mixin.rb +9 -0
  82. data/lib/yodel/models/core/fields/filtered_string_field.rb +5 -0
  83. data/lib/yodel/models/core/fields/filtered_text_field.rb +5 -0
  84. data/lib/yodel/models/core/fields/function_field.rb +28 -0
  85. data/lib/yodel/models/core/fields/hash_field.rb +54 -0
  86. data/lib/yodel/models/core/fields/html_field.rb +15 -0
  87. data/lib/yodel/models/core/fields/image_field.rb +11 -0
  88. data/lib/yodel/models/core/fields/integer_field.rb +25 -0
  89. data/lib/yodel/models/core/fields/password_field.rb +21 -0
  90. data/lib/yodel/models/core/fields/self_field.rb +27 -0
  91. data/lib/yodel/models/core/fields/string_field.rb +15 -0
  92. data/lib/yodel/models/core/fields/tags_field.rb +7 -0
  93. data/lib/yodel/models/core/fields/text_field.rb +7 -0
  94. data/lib/yodel/models/core/fields/time_field.rb +36 -0
  95. data/lib/yodel/models/core/functions/function.rb +471 -0
  96. data/lib/yodel/models/core/functions/functions.rb +2 -0
  97. data/lib/yodel/models/core/functions/trigger.rb +14 -0
  98. data/lib/yodel/models/core/log/log.rb +33 -0
  99. data/lib/yodel/models/core/log/log_entry.rb +12 -0
  100. data/lib/yodel/models/core/model/abstract_model.rb +59 -0
  101. data/lib/yodel/models/core/model/model.rb +460 -0
  102. data/lib/yodel/models/core/model/mongo_model.rb +25 -0
  103. data/lib/yodel/models/core/model/site_model.rb +17 -0
  104. data/lib/yodel/models/core/mongo/mongo.rb +3 -0
  105. data/lib/yodel/models/core/mongo/primary_key_factory.rb +12 -0
  106. data/lib/yodel/models/core/mongo/query.rb +68 -0
  107. data/lib/yodel/models/core/mongo/record_index.rb +89 -0
  108. data/lib/yodel/models/core/record/abstract_record.rb +411 -0
  109. data/lib/yodel/models/core/record/embedded_record.rb +47 -0
  110. data/lib/yodel/models/core/record/mongo_record.rb +83 -0
  111. data/lib/yodel/models/core/record/record.rb +386 -0
  112. data/lib/yodel/models/core/record/section.rb +21 -0
  113. data/lib/yodel/models/core/record/site_record.rb +31 -0
  114. data/lib/yodel/models/core/site/migration.rb +52 -0
  115. data/lib/yodel/models/core/site/remote.rb +61 -0
  116. data/lib/yodel/models/core/site/site.rb +202 -0
  117. data/lib/yodel/models/core/validations/email_address_validation.rb +24 -0
  118. data/lib/yodel/models/core/validations/embedded_records_validation.rb +31 -0
  119. data/lib/yodel/models/core/validations/errors.rb +51 -0
  120. data/lib/yodel/models/core/validations/excluded_from_validation.rb +10 -0
  121. data/lib/yodel/models/core/validations/excludes_combinations_validation.rb +18 -0
  122. data/lib/yodel/models/core/validations/format_validation.rb +10 -0
  123. data/lib/yodel/models/core/validations/included_in_validation.rb +10 -0
  124. data/lib/yodel/models/core/validations/includes_combinations_validation.rb +14 -0
  125. data/lib/yodel/models/core/validations/length_validation.rb +28 -0
  126. data/lib/yodel/models/core/validations/password_confirmation_validation.rb +11 -0
  127. data/lib/yodel/models/core/validations/required_validation.rb +9 -0
  128. data/lib/yodel/models/core/validations/unique_validation.rb +9 -0
  129. data/lib/yodel/models/core/validations/validation.rb +39 -0
  130. data/lib/yodel/models/core/validations/validations.rb +15 -0
  131. data/lib/yodel/models/email/email.rb +79 -0
  132. data/lib/yodel/models/migrations/01_record_model.rb +29 -0
  133. data/lib/yodel/models/migrations/02_page_model.rb +45 -0
  134. data/lib/yodel/models/migrations/03_layout_model.rb +38 -0
  135. data/lib/yodel/models/migrations/04_group_model.rb +61 -0
  136. data/lib/yodel/models/migrations/05_user_model.rb +24 -0
  137. data/lib/yodel/models/migrations/06_snippet_model.rb +13 -0
  138. data/lib/yodel/models/migrations/07_search_page_model.rb +32 -0
  139. data/lib/yodel/models/migrations/08_default_site_options.rb +21 -0
  140. data/lib/yodel/models/migrations/09_security_page_models.rb +36 -0
  141. data/lib/yodel/models/migrations/10_record_proxy_page_model.rb +17 -0
  142. data/lib/yodel/models/migrations/11_email_model.rb +28 -0
  143. data/lib/yodel/models/migrations/12_api_call_model.rb +23 -0
  144. data/lib/yodel/models/migrations/13_redirect_page_model.rb +13 -0
  145. data/lib/yodel/models/migrations/14_menu_model.rb +20 -0
  146. data/lib/yodel/models/models.rb +8 -0
  147. data/lib/yodel/models/pages/form_builder.rb +379 -0
  148. data/lib/yodel/models/pages/html_decorator.rb +132 -0
  149. data/lib/yodel/models/pages/layout.rb +120 -0
  150. data/lib/yodel/models/pages/menu.rb +32 -0
  151. data/lib/yodel/models/pages/page.rb +378 -0
  152. data/lib/yodel/models/pages/pages.rb +7 -0
  153. data/lib/yodel/models/pages/record_proxy_page.rb +188 -0
  154. data/lib/yodel/models/pages/redirect_page.rb +11 -0
  155. data/lib/yodel/models/search/search.rb +1 -0
  156. data/lib/yodel/models/search/search_page.rb +58 -0
  157. data/lib/yodel/models/security/facebook_login_page.rb +55 -0
  158. data/lib/yodel/models/security/group.rb +10 -0
  159. data/lib/yodel/models/security/guests_group.rb +5 -0
  160. data/lib/yodel/models/security/login_page.rb +20 -0
  161. data/lib/yodel/models/security/logout_page.rb +13 -0
  162. data/lib/yodel/models/security/noone_group.rb +5 -0
  163. data/lib/yodel/models/security/owner_group.rb +8 -0
  164. data/lib/yodel/models/security/password.rb +5 -0
  165. data/lib/yodel/models/security/password_reset_page.rb +47 -0
  166. data/lib/yodel/models/security/security.rb +10 -0
  167. data/lib/yodel/models/security/user.rb +33 -0
  168. data/lib/yodel/public/core/css/core.css +257 -0
  169. data/lib/yodel/public/core/css/reset.css +48 -0
  170. data/lib/yodel/public/core/images/cross.png +0 -0
  171. data/lib/yodel/public/core/images/spinner.gif +0 -0
  172. data/lib/yodel/public/core/images/tick.png +0 -0
  173. data/lib/yodel/public/core/images/yodel.png +0 -0
  174. data/lib/yodel/public/core/js/jquery.min.js +18 -0
  175. data/lib/yodel/public/core/js/json2.js +480 -0
  176. data/lib/yodel/public/core/js/yodel_jquery.js +238 -0
  177. data/lib/yodel/request/authentication.rb +76 -0
  178. data/lib/yodel/request/flash.rb +28 -0
  179. data/lib/yodel/request/request.rb +4 -0
  180. data/lib/yodel/requires.rb +47 -0
  181. data/lib/yodel/task_queue/queue_daemon.rb +33 -0
  182. data/lib/yodel/task_queue/queue_worker.rb +32 -0
  183. data/lib/yodel/task_queue/stats_thread.rb +27 -0
  184. data/lib/yodel/task_queue/task.rb +62 -0
  185. data/lib/yodel/task_queue/task_queue.rb +40 -0
  186. data/lib/yodel/types/date.rb +5 -0
  187. data/lib/yodel/types/object_id.rb +11 -0
  188. data/lib/yodel/types/time.rb +5 -0
  189. data/lib/yodel/version.rb +3 -0
  190. data/system/Library/LaunchDaemons/com.yodelcms.dns.plist +26 -0
  191. data/system/Library/LaunchDaemons/com.yodelcms.server.plist +26 -0
  192. data/system/etc/resolver/yodel +2 -0
  193. data/system/usr/local/bin/yodel_command_runner +2 -0
  194. data/system/usr/local/etc/yodel/development_settings.rb +28 -0
  195. data/system/usr/local/etc/yodel/production_settings.rb +27 -0
  196. data/system/var/log/yodel.log +0 -0
  197. data/test/helper.rb +18 -0
  198. data/test/test_yodel.rb +4 -0
  199. data/yodel.gemspec +47 -0
  200. metadata +501 -0
@@ -0,0 +1,15 @@
1
+ class HTMLField < TextField
2
+ def search_terms_set(record)
3
+ to_text(record.get(name)).gsub(/\W+/, ' ').split
4
+ end
5
+
6
+ def to_text(html)
7
+ Hpricot(html.to_s).search('//text()').collect(&:to_s).collect(&:strip).join(' ').strip
8
+ end
9
+
10
+ def default_input_type
11
+ :html
12
+ end
13
+ end
14
+
15
+ Field::TYPES['html'] = HTMLField
@@ -0,0 +1,11 @@
1
+ class ImageField < AttachmentField
2
+ def default_input_type
3
+ :image
4
+ end
5
+
6
+ def typecast(value, record)
7
+ Image.new(value, record, self)
8
+ end
9
+ end
10
+
11
+ Field::TYPES['image'] = ImageField
@@ -0,0 +1,25 @@
1
+ class IntegerField < Field
2
+ def numeric?
3
+ true
4
+ end
5
+
6
+ def json_action(action, value, record)
7
+ case action
8
+ when 'set'
9
+ record.set_raw(name, value.to_i)
10
+ when 'increment'
11
+ record.increment!(name, value.to_i)
12
+ end
13
+ record.changed!(name)
14
+ end
15
+
16
+ def untypecast(value, record)
17
+ value.to_i
18
+ end
19
+
20
+ def from_json(value, record)
21
+ value.to_i
22
+ end
23
+ end
24
+
25
+ Field::TYPES['integer'] = IntegerField
@@ -0,0 +1,21 @@
1
+ class PasswordField < StringField
2
+ undef search_terms_set
3
+ def default_input_type
4
+ :password
5
+ end
6
+
7
+ def validate(record, errors)
8
+ PasswordConfirmationValidation.validate(nil, self, name, nil, record, errors)
9
+ super
10
+ end
11
+
12
+ def from_json(value, record)
13
+ if value.blank?
14
+ throw :ignore_value
15
+ else
16
+ value.to_s
17
+ end
18
+ end
19
+ end
20
+
21
+ Field::TYPES['password'] = PasswordField
@@ -0,0 +1,27 @@
1
+ class SelfField < Field
2
+ def strip_nil?
3
+ true
4
+ end
5
+
6
+ def default_input_type
7
+ nil
8
+ end
9
+
10
+ def validate(record, errors)
11
+ # noop
12
+ end
13
+
14
+ def typecast(value, record)
15
+ record
16
+ end
17
+
18
+ def untypecast(value, record)
19
+ nil
20
+ end
21
+
22
+ def from_json(value, record)
23
+ nil
24
+ end
25
+ end
26
+
27
+ Field::TYPES['self'] = SelfField
@@ -0,0 +1,15 @@
1
+ class StringField < Field
2
+ def search_terms_set(record)
3
+ record.get(name).to_s.gsub(/\W+/, ' ').split
4
+ end
5
+
6
+ def untypecast(value, record)
7
+ value.nil? ? nil : value.to_s
8
+ end
9
+
10
+ def from_json(value, record)
11
+ value.to_s
12
+ end
13
+ end
14
+
15
+ Field::TYPES['string'] = StringField
@@ -0,0 +1,7 @@
1
+ class TagsField < ArrayField
2
+ def from_json(value, record)
3
+ value.to_s.split(',').map(&:strip).reject(&:blank?).uniq
4
+ end
5
+ end
6
+
7
+ Field::TYPES['tags'] = TagsField
@@ -0,0 +1,7 @@
1
+ class TextField < StringField
2
+ def default_input_type
3
+ :textarea
4
+ end
5
+ end
6
+
7
+ Field::TYPES['text'] = TextField
@@ -0,0 +1,36 @@
1
+ class TimeField < Field
2
+ def default_input_type
3
+ :datetime
4
+ end
5
+
6
+ def before_create(record)
7
+ return unless name == 'created_at' || name == 'updated_at'
8
+ record.set(name, Time.now.utc)
9
+ end
10
+
11
+ def before_update(record)
12
+ return unless name == 'updated_at'
13
+ record.set(name, Time.now.utc)
14
+ end
15
+
16
+ def typecast(value, record)
17
+ value.nil? ? nil : Time.at(value)
18
+ end
19
+
20
+ def untypecast(value, record)
21
+ value.blank? ? nil : Time.at(value.to_i).utc
22
+ end
23
+
24
+ def from_json(value, record)
25
+ return nil unless value.present? && (value.is_a?(String) || value.is_a?(Hash))
26
+ if value.is_a?(Hash)
27
+ return nil unless ['year', 'month', 'day', 'hour', 'min'].all? {|field| value.key?(field) && !value[field].blank?}
28
+ sec = value['sec'] || 0
29
+ Time.new(value['year'], value['month'], value['day'], value['hour'], value['min'], sec).utc
30
+ else
31
+ Time.parse(value).utc
32
+ end
33
+ end
34
+ end
35
+
36
+ Field::TYPES['time'] = TimeField
@@ -0,0 +1,471 @@
1
+ class Function
2
+ attr_accessor :instructions, :source
3
+
4
+ def initialize(param)
5
+ if param.is_a?(String)
6
+ @source = param
7
+ @instructions = compile(param)
8
+ else
9
+ @instructions = param
10
+ end
11
+ end
12
+
13
+ def inspect(instruction=nil)
14
+ instruction ||= self.instructions
15
+
16
+ name = instruction.shift
17
+ parameters = instruction.collect do |parameter|
18
+ if parameter.is_a?(Array)
19
+ inspect(parameter)
20
+ else
21
+ parameter
22
+ end
23
+ end
24
+
25
+ "#{name}(#{parameters.join(', ')})"
26
+ end
27
+
28
+
29
+ # ----------------------------------------
30
+ # Compilation
31
+ # ----------------------------------------
32
+ CALL_TOKEN = '.'
33
+ START_PARAMS_TOKEN = '('
34
+ END_PARAMS_TOKEN = ')'
35
+ START_HASH_TOKEN = '{'
36
+ END_HASH_TOKEN = '}'
37
+ PARAM_DELIM_TOKEN = ','
38
+ HASH_DELIM_TOKEN = ':'
39
+ ENTRY_FLAG = '!'
40
+ DOUBLE_QUOTE_TOKEN = '"'
41
+ SINGLE_QUOTE_TOKEN = "'"
42
+
43
+ def compile(source)
44
+ tokens = source.scan(/[\w\-\+]+|\.|\(|\)|\{|\}|:|,|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/)
45
+ parse(tokens)
46
+ end
47
+
48
+ def parse(tokens)
49
+ params = false
50
+ chain = false
51
+ hash = false
52
+ instructions = []
53
+
54
+ until tokens.empty?
55
+ token = tokens.shift
56
+ case token[0]
57
+ when CALL_TOKEN
58
+ chain = true
59
+ when START_PARAMS_TOKEN
60
+ params = true
61
+ instructions += parse(tokens)
62
+ when END_PARAMS_TOKEN
63
+ tokens.unshift(END_PARAMS_TOKEN) unless params
64
+ break
65
+ when ENTRY_FLAG
66
+ hash = true
67
+ instructions << ['entry'] + parse(tokens)
68
+ when START_HASH_TOKEN
69
+ tokens.unshift(ENTRY_FLAG)
70
+ instructions << ['hash'] + parse(tokens)
71
+ when END_HASH_TOKEN
72
+ tokens.unshift(END_HASH_TOKEN) if hash
73
+ break
74
+ when HASH_DELIM_TOKEN
75
+ instructions += parse(tokens)
76
+ when PARAM_DELIM_TOKEN
77
+ if params || hash
78
+ tokens.unshift(ENTRY_FLAG) if hash
79
+ instructions += parse(tokens)
80
+ else
81
+ tokens.unshift(PARAM_DELIM_TOKEN)
82
+ break
83
+ end
84
+ when DOUBLE_QUOTE_TOKEN, SINGLE_QUOTE_TOKEN
85
+ instructions << ['string', token[1...-1]]
86
+ else
87
+ if tokens.first == START_PARAMS_TOKEN
88
+ instructions << [token] + parse(tokens)
89
+ elsif token.to_i.to_s == token
90
+ instructions << ['int', token]
91
+ else
92
+ instructions << ['field', token]
93
+ end
94
+ end
95
+ end
96
+
97
+ if chain
98
+ [['chain'] + instructions]
99
+ else
100
+ instructions
101
+ end
102
+ end
103
+
104
+ # ----------------------------------------
105
+ # Execution
106
+ # ----------------------------------------
107
+ def execute(context, instruction=nil, parent_context=nil)
108
+ instruction ||= self.instructions.first
109
+ name, *params = instruction
110
+ parent_context ||= context
111
+
112
+ case name
113
+ when 'chain'
114
+ chain(context, parent_context, params)
115
+ when 'field'
116
+ get_field(context, parent_context, params.first)
117
+ when 'find'
118
+ find_record(context, parent_context, params[0], params[1])
119
+ when 'changed'
120
+ changed(context, parent_context, params.first)
121
+ when 'previous_value'
122
+ previous_value(context, parent_context, params.first)
123
+ when 'collect'
124
+ collect(context, parent_context, params.first)
125
+ when 'majority'
126
+ majority(context, parent_context, params.first)
127
+ when 'count'
128
+ count(context, parent_context, params.first)
129
+ when 'invert'
130
+ invert(context)
131
+ when 'unique'
132
+ unique(context, parent_context, params.first)
133
+ when 'average'
134
+ average(context, parent_context, params)
135
+ when 'as_a_percentage_of'
136
+ as_a_percentage_of(context, parent_context, params.first)
137
+ when 'present'
138
+ present(context)
139
+ when 'blank'
140
+ blank(context)
141
+ when 'sum'
142
+ sum(context, parent_context, params)
143
+ when 'subtract'
144
+ subtract(context, parent_context, params)
145
+ when 'multiply'
146
+ multiply(context, parent_context, params)
147
+ when 'round'
148
+ round(context)
149
+ when 'if'
150
+ binary_if(context, parent_context, params[0], params[1], params[2])
151
+ when 'greater_than'
152
+ greater_than(context, parent_context, params.first)
153
+ when 'greater_than_or_equal_to'
154
+ greater_than_or_equal_to(context, parent_context, params.first)
155
+ when 'less_than'
156
+ less_than(context, parent_context, params.first)
157
+ when 'less_than_or_equal_to'
158
+ less_than_or_equal_to(context, parent_context, params.first)
159
+ when 'not_equal'
160
+ not_equal(context, parent_context, params.first)
161
+ when 'and'
162
+ binary_and(context, parent_context, params[0], params[1])
163
+ when 'or'
164
+ binary_or(context, parent_context, params[0], params[1])
165
+ when 'include'
166
+ set_include(context, parent_context, params.first)
167
+ when 'strip'
168
+ strip(context)
169
+ when 'format'
170
+ format(context, parent_context, params.first)
171
+ when 'set'
172
+ set_field(context, parent_context, params[0], params[1])
173
+ when 'update'
174
+ update_field(context, parent_context, params[0], params[1])
175
+ when 'min'
176
+ min(context, parent_context, params[0], params[1])
177
+ when 'max'
178
+ max(context, parent_context, params[0], params[1])
179
+ when 'increment'
180
+ increment(context, parent_context, params[0], params[1])
181
+ when 'complement'
182
+ complement(context, parent_context, params[0], params[1])
183
+ when 'each'
184
+ each(context, parent_context, params.first)
185
+ when 'deliver'
186
+ deliver_email(context, parent_context, params[0], params[1])
187
+ when 'call_api'
188
+ call_api(context, parent_context, params[0], params[1])
189
+
190
+ # literals
191
+ when 'string'
192
+ params.first
193
+ when 'int'
194
+ params.first.to_i
195
+ when 'hash'
196
+ Hash[params.collect {|entry| execute(context, entry, parent_context)}]
197
+ when 'entry'
198
+ [execute(context, params.first, parent_context), execute(context, params.last, parent_context)]
199
+ end
200
+ end
201
+
202
+ protected
203
+ def chain(context, parent_context, methods)
204
+ #parent_context = context
205
+ methods.each do |method|
206
+ context = execute(context, method, parent_context)
207
+ end
208
+ context
209
+ end
210
+
211
+ def get_field(context, parent_context, name)
212
+ case name
213
+ when 'self'
214
+ context
215
+ when 'root'
216
+ parent_context
217
+ # TODO: these should be caught as booleans instead of being treated as field names
218
+ when 'true'
219
+ true
220
+ when 'false'
221
+ false
222
+ else
223
+ context.get(name)
224
+ end
225
+ rescue
226
+ nil
227
+ end
228
+
229
+ def find_record(context, parent_context, model_name, key)
230
+ raise "Parent context of find_record must respond to site" unless parent_context.respond_to?(:site)
231
+ model_name = execute(context, model_name, parent_context)
232
+ key = execute(context, key, parent_context)
233
+ model = parent_context.site.model_by_plural_name(model_name)
234
+
235
+ if key == 'id'
236
+ value = BSON::ObjectId.from_string(context)
237
+ else
238
+ value = context
239
+ end
240
+
241
+ model.where(key => value).first
242
+ end
243
+
244
+ def previous_value(context, parent_context, name)
245
+ context.field_was(execute(context, name, parent_context))
246
+ end
247
+
248
+ # TODO: change format from changed('name') to name.changed
249
+ def changed(context, parent_context, name)
250
+ context.changed?(execute(context, name, parent_context))
251
+ end
252
+
253
+ def set_field(context, parent_context, field, value)
254
+ field = execute(parent_context, field, parent_context)
255
+ value = execute(parent_context, value, parent_context)
256
+ context.set(field, value)
257
+ end
258
+
259
+ def update_field(context, parent_context, field, value)
260
+ set_field(context, parent_context, field, value)
261
+ context.save
262
+ end
263
+
264
+ def increment(context, parent_context, field, value)
265
+ field = execute(context, field, parent_context)
266
+ value = execute(context, value, parent_context)
267
+ context.increment!(field, value)
268
+ end
269
+
270
+ def collect(context, parent_context, field)
271
+ raise "Context to collect must respond to collect" unless context.respond_to?(:collect)
272
+ context.collect {|item| execute(item, field, parent_context)}
273
+ end
274
+
275
+ def each(context, parent_context, statement)
276
+ raise "Context to each must respond to each" unless context.respond_to?(:each)
277
+ context.each {|item| execute(item, statement, parent_context)}
278
+ end
279
+
280
+ def majority(context, parent_context, field)
281
+ unless context.respond_to?(:size) && context.respond_to?(:count)
282
+ raise "Majority context must be enumerable"
283
+ end
284
+
285
+ valid = context.count {|item| execute(item, field, parent_context)}
286
+ valid >= (context.size - valid)
287
+ end
288
+
289
+ def count(context, parent_context, field)
290
+ unless context.respond_to?(:size) && context.respond_to?(:count)
291
+ raise "Count context must be enumerable"
292
+ end
293
+
294
+ if field.nil?
295
+ context.size
296
+ else
297
+ context.count {|item| execute(item, field, parent_context)}
298
+ end
299
+ end
300
+
301
+ def invert(context)
302
+ !context
303
+ end
304
+
305
+ def unique(context, parent_context, field)
306
+ collect(context, parent_context, field).uniq
307
+ end
308
+
309
+ def average(context, parent_context, params)
310
+ if context.respond_to?(:collect) && context.respond_to?(:size) && params.size == 1
311
+ count = context.size
312
+ else
313
+ count = params.size
314
+ end
315
+
316
+ return 0.0 if count == 0
317
+ sum(context, parent_context, params).to_f / count.to_f
318
+ end
319
+
320
+ def as_a_percentage_of(context, parent_context, count)
321
+ count = execute(context, count, parent_context)
322
+ context = context.to_f
323
+ count = count.to_f
324
+
325
+ if context == 0 || count == 0
326
+ return 0
327
+ else
328
+ (context / count) * 100
329
+ end
330
+ end
331
+
332
+ def present(context)
333
+ context.present?
334
+ end
335
+
336
+ def blank(context)
337
+ context.blank?
338
+ end
339
+
340
+ def sum(context, parent_context, params)
341
+ if context.respond_to?(:collect) && params.size == 1
342
+ collect(context, parent_context, params.first).compact.inject(&:+)
343
+ else
344
+ params.collect {|method| execute(context, method, parent_context)}.compact.inject(&:+)
345
+ end
346
+ end
347
+
348
+ def subtract(context, parent_context, params)
349
+ if context.respond_to?(:collect) && params.size == 1
350
+ collect(context, parent_context, params.first).compact.inject(&:-)
351
+ else
352
+ params.collect {|method| execute(context, method, parent_context)}.compact.inject(&:-)
353
+ end
354
+ end
355
+
356
+ def multiply(context, parent_context, params)
357
+ if context.respond_to?(:collect) && params.size == 1
358
+ collect(context, parent_context, params.first).compact.inject(&:*)
359
+ else
360
+ params.collect {|method| execute(context, method, parent_context)}.compact.inject(&:*)
361
+ end
362
+ end
363
+
364
+ def complement(context, parent_context, set1, set2)
365
+ set1 = execute(context, set1, parent_context)
366
+ set2 = execute(context, set2, parent_context)
367
+ raise "Sets must be iterable" unless set1.respond_to?(:to_a) && set2.respond_to?(:to_a)
368
+ set2.to_a - set1.to_a
369
+ end
370
+
371
+ def set_include(context, parent_context, item)
372
+ item = execute(context, item, parent_context)
373
+ raise "Context must respond to include?" unless context.respond_to?(:include?)
374
+ context.include?(item)
375
+ end
376
+
377
+ def round(context)
378
+ context.to_f.round
379
+ end
380
+
381
+ def binary_if(context, parent_context, condition, true_exp, false_exp)
382
+ if execute(context, condition, parent_context)
383
+ execute(context, true_exp, parent_context) if true_exp
384
+ else
385
+ execute(context, false_exp, parent_context) if false_exp
386
+ end
387
+ end
388
+
389
+ def greater_than(context, parent_context, value)
390
+ value = execute(parent_context, value, parent_context)
391
+ context > value
392
+ end
393
+
394
+ def less_than(context, parent_context, value)
395
+ value = execute(parent_context, value, parent_context)
396
+ context < value
397
+ end
398
+
399
+ def greater_than_or_equal_to(context, parent_context, value)
400
+ value = execute(parent_context, value, parent_context)
401
+ context >= value
402
+ end
403
+
404
+ def less_than_or_equal_to(context, parent_context, value)
405
+ value = execute(parent_context, value, parent_context)
406
+ context <= value
407
+ end
408
+
409
+ def not_equal(context, parent_context, value)
410
+ value = execute(parent_context, value, parent_context)
411
+ context != value
412
+ end
413
+
414
+ def binary_and(context, parent_context, operand1, operand2)
415
+ operand1 = execute(context, operand1, parent_context)
416
+
417
+ if operand2
418
+ operand2 = execute(context, operand2, parent_context)
419
+ operand1 && operand2
420
+ else
421
+ context && operand1
422
+ end
423
+ end
424
+
425
+ def binary_or(context, parent_context, operand1, operand2)
426
+ operand1 = execute(context, operand1, parent_context)
427
+
428
+ if operand2
429
+ operand2 = execute(context, operand2, parent_context)
430
+ operand1 || operand2
431
+ else
432
+ context || operand1
433
+ end
434
+ end
435
+
436
+ def strip(context)
437
+ context.to_s.strip
438
+ end
439
+
440
+ def deliver_email(context, parent_context, name, hash)
441
+ raise "Context of deliver_email must respond to site" unless context.respond_to?(:site)
442
+ email = context.site.emails[execute(context, name, parent_context)]
443
+ email.deliver(execute(context, hash, parent_context))
444
+ end
445
+
446
+ def call_api(context, parent_context, name, hash)
447
+ raise "Context of call_api must respond to site" unless context.respond_to?(:site)
448
+ api = context.site.api_calls[execute(context, name, parent_context)]
449
+ api.call(execute(context, hash, parent_context))
450
+ end
451
+
452
+ def format(context, parent_context, str)
453
+ str = execute(context, str, parent_context)
454
+ str.gsub(/{{\s*([\w\.]+)\s*}}/) do |field|
455
+ fn = Function.new($1)
456
+ fn.execute(context, nil, parent_context)
457
+ end
458
+ end
459
+
460
+ # TODO: add call styles to min and max:
461
+ # items.min(index)
462
+ # items.collect(index).min
463
+ # min(one, two)
464
+ def min(context, parent_context, one, two)
465
+ [execute(context, one, parent_context), execute(context, two, parent_context)].min
466
+ end
467
+
468
+ def max(context, parent_context, one, two)
469
+ [execute(context, one, parent_context), execute(context, two, parent_context)].max
470
+ end
471
+ end