rtunesu 0.2.4 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. data/.gitignore +3 -0
  2. data/History.txt +0 -5
  3. data/README.txt +2 -2
  4. data/Rakefile +15 -2
  5. data/VERSION +1 -0
  6. data/lib/rtunesu/connection.rb +152 -54
  7. data/lib/rtunesu/document.rb +21 -26
  8. data/lib/rtunesu/entities/course.rb +4 -13
  9. data/lib/rtunesu/entities/division.rb +5 -12
  10. data/lib/rtunesu/entities/external_feed.rb +14 -0
  11. data/lib/rtunesu/entities/group.rb +5 -14
  12. data/lib/rtunesu/entities/permission.rb +1 -5
  13. data/lib/rtunesu/entities/section.rb +15 -11
  14. data/lib/rtunesu/entities/site.rb +6 -13
  15. data/lib/rtunesu/entities/template.rb +4 -0
  16. data/lib/rtunesu/entities/theme.rb +3 -15
  17. data/lib/rtunesu/entities/track.rb +12 -27
  18. data/lib/rtunesu/entity.rb +185 -74
  19. data/lib/rtunesu/log.rb +69 -0
  20. data/lib/rtunesu/subentities.rb +60 -0
  21. data/lib/rtunesu/user.rb +3 -1
  22. data/lib/rtunesu/version.rb +2 -2
  23. data/lib/rtunesu.rb +11 -6
  24. data/lib/show_tree.xml +7 -0
  25. data/spec/connection_spec.rb +6 -30
  26. data/spec/document_spec.rb +82 -8
  27. data/spec/entities/course_spec.rb +13 -40
  28. data/spec/entities/division_spec.rb +13 -3
  29. data/spec/entities/external_feed_spec.rb +21 -0
  30. data/spec/entities/group_spec.rb +14 -3
  31. data/spec/entities/permission_spec.rb +12 -3
  32. data/spec/entities/section_spec.rb +32 -3
  33. data/spec/entities/site_spec.rb +16 -3
  34. data/spec/entities/track_spec.rb +32 -3
  35. data/spec/entity_spec.rb +147 -86
  36. data/spec/fixtures/requests/create_course.xml +17 -0
  37. data/spec/fixtures/requests/create_division.xml +11 -0
  38. data/spec/fixtures/requests/create_group.xml +11 -0
  39. data/spec/fixtures/requests/create_section.xml +11 -0
  40. data/spec/fixtures/requests/delete_course.xml +8 -0
  41. data/spec/fixtures/requests/delete_group.xml +8 -0
  42. data/spec/fixtures/requests/delete_section.txt +8 -0
  43. data/spec/fixtures/requests/show_course.xml +8 -0
  44. data/spec/fixtures/requests/show_group.xml +8 -0
  45. data/spec/fixtures/requests/show_section.xml +8 -0
  46. data/spec/fixtures/requests/update_course.xml +10 -0
  47. data/spec/fixtures/requests/update_group.xml +9 -0
  48. data/spec/fixtures/requests/update_section.txt +10 -0
  49. data/spec/fixtures/responses/failure/create_course.xml +5 -0
  50. data/spec/fixtures/responses/failure/create_division.xml +5 -0
  51. data/spec/fixtures/responses/failure/create_group.xml +5 -0
  52. data/spec/fixtures/responses/failure/create_section.xml +5 -0
  53. data/spec/fixtures/responses/failure/delete_group.xml +5 -0
  54. data/spec/fixtures/responses/failure/show_course.xml +11 -0
  55. data/spec/fixtures/responses/failure/show_division.xml +11 -0
  56. data/spec/fixtures/responses/failure/show_group.xml +11 -0
  57. data/spec/fixtures/responses/failure/show_section.xml +11 -0
  58. data/spec/fixtures/responses/failure/show_site.xml +11 -0
  59. data/spec/fixtures/responses/failure/update_section.txt +5 -0
  60. data/spec/fixtures/responses/success/create_course.xml +5 -0
  61. data/spec/fixtures/responses/success/create_division.xml +5 -0
  62. data/spec/fixtures/responses/success/create_group.xml +5 -0
  63. data/spec/fixtures/responses/success/create_section.xml +5 -0
  64. data/spec/fixtures/responses/success/delete_course.xml +4 -0
  65. data/spec/fixtures/responses/success/delete_division.xml +4 -0
  66. data/spec/fixtures/responses/success/delete_group.xml +4 -0
  67. data/spec/fixtures/responses/success/delete_section.txt +4 -0
  68. data/spec/fixtures/responses/success/delete_section.xml +4 -0
  69. data/spec/fixtures/responses/success/show_course.xml +393 -0
  70. data/spec/fixtures/responses/success/show_division.xml +393 -0
  71. data/spec/fixtures/responses/success/show_group.xml +137 -0
  72. data/spec/fixtures/responses/success/show_section.xml +152 -0
  73. data/spec/fixtures/responses/success/show_site.xml +393 -0
  74. data/spec/fixtures/responses/success/update_course.xml +4 -0
  75. data/spec/fixtures/responses/success/update_division.xml +4 -0
  76. data/spec/fixtures/responses/success/update_group.xml +4 -0
  77. data/spec/fixtures/responses/success/update_section.txt +4 -0
  78. data/spec/fixtures/responses/success/update_section.xml +4 -0
  79. data/spec/fixtures/responses/success/update_site.xml +4 -0
  80. data/spec/spec_helper.rb +72 -7
  81. data/spec/token_generation_spec.rb +23 -0
  82. data/spec/user_spec.rb +3 -3
  83. metadata +81 -76
  84. data/Manifest.txt +0 -77
  85. data/config/hoe.rb +0 -76
  86. data/config/requirements.rb +0 -15
  87. data/lib/multipart.rb +0 -53
  88. data/spec/documents/add_spec.rb +0 -41
  89. data/spec/documents/delete_spec.rb +0 -30
  90. data/spec/documents/merge_spec.rb +0 -30
  91. data/spec/documents/show_tree_spec.rb +0 -16
  92. data/spec/fixtures/add_course.xml +0 -26
  93. data/spec/fixtures/add_division.xml +0 -26
  94. data/spec/fixtures/add_group.xml +0 -27
  95. data/spec/fixtures/add_permission.xml +0 -12
  96. data/spec/fixtures/add_section.xml +0 -34
  97. data/spec/fixtures/add_track.xml +0 -19
  98. data/spec/fixtures/delete_course.xml +0 -8
  99. data/spec/fixtures/delete_division.xml +0 -8
  100. data/spec/fixtures/delete_group.xml +0 -8
  101. data/spec/fixtures/delete_permission.xml +0 -9
  102. data/spec/fixtures/delete_section.xml +0 -8
  103. data/spec/fixtures/delete_track.xml +0 -7
  104. data/spec/fixtures/merge_course.xml +0 -38
  105. data/spec/fixtures/merge_division.xml +0 -47
  106. data/spec/fixtures/merge_group.xml +0 -29
  107. data/spec/fixtures/merge_permission.xml +0 -12
  108. data/spec/fixtures/merge_section.xml +0 -36
  109. data/spec/fixtures/merge_site.xml +0 -31
  110. data/spec/fixtures/merge_track.xml +0 -18
  111. data/spec/fixtures/requests/add_coures_request.xml +0 -0
  112. data/spec/fixtures/show_tree.xml +0 -273
  113. data/spec/fixtures/update_group.xml +0 -7
  114. data/tasks/deployment.rake +0 -34
  115. data/tasks/website.rake +0 -17
  116. data/website/index.html +0 -54
  117. data/website/index.txt +0 -7
  118. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  119. data/website/stylesheets/screen.css +0 -138
  120. data/website/template.html.erb +0 -48
@@ -1,56 +1,144 @@
1
- require 'hpricot'
2
-
3
1
  module RTunesU
4
- # A Base class reprenseting the various entities seen in iTunes U. Subclassed into the actual entity classes (Course, Division, Track, etc). Entity is mostly an object oriented interface to the underlying XML data returned from iTunes U. Most of attributes of an Entity are read by searching the souce XML returned from iTunes U by the Entity class's implemention of method_missing.
5
- # Attribute of an Entity are written through method missing as well. Methods that end in '=' will write data that will be saved to iTunes U.
2
+ # http://deimos.apple.com/rsrc/xsd/iTunesURequest-1.1.3.xsd
3
+ # A Base class reprenseting the various entities seen in iTunes U. Subclassed into the actual entity
4
+ # classes (Course, Division, Track, etc). Entity is mostly an object oriented interface to the
5
+ # underlying XML data returned from iTunes U. Most attributes of an Entity are read by searching
6
+ # the souce XML returned from iTunes U
6
7
  # == Reading and Writing Attributes
7
8
  # c = Course.find(12345, rtunes_connection_object) # finds the Course in iTunes U and stores its XML data
8
- # c.Handle # finds the <Handle> element in the XML data and returns its value (in this case 12345)
9
- # c.Name # finds the <Name> element in the XML data and returns its value (e.g. 'My Example Course')
10
- # c.Name = 'Podcasting: a Revolution' # writes a hash of unsaved data that will be sent to iTunes U.
9
+ # c.handle # finds the <Handle> element in the XML data and returns its value (in this case 12345)
10
+ # c.name # finds the <Name> element in the XML data and returns its value (e.g. 'My Example Course')
11
+ # c.name = 'Podcasting: a Revolution' # writes a hash of unsaved data that will be sent to iTunes U.
11
12
  #
12
13
  # == Accessing related entities
13
- # Related Entity objects are accessed with the pluralized form of their class name. To access a Course's related Group entities, you would use c.Groups. This will return an array of Group objects (or an empty Array object if there are no associated Groups)
14
- # You can set the array of associated entities by using the '=' form of the accessor and add anothe element to the end of an array of related entities with '<<'
14
+ # Related Entity objects are accessed with the pluralized form of their class name.
15
+ # To access a Course's related Group entities, you would use c.groups. This will return an array of
16
+ # Group objects (or an empty Array object if there are no associated Groups)
17
+ # You can set the array of associated entities by using the '=' form of the accessor and add another
18
+ # element to the end of an array of related entities with '<<'
15
19
  # Examples:
16
20
  # c = Course.find(12345, rtunes_connection_object) # finds the Course in iTunes U and stores its XML data
17
- # c.Groups # returns an array of Group entities or an empty array of there are no Group entities
18
- # c.Groups = [Group.new(:Name => 'Lectures')] # assigns the Groups related entity array to an existign array (overwriting any local data about Groups)
19
- # c.Groups << Group.new(:Name => 'Videos') # Adds the new Group object to the end of hte Groups array
20
- # c.Groups.collect {|g| g.Name} # ['Lectures', 'Videos']
21
- #
22
- # == Notes on arbitrary XML
23
- # Because Entity is, at heart, an object oriented wrapper for iTunes U XML data it is possible to add arbitrary (and possibly meaningless or invalidating) data that will be sent to iTunes U. You should have a solid understanding of how Entites relate in iTunes U to avoind sending bad data.
24
- # Examples:
25
- # c = Course.find(12345, rtunes_connection_object)
26
- # c.Junk = 'some junk xml'
27
- # c.save
28
- # # c.save will generate XML that inclucdes
29
- # # <Course>
30
- # # <Junk>some junk xml</Junk>
31
- # # ... some other XML data ...
32
- # # </Course>
33
- # # this XML may raise errors in iTunes U because it doesn't match valid iTunes U documents
21
+ # c.groups # returns an array of Group entities or an empty array of there are no Group entities
22
+ # c.groups = [Group.new(:name => 'Lectures')] # assigns the Groups related entity array to an existing
23
+ # array (overwriting any local data about Groups)
24
+ # c.groups << Group.new(:name => 'Videos') # Adds the new Group object to the end of hte Groups array
25
+ # c.groups.collect {|g| g.name} # ['Lectures', 'Videos']
34
26
  class Entity
35
- attr_accessor :connection, :attributes, :handle, :parent, :parent_handle, :saved, :source_xml
36
27
 
37
- # Creates a new Entity object with attributes based on the hash argument Some of these attributes are assgined to instance variables of the obect (if there is an attr_accessor for it), the rest will be written to a hash of edits that will be saved to iTunes U using method_missing
28
+ attr_accessor :connection, :parent, :parent_handle, :saved, :source_xml
29
+ attr_reader :handle
30
+
31
+ def self.attributes
32
+ @attributes ||= Set.new
33
+ end
34
+
35
+ def self.get_base_connection # :nodoc:
36
+ @base_connection
37
+ end
38
+
39
+ def self.base_connection
40
+ Entity.get_base_connection
41
+ end
42
+
43
+ def self.set_base_connection(connection)
44
+ @base_connection = connection
45
+ end
46
+
47
+ def self.validates!(name, values)
48
+ return
49
+ end
50
+
51
+ def self.composed_of(*names)
52
+ options = names.last.is_a?(Hash) ? names.pop : {}
53
+ self.attributes.merge(names)
54
+ names.each do |name|
55
+ storage_name = options[:as] || name.to_s.camelize
56
+
57
+ define_method(name) do
58
+ value_from_edits_or_store(storage_name)
59
+ end
60
+
61
+ unless options[:readonly]
62
+ define_method(:"#{name}=") do |arg|
63
+ edits[storage_name] = arg
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def self.has_a(*names)
70
+ options = names.last.is_a?(Hash) ? names.pop : {}
71
+ self.attributes.merge(names)
72
+ names.each do |name|
73
+ define_method(name) do
74
+ entity_name = options[:as] || name.to_s.camelize
75
+ instance_variable_get("@#{name}") || instance_variable_set("@#{name}", RTunesU::HasAEntityCollectionProxy.new(self.source_xml./(entity_name), self, entity_name))
76
+ end
77
+
78
+ unless options[:readonly]
79
+ define_method(:"#{name}=") do |arg|
80
+ edits[options[:as] || name.to_s.camelize] = arg
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def self.has_n(*names)
87
+ options = names.last.is_a?(Hash) ? names.pop : {}
88
+ self.attributes.merge(names)
89
+ names.each do |name|
90
+ define_method(name) do
91
+ entity_name = options[:as] || name.to_s.chop.camelize
92
+ instance_variable_get("@#{name}") || instance_variable_set("@#{name}", RTunesU::HasNEntityCollectionProxy.new(self.source_xml./(entity_name), self, entity_name))
93
+ end
94
+
95
+ unless options[:readonly]
96
+ define_method(:"#{name}=") do |arg|
97
+ edits[options[:as] || name.to.camelize] = arg
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ # Creates a new Entity object with attributes based on the hash argument Some of these attributes
104
+ # are assgined to instance variables of the obect (if there is an attr_accessor for it), the rest
105
+ # will be written to a hash of edits that will be saved to iTunes U using method_missing
38
106
  def initialize(attrs = {})
39
- self.attributes = {}
40
107
  attrs.each {|attribute, value| self.send("#{attribute}=", value)}
108
+ self.source_xml = Hpricot.XML('')
41
109
  end
42
-
43
- # Finds a specific entity in iTunes U. To find an entity you will need to know both its type (Course, Group, etc) and handle. Handles uniquely identify entities in iTunes U and the entity type is used to search the returned XML for the specific entity you are looking for. For example,
110
+
111
+ def handle
112
+ @handle ||= handle_from_source
113
+ end
114
+
115
+ def handle_from_source
116
+ return nil unless self.source_xml
117
+ if (handle_elem = self.source_xml % 'Handle')
118
+ handle_elem.innerHTML
119
+ else
120
+ nil
121
+ end
122
+ end
123
+
124
+ # Finds a specific entity in iTunes U. To find an entity you will need to know both its type
125
+ # (Course, Group, etc) and handle. Handles uniquely identify entities in iTunes U and the entity
126
+ # type is used to search the returned XML for the specific entity you are looking for. For example,
44
127
  # Course.find(123456, rtunes_connection_object)
45
- def self.find(handle, connection)
46
- entity = self.new(:handle => handle)
47
- entity.load_from_xml(connection.process(Document::ShowTree.new(entity).xml))
128
+ def self.find(handle, connection = nil)
129
+ connection ||= self.base_connection
130
+
131
+ entity = self.new
132
+ entity.instance_variable_set('@handle', handle)
133
+ entity.load_from_xml(connection.upload_file(RTunesU::SHOW_TREE_FILE, handle))
48
134
  entity
135
+
136
+ rescue LocationNotFound
137
+ raise EntityNotFound, "Could not find #{entity.class_name} with handle of #{handle}."
49
138
  end
50
-
51
- def load_from_xml(xml)
52
- self.source_xml = Hpricot.XML(xml).at("//ITunesUResponse//#{self.class_name}//Handle[text()=#{self.handle}]..")
53
- raise EntityNotFound if self.source_xml.nil?
139
+
140
+ def load_from_xml(xml_or_entity)
141
+ self.source_xml = Hpricot.XML(xml_or_entity).at("//ITunesUResponse//#{self.class_name}//Handle[text()=#{self.handle}]..")
54
142
  end
55
143
 
56
144
  # Edits stores the changes made to an entity
@@ -58,6 +146,10 @@ module RTunesU
58
146
  @edits ||= {}
59
147
  end
60
148
 
149
+ def edited?
150
+ self.edits.any?
151
+ end
152
+
61
153
  # Clear the edits and restores the loaded object to its original form
62
154
  def reload
63
155
  self.edits.clear
@@ -70,32 +162,13 @@ module RTunesU
70
162
  nil
71
163
  end
72
164
 
73
- def method_missing(method_name, args = nil)
74
- # introspect the kind of method call (read one attribute,
75
- # read an array of related items, write one attribute, write an array of related items)
76
- case method_name.to_s.match(/(s)*(=)*$/).captures
77
- when [nil, "="] : self.edits[method_name.to_s[0..-2]] = args
78
- when ["s", "="] : self.edits[method_name.to_s[0..-2]] = args
79
- when [nil, nil] : value_from_edits_or_store(method_name.to_s)
80
- when ["s", nil] : value_from_edits_or_store(method_name.to_s, true)
81
- end
165
+ def value_from_edits_or_store(name)
166
+ self.edits[name] || (self.source_xml % name).innerHTML
82
167
  rescue NoMethodError
83
- raise NoMethodError, "undefined method '#{method_name}' for #{self.class}"
168
+ nil
84
169
  end
85
170
 
86
- def value_from_edits_or_store(name, multi = false)
87
- if multi
88
- begin
89
- self.edits[name] || (self.source_xml / name.to_s.chop).collect {|el| Object.module_eval(el.name).new(:source_xml => el)}
90
- rescue NoMethodError
91
- self.edits[name] = []
92
- end
93
- else
94
- self.edits[name] || (self.source_xml % name).innerHTML
95
- end
96
- end
97
-
98
-
171
+
99
172
  # Returns the name of the object's class ignoring namespacing.
100
173
  # === Use:
101
174
  # course = RTunesU::Course.new
@@ -105,53 +178,91 @@ module RTunesU
105
178
  self.class.to_s.split('::').last
106
179
  end
107
180
 
108
- # Returns the handle of the entitiy's parent. This can either be set directly as a string or interger or will access the parent entity. Sometimes you know the parent_handle without the parent object (for example, stored locally from an earlier request). This allows you to add a new Entity to iTunes U without first firing a reques for a prent entity (For example, if your institution places all inside the same Section, you want to add a new Section to your Site, or a new Group to a course tied to your institution's LMS).
181
+ def base_connection
182
+ self.class.base_connection
183
+ end
184
+
185
+ # Returns the handle of the entitiy's parent. This can either be set directly as a string or interger or
186
+ # will access the parent entity. Sometimes you know the parent_handle without the parent object
187
+ # (for example, stored locally from an earlier request). This allows you to add a new Entity to iTunes U
188
+ # without first firing a request for a parent entity (For example, if your institution places all courses inside the
189
+ # same Section, you want to add a new Section to your Site, or a new Group to a course tied to your
190
+ # institution's LMS).
109
191
  def parent_handle
110
192
  self.parent ? self.parent.handle : @parent_handle
111
193
  end
112
194
 
195
+ # Converts the entities changed attributes and subentities to XML. Called by Document when building
196
+ # documents to transfer to iTunes U.
113
197
  def to_xml(xml_builder = Builder::XmlMarkup.new)
114
198
  xml_builder.tag!(self.class_name) {
115
- self.edits.each {|attribute,edit| edit.is_a?(Array) ? edit.each {|item| item.to_xml(xml_builder)} : xml_builder.tag!(attribute, edit) }
116
- # self.edits.each {|attribute| xml_builder.tag!(attribute.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }, self.attributes[attribute.to_s]) unless self.attributes[attribute.to_s].nil? || self.attributes[attribute.to_s].empty? }
199
+ self.edits.each do |attribute,edit|
200
+ edit.is_a?(SubentityAssociationProxy) ? edit.to_xml(xml_builder) : xml_builder.tag!(attribute, edit)
201
+ end
117
202
  }
118
203
  end
119
204
 
120
205
  # called when .save is called on an object that is already stored in iTunes U
121
- def update(connection)
206
+ def update(connection = nil)
207
+ connection ||= self.base_connection
208
+
122
209
  connection.process(Document::Merge.new(self).xml)
210
+ edits.clear
123
211
  self
124
212
  end
125
213
 
126
214
  # called when .save is called on an object that has no Handle (i.e. does not already exist in iTunes U)
127
- def create(connection)
215
+ def create(connection = nil)
216
+ connection ||= self.base_connection
217
+
128
218
  response = Hpricot.XML(connection.process(Document::Add.new(self).xml))
129
- raise Exception, response.at('error').innerHTML if response.at('error')
130
- self.handle = response.at('AddedObjectHandle').innerHTML
219
+ raise CannotSave, response.at('error').innerHTML if response.at('error')
220
+ @handle = response.at('AddedObjectHandle').innerHTML
221
+ edits.clear
131
222
  self
132
223
  end
133
224
 
134
- # Saves the entity to iTunes U. Save takes single argument (an iTunes U connection object). If the entity is unsaved this will create the entity and populate its handle attribte. If the entity has already been saved it will send the updated data (if any) to iTunes U.
135
- def save(connection)
225
+ # Saves the entity to iTunes U. Save takes single argument (an iTunes U connection object).
226
+ # If the entity is unsaved this will create the entity and populate its handle attribte.
227
+ # If the entity has already been saved it will send the updated data (if any) to iTunes U.
228
+ def save(connection = nil)
229
+ connection ||= self.base_connection
136
230
  saved? ? update(connection) : create(connection)
137
231
  end
138
232
 
233
+ # Has the entity be previously saved in iTunes U
139
234
  def saved?
140
235
  self.handle ? true : false
141
236
  end
142
237
 
143
238
  # Deletes the entity from iTunes U. This cannot be undone.
144
- def delete(connection)
239
+ def delete(connection = nil)
240
+ connection ||= self.base_connection
241
+
145
242
  response = Hpricot.XML(connection.process(Document::Delete.new(self).xml))
146
243
  raise Exception, response.at('error').innerHTML if response.at('error')
147
- self.handle = nil
244
+ @handle = nil
148
245
  self
149
246
  end
247
+
248
+ def inspect
249
+ inspected = "#<#{self.class.to_s} handle:#{@handle.inspect} "
250
+ self.class.attributes.each do |attribute|
251
+ inspected << "#{attribute}:#{self.send(attribute).inspect} "
252
+ end
253
+ inspected << '>'
254
+ end
255
+ end
256
+
257
+ class CannotSave < StandardError
258
+ end
259
+
260
+ class ConnectionRequired < StandardError
150
261
  end
151
262
 
152
- class EntityNotFound < Exception
263
+ class EntityNotFound < StandardError
153
264
  end
154
265
 
155
- class MissingParent < Exception
266
+ class MissingParent < StandardError
156
267
  end
157
268
  end
@@ -0,0 +1,69 @@
1
+ require 'time'
2
+
3
+ module RTunesU
4
+ class Log
5
+ def self.parse(log_text)
6
+ self.new(log_text).parsed
7
+ end
8
+
9
+ def initialize(raw)
10
+ @raw = raw
11
+ @parsed = parse(@raw)
12
+ end
13
+
14
+ attr_reader :raw, :parsed
15
+
16
+ module Errors
17
+ class LogEntryParseError < RuntimeError; end
18
+ class LogParseError < RuntimeError; end
19
+ end
20
+
21
+ private
22
+ def parse_line(line)
23
+ log, path = line.split('"')
24
+ l = log.split << path
25
+ entry = { :version => l[0],
26
+ :site => l[1],
27
+ :time => Time.parse("#{l[2]} #{l[3]}"),
28
+ :action => l[4],
29
+ :uuid => l[5],
30
+ :user => l[6],
31
+ :address => l[7],
32
+ :agent => l[8],
33
+ :path => l[9]
34
+ }
35
+ unless validate(entry)
36
+ raise Errors::LogEntryParseError, "Failed to validate as an ItunesU log entry:\n#{line}"
37
+ end
38
+ entry
39
+ end
40
+
41
+ def parse(log)
42
+ if not log.kind_of? IO and File.exists? log
43
+ log = File.open(log)
44
+ elsif not log.respond_to? 'each_line'
45
+ log = StringIO.new(log.to_s)
46
+ end
47
+ begin
48
+ log.map {|line| parse_line(line)}
49
+ rescue Errors::LogEntryParseError => error
50
+ raise Errors::LogParseError, "Failed to parse <#{log.class}>:\n#{error}"
51
+ end
52
+ end
53
+
54
+ def validate(log_entry)
55
+ # if log_entry[:address].nil? or log_entry[:address].split(".").size != 4
56
+ # puts log_entry[:address].inspect
57
+ # return false
58
+ # elsif log_entry[:agent].nil? or log_entry[:agent].split("/").size != 3
59
+ # puts log_entry[:agent].inspect
60
+ # return false
61
+ # elsif log_entry[:user].nil? or log_entry[:user].split("@").size != 2
62
+ # puts log_entry[:user].inspect
63
+ # return false
64
+ # end
65
+
66
+ return true
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,60 @@
1
+ module RTunesU
2
+ class SubentityAssociationProxy
3
+ delegate :inspect, :to => :target
4
+ def initialize(source_xml, owner, name)
5
+ @source_xml = source_xml
6
+ @owner = owner
7
+ @owner.edits[name] = self
8
+ @edits = []
9
+ self.from_xml(name)
10
+ end
11
+
12
+ end
13
+
14
+ class HasAEntityCollectionProxy < SubentityAssociationProxy
15
+ delegate :class, :to => :target
16
+ attr_reader :target
17
+ def from_xml(name)
18
+ if @source_xml
19
+ @target = "RTunesU::#{name}".constantize.new(:source_xml => @source_xml.first, :parent_handle => @owner.handle)
20
+ else
21
+ @target = "RTunesU::#{name}".constantize.new(:parent_handle => @owner.handle)
22
+ end
23
+ end
24
+
25
+ def to_xml(builder)
26
+ @target.to_xml(builder) if @target.edits.any?
27
+ end
28
+
29
+ def method_missing(method_name, *args)
30
+ @target.send(method_name, args)
31
+ end
32
+ end
33
+
34
+ class HasNEntityCollectionProxy < SubentityAssociationProxy
35
+ delegate :[], :at, :first, :last, :size, :to => :target
36
+ attr_reader :target, :edits
37
+
38
+ def from_xml(name)
39
+ if @source_xml
40
+ @target = @source_xml.collect {|el| Object.module_eval(el.name).new(:source_xml => el, :parent_handle => @owner.handle)}
41
+ else
42
+ @target = []
43
+ end
44
+ end
45
+
46
+ def to_xml(builder)
47
+ self.edits.each {|entity| entity.to_xml(builder)}
48
+ end
49
+
50
+ def <<(entity)
51
+ @target << entity
52
+ @edits << entity
53
+ end
54
+
55
+ def []=(index,entity)
56
+ @target[index] = entity
57
+ @entity[index] = entity
58
+ end
59
+ end
60
+ end
data/lib/rtunesu/user.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  module RTunesU
2
- # Represents a User for iTunes U authentication. Requests to iTunes U require User information in a specific format. This class exists to access data in that specific format. Best combined with a User object from your own authentication or LMS system.
2
+ # Represents a User for iTunes U authentication. Requests to iTunes U require User
3
+ # information in a specific format. This class exists to access data in that specific format.
4
+ # Best combined with a User object from your own authentication or LMS system.
3
5
  # Webservices requests to iTunes U require your institutions administrator credentials
4
6
  # === User
5
7
  # u = RTunesU::User.new('191912121', 'jsmith', 'John Smith', 'jsmith@exmaple.edu')
@@ -1,8 +1,8 @@
1
1
  module RTunesU
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 2
5
- TINY = 4
4
+ MINOR = 3
5
+ TINY = 5
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/lib/rtunesu.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  $:.unshift(File.dirname(__FILE__)) unless
2
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
3
 
4
- require 'multipart'
4
+ require 'hpricot'
5
+ require 'activesupport'
5
6
 
6
7
  require 'rtunesu/version'
7
8
  require 'rtunesu/connection'
@@ -10,16 +11,20 @@ require 'rtunesu/document'
10
11
  require 'rtunesu/entity'
11
12
  require 'rtunesu/entities/course'
12
13
  require 'rtunesu/entities/division'
14
+ require 'rtunesu/entities/external_feed'
13
15
  require 'rtunesu/entities/group'
14
16
  require 'rtunesu/entities/permission'
15
17
  require 'rtunesu/entities/section'
16
18
  require 'rtunesu/entities/site'
17
19
  require 'rtunesu/entities/track'
18
20
  require 'rtunesu/entities/theme'
21
+ require 'rtunesu/log'
22
+ require 'rtunesu/subentities'
19
23
 
20
24
  module RTunesU
21
- API_URL = 'https://deimos.apple.com/WebObjects/Core.woa/API'
22
- API_VERSION = '1.1.1'
23
- BROWSE_URL = 'https://deimos.apple.com/WebObjects/Core.woa/Browse'
24
- SHOW_TREE_URL = 'https://deimos.apple.com/WebObjects/Core.woa/API/ShowTree'
25
+ API_URL = 'https://deimos.apple.com/WebObjects/Core.woa/API'.freeze
26
+ API_VERSION = '1.1.3'.freeze
27
+ BROWSE_URL = 'https://deimos.apple.com/WebObjects/Core.woa/Browse'.freeze
28
+ SHOW_TREE_URL = 'https://deimos.apple.com/WebObjects/Core.woa/API/ShowTree'.freeze
29
+ SHOW_TREE_FILE = File.new(File.join(File.dirname(__FILE__), 'show_tree.xml'))
25
30
  end
data/lib/show_tree.xml ADDED
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ITunesUDocument>
3
+ <Version>1.1.3</Version>
4
+ <ShowTree>
5
+ <KeyGroup>maximal</KeyGroup>
6
+ </ShowTree>
7
+ </ITunesUDocument>
@@ -1,35 +1,16 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
  include RTunesU
3
3
 
4
- describe Connection do
5
- describe 'token generation' do
6
- before do
7
- Time.should_receive(:now).and_return(@current_time)
8
- @current_time.should_receive(:to_i).and_return(1214619134)
9
-
10
- user = mock(RTunesU::User, :id => 0,
11
- :username => 'admin',
12
- :name => 'Admin',
13
- :email => 'admin@example.edu',
14
- :credentials => ['Administrator@urn:mace:itunesu.com:sites:example.edu'],
15
- :to_credential_string => 'Administrator@urn:mace:itunesu.com:sites:example.edu',
16
- :to_identity_string => '"Admin" <admin@example.edu> (admin) [0]')
17
-
18
- @connection = Connection.new(:user => user, :shared_secret => 'STRINGOFTHIRTYTWOLETTERSORDIGITS')
19
- end
20
-
21
- it 'generates a token' do
22
- @connection.generate_authorization_token.should eql("credentials=Administrator%40urn%3Amace%3Aitunesu.com%3Asites%3Aexample.edu&identity=%22Admin%22+%3Cadmin%40example.edu%3E+%28admin%29+%5B0%5D&time=1214619134&signature=121a6cf76c9c5ecda41450d87e3394b9d02c570a5f76b2bd16287f860f068302")
23
- end
24
- end
4
+ describe Connection do
25
5
  describe 'requesting an upload location for a file' do
26
6
  end
27
7
 
28
8
  describe 'API accessing' do
29
9
  before do
30
- Time.should_receive(:now).and_return(@current_time)
31
- @current_time.should_receive(:to_i).and_return(1214619134)
32
-
10
+ now = Time.new
11
+ Time.stub!(:now).and_return(now)
12
+ now.stub!(:to_i).and_return(1214619134)
13
+
33
14
  user = mock(RTunesU::User, :id => 0,
34
15
  :username => 'admin',
35
16
  :name => 'Admin',
@@ -41,13 +22,8 @@ describe Connection do
41
22
  @connection = Connection.new(:user => user, :site => 'example.edu', :shared_secret => 'STRINGOFTHIRTYTWOLETTERSORDIGITS')
42
23
  end
43
24
 
44
- it 'access a web services url for the institution' do
25
+ it 'generates a web services url for the institution' do
45
26
  @connection.webservices_url.should eql('https://deimos.apple.com/WebObjects/Core.woa/API/ProcessWebServicesDocument/example.edu?credentials=Administrator%40urn%3Amace%3Aitunesu.com%3Asites%3Aexample.edu&identity=%22Admin%22+%3Cadmin%40example.edu%3E+%28admin%29+%5B0%5D&time=1214619134&signature=121a6cf76c9c5ecda41450d87e3394b9d02c570a5f76b2bd16287f860f068302')
46
27
  end
47
-
48
- it 'can generate a url for uploading files'
49
- it 'opens an HTTPS connection to iTunes U'
50
- it 'send XML data'
51
- it 'generates a url user access to a location through iTunes U'
52
28
  end
53
29
  end