oai 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README +80 -0
- data/Rakefile +113 -0
- data/bin/oai +68 -0
- data/examples/models/file_model.rb +63 -0
- data/examples/providers/dublin_core.rb +474 -0
- data/lib/oai.rb +7 -13
- data/lib/oai/client.rb +133 -83
- data/lib/oai/{get_record.rb → client/get_record.rb} +0 -0
- data/lib/oai/{header.rb → client/header.rb} +2 -2
- data/lib/oai/{identify.rb → client/identify.rb} +0 -0
- data/lib/oai/{list_identifiers.rb → client/list_identifiers.rb} +0 -0
- data/lib/oai/{list_metadata_formats.rb → client/list_metadata_formats.rb} +0 -0
- data/lib/oai/{list_records.rb → client/list_records.rb} +0 -0
- data/lib/oai/{list_sets.rb → client/list_sets.rb} +1 -1
- data/lib/oai/{metadata_format.rb → client/metadata_format.rb} +0 -0
- data/lib/oai/{record.rb → client/record.rb} +0 -0
- data/lib/oai/{response.rb → client/response.rb} +1 -1
- data/lib/oai/constants.rb +34 -0
- data/lib/oai/exception.rb +72 -1
- data/lib/oai/harvester.rb +38 -0
- data/lib/oai/harvester/config.rb +41 -0
- data/lib/oai/harvester/harvest.rb +144 -0
- data/lib/oai/harvester/logging.rb +70 -0
- data/lib/oai/harvester/mailer.rb +17 -0
- data/lib/oai/harvester/shell.rb +334 -0
- data/lib/oai/provider.rb +300 -0
- data/lib/oai/provider/metadata_format.rb +72 -0
- data/lib/oai/provider/metadata_format/oai_dc.rb +29 -0
- data/lib/oai/provider/model.rb +71 -0
- data/lib/oai/provider/model/activerecord_caching_wrapper.rb +135 -0
- data/lib/oai/provider/model/activerecord_wrapper.rb +136 -0
- data/lib/oai/provider/partial_result.rb +18 -0
- data/lib/oai/provider/response.rb +119 -0
- data/lib/oai/provider/response/error.rb +16 -0
- data/lib/oai/provider/response/get_record.rb +32 -0
- data/lib/oai/provider/response/identify.rb +24 -0
- data/lib/oai/provider/response/list_identifiers.rb +29 -0
- data/lib/oai/provider/response/list_metadata_formats.rb +21 -0
- data/lib/oai/provider/response/list_records.rb +32 -0
- data/lib/oai/provider/response/list_sets.rb +23 -0
- data/lib/oai/provider/response/record_response.rb +68 -0
- data/lib/oai/provider/resumption_token.rb +106 -0
- data/lib/oai/set.rb +14 -5
- data/test/activerecord_provider/config/connection.rb +5 -0
- data/test/activerecord_provider/config/database.yml +6 -0
- data/test/activerecord_provider/database/ar_migration.rb +59 -0
- data/test/activerecord_provider/database/oaipmhtest +0 -0
- data/test/activerecord_provider/fixtures/dc.yml +1501 -0
- data/test/activerecord_provider/helpers/providers.rb +44 -0
- data/test/activerecord_provider/helpers/set_provider.rb +36 -0
- data/test/activerecord_provider/models/dc_field.rb +7 -0
- data/test/activerecord_provider/models/dc_set.rb +6 -0
- data/test/activerecord_provider/models/oai_token.rb +3 -0
- data/test/activerecord_provider/tc_ar_provider.rb +93 -0
- data/test/activerecord_provider/tc_ar_sets_provider.rb +66 -0
- data/test/activerecord_provider/tc_caching_paging_provider.rb +53 -0
- data/test/activerecord_provider/tc_simple_paging_provider.rb +55 -0
- data/test/activerecord_provider/test_helper.rb +4 -0
- data/test/client/helpers/provider.rb +68 -0
- data/test/client/helpers/test_wrapper.rb +11 -0
- data/test/client/tc_exception.rb +36 -0
- data/test/{tc_get_record.rb → client/tc_get_record.rb} +11 -7
- data/test/client/tc_identify.rb +13 -0
- data/test/{tc_libxml.rb → client/tc_libxml.rb} +20 -10
- data/test/{tc_list_identifiers.rb → client/tc_list_identifiers.rb} +10 -8
- data/test/{tc_list_metadata_formats.rb → client/tc_list_metadata_formats.rb} +4 -1
- data/test/{tc_list_records.rb → client/tc_list_records.rb} +4 -1
- data/test/{tc_list_sets.rb → client/tc_list_sets.rb} +4 -2
- data/test/{tc_xpath.rb → client/tc_xpath.rb} +1 -1
- data/test/client/test_helper.rb +5 -0
- data/test/provider/models.rb +230 -0
- data/test/provider/tc_exceptions.rb +63 -0
- data/test/provider/tc_functional_tokens.rb +42 -0
- data/test/provider/tc_provider.rb +69 -0
- data/test/provider/tc_resumption_tokens.rb +46 -0
- data/test/provider/tc_simple_provider.rb +85 -0
- data/test/provider/test_helper.rb +36 -0
- metadata +123 -27
- data/test/tc_exception.rb +0 -38
- 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)" : " " }
|
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
|