oai 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/README +80 -0
  2. data/Rakefile +113 -0
  3. data/bin/oai +68 -0
  4. data/examples/models/file_model.rb +63 -0
  5. data/examples/providers/dublin_core.rb +474 -0
  6. data/lib/oai.rb +7 -13
  7. data/lib/oai/client.rb +133 -83
  8. data/lib/oai/{get_record.rb → client/get_record.rb} +0 -0
  9. data/lib/oai/{header.rb → client/header.rb} +2 -2
  10. data/lib/oai/{identify.rb → client/identify.rb} +0 -0
  11. data/lib/oai/{list_identifiers.rb → client/list_identifiers.rb} +0 -0
  12. data/lib/oai/{list_metadata_formats.rb → client/list_metadata_formats.rb} +0 -0
  13. data/lib/oai/{list_records.rb → client/list_records.rb} +0 -0
  14. data/lib/oai/{list_sets.rb → client/list_sets.rb} +1 -1
  15. data/lib/oai/{metadata_format.rb → client/metadata_format.rb} +0 -0
  16. data/lib/oai/{record.rb → client/record.rb} +0 -0
  17. data/lib/oai/{response.rb → client/response.rb} +1 -1
  18. data/lib/oai/constants.rb +34 -0
  19. data/lib/oai/exception.rb +72 -1
  20. data/lib/oai/harvester.rb +38 -0
  21. data/lib/oai/harvester/config.rb +41 -0
  22. data/lib/oai/harvester/harvest.rb +144 -0
  23. data/lib/oai/harvester/logging.rb +70 -0
  24. data/lib/oai/harvester/mailer.rb +17 -0
  25. data/lib/oai/harvester/shell.rb +334 -0
  26. data/lib/oai/provider.rb +300 -0
  27. data/lib/oai/provider/metadata_format.rb +72 -0
  28. data/lib/oai/provider/metadata_format/oai_dc.rb +29 -0
  29. data/lib/oai/provider/model.rb +71 -0
  30. data/lib/oai/provider/model/activerecord_caching_wrapper.rb +135 -0
  31. data/lib/oai/provider/model/activerecord_wrapper.rb +136 -0
  32. data/lib/oai/provider/partial_result.rb +18 -0
  33. data/lib/oai/provider/response.rb +119 -0
  34. data/lib/oai/provider/response/error.rb +16 -0
  35. data/lib/oai/provider/response/get_record.rb +32 -0
  36. data/lib/oai/provider/response/identify.rb +24 -0
  37. data/lib/oai/provider/response/list_identifiers.rb +29 -0
  38. data/lib/oai/provider/response/list_metadata_formats.rb +21 -0
  39. data/lib/oai/provider/response/list_records.rb +32 -0
  40. data/lib/oai/provider/response/list_sets.rb +23 -0
  41. data/lib/oai/provider/response/record_response.rb +68 -0
  42. data/lib/oai/provider/resumption_token.rb +106 -0
  43. data/lib/oai/set.rb +14 -5
  44. data/test/activerecord_provider/config/connection.rb +5 -0
  45. data/test/activerecord_provider/config/database.yml +6 -0
  46. data/test/activerecord_provider/database/ar_migration.rb +59 -0
  47. data/test/activerecord_provider/database/oaipmhtest +0 -0
  48. data/test/activerecord_provider/fixtures/dc.yml +1501 -0
  49. data/test/activerecord_provider/helpers/providers.rb +44 -0
  50. data/test/activerecord_provider/helpers/set_provider.rb +36 -0
  51. data/test/activerecord_provider/models/dc_field.rb +7 -0
  52. data/test/activerecord_provider/models/dc_set.rb +6 -0
  53. data/test/activerecord_provider/models/oai_token.rb +3 -0
  54. data/test/activerecord_provider/tc_ar_provider.rb +93 -0
  55. data/test/activerecord_provider/tc_ar_sets_provider.rb +66 -0
  56. data/test/activerecord_provider/tc_caching_paging_provider.rb +53 -0
  57. data/test/activerecord_provider/tc_simple_paging_provider.rb +55 -0
  58. data/test/activerecord_provider/test_helper.rb +4 -0
  59. data/test/client/helpers/provider.rb +68 -0
  60. data/test/client/helpers/test_wrapper.rb +11 -0
  61. data/test/client/tc_exception.rb +36 -0
  62. data/test/{tc_get_record.rb → client/tc_get_record.rb} +11 -7
  63. data/test/client/tc_identify.rb +13 -0
  64. data/test/{tc_libxml.rb → client/tc_libxml.rb} +20 -10
  65. data/test/{tc_list_identifiers.rb → client/tc_list_identifiers.rb} +10 -8
  66. data/test/{tc_list_metadata_formats.rb → client/tc_list_metadata_formats.rb} +4 -1
  67. data/test/{tc_list_records.rb → client/tc_list_records.rb} +4 -1
  68. data/test/{tc_list_sets.rb → client/tc_list_sets.rb} +4 -2
  69. data/test/{tc_xpath.rb → client/tc_xpath.rb} +1 -1
  70. data/test/client/test_helper.rb +5 -0
  71. data/test/provider/models.rb +230 -0
  72. data/test/provider/tc_exceptions.rb +63 -0
  73. data/test/provider/tc_functional_tokens.rb +42 -0
  74. data/test/provider/tc_provider.rb +69 -0
  75. data/test/provider/tc_resumption_tokens.rb +46 -0
  76. data/test/provider/tc_simple_provider.rb +85 -0
  77. data/test/provider/test_helper.rb +36 -0
  78. metadata +123 -27
  79. data/test/tc_exception.rb +0 -38
  80. data/test/tc_identify.rb +0 -8
data/README ADDED
@@ -0,0 +1,80 @@
1
+ = ruby-oai
2
+
3
+ == DESCRIPTION
4
+
5
+ ruby-oai is a Open Archives Protocol for Metadata Harvesting (OAI-PMH[http://openarchives.org])
6
+ library for Ruby. If you're not familiar with OAI-PMH[http://openarchives.org] it is the most used
7
+ protocol for sharing metadata between digital library repositories.
8
+
9
+ The OAI-PMH[http://openarchives.org] spec defines six verbs (Identify, ListIdentifiers, ListRecords,
10
+ GetRecords, ListSets, ListMetadataFormat) used for discovery and sharing of
11
+ metadata.
12
+
13
+ The ruby-oai gem includes a client library, a server/provider library and
14
+ a interactive harvesting shell.
15
+
16
+ === client
17
+
18
+ The OAI client library is used for harvesting metadata from repositories.
19
+ For example to initiate a ListRecords request to pubmed you can:
20
+
21
+ require 'oai'
22
+ client = OAI::Client.new 'http://www.pubmedcentral.gov/oai/oai.cgi'
23
+ for record in client.list_records
24
+ puts record.metadata
25
+ end
26
+
27
+ See OAI::Client for more details
28
+
29
+ === provider
30
+
31
+ The OAI provider library handles serving local content to other clients.
32
+
33
+ Setting up a simple provider:
34
+
35
+ class MyProvider < Oai::Provider
36
+ repository_name 'My little OAI provider'
37
+ repository_url 'http://localhost/provider'
38
+ record_prefix 'oai:localhost'
39
+ admin_email 'root@localhost' # String or Array
40
+ source_model MyModel.new # Subclass of OAI::Provider::Model
41
+ end
42
+
43
+ See OAI::Provider for more details
44
+
45
+ === interactive harvester
46
+
47
+ The OAI-PMH[http://openarchives.org] client shell allows OAI Harvesting to be configured in
48
+ an interactive manner. Typing 'oai' on the command line starts the
49
+ shell.
50
+
51
+ After initial configuration, the shell can be used to manage harvesting
52
+ operations.
53
+
54
+ See OAI::Harvester::Shell for more details
55
+
56
+ == INSTALLATION
57
+
58
+ Normally the best way to install oai is from rubyforge using the gem
59
+ command line tool:
60
+
61
+ % gem install oai
62
+
63
+ If you're reading this you've presumably got the tarball or zip distribution.
64
+ So you'll need to:
65
+
66
+ % rake package
67
+ % gem install pkg/oai-x.y.z.gem
68
+
69
+ Where x.y.z is the version of the gem that was generated.
70
+
71
+ == TODO
72
+
73
+ * consolidate response classes used by provider and client
74
+ * automatic validation of metadata schemas
75
+ * email the authors with your suggestions
76
+
77
+ == AUTHORS
78
+
79
+ - Ed Summers <ehs@pobox>
80
+ - William Groppe <will.groppe@gmail.com>
data/Rakefile ADDED
@@ -0,0 +1,113 @@
1
+ RUBY_OAI_VERSION = '0.0.4'
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+ require 'rake/rdoctask'
7
+ require 'rake/packagetask'
8
+ require 'rake/gempackagetask'
9
+
10
+ task :default => ["test"]
11
+
12
+ task :test => ["test:client", "test:provider"]
13
+
14
+ spec = Gem::Specification.new do |s|
15
+ s.name = 'oai'
16
+ s.version = RUBY_OAI_VERSION
17
+ s.author = 'Ed Summers'
18
+ s.email = 'ehs@pobox.com'
19
+ s.homepage = 'http://www.textualize.com/ruby_oai_0'
20
+ s.platform = Gem::Platform::RUBY
21
+ s.summary = 'A ruby library for working with the Open Archive Initiative Protocol for Metadata Harvesting (OAI-PMH)'
22
+ s.require_path = 'lib'
23
+ s.autorequire = 'oai'
24
+ s.has_rdoc = true
25
+ s.bindir = 'bin'
26
+ s.executables = 'oai'
27
+
28
+ s.add_dependency('activesupport', '>=1.3.1')
29
+ s.add_dependency('chronic', '>=0.0.3')
30
+ s.add_dependency('builder', '>=2.0.0')
31
+
32
+ s.files = %w(README Rakefile) +
33
+ Dir.glob("{bin,test,lib}/**/*") +
34
+ Dir.glob("examples/**/*.rb")
35
+ end
36
+
37
+ Rake::GemPackageTask.new(spec) do |pkg|
38
+ pkg.need_zip = true
39
+ pkg.need_tar = true
40
+ pkg.gem_spec = spec
41
+ end
42
+
43
+ namespace :test do
44
+ Rake::TestTask.new('client') do |t|
45
+ t.libs << ['lib', 'test/client']
46
+ t.pattern = 'test/client/tc_*.rb'
47
+ t.verbose = true
48
+ end
49
+
50
+ Rake::TestTask.new('provider') do |t|
51
+ t.libs << ['lib', 'test/provider']
52
+ t.pattern = 'test/provider/tc_*.rb'
53
+ t.verbose = true
54
+ end
55
+
56
+ desc "Active Record base Provider Tests"
57
+ Rake::TestTask.new('activerecord_provider') do |t|
58
+ t.libs << ['lib', 'test/activerecord_provider']
59
+ t.pattern = 'test/activerecord_provider/tc_*.rb'
60
+ t.verbose = true
61
+ end
62
+
63
+ desc 'Measures test coverage'
64
+ # borrowed from here: http://clarkware.com/cgi/blosxom/2007/01/05#RcovRakeTask
65
+ task :coverage do
66
+ rm_f "coverage"
67
+ rm_f "coverage.data"
68
+ system("rcov --aggregate coverage.data --text-summary -Ilib:test/provider test/provider/tc_*.rb")
69
+ system("rcov --aggregate coverage.data --text-summary -Ilib:test/client test/client/tc_*.rb")
70
+ system("open coverage/index.html") if PLATFORM['darwin']
71
+ end
72
+
73
+ end
74
+
75
+ task 'test:activerecord_provider' => :create_database
76
+
77
+ task :environment do
78
+ unless defined? OAI_PATH
79
+ OAI_PATH = File.dirname(__FILE__) + '/lib/oai'
80
+ $LOAD_PATH << OAI_PATH
81
+ $LOAD_PATH << File.dirname(__FILE__) + '/test'
82
+ end
83
+ end
84
+
85
+ task :drop_database => :environment do
86
+ %w{rubygems active_record yaml}.each { |lib| require lib }
87
+ require 'activerecord_provider/database/ar_migration'
88
+ require 'activerecord_provider/config/connection'
89
+ begin
90
+ OAIPMHTables.down
91
+ rescue
92
+ end
93
+ end
94
+
95
+ task :create_database => :drop_database do
96
+ OAIPMHTables.up
97
+ end
98
+
99
+ task :load_fixtures => :create_database do
100
+ require 'test/activerecord_provider/models/dc_field'
101
+ fixtures = YAML.load_file(
102
+ File.join('test', 'activerecord_provider', 'fixtures', 'dc.yml')
103
+ )
104
+ fixtures.keys.sort.each do |key|
105
+ DCField.create(fixtures[key])
106
+ end
107
+ end
108
+
109
+ Rake::RDocTask.new('doc') do |rd|
110
+ rd.rdoc_files.include("lib/**/*.rb", "README")
111
+ rd.main = 'README'
112
+ rd.rdoc_dir = 'doc'
113
+ end
data/bin/oai ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby -rubygems
2
+ #
3
+ # Created by William Groppe on 2006-11-05.
4
+ # Copyright (c) 2006. All rights reserved.
5
+
6
+ require 'optparse'
7
+
8
+ DIRECTORY_LAYOUT = "%Y/%m".freeze
9
+
10
+ require 'oai/harvester'
11
+
12
+ include OAI::Harvester
13
+
14
+ conf = OAI::Harvester::Config.load
15
+
16
+ startup = :interactive
17
+
18
+ rexml = false
19
+
20
+ opts = OptionParser.new do |opts|
21
+ opts.banner = "Usage: oai ..."
22
+ opts.define_head "#{File.basename($0)}, a OAI harvester shell."
23
+ opts.separator ""
24
+ opts.separator "Options:"
25
+
26
+ opts.on("-D", "--daemon", "Non-interactive mode, to be called via scheduler") { startup = :daemon }
27
+ opts.on("-R", "--rexml", "Use rexml even if libxml is available") { rexml = true }
28
+ opts.on("-?", "--help", "Show this message") do
29
+ puts opts
30
+ exit
31
+ end
32
+
33
+ # Another typical switch to print the version.
34
+ opts.on_tail("-v", "--version", "Show version") do
35
+ class << Gem; attr_accessor :loaded_specs; end
36
+ puts Gem.loaded_specs['oai'].version
37
+ exit
38
+ end
39
+ end
40
+
41
+ begin
42
+ opts.parse! ARGV
43
+ rescue
44
+ puts opts
45
+ exit
46
+ end
47
+
48
+ unless rexml
49
+ begin # Try to load libxml to speed up harvesting
50
+ require 'xml/libxml'
51
+ rescue LoadError
52
+ end
53
+ end
54
+
55
+ case startup
56
+ when :interactive
57
+ shell = Shell.new(conf)
58
+ shell.start
59
+ when :daemon
60
+ if conf.storage
61
+ harvest = Harvest.new(conf)
62
+ harvest.start(harvestable_sites(conf))
63
+ else
64
+ puts "Missing or corrupt configuration file, cannot continue."
65
+ exit(-1)
66
+ end
67
+ end
68
+
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by William Groppe on 2007-02-01.
4
+ #
5
+ # Simple file based Model. Basically just serves a directory of xml files to the
6
+ # Provider.
7
+ #
8
+ class File
9
+ def id
10
+ File.basename(self.path)
11
+ end
12
+
13
+ def to_oai_dc
14
+ self.read
15
+ end
16
+ end
17
+
18
+ class FileModel < OAI::Provider::Model
19
+ include OAI::Provider
20
+
21
+ def initialize(directory = 'data')
22
+ # nil specifies no partial results aka resumption tokens, and 'mtime' is the
23
+ # method that the provider will call for determining the timestamp
24
+ super(nil, 'mtime')
25
+ @directory = directory
26
+ end
27
+
28
+ def earliest
29
+ e = Dir["#{@directory}/*.xml"].min { |a,b| File.stat(a).mtime <=> File.stat(b).mtime }
30
+ File.stat(e).mtime.utc.xmlschema
31
+ end
32
+
33
+ def latest
34
+ e = Dir["#{@directory}/*.xml"].max { |a,b| File.stat(a).mtime <=> File.stat(b).mtime }
35
+ File.stat(e).mtime.utc.xmlschema
36
+ end
37
+
38
+ def sets
39
+ nil
40
+ end
41
+
42
+ def find(selector, opts={})
43
+ return nil unless selector
44
+
45
+ case selector
46
+ when :all
47
+ records = Dir["#{@directory}/*.xml"].sort.collect do |file|
48
+ File.new(file) unless File.stat(file).mtime.utc < opts[:from] or
49
+ File.stat(file).mtime.utc > opts[:until]
50
+ end
51
+ records
52
+ else
53
+ Find.find("#{@directory}/#{selector}") rescue nil
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ # == Example Usage:
60
+ # class FileProvider < OAI::Provider::Base
61
+ # repository_name 'XML File Provider'
62
+ # source_model FileModel.new('/tmp')
63
+ # end
@@ -0,0 +1,474 @@
1
+ #!/usr/local/bin/ruby -rubygems
2
+ require 'camping'
3
+ require 'camping/session'
4
+ require 'oai/provider'
5
+
6
+ # Extremely simple demo Camping application to illustrate OAI Provider integration
7
+ # with Camping.
8
+ #
9
+ # William Groppe 2/1/2007
10
+ #
11
+
12
+ Camping.goes :DublinCore
13
+
14
+ module DublinCore
15
+ include Camping::Session
16
+
17
+ FIELDS = ['title', 'creator', 'subject', 'description',
18
+ 'publisher', 'contributor', 'date', 'type', 'format',
19
+ 'identifier', 'source', 'language', 'relation', 'coverage', 'rights']
20
+
21
+ def DublinCore.create
22
+ Camping::Models::Session.create_schema
23
+ DublinCore::Models.create_schema :assume =>
24
+ (DublinCore::Models::Obj.table_exists? ? 1.0 : 0.0)
25
+ end
26
+
27
+ end
28
+
29
+ module DublinCore::Models
30
+ Base.logger = Logger.new("dublin_core.log")
31
+ Base.inheritance_column = 'field_type'
32
+ Base.default_timezone = :utc
33
+
34
+ class Obj < Base # since Object is reserved
35
+ has_and_belongs_to_many :fields, :join_table => 'dublincore_field_links',
36
+ :foreign_key => 'obj_id', :association_foreign_key => 'field_id'
37
+ DublinCore::FIELDS.each do |field|
38
+ class_eval(%{
39
+ def #{field.pluralize}
40
+ fields.select do |f|
41
+ f if f.field_type == "DC#{field.capitalize}"
42
+ end
43
+ end
44
+ });
45
+ end
46
+ end
47
+
48
+ class Field < Base
49
+ has_and_belongs_to_many :objs, :join_table => 'dublincore_field_links',
50
+ :foreign_key => 'field_id', :association_foreign_key => 'obj_id'
51
+ validates_presence_of :field_type, :message => "can't be blank"
52
+
53
+ # Support sorting by value
54
+ def <=>(other)
55
+ self.to_s <=> other.to_s
56
+ end
57
+
58
+ def to_s
59
+ value
60
+ end
61
+ end
62
+
63
+ DublinCore::FIELDS.each do |field|
64
+ module_eval(%{
65
+ class DC#{field.capitalize} < Field; end
66
+ })
67
+ end
68
+
69
+ # OAI Provider configuration
70
+ class CampingProvider < OAI::Provider::Base
71
+ repository_name 'Camping Test OAI Repository'
72
+ source_model ActiveRecordWrapper.new(Obj)
73
+ end
74
+
75
+ class CreateTheBasics < V 1.0
76
+ def self.up
77
+ create_table :dublincore_objs, :force => true do |t|
78
+ t.column :source, :string
79
+ t.column :created_at, :datetime
80
+ t.column :updated_at, :datetime
81
+ end
82
+
83
+ create_table :dublincore_field_links, :id => false, :force => true do |t|
84
+ t.column :obj_id, :integer, :null => false
85
+ t.column :field_id, :integer, :null => false
86
+ end
87
+
88
+ create_table :dublincore_fields, :force => true do |t|
89
+ t.column :field_type, :string, :limit => 30, :null => false
90
+ t.column :value, :text, :null => false
91
+ end
92
+
93
+ add_index :dublincore_fields, [:field_type, :value], :uniq => true
94
+ add_index :dublincore_field_links, :field_id
95
+ add_index :dublincore_field_links, [:obj_id, :field_id]
96
+ end
97
+
98
+ def self.down
99
+ drop_table :dublincore_objs
100
+ drop_table :dublincore_field_links
101
+ drop_table :dublincore_fields
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ module DublinCore::Controllers
108
+
109
+ # Now setup a URL('/oai' by default) to handle OAI requests
110
+ class Oai
111
+ def get
112
+ @headers['Content-Type'] = 'text/xml'
113
+ provider = Models::CampingProvider.new
114
+ provider.process_request(@input.merge(:url => "http:"+URL(Oai).to_s))
115
+ end
116
+ end
117
+
118
+ class Index < R '/', '/browse/(\w+)', '/browse/(\w+)/page/(\d+)'
119
+ def get(field = nil, page = 1)
120
+ @field = field
121
+ @page = page.to_i
122
+ @browse = {}
123
+ if !@field
124
+ FIELDS.each do |field|
125
+ @browse[field] = Field.count(
126
+ :conditions => ["field_type = ?", "DC#{field.capitalize}"])
127
+ end
128
+ @home = true
129
+ @count = @browse.keys.size
130
+ else
131
+ @count = Field.count(:conditions => ["field_type = ?", "DC#{@field.capitalize}"])
132
+ fields = Field.find(:all,
133
+ :conditions => ["field_type = ?", "DC#{@field.capitalize}"],
134
+ :order => "value asc", :limit => DublinCore::LIMIT,
135
+ :offset => (@page - 1) * DublinCore::LIMIT)
136
+
137
+ fields.each do |field|
138
+ @browse[field] = field.objs.size
139
+ end
140
+ end
141
+ render :browse
142
+ end
143
+ end
144
+
145
+ class Search < R '/search', '/search/page/(\d+)'
146
+
147
+ def get(page = 1)
148
+ @page = page.to_i
149
+ if input.terms
150
+ @state.terms = input.terms if input.terms
151
+
152
+ start = Time.now
153
+ ids = search(input.terms, @page - 1)
154
+ finish = Time.now
155
+ @search_time = (finish - start)
156
+ @objs = Obj.find(ids)
157
+ else
158
+ @count = 0
159
+ @objs = []
160
+ end
161
+
162
+ render :search
163
+ end
164
+
165
+ end
166
+
167
+ class LinkedTo < R '/linked/(\d+)', '/linked/(\d+)/page/(\d+)'
168
+ def get(field, page = 1)
169
+ @page = page.to_i
170
+ @field = field
171
+ @count = Field.find(field).objs.size
172
+ @objs = Field.find(field).objs.find(:all,
173
+ :limit => DublinCore::LIMIT,
174
+ :offset => (@page - 1) * DublinCore::LIMIT)
175
+ render :records
176
+ end
177
+ end
178
+
179
+ class Add
180
+ def get
181
+ @obj = Obj.create
182
+ render :edit
183
+ end
184
+ end
185
+
186
+ class View < R '/view/(\d+)'
187
+ def get obj_id
188
+ obj = Obj.find(obj_id)
189
+ # Get rid of completely empty records
190
+ obj.destroy if obj.fields.empty?
191
+
192
+ @count = 1
193
+ @objs = [obj]
194
+ if Obj.exists?(obj.id)
195
+ render :records if Obj.exists?(obj.id)
196
+ else
197
+ redirect Index
198
+ end
199
+ end
200
+ end
201
+
202
+ class Edit < R '/edit', '/edit/(\d+)'
203
+ def get obj_id
204
+ @obj = Obj.find obj_id
205
+ render :edit
206
+ end
207
+
208
+ def post
209
+ case input.action
210
+ when 'Save'
211
+ @obj = Obj.find input.obj_id
212
+ @obj.fields.clear
213
+ input.keys.each do |key|
214
+ next unless key =~ /^DublinCore::Models::\w+/
215
+ next unless input[key] && !input[key].empty?
216
+ input[key].to_a.each do |value|
217
+ @obj.fields << key.constantize.find_or_create_by_value(value)
218
+ end
219
+ end
220
+ redirect View, @obj
221
+ when 'Discard'
222
+ @obj = Obj.find input.obj_id
223
+
224
+ # Get rid of completely empty records
225
+ @obj.destroy if @obj.fields.empty?
226
+
227
+ if Obj.exists?(@obj.id)
228
+ redirect View, @obj
229
+ else
230
+ redirect Index
231
+ end
232
+ when 'Delete'
233
+ Obj.find(input.obj_id).destroy
234
+ render :delete_success
235
+ end
236
+ end
237
+ end
238
+
239
+ class DataAdd < R '/data/add'
240
+ def post
241
+ if input.field_value && !input.field_value.empty?
242
+ model = "DublinCore::Models::#{input.field_type}".constantize
243
+ obj = Obj.find(input.obj_id)
244
+ obj.fields << model.find_or_create_by_value(input.field_value)
245
+ end
246
+ redirect Edit, input.obj_id
247
+ end
248
+ end
249
+
250
+ class Style < R '/styles.css'
251
+ def get
252
+ @headers["Content-Type"] = "text/css; charset=utf-8"
253
+ @body = %{
254
+ body { width: 750px; margin: 0; margin-left: auto; margin-right: auto; padding: 0;
255
+ color: black; background-color: white; }
256
+ a { color: #CC6600; text-decoration: none; }
257
+ a:visited { color: #CC6600; text-decoration: none;}
258
+ a:hover { text-decoration: underline; }
259
+ a.stealthy { color: black; }
260
+ a.stealthy:visited { color: black; }
261
+ .header { text-align: right; padding-right: .5em; }
262
+ div.search { text-align: right; position: relative; top: -1em; }
263
+ div.search form input { margin-right: .25em; }
264
+ .small { font-size: 70%; }
265
+ .tiny { font-size: 60%; }
266
+ .totals { font-size: 60%; margin-left: .25em; vertical-align: super; }
267
+ .field_labels { font-size: 60%; margin-left: 1em; vertical-align: super; }
268
+ h2 {color: #CC6600; padding: 0; margin-bottom: .15em; font-size: 160%;}
269
+ h3.header { padding:0; margin:0; position: relative; top: -2.8em;
270
+ padding-bottom: .25em; padding-right: 5em; font-size: 80%; }
271
+ h1.header a { color: #FF9900; text-decoration: none;
272
+ font: bold 250% "Trebuchet MS",Trebuchet,Georgia, Serif;
273
+ letter-spacing:-4px; }
274
+
275
+ div.pagination { text-align: center; }
276
+ ul.pages { list-style: none; padding: 0; display: inline;}
277
+ ul.pages li { display: inline; }
278
+ form.controls { text-align: right; }
279
+ ul.undecorated { list-style: none; padding-left: 1em; margin-bottom: 5em;}
280
+ .content { padding-left: 2em; padding-right: 2em; }
281
+ table { padding: 0; background-color: #CCEECC; font-size: 75%;
282
+ width: 100%; border: 1px solid black; margin: 1em; margin-left: auto; margin-right: auto; }
283
+ table.obj tr.controls { text-align: right; font-size: 100%; background-color: #AACCAA; }
284
+ table.obj td.label { width: 7em; padding-left: .25em; border-right: 1px solid black; }
285
+ table.obj td.value input { width: 80%; margin: .35em; }
286
+ input.button { width: 5em; margin-left: .5em; }
287
+ table.add tr.controls td { padding: .5em; font-size: 100%; background-color: #AACCAA; }
288
+ table.add td { width: 10%; }
289
+ table.add td.value { width: 80%; }
290
+ table.add td.value input { width: 100%; margin: .35em; }
291
+ }
292
+ end
293
+ end
294
+ end
295
+
296
+ module DublinCore::Helpers
297
+
298
+ def paginate(klass, term = nil)
299
+ @total_pages = count/DublinCore::LIMIT + 1
300
+ div.pagination do
301
+ p "#{@page} of #{@total_pages} pages"
302
+ ul.pages do
303
+ li { link_if("<<", klass, term, 1) }
304
+ li { link_if("<", klass, term, @page - 1) }
305
+ page_window.each do |page|
306
+ li { link_if("#{page}", klass, term, page) }
307
+ end
308
+ li { link_if(">", klass, term, @page + 1) }
309
+ li { link_if(">>", klass, term, @total_pages) }
310
+ end
311
+ end
312
+ end
313
+
314
+ private
315
+
316
+ def link_if(string, klass, term, page)
317
+ return "#{string} " if (@page == page || 1 > page || page > @total_pages)
318
+ a(string, :href => term.nil? ? R(klass, page) : R(klass, term, page)) << " "
319
+ end
320
+
321
+ def page_window
322
+ return 1..@total_pages if @total_pages < 9
323
+ size = @total_pages > 9 ? 9 : @total_pages
324
+ start = @page - size/2 > 0 ? @page - size/2 : 1
325
+ start = @total_pages - size if start+size > @total_pages
326
+ start..start+size
327
+ end
328
+
329
+ end
330
+
331
+ module DublinCore::Views
332
+
333
+ def layout
334
+ html do
335
+ head do
336
+ title "Dublin Core - Simple Asset Cataloger"
337
+ link :rel => 'stylesheet', :type => 'text/css',
338
+ :href => '/styles.css', :media => 'screen'
339
+ end
340
+ body do
341
+ h1.header { a 'Nugget Explorer', :href => R(Index) }
342
+ h3.header { "exposing ugly metadata" }
343
+ div.search do
344
+ form({:method => 'get', :action => R(Search)}) do
345
+ input :name => 'terms', :type => 'text'
346
+ input.button :type => :submit, :value => 'Search'
347
+ end
348
+ end
349
+ a("Home", :href => R(Index)) unless @home
350
+ div.content do
351
+ self << yield
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ def browse
358
+ if @browse.empty?
359
+ p 'No objects found, try adding one.'
360
+ else
361
+ h3 "Browsing" << (" '#{@field}'" if @field).to_s
362
+ ul.undecorated do
363
+ @browse.keys.sort.each do |key|
364
+ li { _key_value(key, @browse[key]) }
365
+ end
366
+ end
367
+ paginate(Index, @field) if @count > DublinCore::LIMIT
368
+ end
369
+ end
370
+
371
+ def delete_success
372
+ p "Delete was successful"
373
+ end
374
+
375
+ def search
376
+ p.results { span "#{count} results for '#{@state.terms}'"; span.tiny "(#{@search_time} secs)" }
377
+ ul.undecorated do
378
+ @result.keys.sort.each do |record|
379
+ li do
380
+ a(record.value, :href => R(LinkedTo, record.id))
381
+ span.totals "(#{@result[record]})"
382
+ span.field_labels "#{record.field_type.sub(/^DC/, '').downcase} "
383
+ end
384
+ end
385
+ end
386
+ paginate(Search) if @count > DublinCore::LIMIT
387
+ end
388
+
389
+ def edit
390
+ h3 "Editing Record"
391
+ p "To remove a field entry, just remove it's content."
392
+ _form(@obj, :action => R(Edit, @obj))
393
+ end
394
+
395
+ def records
396
+ @objs.each { |obj| _obj(obj) }
397
+ paginate(LinkedTo, @field) if @count > DublinCore::LIMIT
398
+ end
399
+
400
+ def _obj(obj, edit = false)
401
+ table.obj :cellspacing => 0 do
402
+ _edit_controls(obj, edit)
403
+ DublinCore::FIELDS.each do |field|
404
+ obj.send(field.pluralize.intern).each_with_index do |value, index|
405
+ tr do
406
+ td.label { 0 == index ? "#{field}(s)" : "&nbsp;" }
407
+ if edit
408
+ td.value do
409
+ input :name => value.class,
410
+ :type => 'text',
411
+ :value => value.to_s
412
+ end
413
+ else
414
+ td.value { a.stealthy(value, :href => R(LinkedTo, value.id)) }
415
+ end
416
+ end
417
+ end
418
+ end
419
+ end
420
+ end
421
+
422
+ def _form(obj, action)
423
+ form.controls(:method => 'post', :action => R(Edit)) do
424
+ input :type => 'hidden', :name => 'obj_id', :value => obj.id
425
+ _obj(obj, true)
426
+ input.button :type => :submit, :name => 'action', :value => 'Save'
427
+ input.button :type => :submit, :name => 'action', :value => 'Discard'
428
+ end
429
+ form(:method => 'post', :action => R(DataAdd)) do
430
+ input :type => 'hidden', :name => 'obj_id', :value => obj.id
431
+ table.add :cellspacing => 0 do
432
+ tr.controls do
433
+ td(:colspan => 3) { "Add an entry. (All changes above will be lost, so save them first)" }
434
+ end
435
+ tr do
436
+ td do
437
+ select(:name => 'field_type') do
438
+ DublinCore::FIELDS.each do |field|
439
+ option field, :value => "DC#{field.capitalize}"
440
+ end
441
+ end
442
+ end
443
+ td.value { input :name => 'field_value', :type => 'text' }
444
+ td { input.button :type => 'submit', :value => 'Add' }
445
+ end
446
+ end
447
+ end
448
+ end
449
+
450
+ def _edit_controls(obj, edit)
451
+ tr.controls do
452
+ td :colspan => 2 do
453
+ edit ? input(:type => 'submit', :name => 'action', :value => 'Delete') :
454
+ a('edit', :href => R(Edit, obj))
455
+ end
456
+ end
457
+ end
458
+
459
+
460
+ def _key_value(key, value)
461
+ if value > 0
462
+ if key.kind_of?(DublinCore::Models::Field)
463
+ a(key, :href => R(LinkedTo, key.id))
464
+ else
465
+ a(key.to_s, :href => R(Index, key))
466
+ end
467
+ span.totals "(#{value})"
468
+ else
469
+ span key
470
+ span.totals "(#{value})"
471
+ end
472
+ end
473
+
474
+ end