oai 0.0.3 → 0.0.4

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.
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