rtunesu 0.2.4 → 0.3.5
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/.gitignore +3 -0
- data/History.txt +0 -5
- data/README.txt +2 -2
- data/Rakefile +15 -2
- data/VERSION +1 -0
- data/lib/rtunesu/connection.rb +152 -54
- data/lib/rtunesu/document.rb +21 -26
- data/lib/rtunesu/entities/course.rb +4 -13
- data/lib/rtunesu/entities/division.rb +5 -12
- data/lib/rtunesu/entities/external_feed.rb +14 -0
- data/lib/rtunesu/entities/group.rb +5 -14
- data/lib/rtunesu/entities/permission.rb +1 -5
- data/lib/rtunesu/entities/section.rb +15 -11
- data/lib/rtunesu/entities/site.rb +6 -13
- data/lib/rtunesu/entities/template.rb +4 -0
- data/lib/rtunesu/entities/theme.rb +3 -15
- data/lib/rtunesu/entities/track.rb +12 -27
- data/lib/rtunesu/entity.rb +185 -74
- data/lib/rtunesu/log.rb +69 -0
- data/lib/rtunesu/subentities.rb +60 -0
- data/lib/rtunesu/user.rb +3 -1
- data/lib/rtunesu/version.rb +2 -2
- data/lib/rtunesu.rb +11 -6
- data/lib/show_tree.xml +7 -0
- data/spec/connection_spec.rb +6 -30
- data/spec/document_spec.rb +82 -8
- data/spec/entities/course_spec.rb +13 -40
- data/spec/entities/division_spec.rb +13 -3
- data/spec/entities/external_feed_spec.rb +21 -0
- data/spec/entities/group_spec.rb +14 -3
- data/spec/entities/permission_spec.rb +12 -3
- data/spec/entities/section_spec.rb +32 -3
- data/spec/entities/site_spec.rb +16 -3
- data/spec/entities/track_spec.rb +32 -3
- data/spec/entity_spec.rb +147 -86
- data/spec/fixtures/requests/create_course.xml +17 -0
- data/spec/fixtures/requests/create_division.xml +11 -0
- data/spec/fixtures/requests/create_group.xml +11 -0
- data/spec/fixtures/requests/create_section.xml +11 -0
- data/spec/fixtures/requests/delete_course.xml +8 -0
- data/spec/fixtures/requests/delete_group.xml +8 -0
- data/spec/fixtures/requests/delete_section.txt +8 -0
- data/spec/fixtures/requests/show_course.xml +8 -0
- data/spec/fixtures/requests/show_group.xml +8 -0
- data/spec/fixtures/requests/show_section.xml +8 -0
- data/spec/fixtures/requests/update_course.xml +10 -0
- data/spec/fixtures/requests/update_group.xml +9 -0
- data/spec/fixtures/requests/update_section.txt +10 -0
- data/spec/fixtures/responses/failure/create_course.xml +5 -0
- data/spec/fixtures/responses/failure/create_division.xml +5 -0
- data/spec/fixtures/responses/failure/create_group.xml +5 -0
- data/spec/fixtures/responses/failure/create_section.xml +5 -0
- data/spec/fixtures/responses/failure/delete_group.xml +5 -0
- data/spec/fixtures/responses/failure/show_course.xml +11 -0
- data/spec/fixtures/responses/failure/show_division.xml +11 -0
- data/spec/fixtures/responses/failure/show_group.xml +11 -0
- data/spec/fixtures/responses/failure/show_section.xml +11 -0
- data/spec/fixtures/responses/failure/show_site.xml +11 -0
- data/spec/fixtures/responses/failure/update_section.txt +5 -0
- data/spec/fixtures/responses/success/create_course.xml +5 -0
- data/spec/fixtures/responses/success/create_division.xml +5 -0
- data/spec/fixtures/responses/success/create_group.xml +5 -0
- data/spec/fixtures/responses/success/create_section.xml +5 -0
- data/spec/fixtures/responses/success/delete_course.xml +4 -0
- data/spec/fixtures/responses/success/delete_division.xml +4 -0
- data/spec/fixtures/responses/success/delete_group.xml +4 -0
- data/spec/fixtures/responses/success/delete_section.txt +4 -0
- data/spec/fixtures/responses/success/delete_section.xml +4 -0
- data/spec/fixtures/responses/success/show_course.xml +393 -0
- data/spec/fixtures/responses/success/show_division.xml +393 -0
- data/spec/fixtures/responses/success/show_group.xml +137 -0
- data/spec/fixtures/responses/success/show_section.xml +152 -0
- data/spec/fixtures/responses/success/show_site.xml +393 -0
- data/spec/fixtures/responses/success/update_course.xml +4 -0
- data/spec/fixtures/responses/success/update_division.xml +4 -0
- data/spec/fixtures/responses/success/update_group.xml +4 -0
- data/spec/fixtures/responses/success/update_section.txt +4 -0
- data/spec/fixtures/responses/success/update_section.xml +4 -0
- data/spec/fixtures/responses/success/update_site.xml +4 -0
- data/spec/spec_helper.rb +72 -7
- data/spec/token_generation_spec.rb +23 -0
- data/spec/user_spec.rb +3 -3
- metadata +81 -76
- data/Manifest.txt +0 -77
- data/config/hoe.rb +0 -76
- data/config/requirements.rb +0 -15
- data/lib/multipart.rb +0 -53
- data/spec/documents/add_spec.rb +0 -41
- data/spec/documents/delete_spec.rb +0 -30
- data/spec/documents/merge_spec.rb +0 -30
- data/spec/documents/show_tree_spec.rb +0 -16
- data/spec/fixtures/add_course.xml +0 -26
- data/spec/fixtures/add_division.xml +0 -26
- data/spec/fixtures/add_group.xml +0 -27
- data/spec/fixtures/add_permission.xml +0 -12
- data/spec/fixtures/add_section.xml +0 -34
- data/spec/fixtures/add_track.xml +0 -19
- data/spec/fixtures/delete_course.xml +0 -8
- data/spec/fixtures/delete_division.xml +0 -8
- data/spec/fixtures/delete_group.xml +0 -8
- data/spec/fixtures/delete_permission.xml +0 -9
- data/spec/fixtures/delete_section.xml +0 -8
- data/spec/fixtures/delete_track.xml +0 -7
- data/spec/fixtures/merge_course.xml +0 -38
- data/spec/fixtures/merge_division.xml +0 -47
- data/spec/fixtures/merge_group.xml +0 -29
- data/spec/fixtures/merge_permission.xml +0 -12
- data/spec/fixtures/merge_section.xml +0 -36
- data/spec/fixtures/merge_site.xml +0 -31
- data/spec/fixtures/merge_track.xml +0 -18
- data/spec/fixtures/requests/add_coures_request.xml +0 -0
- data/spec/fixtures/show_tree.xml +0 -273
- data/spec/fixtures/update_group.xml +0 -7
- data/tasks/deployment.rake +0 -34
- data/tasks/website.rake +0 -17
- data/website/index.html +0 -54
- data/website/index.txt +0 -7
- data/website/javascripts/rounded_corners_lite.inc.js +0 -285
- data/website/stylesheets/screen.css +0 -138
- data/website/template.html.erb +0 -48
data/lib/rtunesu/entity.rb
CHANGED
|
@@ -1,56 +1,144 @@
|
|
|
1
|
-
require 'hpricot'
|
|
2
|
-
|
|
3
1
|
module RTunesU
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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.
|
|
9
|
-
# c.
|
|
10
|
-
# c.
|
|
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.
|
|
14
|
-
#
|
|
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.
|
|
18
|
-
# c.
|
|
19
|
-
#
|
|
20
|
-
# c.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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(
|
|
52
|
-
self.source_xml = Hpricot.XML(
|
|
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
|
|
74
|
-
|
|
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
|
-
|
|
168
|
+
nil
|
|
84
169
|
end
|
|
85
170
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|
116
|
-
|
|
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
|
|
130
|
-
|
|
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).
|
|
135
|
-
|
|
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
|
-
|
|
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 <
|
|
263
|
+
class EntityNotFound < StandardError
|
|
153
264
|
end
|
|
154
265
|
|
|
155
|
-
class MissingParent <
|
|
266
|
+
class MissingParent < StandardError
|
|
156
267
|
end
|
|
157
268
|
end
|
data/lib/rtunesu/log.rb
ADDED
|
@@ -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
|
|
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')
|
data/lib/rtunesu/version.rb
CHANGED
data/lib/rtunesu.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
$:.unshift(File.dirname(__FILE__)) unless
|
|
2
|
-
|
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
|
3
3
|
|
|
4
|
-
require '
|
|
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.
|
|
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
data/spec/connection_spec.rb
CHANGED
|
@@ -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.
|
|
31
|
-
|
|
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 '
|
|
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
|