activesp 0.0.1 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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