redmine_api_helper 0.3.24

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.

Potentially problematic release.


This version of redmine_api_helper might be problematic. Click here for more details.

Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +2 -0
  3. data/.gitignore +11 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +339 -0
  7. data/README.md +30 -0
  8. data/Rakefile +2 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/lib/date_helper/date.rb +311 -0
  12. data/lib/odf_writer/bookmark.rb +110 -0
  13. data/lib/odf_writer/bookmark_reader.rb +77 -0
  14. data/lib/odf_writer/document.rb +372 -0
  15. data/lib/odf_writer/field.rb +174 -0
  16. data/lib/odf_writer/field_reader.rb +78 -0
  17. data/lib/odf_writer/image.rb +158 -0
  18. data/lib/odf_writer/image_reader.rb +76 -0
  19. data/lib/odf_writer/images.rb +89 -0
  20. data/lib/odf_writer/list_style.rb +331 -0
  21. data/lib/odf_writer/nested.rb +156 -0
  22. data/lib/odf_writer/odf_helper.rb +56 -0
  23. data/lib/odf_writer/parser/default.rb +685 -0
  24. data/lib/odf_writer/path_finder.rb +114 -0
  25. data/lib/odf_writer/section.rb +120 -0
  26. data/lib/odf_writer/section_reader.rb +61 -0
  27. data/lib/odf_writer/style.rb +417 -0
  28. data/lib/odf_writer/table.rb +135 -0
  29. data/lib/odf_writer/table_reader.rb +61 -0
  30. data/lib/odf_writer/template.rb +222 -0
  31. data/lib/odf_writer/text.rb +97 -0
  32. data/lib/odf_writer/text_reader.rb +77 -0
  33. data/lib/odf_writer/version.rb +29 -0
  34. data/lib/redmine_api_helper/api_helper.rb +333 -0
  35. data/lib/redmine_api_helper/args_helper.rb +106 -0
  36. data/lib/redmine_api_helper/attachments_api_helper.rb +52 -0
  37. data/lib/redmine_api_helper/define_api_helpers.rb +78 -0
  38. data/lib/redmine_api_helper/document_categories_api_helper.rb +38 -0
  39. data/lib/redmine_api_helper/groups_api_helper.rb +80 -0
  40. data/lib/redmine_api_helper/helpers.rb +50 -0
  41. data/lib/redmine_api_helper/issue_priorities_api_helper.rb +38 -0
  42. data/lib/redmine_api_helper/issue_relations_api_helper.rb +66 -0
  43. data/lib/redmine_api_helper/issue_statuses_api_helper.rb +36 -0
  44. data/lib/redmine_api_helper/issues_api_helper.rb +124 -0
  45. data/lib/redmine_api_helper/my_account_api_helper.rb +45 -0
  46. data/lib/redmine_api_helper/news_api_helper.rb +73 -0
  47. data/lib/redmine_api_helper/project_memberships_api_helper.rb +77 -0
  48. data/lib/redmine_api_helper/projects_api_helper.rb +73 -0
  49. data/lib/redmine_api_helper/roles_api_helper.rb +52 -0
  50. data/lib/redmine_api_helper/scripts_api_helper.rb +87 -0
  51. data/lib/redmine_api_helper/search_api_helper.rb +38 -0
  52. data/lib/redmine_api_helper/time_entries_api_helper.rb +73 -0
  53. data/lib/redmine_api_helper/time_entry_activities_api_helper.rb +38 -0
  54. data/lib/redmine_api_helper/trackers_api_helper.rb +38 -0
  55. data/lib/redmine_api_helper/users_api_helper.rb +73 -0
  56. data/lib/redmine_api_helper/version.rb +24 -0
  57. data/lib/redmine_api_helper/wiki_pages_api_helper.rb +66 -0
  58. data/lib/redmine_api_helper.rb +88 -0
  59. data/redmine_api_helper.gemspec +35 -0
  60. metadata +148 -0
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Ruby Gem to create a self populating Open Document Format (.odf) text file.
4
+ #
5
+ # Copyright 2021 Stephan Wenzel <stephan.wenzel@drwpatent.de>
6
+ #
7
+ # This program is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU General Public License
9
+ # as published by the Free Software Foundation; either version 2
10
+ # of the License, or (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
+ #
21
+
22
+ module ODFWriter
23
+
24
+ ########################################################################################
25
+ #
26
+ # TextReader: find all texts and set name
27
+ #
28
+ ########################################################################################
29
+ class TextReader < FieldReader
30
+
31
+ attr_accessor :name
32
+
33
+ ######################################################################################
34
+ #
35
+ # initialize
36
+ #
37
+ ######################################################################################
38
+ def initialize(opts={})
39
+ @name = opts[:name]
40
+ end #def
41
+
42
+ ######################################################################################
43
+ #
44
+ # paths
45
+ #
46
+ ######################################################################################
47
+ def paths( file, doc)
48
+
49
+ # find nodes with matching field elements matching [FIELD] pattern
50
+ nodes = doc.xpath("//text()").select{|node| scan(node).present? }
51
+
52
+ # find path for each field
53
+ paths = nil
54
+ nodes.each do |node|
55
+ leaf = {:texts => scan(node)}
56
+ paths = PathFinder.trail(node, leaf, :root => file, :paths => paths)
57
+ end #each
58
+ paths.to_h
59
+
60
+ end #def
61
+
62
+ ######################################################################################
63
+ # private
64
+ ######################################################################################
65
+
66
+ private
67
+
68
+ def scan(node)
69
+ if name
70
+ node.text.scan(/(?<=#{Regexp.escape Text::DELIMITERS[0]})#{name.upcase}(?=#{Regexp.escape Text::DELIMITERS[1]})/)
71
+ else
72
+ node.text.scan(/(?<=#{Regexp.escape Text::DELIMITERS[0]})[A-Z0-9_]+?(?=#{Regexp.escape Text::DELIMITERS[1]})/)
73
+ end
74
+ end #def
75
+
76
+ end #class
77
+ end #module
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Ruby Gem to create a self populating Open Document Format (.odf) text file.
4
+ #
5
+ # Copyright 2021 Stephan Wenzel <stephan.wenzel@drwpatent.de>
6
+ #
7
+ # This program is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU General Public License
9
+ # as published by the Free Software Foundation; either version 2
10
+ # of the License, or (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
+ #
21
+
22
+ module ODFWriter
23
+ # 1.0.2
24
+ # - added arrify to document.rb
25
+ #
26
+ # 1.0.1 initial commit
27
+ #
28
+ VERSION = "1.0.2"
29
+ end #module
@@ -0,0 +1,333 @@
1
+ ##
2
+ # aids creating fiddles for redmine_scripting_engine
3
+ #
4
+ # Copyright 2021 Stephan Wenzel <stephan.wenzel@drwpatent.de>
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License
8
+ # as published by the Free Software Foundation; either version 2
9
+ # of the License, or (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
+ #
20
+
21
+ require 'net/http'
22
+
23
+ module RedmineAPIHelper
24
+ module APIHelper
25
+
26
+ ########################################################################################
27
+ # api functions
28
+ ########################################################################################
29
+ class TooManyRedirects < StandardError; end
30
+ LIMIT_REDIRECTS = 10
31
+ USER_AGENT = ["Redmine API Helper", RedmineAPIHelper::VERSION].join(" ")
32
+
33
+ ########################################################################################
34
+ # returns error
35
+ ########################################################################################
36
+ def error(err)
37
+ OpenStruct.new(:error => err.message, :backtrace => err.backtrace)
38
+ end #def
39
+
40
+ ########################################################################################
41
+ # assembles url from fragments
42
+ ########################################################################################
43
+ def url_path(*fragments, **options)
44
+ [fragments.map do |fragment|
45
+ fragment.to_s.gsub(/\/\z/,"")
46
+ end.join("/"),
47
+ options.to_query.presence
48
+ ].compact.join("?")
49
+ end #def
50
+
51
+ ########################################################################################
52
+ # lists objects, corresponds to controller#index
53
+ ########################################################################################
54
+ def list_objects(objects, params={})
55
+ jget(:url => send("#{objects}_url"), :params => params ).send(objects)
56
+ rescue Exception => err
57
+ error(err)
58
+ end #def
59
+
60
+ def list_project_objects(project_id, objects, params={})
61
+ jget(:url => send("project_#{objects}_url", project_id), :params => params ).send(objects)
62
+ rescue Exception => err
63
+ error(err)
64
+ end #def
65
+
66
+ ########################################################################################
67
+ # reads object having id, corresponds to controller#show
68
+ ########################################################################################
69
+ def read_object(object, id, params={})
70
+ jget(:url => send("#{object}_url", id), :params => params ).send(object)
71
+ rescue Exception => err
72
+ error(err)
73
+ end #def
74
+
75
+ def read_project_object(project_id, object, id, params={})
76
+ jget(:url => send("project_#{object}_url", project_id, id), :params => params ).send(object)
77
+ rescue Exception => err
78
+ error(err)
79
+ end #def
80
+
81
+ ########################################################################################
82
+ # creates a new object with params, corresponds to controller#create
83
+ ########################################################################################
84
+ def create_object(object, params={})
85
+ jpost( {object => params}, :url => send("#{object.to_s.pluralize}_url") ).send(object)
86
+ rescue Exception => err
87
+ error(err)
88
+ end #def
89
+
90
+ def create_project_object(project_id, object, params={})
91
+ jpost( {object => params}, :url => send("project_#{object.to_s.pluralize}_url", project_id) ).send(object)
92
+ rescue Exception => err
93
+ error(err)
94
+ end #def
95
+
96
+ ########################################################################################
97
+ # updates an existing object with params, corresponds to controller#update
98
+ ########################################################################################
99
+ def update_object(object, id, params={})
100
+ jput( {object => params}, :url => send("#{object}_url", id) )
101
+ rescue Exception => err
102
+ error(err)
103
+ end #def
104
+
105
+ def update_project_object(project_id, object, id, params={})
106
+ jput( {object => params}, :url => send("project_#{object}_url", project_id, id) )
107
+ rescue Exception => err
108
+ error(err)
109
+ end #def
110
+
111
+ ########################################################################################
112
+ # deletes an existing object with params, corresponds to controller#destroy
113
+ ########################################################################################
114
+ def destroy_object(object, id, params={})
115
+ jdel(:url => send("#{object}_url", id), :params => params )
116
+ rescue Exception => err
117
+ error(err)
118
+ end #def
119
+
120
+ def destroy_project_object(project_id, object, id, params={})
121
+ jdel(:url => send("project_#{object}_url", project_id, id), :params => params )
122
+ rescue Exception => err
123
+ error(err)
124
+ end #def
125
+
126
+ ########################################################################################
127
+ # fetch(options), get request
128
+ ########################################################################################
129
+ def fetch(options={})
130
+
131
+ # create query parameters
132
+ params = options[:params].to_h.to_query
133
+ url = options[:url]
134
+
135
+ # create GET request
136
+ uri = URI.parse([url, params.presence].compact.join("?"))
137
+ req = Net::HTTP::Get.new(uri.request_uri)
138
+
139
+ # create HTTP handler
140
+ http = Net::HTTP.new(uri.host, uri.port)
141
+ http.use_ssl = uri.scheme.downcase == "https"
142
+
143
+ # get request
144
+ @http_response = http.request(req)
145
+
146
+ case @http_response
147
+
148
+ when Net::HTTPSuccess
149
+ @http_response.body.present? ? @http_response.body : serialize(:response => "OK")
150
+
151
+ when Net::HTTPRedirection
152
+ options[:redirects] = options[:redirects].to_i + 1
153
+ raise TooManyRedirects if options[:redirects] > LIMIT_REDIRECTS
154
+ fetch( options.merge(:url => response['location']) )
155
+
156
+ else
157
+ serialize(:response => @http_response.code)
158
+
159
+ end
160
+
161
+ end #def
162
+
163
+ ########################################################################################
164
+ # jget(options), get request
165
+ ########################################################################################
166
+ def jget(options={})
167
+
168
+ index = options[:index].to_i
169
+ json = options[:json].nil? || !!options[:json]
170
+ params = json ? options[:params].to_h.merge(:format => "json").to_query : options[:params].to_h.to_query
171
+ content_type = json ? "application/json" : options[:content_type]
172
+ api = options[:api_key].nil? ? true : !!options[:api_key]
173
+ url = options[:url].presence || args.objects[index].object_url
174
+
175
+ # create GET request
176
+ uri = URI.parse( [url, params.presence].compact.join("?"))
177
+ req = Net::HTTP::Get.new(uri.request_uri)
178
+ req["Content-Type"] = content_type
179
+ req['X-Redmine-API-Key'] = args.api_key if api
180
+ req["Referer"] = args.deep_try(:eparams, :url) || args.deep_try(:urls, :back)
181
+ req["User-Agent"] = USER_AGENT
182
+ req["Accept"] = "*/*"
183
+ req.basic_auth(args.site_user, args.site_password) if args.site_user.present? || args.site_password.present?
184
+
185
+ # create HTTP handler
186
+ http = Net::HTTP.new(uri.host, uri.port)
187
+ http.use_ssl = uri.scheme.downcase == "https"
188
+
189
+ # get request
190
+ @http_response = http.request(req)
191
+
192
+ # follow redirection or get result code
193
+ handle_response(options)
194
+
195
+ end #def
196
+
197
+ ########################################################################################
198
+ # jput(body, options), put request
199
+ ########################################################################################
200
+ def jput(body, options={})
201
+
202
+ index = options[:index].to_i
203
+ json = options[:json].nil? || !!options[:json]
204
+ params = json ? options[:params].to_h.merge(:format => "json").to_query : options[:params].to_h.to_query
205
+ content_type = json ? "application/json" : options[:content_type]
206
+ api = options[:api_key].nil? ? true : !!options[:api_key]
207
+ url = options[:url].presence || args.objects[index].object_url
208
+
209
+ # create PUT request
210
+ uri = URI.parse( [url, params.presence].compact.join("?"))
211
+ req = Net::HTTP::Put.new(uri.request_uri)
212
+ req["Content-Type"] = content_type
213
+ req['X-Redmine-API-Key'] = args.api_key if api
214
+ req["Referer"] = args.deep_try(:eparams, :url) || args.deep_try(:urls, :back)
215
+ req["User-Agent"] = USER_AGENT
216
+ req["Accept"] = "*/*"
217
+ req.basic_auth(args.site_user, args.site_password) if args.site_user.present? || args.site_password.present?
218
+
219
+ # create body
220
+ req.body = deserialize(body).to_json
221
+
222
+ # create HTTP handler
223
+ http = Net::HTTP.new(uri.host, uri.port)
224
+ http.use_ssl = uri.scheme.downcase == "https"
225
+
226
+ # get request
227
+ @http_response = http.request(req)
228
+
229
+ # follow redirection or get result code
230
+ handle_response(options)
231
+
232
+ end #def
233
+
234
+ ########################################################################################
235
+ # jpost(body, options), post request
236
+ ########################################################################################
237
+ def jpost(body, options={})
238
+
239
+ index = options[:index].to_i
240
+ json = options[:json].nil? || !!options[:json]
241
+ params = json ? options[:params].to_h.merge(:format => "json").to_query : options[:params].to_h.to_query
242
+ content_type = json ? "application/json" : options[:content_type]
243
+ api = options[:api_key].nil? ? true : !!options[:api_key]
244
+ url = options[:url].presence || args.objects[index].object_url
245
+
246
+ # create POST request
247
+ uri = URI.parse( [url, params.presence].compact.join("?"))
248
+ req = Net::HTTP::Post.new(uri.request_uri)
249
+ req["Content-Type"] = content_type
250
+ req['X-Redmine-API-Key'] = args.api_key if api
251
+ req["Referer"] = args.deep_try(:eparams, :url) || args.deep_try(:urls, :back)
252
+ req["User-Agent"] = USER_AGENT
253
+ req["Accept"] = "*/*"
254
+ req.basic_auth(args.site_user, args.site_password) if args.site_user.present? || args.site_password.present?
255
+
256
+ # create body
257
+ req.body = deserialize(body).to_json
258
+
259
+ # create HTTP handler
260
+ http = Net::HTTP.new(uri.host, uri.port)
261
+ http.use_ssl = uri.scheme.downcase == "https"
262
+
263
+ # get request
264
+ @http_response = http.request(req)
265
+
266
+ # follow redirection or get result code
267
+ handle_response(options)
268
+
269
+ end #def
270
+
271
+ ########################################################################################
272
+ # jdel(options), delete request
273
+ ########################################################################################
274
+ def jdel(options={})
275
+
276
+ index = options[:index].to_i
277
+ json = options[:json].nil? || !!options[:json]
278
+ params = json ? options[:params].to_h.merge(:format => "json").to_query : options[:params].to_h.to_query
279
+ content_type = json ? "application/json" : options[:content_type]
280
+ api = options[:api_key].nil? ? true : !!options[:api_key]
281
+ url = options[:url].presence || args.objects[index].object_url
282
+
283
+ # create DELETE request
284
+ uri = URI.parse( [url, params.presence].compact.join("?"))
285
+ req = Net::HTTP::Delete.new(uri.request_uri)
286
+ req["Content-Type"] = content_type
287
+ req['X-Redmine-API-Key'] = args.api_key if api
288
+ req["Referer"] = args.deep_try(:eparams, :url) || args.deep_try(:urls, :back)
289
+ req["User-Agent"] = USER_AGENT
290
+ req["Accept"] = "*/*"
291
+ req.basic_auth(args.site_user, args.site_password) if args.site_user.present? || args.site_password.present?
292
+
293
+ # create HTTP handler
294
+ http = Net::HTTP.new(uri.host, uri.port)
295
+ http.use_ssl = uri.scheme.downcase == "https"
296
+
297
+ # get request
298
+ @http_response = http.request(req)
299
+
300
+ # follow redirection or get result code
301
+ handle_response(options)
302
+
303
+ end #def
304
+
305
+ ########################################################################################
306
+ # private
307
+ ########################################################################################
308
+ private
309
+
310
+ ########################################################################################
311
+ # handle_response
312
+ ########################################################################################
313
+ def handle_response(options)
314
+
315
+ case @http_response
316
+
317
+ when Net::HTTPSuccess
318
+ @http_response.body.present? ? serialize(JSON.parse(@http_response.body)) : serialize(:result => @http_response.code)
319
+
320
+ when Net::HTTPRedirection
321
+ options[:redirects] = options[:redirects].to_i + 1
322
+ raise TooManyRedirects if options[:redirects] > LIMIT_REDIRECTS
323
+ function = caller_locations(1,1)[0].label.to_sym
324
+ send(function, options)
325
+
326
+ else
327
+ @http_response.body.present? ? serialize(JSON.parse(@http_response.body)) : serialize(:result => @http_response.code)
328
+
329
+ end
330
+ end #def
331
+
332
+ end #module
333
+ end #module
@@ -0,0 +1,106 @@
1
+ ##
2
+ # aids creating fiddles for redmine_scripting_engine
3
+ #
4
+ # Copyright x 2021 Stephan Wenzel <stephan.wenzel@drwpatent.de>
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License
8
+ # as published by the Free Software Foundation; either version 2
9
+ # of the License, or (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
+ #
20
+
21
+ module RedmineAPIHelper
22
+ module ArgsHelper
23
+
24
+ ########################################################################################
25
+ # iterates over current object, set index for functions accessing current object
26
+ ########################################################################################
27
+ def iterate(&block)
28
+ args.objects.map do |object|
29
+ obj = yield object
30
+ @index += 1 unless @index + 1 >= args.objects.length
31
+ obj
32
+ end
33
+ end #def
34
+
35
+ ########################################################################################
36
+ # gets value of field in current object
37
+ ########################################################################################
38
+ def value( *fields )
39
+ args.objects[index].deep_try(*fields)
40
+ end #def
41
+
42
+ ########################################################################################
43
+ # serializes object to OpenStruct
44
+ ########################################################################################
45
+ def serialize(object, **options)
46
+ if object.is_a?(Hash)
47
+ OpenStruct.new(object.map{ |key, val| [ key, serialize(val, options) ] }.to_h)
48
+ elsif object.is_a?(Array)
49
+ object.map{ |obj| serialize(obj, options) }
50
+ else # assumed to be a primitive value
51
+ if options[:parse]
52
+ JSON.parse(object, object_class:OpenStruct) rescue object
53
+ else
54
+ object
55
+ end
56
+ end
57
+ end #def
58
+
59
+ ########################################################################################
60
+ # serializes JSON string to OpenStruct
61
+ ########################################################################################
62
+ def jparse(object)
63
+ serialize(object, :parse => true)
64
+ end #def
65
+
66
+ ########################################################################################
67
+ # deserializes object from OpenStruct
68
+ ########################################################################################
69
+ def deserialize(object)
70
+ if object.is_a?(OpenStruct)
71
+ return deserialize( object.to_h )
72
+ elsif object.is_a?(Hash)
73
+ return object.map{|key, obj| [key, deserialize(obj)]}.to_h
74
+ elsif object.is_a?(Array)
75
+ return object.map{|obj| deserialize(obj)}
76
+ else # assumed to be a primitive value
77
+ return object
78
+ end
79
+ end #def
80
+
81
+ ########################################################################################
82
+ # print pretty arguments passed to ruby script by plugin
83
+ ########################################################################################
84
+ def pretty(a=args)
85
+ JSON.pretty_generate(deserialize(a))
86
+ end #def
87
+
88
+ ########################################################################################
89
+ # print pretty response returned from http request
90
+ ########################################################################################
91
+ def pretty_response(hr=@http_response)
92
+ JSON.pretty_generate({
93
+ :code => hr.try(:code),
94
+ :body => JSON.parse(hr.try(:body).to_s)
95
+ })
96
+ end #def
97
+
98
+ ########################################################################################
99
+ # create html link
100
+ ########################################################################################
101
+ def link_to(body, url)
102
+ "<a href='#{url}'>#{body}</a>"
103
+ end #def
104
+
105
+ end #module
106
+ end #module