activesp 0.0.1 → 0.0.4

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/lib/activesp/list.rb CHANGED
@@ -1,3 +1,28 @@
1
+ # Copyright (c) 2010 XAOP bvba
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation
5
+ # files (the "Software"), to deal in the Software without
6
+ # restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following
10
+ # conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ #
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ #
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
1
26
  module ActiveSP
2
27
 
3
28
  class List < Base
@@ -7,9 +32,15 @@ module ActiveSP
7
32
  extend PersistentCaching
8
33
  include Util
9
34
 
10
- attr_reader :site, :id
35
+ # The containing site
36
+ # @return [Site]
37
+ attr_reader :site
38
+ # The ID of the list
39
+ # @return [String]
40
+ attr_reader :id
11
41
 
12
42
  persistent { |site, id, *a| [site.connection, [:list, id]] }
43
+ # @private
13
44
  def initialize(site, id, title = nil, attributes_before_type_cast1 = nil, attributes_before_type_cast2 = nil)
14
45
  @site, @id = site, id
15
46
  @Title = title if title
@@ -17,130 +48,158 @@ module ActiveSP
17
48
  @attributes_before_type_cast2 = attributes_before_type_cast2 if attributes_before_type_cast2
18
49
  end
19
50
 
51
+ # The URL of the list
52
+ # @return [String]
20
53
  def url
21
- view_url = File.dirname(attributes["DefaultViewUrl"])
54
+ # Dirty. Used to use RootFolder, but if you get the data from the bulk calls, RootFolder is the empty
55
+ # string rather than what it should be. That's what you get with web services as an afterthought I guess.
56
+ view_url = ::File.dirname(attributes["DefaultViewUrl"])
22
57
  result = URL(@site.url).join(view_url).to_s
23
- if File.basename(result) == "Forms" and dir = File.dirname(result) and dir.length > @site.url.length
58
+ if ::File.basename(result) == "Forms" and dir = ::File.dirname(result) and dir.length > @site.url.length
24
59
  result = dir
25
60
  end
26
61
  result
27
62
  end
28
63
  cache :url
29
64
 
65
+ # @private
30
66
  def relative_url
31
67
  @site.relative_url(url)
32
68
  end
33
69
 
70
+ # See {Base#key}
71
+ # @return [String]
34
72
  def key
35
73
  encode_key("L", [@site.key, @id])
36
74
  end
37
75
 
76
+ # @private
38
77
  def Title
39
78
  data1["Title"].to_s
40
79
  end
41
80
  cache :Title
42
81
 
43
- def items(options = {})
82
+ # Yields the items in this list according to the given options. Note that this method does not
83
+ # recurse into folders. I believe specifying a folder of '' actually does recurse
84
+ # @param [Hash] options Options
85
+ # @option options [Folder, :all] :folder (nil) The folder to search in
86
+ # @option options [String] :query (nil) The query to execute as an XML fragment
87
+ # @option options [Boolean] :no_preload (nil) If set to true, the attributes are not preloaded. Can be more efficient if you only need the list of items and not their attributes
88
+ # @yieldparam [Item] item
89
+ def each_item(options = {})
90
+ options = options.dup
44
91
  folder = options.delete(:folder)
45
- query = options.delete(:query)
46
- query = query ? { "query" => query } : {}
92
+ # Always include a query because for some reason SP is capable of not finding certain
93
+ # items otherwise.
94
+ query = { "query" => options.delete(:query) || "<Query><Where></Where></Query>" }
47
95
  no_preload = options.delete(:no_preload)
48
96
  options.empty? or raise ArgumentError, "unknown options #{options.keys.map { |k| k.inspect }.join(", ")}"
49
97
  query_options = Builder::XmlMarkup.new.QueryOptions do |xml|
50
- xml.Folder(folder.url) if folder
98
+ xml.Folder(folder == :all ? "" : folder.url) if folder
51
99
  end
52
100
  if no_preload
53
101
  view_fields = Builder::XmlMarkup.new.ViewFields do |xml|
54
102
  %w[FSObjType ID UniqueId ServerUrl].each { |f| xml.FieldRef("Name" => f) }
55
103
  end
56
- result = call("Lists", "get_list_items", { "listName" => @id, "viewFields" => viewFields, "queryOptions" => query_options }.merge(query))
57
- result.xpath("//z:row", NS).map do |row|
58
- attributes = clean_item_attributes(row.attributes)
59
- (attributes["FSObjType"][/1$/] ? Folder : Item).new(
60
- self,
61
- attributes["ID"],
62
- folder,
63
- attributes["UniqueId"],
64
- attributes["ServerUrl"]
65
- )
104
+ get_list_items(view_fields, query_options, query) do |attributes|
105
+ yield construct_item(folder, attributes, nil)
66
106
  end
67
107
  else
68
- begin
69
- result = call("Lists", "get_list_items", { "listName" => @id, "viewFields" => "<ViewFields></ViewFields>", "queryOptions" => query_options }.merge(query))
70
- result.xpath("//z:row", NS).map do |row|
71
- attributes = clean_item_attributes(row.attributes)
72
- (attributes["FSObjType"][/1$/] ? Folder : Item).new(
73
- self,
74
- attributes["ID"],
75
- folder,
76
- attributes["UniqueId"],
77
- attributes["ServerUrl"],
78
- attributes
79
- )
108
+ __each_item(query_options, query) do |attributes|
109
+ yield construct_item(folder, attributes, attributes)
110
+ end
111
+ end
112
+ end
113
+ association :items
114
+
115
+ def each_document(parameters = {}, &blk)
116
+ query = Builder::XmlMarkup.new.Query do |xml|
117
+ xml.Where do |xml|
118
+ xml.Neq do |xml|
119
+ xml.FieldRef(:Name => "FSObjType")
120
+ xml.Value(1, :Type => "Text")
80
121
  end
81
- rescue Savon::SOAPFault => e
82
- if e.message[/lookup column threshold/]
83
- fields = self.fields.map { |f| f.name }
84
- split_factor = 2
85
- begin
86
- split_size = (fields.length + split_factor - 1) / split_factor
87
- parts = []
88
- split_factor.times do |i|
89
- lo = i * split_size
90
- hi = [(i + 1) * split_size, fields.length].min - 1
91
- view_fields = Builder::XmlMarkup.new.ViewFields do |xml|
92
- fields[lo..hi].each { |f| xml.FieldRef("Name" => f) }
93
- end
94
- by_id = {}
95
- result = call("Lists", "get_list_items", { "listName" => @id, "viewFields" => view_fields, "queryOptions" => query_options }.merge(query))
96
- result.xpath("//z:row", NS).map do |row|
97
- attributes = clean_item_attributes(row.attributes)
98
- by_id[attributes["ID"]] = attributes
99
- end
100
- parts << by_id
101
- end
102
- parts[0].map do |id, attrs|
103
- parts[1..-1].each do |part|
104
- attrs.merge!(part[id])
105
- end
106
- (attrs["FSObjType"][/1$/] ? Folder : Item).new(
107
- self,
108
- attrs["ID"],
109
- folder,
110
- attrs["UniqueId"],
111
- attrs["ServerUrl"],
112
- attrs
113
- )
114
- end
115
- rescue Savon::SOAPFault => e
116
- if e.message[/lookup column threshold/]
117
- split_factor += 1
118
- retry
119
- else
120
- raise
121
- end
122
- end
123
- else
124
- raise
122
+ end
123
+ end
124
+ each_item(parameters.merge(:query => query), &blk)
125
+ end
126
+ association :documents do
127
+ def create(parameters = {})
128
+ @object.create_document(parameters)
129
+ end
130
+ end
131
+
132
+ def each_folder(parameters = {}, &blk)
133
+ query = Builder::XmlMarkup.new.Query do |xml|
134
+ xml.Where do |xml|
135
+ xml.Eq do |xml|
136
+ xml.FieldRef(:Name => "FSObjType")
137
+ xml.Value(1, :Type => "Text")
125
138
  end
126
139
  end
127
140
  end
141
+ each_item(parameters.merge(:query => query), &blk)
142
+ end
143
+ association :folders do
144
+ def create(parameters = {})
145
+ @object.create_folder(parameters)
146
+ end
128
147
  end
129
148
 
149
+ # Returns the item with the given name or nil if there is no item with the given name
150
+ # @return [Item]
130
151
  def item(name)
131
152
  query = Builder::XmlMarkup.new.Query do |xml|
132
153
  xml.Where do |xml|
133
154
  xml.Eq do |xml|
134
155
  xml.FieldRef(:Name => "FileLeafRef")
135
- xml.Value(name, :Type => "String")
156
+ xml.Value(name, :Type => "Text")
136
157
  end
137
158
  end
138
159
  end
139
160
  items(:query => query).first
140
161
  end
141
162
 
142
- def /(name)
143
- item(name)
163
+ alias / item
164
+
165
+ def create_document(parameters = {})
166
+ when_list { return create_list_item(parameters) }
167
+ when_document_library { return create_library_document(parameters) }
168
+ raise_on_unknown_type
169
+ end
170
+
171
+ def create_folder(parameters = {})
172
+ name = parameters.delete("FileLeafRef") or raise ArgumentError, "Specify the folder name in the 'FileLeafRef' parameter"
173
+
174
+ create_list_item(parameters.merge(:folder_name => name))
175
+ end
176
+
177
+ def changes_since_token(token, options = {})
178
+ options = options.dup
179
+ no_preload = options.delete(:no_preload)
180
+ options.empty? or raise ArgumentError, "unknown options #{options.keys.map { |k| k.inspect }.join(", ")}"
181
+
182
+ if no_preload
183
+ view_fields = Builder::XmlMarkup.new.ViewFields do |xml|
184
+ %w[FSObjType ID UniqueId ServerUrl].each { |f| xml.FieldRef("Name" => f) }
185
+ end
186
+ else
187
+ view_fields = Builder::XmlMarkup.new.ViewFields
188
+ end
189
+ result = call("Lists", "get_list_item_changes_since_token", "listName" => @id, 'queryOptions' => '<queryOptions xmlns:s="http://schemas.microsoft.com/sharepoint/soap/" ><QueryOptions/></queryOptions>', 'changeToken' => token, 'viewFields' => view_fields)
190
+ updates = []
191
+ result.xpath("//z:row", NS).each do |row|
192
+ attributes = clean_item_attributes(row.attributes)
193
+ updates << construct_item(:unset, attributes, no_preload ? nil : attributes)
194
+ end
195
+ deletes = []
196
+ result.xpath("//sp:Changes/sp:Id", NS).each do |row|
197
+ if row["ChangeType"].to_s == "Delete"
198
+ deletes << encode_key("I", [key, row.text.to_s])
199
+ end
200
+ end
201
+ new_token = result.xpath("//sp:Changes", NS).first["LastChangeToken"].to_s
202
+ { :updates => updates, :deletes => deletes, :new_token => new_token }
144
203
  end
145
204
 
146
205
  def fields
@@ -151,13 +210,14 @@ module ActiveSP
151
210
  end
152
211
  end.compact
153
212
  end
154
- cache :fields, :dup => true
213
+ cache :fields, :dup => :always
155
214
 
156
215
  def fields_by_name
157
- fields.inject({}) { |h, f| h[f.attributes["StaticName"]] = f ; h }
216
+ fields.inject({}) { |h, f| h[decode_field_name(f.attributes["StaticName"])] = f ; h }
158
217
  end
159
- cache :fields_by_name, :dup => true
218
+ cache :fields_by_name, :dup => :always
160
219
 
220
+ # @private
161
221
  def field(id)
162
222
  fields.find { |f| f.ID == id }
163
223
  end
@@ -168,8 +228,9 @@ module ActiveSP
168
228
  ContentType.new(@site, self, content_type["ID"], content_type["Name"], content_type["Description"], content_type["Version"], content_type["Group"])
169
229
  end
170
230
  end
171
- cache :content_types, :dup => true
231
+ cache :content_types, :dup => :always
172
232
 
233
+ # @private
173
234
  def content_type(id)
174
235
  content_types.find { |t| t.id == id }
175
236
  end
@@ -183,12 +244,85 @@ module ActiveSP
183
244
  end
184
245
  cache :permission_set
185
246
 
247
+ # See {Base#save}
248
+ # @return [void]
249
+ def save
250
+ p untype_cast_attributes(@site, nil, internal_attribute_types, changed_attributes)
251
+ end
252
+
253
+ # @private
186
254
  def to_s
187
255
  "#<ActiveSP::List Title=#{self.Title}>"
188
256
  end
189
257
 
258
+ # @private
190
259
  alias inspect to_s
191
260
 
261
+ # @private
262
+ def when_document_library
263
+ yield if %w[1].include?(attributes["BaseType"])
264
+ end
265
+
266
+ # @private
267
+ def when_list
268
+ yield if %w[0 5].include?(attributes["BaseType"])
269
+ end
270
+
271
+ # @private
272
+ def raise_on_unknown_type
273
+ base_type = attributes["BaseType"]
274
+ raise "not yet BaseType = #{base_type.inspect}" unless %w[0 1 5].include?(base_type)
275
+ end
276
+
277
+ # @private
278
+ def __each_item(query_options, query)
279
+ get_list_items("<ViewFields></ViewFields>", query_options, query) do |attributes|
280
+ yield attributes
281
+ end
282
+ rescue Savon::SOAPFault => e
283
+ # This is where it gets ugly... Apparently there is a limit to the number of columns
284
+ # you can retrieve with this operation. Joy!
285
+ if e.message[/lookup column threshold/]
286
+ fields = self.fields.map { |f| f.Name }
287
+ split_factor = 2
288
+ begin
289
+ split_size = (fields.length + split_factor - 1) / split_factor
290
+ parts = []
291
+ split_factor.times do |i|
292
+ lo = i * split_size
293
+ hi = [(i + 1) * split_size, fields.length].min - 1
294
+ view_fields = Builder::XmlMarkup.new.ViewFields do |xml|
295
+ fields[lo..hi].each { |f| xml.FieldRef("Name" => f) }
296
+ end
297
+ by_id = {}
298
+ get_list_items(view_fields, query_options, query) do |attributes|
299
+ by_id[attributes["ID"]] = attributes
300
+ end
301
+ parts << by_id
302
+ end
303
+ parts[0].each do |id, attrs|
304
+ parts[1..-1].each do |part|
305
+ attrs.merge!(part[id])
306
+ end
307
+ yield attrs
308
+ end
309
+ rescue Savon::SOAPFault => e
310
+ if e.message[/lookup column threshold/]
311
+ split_factor += 1
312
+ retry
313
+ else
314
+ raise
315
+ end
316
+ end
317
+ else
318
+ raise
319
+ end
320
+ end
321
+
322
+ def ==(object)
323
+ ::ActiveSP::List === object && self.ID == object.ID
324
+ end
325
+
192
326
  private
193
327
 
194
328
  def data1
@@ -295,7 +429,76 @@ module ActiveSP
295
429
  { :mask => Integer(row["Mask"]), :accessor => accessor }
296
430
  end
297
431
  end
298
- cache :permissions, :dup => true
432
+ cache :permissions, :dup => :always
433
+
434
+ def get_list_items(view_fields, query_options, query)
435
+ result = call("Lists", "get_list_items", { "listName" => @id, "viewFields" => view_fields, "queryOptions" => query_options }.merge(query))
436
+ result.xpath("//z:row", NS).each do |row|
437
+ yield clean_item_attributes(row.attributes)
438
+ end
439
+ end
440
+
441
+ def construct_item(folder, attributes, all_attributes)
442
+ (attributes["FSObjType"][/1$/] ? Folder : Item).new(
443
+ self,
444
+ attributes["ID"],
445
+ folder == :all ? :unset : folder,
446
+ attributes["UniqueId"],
447
+ attributes["ServerUrl"],
448
+ all_attributes
449
+ )
450
+ end
451
+
452
+ def create_library_document(parameters)
453
+ parameters = parameters.dup
454
+ content = parameters.delete(:content) or raise ArgumentError, "Specify the content in the :content parameter"
455
+ folder = parameters.delete(:folder)
456
+ overwrite = parameters.delete(:overwrite)
457
+ file_name = parameters.delete("FileLeafRef") or raise ArgumentError, "Specify the file name in the 'FileLeafRef' parameter"
458
+ raise ArgumentError, "document with file name #{file_name.inspect} already exists" if item(file_name) && !overwrite
459
+ destination_urls = Builder::XmlMarkup.new.wsdl(:string, URI.escape(::File.join(folder || url, file_name)))
460
+ parameters = type_check_attributes_for_creation(fields_by_name, parameters)
461
+ attributes = untype_cast_attributes(@site, self, fields_by_name, parameters)
462
+ fields = construct_xml_for_copy_into_items(fields_by_name, attributes)
463
+ source_url = escape_xml(file_name)
464
+ result = call("Copy", "copy_into_items", "DestinationUrls" => destination_urls, "Stream" => Base64.encode64(content.to_s), "SourceUrl" => source_url, "Fields" => fields)
465
+ copy_result = result.xpath("//sp:CopyResult", NS).first
466
+ error_code = copy_result["ErrorCode"]
467
+ if error_code != "Success"
468
+ raise "#{error_code} : #{copy_result["ErrorMessage"]}"
469
+ else
470
+ item(file_name)
471
+ end
472
+ end
473
+
474
+ def create_list_item(parameters)
475
+ parameters = parameters.dup
476
+ folder = parameters.delete(:folder)
477
+ folder_name = parameters.delete(:folder_name)
478
+ parameters = type_check_attributes_for_creation(fields_by_name, parameters)
479
+ attributes = untype_cast_attributes(@site, self, fields_by_name, parameters)
480
+ updates = Builder::XmlMarkup.new.Batch("OnError" => "Continue", "ListVersion" => 1) do |xml|
481
+ xml.Method("ID" => 1, "Cmd" => "New") do
482
+ xml.Field("New", "Name" => "ID")
483
+ construct_xml_for_update_list_items(xml, fields_by_name, attributes)
484
+ if folder_name
485
+ xml.Field(::File.join(folder || url, folder_name), "Name" => "FileRef")
486
+ xml.Field(1, "Name" => "FSObjType")
487
+ else
488
+ xml.Field(::File.join(folder, Time.now.strftime("%Y%m%d%H%M%S-#{rand(16**3).to_s(16)}")), "Name" => "FileRef") if folder
489
+ end
490
+ end
491
+ end
492
+ result = call("Lists", "update_list_items", "listName" => self.id, "updates" => updates)
493
+ create_result = result.xpath("//sp:Result", NS).first
494
+ error_text = create_result.xpath("./sp:ErrorText", NS).first
495
+ if !error_text
496
+ row = result.xpath("//z:row", NS).first
497
+ construct_item(nil, clean_item_attributes(row.attributes), nil)
498
+ else
499
+ raise "cannot create item: #{error_text.text.to_s}"
500
+ end
501
+ end
299
502
 
300
503
  end
301
504