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