rtunesu 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/History.txt +4 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +77 -0
  4. data/README.txt +38 -0
  5. data/Rakefile +4 -0
  6. data/config/hoe.rb +75 -0
  7. data/config/requirements.rb +15 -0
  8. data/lib/multipart.rb +53 -0
  9. data/lib/rtunesu/connection.rb +111 -0
  10. data/lib/rtunesu/document.rb +84 -0
  11. data/lib/rtunesu/entities/course.rb +8 -0
  12. data/lib/rtunesu/entities/division.rb +16 -0
  13. data/lib/rtunesu/entities/group.rb +8 -0
  14. data/lib/rtunesu/entities/permission.rb +7 -0
  15. data/lib/rtunesu/entities/section.rb +15 -0
  16. data/lib/rtunesu/entities/site.rb +7 -0
  17. data/lib/rtunesu/entities/theme.rb +4 -0
  18. data/lib/rtunesu/entities/track.rb +13 -0
  19. data/lib/rtunesu/entity.rb +154 -0
  20. data/lib/rtunesu/user.rb +26 -0
  21. data/lib/rtunesu/version.rb +9 -0
  22. data/lib/rtunesu.rb +25 -0
  23. data/script/console +10 -0
  24. data/script/destroy +14 -0
  25. data/script/generate +14 -0
  26. data/script/txt2html +82 -0
  27. data/setup.rb +1585 -0
  28. data/spec/connection_spec.rb +53 -0
  29. data/spec/document_spec.rb +16 -0
  30. data/spec/documents/add_spec.rb +41 -0
  31. data/spec/documents/delete_spec.rb +30 -0
  32. data/spec/documents/merge_spec.rb +30 -0
  33. data/spec/documents/show_tree_spec.rb +16 -0
  34. data/spec/entities/course_spec.rb +47 -0
  35. data/spec/entities/division_spec.rb +8 -0
  36. data/spec/entities/group_spec.rb +8 -0
  37. data/spec/entities/permission_spec.rb +8 -0
  38. data/spec/entities/section_spec.rb +8 -0
  39. data/spec/entities/site_spec.rb +8 -0
  40. data/spec/entities/track_spec.rb +8 -0
  41. data/spec/entity_spec.rb +108 -0
  42. data/spec/fixtures/add_course.xml +26 -0
  43. data/spec/fixtures/add_division.xml +26 -0
  44. data/spec/fixtures/add_group.xml +27 -0
  45. data/spec/fixtures/add_permission.xml +12 -0
  46. data/spec/fixtures/add_section.xml +34 -0
  47. data/spec/fixtures/add_track.xml +19 -0
  48. data/spec/fixtures/delete_course.xml +8 -0
  49. data/spec/fixtures/delete_division.xml +8 -0
  50. data/spec/fixtures/delete_group.xml +8 -0
  51. data/spec/fixtures/delete_permission.xml +9 -0
  52. data/spec/fixtures/delete_section.xml +8 -0
  53. data/spec/fixtures/delete_track.xml +7 -0
  54. data/spec/fixtures/merge_course.xml +38 -0
  55. data/spec/fixtures/merge_division.xml +47 -0
  56. data/spec/fixtures/merge_group.xml +29 -0
  57. data/spec/fixtures/merge_permission.xml +12 -0
  58. data/spec/fixtures/merge_section.xml +36 -0
  59. data/spec/fixtures/merge_site.xml +31 -0
  60. data/spec/fixtures/merge_track.xml +18 -0
  61. data/spec/fixtures/requests/add_coures_request.xml +0 -0
  62. data/spec/fixtures/responses/generic_entity_response.xml +17 -0
  63. data/spec/fixtures/responses/show_tree_course.xml +676 -0
  64. data/spec/fixtures/show_tree.xml +273 -0
  65. data/spec/fixtures/update_group.xml +7 -0
  66. data/spec/spec.opts +1 -0
  67. data/spec/spec_helper.rb +10 -0
  68. data/spec/user_spec.rb +18 -0
  69. data/tasks/deployment.rake +34 -0
  70. data/tasks/environment.rake +7 -0
  71. data/tasks/rspec.rake +21 -0
  72. data/tasks/website.rake +17 -0
  73. data/website/index.html +141 -0
  74. data/website/index.txt +83 -0
  75. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  76. data/website/stylesheets/screen.css +138 -0
  77. data/website/template.html.erb +48 -0
  78. metadata +144 -0
@@ -0,0 +1,154 @@
1
+ require 'hpricot'
2
+
3
+ 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.
6
+ # == Reading and Writing Attributes
7
+ # 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.
11
+ #
12
+ # == 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 '<<'
15
+ # Examples:
16
+ # 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
34
+ class Entity
35
+ attr_accessor :connection, :attributes, :handle, :parent, :parent_handle, :saved, :source_xml
36
+
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
38
+ def initialize(attrs = {})
39
+ self.attributes = {}
40
+ attrs.each {|attribute, value| self.send("#{attribute}=", value)}
41
+ 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,
44
+ # 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))
48
+ entity
49
+ 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?
54
+ end
55
+
56
+ # Edits stores the changes made to an entity
57
+ def edits
58
+ @edits ||= {}
59
+ end
60
+
61
+ # Clear the edits and restores the loaded object to its original form
62
+ def reload
63
+ self.edits.clear
64
+ end
65
+
66
+ # Returns the parent of the entity
67
+ def parent
68
+ @parent ||= Object.module_eval(self.source_xml.parent.name).new(:source_xml => self.source_xml.parent)
69
+ rescue
70
+ nil
71
+ end
72
+
73
+ def method_missing(method_name, args = nil)
74
+ # introspect the kind of method call (read one attribute, read an array of related items, write one attribute, write an array of related items)
75
+ case method_name.to_s.match(/(s)*(=)*$/).captures
76
+ when [nil, "="] : self.edits[method_name.to_s[0..-2]] = args
77
+ when ["s", "="] : self.edits[method_name.to_s[0..-2]] = args
78
+ when [nil, nil] : value_from_edits_or_store(method_name.to_s)
79
+ when ["s", nil] : value_from_edits_or_store(method_name.to_s, true)
80
+ end
81
+ rescue NoMethodError
82
+ raise NoMethodError, "undefined method '#{method_name}' for #{self.class}"
83
+ end
84
+
85
+ def value_from_edits_or_store(name, multi = false)
86
+ if multi
87
+ begin
88
+ self.edits[name] || (self.source_xml / name.to_s.chop).collect {|el| Object.module_eval(el.name).new(:source_xml => el)}
89
+ rescue NoMethodError
90
+ self.edits[name] = []
91
+ end
92
+ else
93
+ self.edits[name] || (self.source_xml % name).innerHTML
94
+ end
95
+ end
96
+
97
+
98
+ # Returns the name of the object's class ignoring namespacing.
99
+ # === Use:
100
+ # course = RTunesU::Course.new
101
+ # course.class #=> 'RTunesU::Course'
102
+ # course.class_name #=> 'Course'
103
+ def class_name
104
+ self.class.to_s.split(':').last
105
+ end
106
+
107
+ # 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).
108
+ def parent_handle
109
+ self.parent ? self.parent.handle : @parent_handle
110
+ end
111
+
112
+ def to_xml(xml_builder = Builder::XmlMarkup.new)
113
+ xml_builder.tag!(self.class_name) {
114
+ self.edits.each {|attribute,edit| edit.is_a?(Array) ? edit.each {|item| item.to_xml(xml_builder)} : xml_builder.tag!(attribute, edit) }
115
+ # 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? }
116
+ }
117
+ end
118
+
119
+ def update(connection)
120
+ connection.process(Document::Merge.new(self).xml)
121
+ self
122
+ end
123
+
124
+ def create(connection)
125
+ response = Hpricot.XML(connection.process(Document::Add.new(self).xml))
126
+ raise Exception, response.at('error').innerHTML if response.at('error')
127
+ self.handle = response.at('AddedObjectHandle').innerHTML
128
+ self
129
+ end
130
+
131
+ # 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.
132
+ def save(connection)
133
+ saved? ? update(connection) : create(connection)
134
+ end
135
+
136
+ def saved?
137
+ self.handle ? true : false
138
+ end
139
+
140
+ # Deletes the entity from iTunes U. This cannot be undone.
141
+ def delete(connection)
142
+ response = Hpricot.XML(connection.process(Document::Delete.new(self).xml))
143
+ raise Exception, response.at('error').innerHTML if response.at('error')
144
+ self.handle = nil
145
+ self
146
+ end
147
+ end
148
+
149
+ class EntityNotFound < Exception
150
+ end
151
+
152
+ class MissingParent < Exception
153
+ end
154
+ end
@@ -0,0 +1,26 @@
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.
3
+ # Webservices requests to iTunes U require your institutions administrator credentials
4
+ # === User
5
+ # u = RTunesU::User.new('191912121', 'jsmith', 'John Smith', 'jsmith@exmaple.edu')
6
+ # u.credentials = ['Instructor@urn:mace:example.edu', 'Learner@urn:mace:example.edu']
7
+ # u.to_identity_string #=> '"John Smith" <jsmith@example.edu> (jsmith) [191912121]'
8
+ # u.to_credential_string #=> 'Instructor@urn:mace:example.edu;Learner@urn:mace:example.edu'
9
+ # === Interaction with other classes
10
+ # a User object is required for creating a Connection object
11
+ class User
12
+ attr_accessor :name, :email, :username, :id, :credentials
13
+
14
+ def initialize(id, username, name, email)
15
+ self.id, self.username, self.name, self.email = id, username, name, email
16
+ end
17
+
18
+ def to_identity_string
19
+ '"%s" <%s> (%s) [%s]' % [name, email, username, id]
20
+ end
21
+
22
+ def to_credential_string
23
+ self.credentials.join(';')
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ module RTunesU
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 2
5
+ TINY = 1
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
data/lib/rtunesu.rb ADDED
@@ -0,0 +1,25 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'multipart'
5
+
6
+ require 'rtunesu/version'
7
+ require 'rtunesu/connection'
8
+ require 'rtunesu/user'
9
+ require 'rtunesu/document'
10
+ require 'rtunesu/entity'
11
+ require 'rtunesu/entities/course'
12
+ require 'rtunesu/entities/division'
13
+ require 'rtunesu/entities/group'
14
+ require 'rtunesu/entities/permission'
15
+ require 'rtunesu/entities/section'
16
+ require 'rtunesu/entities/site'
17
+ require 'rtunesu/entities/track'
18
+ require 'rtunesu/entities/theme'
19
+
20
+ 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
+ end
data/script/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/rtunesu.rb'}"
9
+ puts "Loading rtunesu gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
data/script/generate ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
data/script/txt2html ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ GEM_NAME = 'rtunesu' # what ppl will type to install your gem
4
+ RUBYFORGE_PROJECT = 'rtunesu'
5
+
6
+ require 'rubygems'
7
+ begin
8
+ require 'newgem'
9
+ require 'rubyforge'
10
+ rescue LoadError
11
+ puts "\n\nGenerating the website requires the newgem RubyGem"
12
+ puts "Install: gem install newgem\n\n"
13
+ exit(1)
14
+ end
15
+ require 'redcloth'
16
+ require 'syntax/convertors/html'
17
+ require 'erb'
18
+ require File.dirname(__FILE__) + "/../lib/#{GEM_NAME}/version.rb"
19
+
20
+ version = RTunesU::VERSION::STRING
21
+ download = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
22
+
23
+ def rubyforge_project_id
24
+ RubyForge.new.autoconfig["group_ids"][RUBYFORGE_PROJECT]
25
+ end
26
+
27
+ class Fixnum
28
+ def ordinal
29
+ # teens
30
+ return 'th' if (10..19).include?(self % 100)
31
+ # others
32
+ case self % 10
33
+ when 1: return 'st'
34
+ when 2: return 'nd'
35
+ when 3: return 'rd'
36
+ else return 'th'
37
+ end
38
+ end
39
+ end
40
+
41
+ class Time
42
+ def pretty
43
+ return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
44
+ end
45
+ end
46
+
47
+ def convert_syntax(syntax, source)
48
+ return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
49
+ end
50
+
51
+ if ARGV.length >= 1
52
+ src, template = ARGV
53
+ template ||= File.join(File.dirname(__FILE__), '/../website/template.html.erb')
54
+ else
55
+ puts("Usage: #{File.split($0).last} source.txt [template.html.erb] > output.html")
56
+ exit!
57
+ end
58
+
59
+ template = ERB.new(File.open(template).read)
60
+
61
+ title = nil
62
+ body = nil
63
+ File.open(src) do |fsrc|
64
+ title_text = fsrc.readline
65
+ body_text_template = fsrc.read
66
+ body_text = ERB.new(body_text_template).result(binding)
67
+ syntax_items = []
68
+ body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</\1>!m){
69
+ ident = syntax_items.length
70
+ element, syntax, source = $1, $2, $3
71
+ syntax_items << "<#{element} class='syntax'>#{convert_syntax(syntax, source)}</#{element}>"
72
+ "syntax-temp-#{ident}"
73
+ }
74
+ title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
75
+ body = RedCloth.new(body_text).to_html
76
+ body.gsub!(%r!(?:<pre><code>)?syntax-temp-(\d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
77
+ end
78
+ stat = File.stat(src)
79
+ created = stat.ctime
80
+ modified = stat.mtime
81
+
82
+ $stdout << template.result(binding)