diff --git a/METADATA.md b/METADATA.md
index c00930a0..8b67b23f 100644
--- a/METADATA.md
+++ b/METADATA.md
@@ -76,11 +76,12 @@ The kind of document. Currently we have the following kinds of documents:
* `benefit-report`
* `tag`: the tag itself (a controlled concept)
* `tag-page` (distinct from tag as a concept)
-* `author-page`
+* `personal-profile-page`: a foaf personal profile document
* `archive-page`
* `vocabulary`
* `dependency`
* `person`
+* `member`: A member of the company
* `base-dictionary`
* `extra-dictionary`
* `acronym-dictionary`
@@ -110,3 +111,15 @@ resolve to a document in the archive.
### `redirect_to_id`
The item to which this item should be redirected in some way.
+
+## Metadata relating to people (and member) items
+
+### `web_id` {.optional}
+
+The [WebID](https://www.w3.org/2005/Incubator/webid/spec/identity/) of the
+person, if the person has one.
+
+### `hashtag` {.optional}
+
+The fragment identifier to use to identify the person denoted by the WebID. By
+default uses the identifier `me`.
diff --git a/etc/people.yaml b/etc/people.yaml
new file mode 100644
index 00000000..1c9a887a
--- /dev/null
+++ b/etc/people.yaml
@@ -0,0 +1,4 @@
+---
+- name: Sarven Capadisli
+ web_id: https://csarven.ca/#i
+...
diff --git a/items/static/blog/posts/defining-the-scholarly-commons/persistent-identifiers/index.md b/items/static/blog/posts/defining-the-scholarly-commons/persistent-identifiers/index.md
index 6e7b010f..6c03e9ab 100644
--- a/items/static/blog/posts/defining-the-scholarly-commons/persistent-identifiers/index.md
+++ b/items/static/blog/posts/defining-the-scholarly-commons/persistent-identifiers/index.md
@@ -381,11 +381,11 @@ Here's a tiny example:
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
-<<%= iri_for(me) %>> a foaf:Person ;
+<<%= me.fetch(:web_id) %>> a foaf:Person ;
rdfs:label "<%= full_name(me) %>"@en ;
foaf:familyName "<%= me[:last_name] %>"@en ;
foaf:givenName "<%= me[:first_name] %>"@en ;
- foaf:img <<%= photo_url(me, global: true) %>> ;
+ foaf:img <<%= path_to_photo(me, absolute: true) %>> ;
foaf:mbox <mailto:<%= me[:email] %>> ;
foaf:name "<%= full_name(me) %>"@en ;
cc:license <https://creativecommons.org/licenses/by/4.0/> .
diff --git a/layouts/blog/partials/citations.erb b/layouts/blog/partials/citations.erb
index cf881477..313df35c 100644
--- a/layouts/blog/partials/citations.erb
+++ b/layouts/blog/partials/citations.erb
@@ -8,7 +8,7 @@
<% end %>
diff --git a/lib/data_sources/company.rb b/lib/data_sources/company.rb
index 0bed1397..943795d9 100644
--- a/lib/data_sources/company.rb
+++ b/lib/data_sources/company.rb
@@ -2,6 +2,7 @@
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/string/inflections'
+require 'digest'
Class.new(Nanoc::DataSource) do
identifier :company
@@ -13,17 +14,19 @@ def up
def items
items = []
+ company = @company_info[:company]
items << new_item(
'',
{
mtime: mtime_of(@config[:company_metafile]),
is_hidden: true,
- }.merge(@company_info[:company]),
+ }.merge(company),
Nanoc::Identifier.new('/company/_'),
+ attributes_checksum_data: Digest::SHA1.digest(Marshal.dump(company)),
)
- @company_info[:people].each do |person|
- items << person_to_item(person)
+ @company_info[:members].each do |member|
+ items << member_to_item(member)
end
items
@@ -31,19 +34,22 @@ def items
protected
- def person_to_item(person)
- full_name = "#{person[:first_name]} #{person[:last_name]}"
+ def member_to_item(member)
+ full_name = member[:full_name] || "#{member[:first_name]} #{member[:last_name]}"
slug = full_name.parameterize
attributes = {
- kind: 'person',
+ kind: 'member',
+ slug: slug,
+ mtime: mtime_of(@config[:company_metafile]),
is_hidden: true,
}
new_item(
full_name,
- attributes.merge(person),
- Nanoc::Identifier.new("/company/people/_#{slug}"),
+ attributes.merge(member),
+ Nanoc::Identifier.new("/company/members/_#{slug}"),
+ attributes_checksum_data: Digest::SHA1.digest(Marshal.dump(member)),
)
end
diff --git a/lib/data_sources/people.rb b/lib/data_sources/people.rb
new file mode 100644
index 00000000..326c613c
--- /dev/null
+++ b/lib/data_sources/people.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/string/inflections'
+require 'digest'
+
+Class.new(Nanoc::DataSource) do
+ identifier :people
+
+ def up
+ @people = YAML.load_file(@config[:people_data]).map(&:symbolize_keys)
+ end
+
+ def items
+ @people.map do |person|
+ person_to_item(person)
+ end
+ end
+
+ protected
+
+ def person_to_item(person)
+ slug = person[:name].parameterize
+
+ attributes = {
+ kind: 'person',
+ slug: slug,
+ mtime: mtime_of(@config[:people_data]),
+ is_hidden: true,
+ }
+
+ new_item(
+ person[:name],
+ attributes.merge(person),
+ Nanoc::Identifier.new("/people/_#{slug}"),
+ attributes_checksum_data: Digest::SHA1.digest(Marshal.dump(person)),
+ )
+ end
+
+ def mtime_of(meta_filename)
+ File.stat(meta_filename).mtime
+ end
+end
diff --git a/lib/data_sources/vocabularies.rb b/lib/data_sources/vocabularies.rb
index 86340586..16d58e17 100644
--- a/lib/data_sources/vocabularies.rb
+++ b/lib/data_sources/vocabularies.rb
@@ -66,7 +66,7 @@ def cleanup(text)
def vocab_uri(vocab)
# HACK: until we have better handling of ontology definitions at a
- # different uri than the namespace uri.
+ # different uri than the vocabulary namespace.
if vocab == RDF::Vocab::VOID
'http://vocab.deri.ie/void'
elsif vocab == RDF::Vocab::CC
diff --git a/lib/filters/vcard.rb b/lib/filters/vcard.rb
index 058fd64e..e8d01738 100644
--- a/lib/filters/vcard.rb
+++ b/lib/filters/vcard.rb
@@ -100,7 +100,6 @@ def run(_content, params = {})
# Last chance property setting for individuals
photo_uri ||= path_to_photo(@item, absolute: true)
- uid ||= web_id(@item)
end
vcard.nickname(nick_name) unless nick_name.nil?
diff --git a/lib/helpers/atom_feed.rb b/lib/helpers/atom_feed.rb
index de2b56d8..ed33585e 100644
--- a/lib/helpers/atom_feed.rb
+++ b/lib/helpers/atom_feed.rb
@@ -86,7 +86,7 @@ def validate_entries
end
def build_for_feed(xml)
- root_url = @config[:base_url] + '/'
+ root_url = @config[:base_url] + '/'
xml.instruct!
xml.feed(xmlns: 'http://www.w3.org/2005/Atom', 'xml:base' => root_url) do
@@ -151,16 +151,20 @@ def build_for_entry(entry, xml)
end
end
- # @option params [Number] :limit
- # @option params [Array] :entries
- # @option params [Boolean] :preserve_order
+ # Generate an Atom feed.
+ #
+ # @param [Hash] params General parameters for the feed.
+ # @option params [Number] :limit (5) The limiting number of entries for the feed.
+ # @option params [Array] :entries ([]) An array of items to publish.
+ # @option params [Boolean] :preserve_order (false) Whether to preserve the order
+ # of items as given. Otherwise items will be sorted by published or created date.
# @option params [Proc] :content_proc
# @option params [Proc] :excerpt_proc
- # @option params [String] :title
- # @option params [String] :author_name
- # @option params [String] :author_uri
- # @option params [String] :icon
- # @option params [String] :logo
+ # @option params [String] :title The title of the feed.
+ # @option params [String] :author_name The default author of the feed.
+ # @option params [String] :author_uri The default author URI of the feed.
+ # @option params [String] :icon The icon associated with the feed.
+ # @option params [String] :logo The logo associated with the feed.
#
# @return [String]
def atom_feed(params = {})
diff --git a/lib/helpers/blogging.rb b/lib/helpers/blogging.rb
index 0a25a104..d905b392 100644
--- a/lib/helpers/blogging.rb
+++ b/lib/helpers/blogging.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
require_relative 'link_to'
+require_relative 'people'
module LifePreserver
module Blogging
include LinkTo
+ include People
def blog_post?(item)
item.identifier =~ %r{^/static/blog/posts/.*\.(md|html)}
@@ -52,18 +54,24 @@ def post_summary(post_rep, snapshot: nil, read_more_text: 'Read more ⇢', separ
# Create a link for the author of this page
#
def link_for_author(author, rel_tag: true)
- %(#{h author})
+ author = person_by_name(author)
+ %(#{h full_name(author)})
end
- # run during preprocessing
- def generate_author_uris(item_set = nil)
+ # Populates the `author_uri` for the given blog post items. If not set
+ # specifically, falls back to the WebID of the author.
+ #
+ # @note Run during preprocessing.
+ #
+ # @param [Array] item_set An array of blog post items.
+ #
+ # @return [void]
+ def populate_author_uris(item_set = nil)
item_set ||= blog_posts
- validate_config
-
item_set.each do |item|
author = person_by_name(item[:author_name])
if author
- item[:author_uri] ||= "#{@config[:base_url]}#{description_path(author)}"
+ item[:author_uri] ||= author.fetch(:web_id)
end
end
end
@@ -71,13 +79,5 @@ def generate_author_uris(item_set = nil)
def bibtex_key(item, author)
item[:bibtex_key] || [author.fetch(:last_name), author.fetch(:first_name)[0], attribute_to_time(item[:published_at]).year, Digest::SHA1.hexdigest(item[:title])[0..6]].join(':').downcase
end
-
- protected
-
- def validate_config
- if @config[:base_url].nil?
- raise Nanoc::Int::Errors::GenericTrivial.new('Cannot generate author URIs: site configuration has no base_url')
- end
- end
end
end
diff --git a/lib/helpers/company.rb b/lib/helpers/company.rb
index 1cc18827..ef20594a 100644
--- a/lib/helpers/company.rb
+++ b/lib/helpers/company.rb
@@ -1,29 +1,23 @@
# frozen_string_literal: true
-require 'rdf'
+require 'active_support/core_ext/string/inflections'
+require 'date'
+
require_relative 'text'
+require_relative 'link_to'
module LifePreserver
module Company
include Text
+ include LinkTo
- def full_name(person)
- "#{person[:first_name]} #{person[:last_name]}"
- end
-
- # Get an IRI for an object
+ # Fetch all the attributes of the company item.
#
- # @return [RDF::IRI]
- def iri_for(obj)
- if obj.is_a?(String)
- RDF::IRI.new(obj)
- elsif obj.respond_to?(:to_uri)
- obj.to_uri
- elsif obj.respond_to?(:identifier) && obj.identifier =~ '/company/people/*'
- RDF::IRI.new("#{@config[:base_url]}#{@config[:company][:page_path]}##{full_name(obj).to_slug}")
- else
- raise ArgumentError, "Not sure how to get an IRI for an object of type `#{obj.class}`."
- end
+ # @see {file:lib/data_sources/company.rb}
+ #
+ # @return [Hash] The attributes of the company item.
+ def company
+ @items['/company/_'].attributes
end
def path_to_logo(absolute: false)
@@ -31,32 +25,88 @@ def path_to_logo(absolute: false)
path_to(logo_item, absolute: absolute)
end
- def photo_url(person, global: false)
- "#{@config[:base_url] if global}#{@config[:site][:images_path]}/#{full_name(person).to_slug}.jpg"
+ def members
+ @items.find_all('/company/members/_*')
end
- # Fetch all the attributes of the company item.
+ def sorted_members
+ members.sort_by { |p| [p[:last_name], p[:first_name]] }
+ end
+
+ # @return [Boolean] True if a member of the company, false if not
+ def member?(member)
+ members.member?(member)
+ end
+
+ # @param [String] full_name
#
- # @return [Hash]
- def company
- blk = -> { @items['/company/_'].attributes }
- if @items.frozen?
- @company_attributes ||= blk.call
- else
- blk.call
- end
+ # @return [Nanoc::BasicItemView, nil] A member item, if found
+ def member_by_name(full_name)
+ members.find { |member| full_name(member) == full_name }
+ end
+
+ # @param [Nanoc::BasicItemView] member a member item
+ #
+ # @return [Array] an sorted array of seats
+ def sorted_seats(member)
+ member.fetch(:seats).sort_by { |s| s[:started_at] }
+ end
+
+ # Find the current seat for a member, i.e. the longest-standing seat that has not ended.
+ #
+ # @param [Nanoc::BasicItemView] member a member item
+ #
+ # @return [Hash] the current seat of the member
+ def current_seat(member)
+ sorted_seats(member).find { |seat| !seat.key?(:ended_at) || seat.fetch(:ended_at) > Date.today }
+ end
+
+ # Return the path to the profile page of the given member.
+ #
+ # @param [Nanoc::CompilationItemView] member The company member.
+ # @param [Boolean] absolute (false) Whether to return an absolute path or not.
+ #
+ # @return [String] The path to the profile page, if the item exists.
+ def path_to_profile_page(member, absolute: false)
+ profile_page_item = @items["#{@config[:static_root]}#{@config[:people][:page_path]}/#{member.fetch(:slug)}/index.*"]
+ path_to(profile_page_item, absolute: absolute)
end
- def sorted_people
- people = @items.find_all('/company/people/*')
- people.sort_by do |p|
- [p[:last_name], p[:first_name]]
+ # Populate identifiers for members without an external WebID.
+ #
+ # @note Called during preprocessing before items have been given a path,
+ # so we have to build this public path manually.
+ #
+ # @note This an exception to the normal URI design for this site (see
+ # {file:URI_DESIGN.md}) in order to conform to the current draft of the
+ # WebID spec (see https://www.w3.org/2005/Incubator/webid/spec/identity)
+ # which states that for "WebIDs with fragment identifiers (e.g. #me), the
+ # URI without the fragment denotes the Profile Document."
+ #
+ # @see LifePreserver::People#populate_people_identifiers To compare the
+ # configuration of identifiers for people.
+ #
+ # @return [void]
+ def populate_member_identifiers
+ members.each do |member|
+ member[:web_id] ||= "#{@config[:base_url]}#{@config[:people][:page_path]}/#{member.fetch(:slug)}/##{member.fetch(:hashtag, 'me')}"
end
end
- def person_by_name(full_name)
- people = @items.find_all('/company/people/*')
- people.find { |person| full_name(person) == full_name }
+ # Creates in-memory personal profile page items for all company members.
+ #
+ # @see https://github.com/pentandra/lifepreserver/issues/79
+ #
+ # @return [void]
+ def generate_personal_profile_pages
+ members.each do |member|
+ @items.create(
+ %(<%= render('/personal_profile.*', person_name: '#{full_name(member)}') %>),
+ { title: full_name(member), kind: 'personal-profile-page', description: "All about #{full_name(member)}" },
+ "#{@config[:static_root]}#{@config[:people][:page_path]}/#{member.fetch(:slug)}/index.erb",
+ binary: false,
+ )
+ end
end
end
end
diff --git a/lib/helpers/link_to.rb b/lib/helpers/link_to.rb
index bc4434f6..f0543e40 100644
--- a/lib/helpers/link_to.rb
+++ b/lib/helpers/link_to.rb
@@ -103,7 +103,7 @@ def path_to(target, rep: :default, snapshot: :last, fragment: nil, absolute: fal
end
# Chop off last slash for concept URIs
- # @see URI_DESIGN.md
+ # @see {file:URI_DESIGN.md}
path.chop! if concept && path.end_with?('/')
# Assemble fragment identifier, if given
diff --git a/lib/helpers/people.rb b/lib/helpers/people.rb
new file mode 100644
index 00000000..605a23bd
--- /dev/null
+++ b/lib/helpers/people.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require_relative 'company'
+require_relative 'link_to'
+
+module LifePreserver
+ module People
+ include Company
+ include LinkTo
+
+ # Grab all the people, including company members.
+ def people
+ members + @items.find_all('/people/_*')
+ end
+
+ # TODO: Fix sorting
+ def sorted_people
+ people.sort_by { |p| p.fetch(:name, full_name(p)).split.reverse }
+ end
+
+ def person_by_name(name)
+ people.find { |person| full_name(person) == name }
+ end
+
+ # Fetch the full name of the person.
+ #
+ # Looks for the `:full_name` key, then `:name`, then assembles a name using
+ # the `:first_name` and `:last_name` keys as a last resort.
+ #
+ # @param [Nanoc::BasicItemView] person An item with kind `person`.
+ #
+ # @return [String] The full name of the person.
+ def full_name(person)
+ person[:full_name] || person[:name] || "#{person.fetch(:first_name)} #{person.fetch(:last_name)}"
+ end
+
+ # Get path to a person's photo.
+ #
+ # Assumes and verifies that all photos of people are stored in the
+ # site's images path. (I think this is good practice.) Looks first
+ # for a filename at the `:photo` key, then defaults to the slug if
+ # that key is not found.
+ #
+ # @param [Nanoc::BasicItemView] person a person item
+ # @param [Boolean] absolute (false) return an absolute URI?
+ #
+ # @return [String] path to photo
+ def path_to_photo(person, absolute: false)
+ photo_filename = person[:photo] || person[:slug] + '.jpg'
+ photo_item = @items["#{@config[:static_root]}#{@config[:site][:images_path]}/#{photo_filename}"]
+ path_to(photo_item, absolute: absolute)
+ end
+
+ # Populate identifiers for referenced people without a WebID. If a person
+ # does not have an external WebID, this creates a local identifier for
+ # the person that will redirect to the people index document.
+ #
+ # @note Called during preprocessing before items have been given a path,
+ # so we have to build this public path manually.
+ #
+ # @see https://github.com/pentandra/lifepreserver/issues/85
+ #
+ # @see LifePreserver::Company#populate_member_identifiers To compare the
+ # configuration of identifiers for people.
+ #
+ # @return [void]
+ def populate_people_identifiers
+ @items.find_all('/people/_*').each do |person|
+ person[:web_id] ||= "#{@config[:base_url]}#{@config[:people][:page_path]}##{member.fetch(:slug)}"
+ end
+ end
+ end
+end
diff --git a/lib/helpers/weblog.rb b/lib/helpers/weblog.rb
index 2ef3fe86..03605291 100644
--- a/lib/helpers/weblog.rb
+++ b/lib/helpers/weblog.rb
@@ -81,19 +81,6 @@ def archive_years(posts = nil)
.to_a
end
- # Creates in-memory author pages from partial: layouts/blog/author.html
- def generate_author_pages(item_set = nil)
- item_set ||= published_weblog
- authors(item_set).each do |author|
- @items.create(
- %(<%= render('/blog/author.*', author: '#{author}') %>),
- { title: "Weblog postings by #{author}", kind: 'author-page', is_hidden: true, description: "All posts written by #{author}" },
- "#{@config[:static_root]}#{@config[:blog][:authors_path]}/#{author.to_slug}/index.erb",
- binary: false,
- )
- end
- end
-
# Creates in-memory blog archive pages from partial: layouts/blog/archive.html
def generate_blog_archives(item_set = nil)
item_set ||= published_weblog
diff --git a/lib/life_preserver.rb b/lib/life_preserver.rb
index 3a68d2c7..6ae0769f 100644
--- a/lib/life_preserver.rb
+++ b/lib/life_preserver.rb
@@ -35,3 +35,4 @@ module LifePreserver
include LifePreserver::Dependencies
include LifePreserver::Proposals
include LifePreserver::ChildParent
+include LifePreserver::People
diff --git a/nanoc.yaml b/nanoc.yaml
index c224c4d7..94f8c82c 100644
--- a/nanoc.yaml
+++ b/nanoc.yaml
@@ -61,6 +61,10 @@ data_sources:
- type: company
company_metafile: 'var/company.yaml'
+ - type: people
+ people_data: 'etc/people.yaml'
+
+
# Config for Nanoc environments
environments:
@@ -75,7 +79,6 @@ environments:
path: /blog
tags_path: /tags
archives_path: /archives
- authors_path: /authors
feed_path: /blog/feed
cache:
@@ -93,6 +96,8 @@ environments:
tag_url: "https://github.com/pentandra/lifepreserver/releases/tag"
version_history_url: "https://github.com/lifepreserver/commits/master"
+ people:
+ page_path: /people
site: &site
name: Pentandra
@@ -119,7 +124,7 @@ environments:
checks:
<<: *checks
internal_links:
- exclude: [ '^/blog/feed', '^/tags', '^/archives', '^/authors' ]
+ exclude: [ '^/blog/feed', '^/tags', '^/archives' ]
server:
name: 'localhost'
diff --git a/rules/blog.rb b/rules/blog.rb
index 61d71e08..c53af963 100644
--- a/rules/blog.rb
+++ b/rules/blog.rb
@@ -15,9 +15,9 @@
write item.identifier.without_ext + '.atom'
end
-# Weblog tags, authors, and archives
+# Weblog tags and archives
-compile '/static/{tags,authors,archives}/**/*' do
+compile '/static/{tags,archives}/**/*' do
filter :erb, @config[:erb]
filter :rubypantsunicode
filter :cache_buster if @config[:production]
diff --git a/rules/preprocessing.rb b/rules/preprocessing.rb
index 20fcd399..55032246 100644
--- a/rules/preprocessing.rb
+++ b/rules/preprocessing.rb
@@ -48,11 +48,13 @@ def extract_metadata_from_specification_documents
end
end
- generate_author_uris(weblog)
+ populate_people_identifiers
+ populate_member_identifiers
+ populate_author_uris(weblog)
+ generate_personal_profile_pages
if @config[:site][:generate_meta]
generate_tag_pages
- generate_author_pages
generate_blog_archives
end
diff --git a/var/company.yaml b/var/company.yaml
index 26fd6332..5979807c 100644
--- a/var/company.yaml
+++ b/var/company.yaml
@@ -15,29 +15,33 @@ company:
Pentandra’s mission is to open up the knowledge of the world, so that light
and understanding will be more accessible to everyone.
twitter: PentandraInc
-people:
+members:
- first_name: Chris
last_name: Chapman
gender: male
- title: Co-founder and Super Coder
email: chris@pentandra.com
twitter: cd_chapman
+ gplus: 102332274449851755462
github: cdchapman
phone_ext: 1000
- member_of:
- - Core Team
+ seats:
+ - title: Co-founder and Super Coder
+ started_at: 2011-07-27
+ group: Core Team
summary: >-
Chris loves to tackle difficult problems and dreams (literally) about
making research easier. He is our systems thinker.
- first_name: Katie
last_name: Chapman
gender: female
- title: Co-founder and Exception Thinker
email: katie@pentandra.com
twitter: KAMChapman
+ gplus: 104975395018953249008
phone_ext: 1001
- member_of:
- - Core Team
+ seats:
+ - title: Co-founder and Exception Thinker
+ started_at: 2011-07-27
+ group: Core Team
summary: >-
Katie loves to solve a good puzzle and thrives on the details. She is
also our finance and tax geek and actually enjoys it.