Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Versions for OSF Archive #585

Merged
merged 11 commits into from Jan 11, 2017
1 change: 0 additions & 1 deletion .started-issues
@@ -1,4 +1,3 @@
478
DLTP-638
DLTP-717
DLTP-764
10 changes: 10 additions & 0 deletions app/assets/stylesheets/modules/attributes.css.scss
Expand Up @@ -48,3 +48,13 @@
}
}
}

.table.related-to-works {
.selected-row {
border: 2px solid $blue-light;
}

.modifier.current {
width: 3em;
}
}
3 changes: 2 additions & 1 deletion app/mappers/as_jsonld_mapper.rb
Expand Up @@ -31,6 +31,7 @@ def self.call(curation_concern, **keywords)
'vracore'.to_sym => 'http://purl.org/vra/',
'frels'.to_sym => 'info:fedora/fedora-system:def/relations-external#',
'ms'.to_sym => 'http://www.ndltd.org/standards/metadata/etdms/1.1/',
'pav'.to_sym => 'http://purl.org/pav/',
"fedora-model".to_sym => "info:fedora/fedora-system:def/model#",
"hydra".to_sym => "http://projecthydra.org/ns/relations#",
"hasModel".to_sym => {"@id" => "fedora-model:hasModel", "@type" => "@id" },
Expand All @@ -40,7 +41,7 @@ def self.call(curation_concern, **keywords)
"isMemberOfCollection".to_sym => {"@id" => "frels:isMemberOfCollection", "@type" => "@id" },
"isEditorOf".to_sym => {"@id" => "hydra:isEditorOf", "@type" => "@id" },
"hasMember".to_sym => {"@id" => "frels:hasMember", "@type" => "@id" },
"previousVersion".to_sym => "http://purl.org/pav/previousVersion"
'previousVersion'.to_sym => 'http://purl.org/pav/previousVersion'
}

DEFAULT_ATTRIBUTES = {
Expand Down
8 changes: 7 additions & 1 deletion app/repository_datastreams/osf_archive_datastream.rb
Expand Up @@ -10,7 +10,13 @@ class OsfArchiveDatastream < ActiveFedora::NtriplesRDFDatastream
index.as :stored_searchable
end

map.source(to: 'source', in: RDF::DC)
map.source(to: 'source', in: RDF::DC) do |index|
index.as :stored_searchable
end

map.osf_project_identifier(to: 'osfProjectIdentifier', in: RDF::ND) do |index|
index.as :stored_searchable
end

map.subject(to: 'subject', in: RDF::DC) do |index|
index.as :stored_searchable
Expand Down
66 changes: 62 additions & 4 deletions app/repository_models/osf_archive.rb
@@ -1,3 +1,16 @@
# Models two "types" of OSF Archives:
#
# * OSF Project
# * OSF Registration
#
# The OSF Registration is a "snapshot" of an OSF Project. The Registration has a URL
# separate from the OSF Project; It points back to the OSF Project. In theory, if
# we ingest the same OSF Registration we will have the same information; In practice,
# some of the data for the registration may point to external sources that have changed
# between ingests of that OSF Registration.
#
# The OSF Project is a living/mutable source. It represents the current state of the project.
# When we ingest an OSF Project, that current state is captured.
class OsfArchive < ActiveFedora::Base
include ActiveModel::Validations
include CurationConcern::Work
Expand All @@ -8,23 +21,64 @@ class OsfArchive < ActiveFedora::Base
include ActiveFedora::RegisteredAttributes
include CurationConcern::RemotelyIdentifiedByDoi::Attributes
include CurationConcern::WithJsonMapper

before_validation :set_initial_values, on: :create

belongs_to :previousVersion, property: :previousVersion, class_name: "OsfArchive"

class_attribute :human_readable_short_description
self.human_readable_short_description = "Change me."

def self.human_readable_type
'OSF Archive'
end

# These are included as convenience for developers.
SOLR_KEY_OSF_PROJECT_IDENTIFIER = 'desc_metadata__osf_project_identifier_tesim'.freeze
SOLR_KEY_ARCHIVED_DATE = 'desc_metadata__date_archived_dtsi'.freeze
SOLR_KEY_SOURCE = 'desc_metadata__source_tesim'.freeze

# Retrieve all archived versions of the source project (including registrations)
# in date_archived descending order.
#
# @return [Array<OsfArchive>]
# @see ./spec/repository_models/osf_archive_spec.rb
def archived_versions_of_source_project
@archived_versions_of_source_project ||= begin
conditions = { SOLR_KEY_OSF_PROJECT_IDENTIFIER => osf_project_identifier }
options = { sort: "#{SOLR_KEY_ARCHIVED_DATE} desc" }
solr_results = self.class.find_with_conditions(conditions, options)
ActiveFedora::SolrService.reify_solr_results(solr_results, load_from_solr: true)
end
end

def set_initial_values
self.date_modified = Date.today
self.date_archived = Date.today
self.type = human_readable_type
self.date_archived ||= Date.today
determine_type_and_osf_project_identifier
end
private :set_initial_values

def determine_type_and_osf_project_identifier
if osf_project_identifier.present?
if osf_project_identifier == osf_source_slug
self.type = "OSF Project"
else
self.type = "OSF Registration"
end
else
if osf_source_slug.present?
self.osf_project_identifier = osf_source_slug
self.type = "OSF Project"
end
end
end
private :determine_type_and_osf_project_identifier

def osf_source_slug
self.source.to_s.split('/').last
end

has_metadata 'descMetadata', type: OsfArchiveDatastream

attribute :affiliation,
Expand All @@ -41,14 +95,18 @@ def set_initial_values

attribute :source,
datastream: :descMetadata, multiple: false,
label: 'Original OSF Project',
label: 'Original OSF URL',
validates: {
format: {
with: URI::regexp(%w(http https ftp)),
message: 'must be a valid URL.'
}
}

attribute :osf_project_identifier,
datastream: :descMetadata, multiple: false,
label: 'OSF Project Identifier'

attribute :subject,
datastream: :descMetadata, multiple: true

Expand Down
29 changes: 28 additions & 1 deletion app/views/curation_concern/osf_archives/_attributes.html.erb
Expand Up @@ -9,7 +9,8 @@
<%= curation_concern_attribute_to_html(curation_concern, :date_created, 'Date Created') %>
<%= curation_concern_attribute_to_html(curation_concern, :subject, "Subject") %>
<%= curation_concern_attribute_to_html(curation_concern, :date_archived, "Archive Date") %>
<%= curation_concern_attribute_to_html(curation_concern, :source, "Original OSF Project") %>
<%= curation_concern_attribute_to_html(curation_concern, :source, "Original OSF URL") %>
<%= curation_concern_attribute_to_html(curation_concern, :osf_project_identifier, "OSF Project Identifier") %>
<%= curation_concern_attribute_to_html(curation_concern, :language, "Language") %>
<%= decode_administrative_unit(curation_concern, :administrative_unit, "Departments and Units") %>
<%= curation_concern_attribute_to_html(curation_concern, :library_collections, "Member of") %>
Expand All @@ -31,3 +32,29 @@
<% end %>
</tbody>
</table>

<h2>Version History</h2>
<table class="table table-striped <%= dom_class(curation_concern) %> related-to-works with-headroom">
<caption class="table-heading">
<p>These are different snapshots of the same OSF Project.</p>
</caption>
<thead>
<tr>
<th>&nbsp;</th>
<th>CurateND Identifier</th>
<th>Date Archived</th>
</tr>
</thead>
<tbody>
<% curation_concern.archived_versions_of_source_project.each do |archived_version| %>
<% is_viewing_version = (archived_version.pid == curation_concern.pid) %>
<tr class="referenced_by_works attributes <%= 'selected-row' if is_viewing_version %>">
<td class="modifier current"><%= is_viewing_version ? 'Viewing' : '&nbsp;'.html_safe %></td>
<td class="attribute noid">
<%= link_to_unless(is_viewing_version, archived_version.noid, polymorphic_path([:curation_concern, archived_version])) %>
</td>
<td class="attribute date_archived"><%= content_tag :time, archived_version.date_archived.strftime('%Y-%m-%d'), datetime: archived_version.date_archived.iso8601 %></td>
</tr>
<% end %>
</tbody>
</table>
@@ -1,5 +1,6 @@
<fieldset class="optional prompt">
<legend>Additional Information</legend>
<%= f.input :osf_project_identifier, label: "OSF Project Identifier", input_html: { class: 'input-xlarge' } %>
<%= f.input :affiliation,
label: 'Academic Status',
as: :select,
Expand Down
Expand Up @@ -11,5 +11,5 @@
},
label: 'Description'
%>
<%= f.input :source, label: "Original OSF Project", input_html: {class: 'input-xlarge', type:'url'} %>
<%= f.input :source, label: "Original OSF URL", input_html: {class: 'input-xlarge', type:'url'} %>
</fieldset>
3 changes: 3 additions & 0 deletions config/predicate_mappings.yml
Expand Up @@ -5,6 +5,7 @@
fedora-model: info:fedora/fedora-system:def/model#
fedora-relations-model: info:fedora/fedora-system:def/relations-external#
hydramata-rel: http://projecthydra.org/ns/relations#
pav: 'http://purl.org/pav/'

# namespace mappings---
# you can add specific mappings for your institution by providing the following:
Expand Down Expand Up @@ -49,6 +50,8 @@
:is_deployment_of: isDeploymentOf
:has_service: hasService
:has_model: hasModel
http://purl.org/pav/:
:previousVersion: previousVersion
http://www.openarchives.org/OAI/2.0/:
:oai_item_id: itemID
http://projecthydra.org/ns/relations#:
Expand Down
1 change: 1 addition & 0 deletions lib/rdf/nd.rb
Expand Up @@ -2,5 +2,6 @@
module RDF
class ND < Vocabulary("https://library.nd.edu/ns/terms/")
property :alephIdentifier
property :osfProjectIdentifier, comment: "The identifier of the source OSF Project. Extends dc:identifier."
end
end
5 changes: 5 additions & 0 deletions lib/rdf/pav.rb
@@ -0,0 +1,5 @@
module RDF
class PAV < Vocabulary("http://purl.org/pav/")
property :previousVersion
end
end
16 changes: 16 additions & 0 deletions script/one-off/DLTP-721-seed-data.rb
@@ -0,0 +1,16 @@
attributes = {
osf_project_identifier: 'abcde',
source: 'https://osf.io/abcde',
title: 'Example OSF Project',
description: "Many Bothan's died to bring you this project",
visibility: Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_PUBLIC
}

previous_project = OsfArchive.create!(attributes.merge(date_archived: '2016-01-02'))
registration = OsfArchive.create!(attributes.merge(date_archived: '2016-02-02', source: 'https://osf.io/12345'))
project = OsfArchive.create!(attributes.merge(date_archived: '2016-03-02', previousVersion: previous_project))

puts "Previous Project URL: http://localhost:3000/show/#{previous_project.noid}"
puts "Registration URL: http://localhost:3000/show/#{registration.noid}"
puts "Project URL: http://localhost:3000/show/#{project.noid}"
`open http://localhost:3000/show/#{project.noid}`
3 changes: 3 additions & 0 deletions spec/factories/osf_archives_factory.rb
Expand Up @@ -12,7 +12,10 @@
date_created { Date.today }
date_modified { Date.today }
date_archived { Date.today }
# the source and osf_project_identifier are related
source { 'https://osf.io/xxxxx/' }
osf_project_identifier { 'xxxxx' }

visibility Hydra::AccessControls::AccessRight::VISIBILITY_TEXT_VALUE_AUTHENTICATED

before(:create) { |work, evaluator|
Expand Down
80 changes: 77 additions & 3 deletions spec/repository_models/osf_archive_spec.rb
Expand Up @@ -6,21 +6,95 @@
it { should have_unique_field(:title) }
it { should have_unique_field(:source) }
it { should have_unique_field(:type) }
it { should have_unique_field(:osf_project_identifier) }

it_behaves_like 'is_a_curation_concern_model'
it_behaves_like 'with_access_rights'
it_behaves_like 'can_be_a_member_of_library_collections'
it_behaves_like 'is_embargoable'
it_behaves_like 'with_json_mapper'

describe 'new archive' do
describe 'new archive (after validation)' do
let(:archive) {OsfArchive.new}

it "should set initialize dates and stamp type on create" do
it "should set initialize dates" do
archive.valid?
expect(archive.date_modified).to eq(Date.today)
expect(archive.date_archived).to eq(Date.today)
expect(archive.type).to eq('OSF Archive')
end

[
{ attributes: { osf_project_identifier: "abcde", source: 'https://osf.io/abcde' }, type: 'OSF Project' },
{ attributes: { osf_project_identifier: "12345", source: 'https://osf.io/abcde' }, type: 'OSF Registration' },
{ attributes: { osf_project_identifier: "", source: 'https://osf.io/abcde' }, type: 'OSF Project' },
{ attributes: { osf_project_identifier: "", source: "" }, type: nil },
{ attributes: { osf_project_identifier: "12345", source: "" }, type: 'OSF Registration' }
].each_with_index do |data, index|
it "will assign dc:type of #{data.fetch(:type).inspect} for attributes #{data.fetch(:attributes).inspect} (Scenario ##{index})" do
osf_archive = OsfArchive.new(data.fetch(:attributes))
osf_archive.valid? # required because we are setting via before validation
expect(osf_archive.type).to eq(data.fetch(:type))
end
end
end

describe 'related versions of OSF Archive objects' do
let(:osf_project_identifier) { 'abcde' }
let(:osf_registration_identifier) { '12345' }
let!(:previous_version) do
FactoryGirl.create(
:osf_archive,
source: "https://osf.io/#{osf_project_identifier}",
osf_project_identifier: osf_project_identifier,
date_archived: 2.days.ago
)
end
let!(:current_version) do
FactoryGirl.create(
:osf_archive,
previousVersion: previous_version,
source: "https://osf.io/#{osf_project_identifier}",
osf_project_identifier: osf_project_identifier,
date_archived: 1.days.ago
)
end
let!(:project_registration) do
FactoryGirl.create(
:osf_archive,
source: "https://osf.io/#{osf_registration_identifier}",
osf_project_identifier: osf_project_identifier,
date_archived: 0.days.ago
)
end

it 'has expected JSON-LD, RELS-EXT, setter/getter behavior, and #archived_versions_of_source_project' do
osf_archive = OsfArchive.find(current_version.id)

# Ensuring that the previousVersion works
expect(osf_archive.previousVersion).to eq(previous_version)

# Ensuring a well configured SOLR
to_solr = osf_archive.to_solr
expect(to_solr.fetch(OsfArchive::SOLR_KEY_SOURCE)).to eq([osf_archive.source])
expect(to_solr.fetch(OsfArchive::SOLR_KEY_OSF_PROJECT_IDENTIFIER)).to eq([osf_project_identifier])
expect(to_solr.key?(OsfArchive::SOLR_KEY_ARCHIVED_DATE)).to eq(true)

# Ensuring #archived_versions_of_source_project
archived_versions_of_source_project = osf_archive.archived_versions_of_source_project
expect(archived_versions_of_source_project).to eq([
previous_version, current_version, project_registration
])

jsonld = osf_archive.as_jsonld
# Ensuring that the as_jsonld contains the correct relationship
expect(jsonld.fetch('@context').fetch('pav')).to eq('http://purl.org/pav/')
expect(jsonld.fetch('@context').fetch('previousVersion')).to eq('pav:previousVersion')
expect(jsonld.fetch("previousVersion")).to eq({"@id" => previous_version.pid})

# Ensuring that we have a meaningful RELS-EXT
rels_ext = osf_archive.datastreams.fetch('RELS-EXT').content
expect(rels_ext).to include(%(xmlns:pav="http://purl.org/pav/"))
expect(rels_ext).to include(%(<pav:previousVersion rdf:resource="info:fedora/#{previous_version.pid}"></pav:previousVersion>))
end
end
end