Bladeren bron

Functional, bare-bones prototype:

* Rewrite model typedefs in Lua
* Add more typedefs
* Model cache
* Very crude but functional HTML generator
scossu 3 weken geleden
bovenliggende
commit
b4dd5084af

+ 1 - 0
.gitignore

@@ -43,4 +43,5 @@ luac.out
 vgcore*
 data/ores/*
 data/dres/*
+out/*
 !.keep

+ 8 - 1
config/app.lua

@@ -14,7 +14,7 @@ return {
         -- Single-valued fields. TODO rely on content model cardinality.
         single_values = {
             ["dc:identifier"] = true,
-            ["dc:type"] = true,
+            ["pas:contentType"] = true,
             ["dc:title"] = true,
             ["path"] = true,
             ["pas:sourcePath"] = true,
@@ -42,6 +42,13 @@ return {
         rdfs = "http://www.w3.org/2000/01/rdf-schema#",
         xsd = "http://www.w3.org/2001/XMLSchema#",
     },
+
+    -- Static and dynamic site settings.
+    site = {
+        title = "Pocket Archive demo site"
+    },
+
+    -- Static site generation settings.
     htmlgen = {
         out_dir = "./out/html",
     },

+ 4 - 0
config/model/typedef/agent.lua

@@ -0,0 +1,4 @@
+return {
+  label = "Agent",
+  broader = "pas:Anything"
+}

+ 0 - 3
config/model/typedef/agent.yml

@@ -1,3 +0,0 @@
----
-label: Agent
-broader: "pas:Anything"

+ 50 - 0
config/model/typedef/anything.lua

@@ -0,0 +1,50 @@
+return {
+    id = "pas:Anything",
+    label = "Anything",
+    description = "Superclass of every resource type in the system.",
+    abstract = "true",
+
+    properties = {
+        ["pas:id"] = {
+            label = "Primary ID",
+            type = "string",
+            min_cardinality = 1,
+            max_cardinality = 1,
+        },
+        ["pas:refId"] = {
+            label = "External system ID",
+            type = "string",
+        },
+        ["pas:prefLabel"] = {
+            label = "Preferred Label",
+            type = "string",
+            max_cardinality = 1,
+        },
+        ["pas:altLabel"] = {
+            label = "Alternative Label",
+            type = "string",
+        },
+        ["pas:created"] = {
+            label = "Created On",
+            type = "timestamp",
+            min_cardinality = 1,
+            max_cardinality = 1,
+        },
+        ["pas:updated"] = {
+            label = "Last Updated On",
+            type = "timestamp",
+            min_cardinality = 1,
+            max_cardinality = 1,
+        },
+        ["pas:createdBy"] = {
+            label = "Created By",
+            type = "rel",
+            range = {["pas:Agent"] = true},
+        },
+        ["pas:updatedBy"] = {
+            label = "Last Updated By",
+            type = "rel",
+            range = {["pas:Agent"] = true},
+        },
+    },
+}

+ 0 - 46
config/model/typedef/anything.yml

@@ -1,46 +0,0 @@
----
-label: Anything
-description: Superclass of every resource type in the system.
-abstract: true
-
-properties:
-  "pas:id":
-    label: Primary ID
-    type: string
-    min_cardinality: 1
-    max_cardinality: 1
-
-  "pas:refId":
-    label: External system ID
-    type: string
-
-  "pas:prefLabel":
-    label: Preferred Label
-    type: string
-    max_cardinality: 1
-
-  "pas:altLabel":
-    label: Alternative Label
-    type: string
-
-  "pas:created":
-    label: Created On
-    type: timestamp
-    min_cardinality: 1
-    max_cardinality: 1
-
-  "pas:updated":
-    label: Last Updated On
-    type: timestamp
-    min_cardinality: 1
-    max_cardinality: 1
-
-  "pas:createdBy":
-    label: Created By
-    type: rel
-    range: ["pas:Agent"]
-
-  "pas:updatedBy":
-    label: Last Updated By
-    type: rel
-    range: ["pas:Agent"]

+ 14 - 0
config/model/typedef/artifact.lua

@@ -0,0 +1,14 @@
+return {
+    id = "pas:Artifact",
+    label = "Artifact",
+    description = "Intellectual work.",
+    broader = "pas:Anything",
+
+    properties = {
+        ["pas:hasFile"] = {
+            label = "Has file",
+            type = "rel",
+            range = {["pas:File"] = true},
+        },
+    },
+}

+ 0 - 10
config/model/typedef/artifact.yml

@@ -1,10 +0,0 @@
----
-label: Artifact
-description: Intellectual work.
-broader: "pas:Anything"
-
-properties:
-  "pas:hasFile":
-    label: Has file
-    type: rel
-    range: ["pas:File"]

+ 6 - 0
config/model/typedef/document.lua

@@ -0,0 +1,6 @@
+return {
+    id = "pas:Document",
+    label = "Document",
+    broader = "pas:Artifact",
+}
+

+ 13 - 0
config/model/typedef/file.lua

@@ -0,0 +1,13 @@
+return {
+    label = "File",
+    broader = "pas:Anything",
+
+    properties = {
+        ["pas:location"] = {
+            label = "Location",
+            type = "string",
+            min_cardinality = 1,
+            max_cardinality = 1,
+        }
+    }
+}

+ 0 - 10
config/model/typedef/file.yml

@@ -1,10 +0,0 @@
----
-label: File
-broader: "pas:Anything"
-
-properties:
-  "pas:location":
-    label: Location
-    type: string
-    min_cardinality: 1
-    max_cardinality: 1

+ 36 - 0
config/model/typedef/part.lua

@@ -0,0 +1,36 @@
+--[[
+Part content type = "a logical subdivision within an artifact.",
+Resources of this type may be used to build logical structures implemented
+as linked lists.
+
+The `pas = "first` property points to the first child in a",
+linked list nested inside the current resource. There may be any number of
+linked list heads under the same Part, or none.
+
+The `pas = "next` property points to the next sibling in a linked list. The",
+last item in a list is identified by the lack of this property.
+
+In a more complex hierarchy, any given Part may have both "first" and
+"next" properties.
+--]]
+
+return {
+    id = "pas:Part",
+    label = "Part",
+
+    broader = "pas:Anything",
+
+    properties = {
+        ["pas:first"] = {
+            label = "First child",
+            type = "resource",
+            range = {["pas:Part"] = true},
+        },
+        ["pas:next"] = {
+            label = "Next sibling",
+            type = "resource",
+            range = {["pas:Part"] = true},
+            max_cardinality = "1",
+        }
+    }
+}

+ 0 - 33
config/model/typedef/part.yml

@@ -1,33 +0,0 @@
-#
-# Part content type: a logical subdivision within an artifact.
-# Resources of this type may be used to build logical structures implemented
-# as linked lists.
-#
-# The `pas:first` property points to the first child in a
-# linked list nested inside the current resource. There may be any number of
-# linked list heads under the same Part, or none.
-#
-# The `pas:next` property points to the next sibling in a linked list. The
-# last item in a list is identified by the lack of this property.
-#
-# In a more complex hierarchy, any given Part may have both "first" and
-# "next" properties.
-
----
-id: "pas:Part"
-ls_code: Part
-label: Part
-
-broader: "pas:Anything"
-
-properties:
-  "pas:first":
-    label: First child
-    type: resource
-    range: ["pas:Part"]
-
-  "pas:next":
-    label: Next sibling
-    type: resource
-    range: ["pas:Part"]
-    max_cardinality: 1

+ 17 - 0
config/model/typedef/postcard.lua

@@ -0,0 +1,17 @@
+return {
+    id = "pas:Postcard",
+    label = "Postcard",
+    broader = "pas:Document",
+    properties = {
+        ["pas:recto"] = {
+            label = "Recto",
+            type = "rel",
+            range = {["pas:Part"] = true},
+        },
+        ["pas:verso"] = {
+            label = "Verso",
+            type = "rel",
+            range = {["pas:Part"] = true},
+        },
+    }
+}

+ 5 - 0
config/model/typedef/still_image.lua

@@ -0,0 +1,5 @@
+return {
+    id = "pas:StillImage",
+    label = "Still Image",
+    broader = "pas:Artifact",
+}

+ 5 - 0
config/model/typedef/still_image_file.lua

@@ -0,0 +1,5 @@
+return {
+    id = "dctype:StillImage",
+    label = "Still Image File",
+    broader = "pas:File",
+}

+ 1 - 1
pocket_archive-scm-1.rockspec

@@ -30,7 +30,7 @@ build = {
     type = "builtin",
     modules = {
         ["pocket_archive"] = "src/core.lua",
-        ["pocket_archive.model_parser"] = "src/model_parser.lua",
+        ["pocket_archive.model"] = "src/model.lua",
         ["pocket_archive.submission"] = "src/submission.lua",
         ["pocket_archive.html_generator"] = "src/html_generator.lua",
         ["pocket_archive.monocypher"] = {

+ 1 - 1
scratch.lua

@@ -20,4 +20,4 @@ idx = graph.list(pkar.store)
 --assert(#idx == 5)
 
 html = hgen.generate_idx()
-pp.dump(html)
+res_html = hgen.generate_resources()

+ 74 - 7
src/html_generator.lua

@@ -1,32 +1,99 @@
 local datafile = require "datafile"
+local dir = require "pl.dir"
 local etlua = require "etlua"
 local pp = require "pl.pretty"
 
 local term = require "lsup.term"
+local triple = require "lsup.triple"
 local graph = require "lsup.graph"
 
 local pkar = require "pocket_archive"
 
 
-local M = {}
+-- Compile all templates once.
+-- TODO Add override for user-maintained templates.
+local fh, idx_tpl, res_tpl, err
+fh = datafile.open("templates/index.html")
+idx_tpl, err = etlua.compile(fh:read("a"))
+if not idx_tpl then error(err) end
+fh:close()
+
+fh = datafile.open("templates/res.html")
+res_tpl, err = etlua.compile(fh:read("a"))
+if not res_tpl then error(err) end
+fh:close()
+
+
+local M = {
+    res_dir = pkar.config.htmlgen.out_dir .. "/res",
+}
+
+M.generate_resource = function(gr_uri)
+    local gr = graph.get(gr_uri, pkar.store)
+    local dmd = {}
+    local rel = {}
+
+    for trp in gr:lookup(gr_uri) do
+        -- TODO transform data here according to model.
+        dmd[trp.p.data] = trp.o
+    end
+    pp.dump(rsrc_data)
+
+    out_html = res_tpl({
+        title = pkar.config.site.title or "Pocket Archive",
+        uri = gr_uri,
+        pkar = pkar,
+        dmd = dmd,
+        rel = rel,
+    })
+
+    local res_id = gr_uri.data:gsub(pkar.nsm:get_ns("par"), "")
+    local ofh = assert(io.open(string.format(
+            "%s/%s.html", M.res_dir, res_id), "w"))
+    return ofh:write(out_html)
+end
+
+
+M.generate_resources = function()
+    -- TODO parallelize
+    dir.makepath(M.res_dir)
+    local index_ts = graph.list(pkar.store)
+    for gr_uri in pairs(index_ts) do M.generate_resource(gr_uri) end
+end
+
 
 M.generate_idx = function()
     local obj_idx = {}
     local index_ts = graph.list(pkar.store)
     for gr_uri in pairs(index_ts) do
         local obj = {}
+        local cur
         gr = graph.get(gr_uri, pkar.store)
-        for trp in gr:lookup(gr_uri, term.new_iriref("dc:title")) do
-            if trp.o then obj.title = trp.o end
+        -- Get all subject of type: Artifact.
+        s_ts = gr:term_set(
+            term.new_iriref("rdf:type", pkar.nsm), triple.POS_P,
+            term.new_iriref("pas:Artifact", pkar.nsm), triple.POS_O
+        )
+        for s in pairs(s_ts) do
+            for trp in gr:lookup(s, term.new_iriref("dc:title", pkar.nsm)) do
+                obj.title = trp.o
+                break  -- Only one value to use
+            end
         end
         if obj.title then obj_idx[gr_uri.data] = obj end
     end
 
-    local fh = datafile.open("templates/index.html")
-    local idx_tpl, err = etlua.compile(fh:read("a"))
-    if not idx then error(err) end
     pp.dump(obj_idx)
-    print(idx_tpl({obj_idx = obj_idx, term = term}))
+    out_html = idx_tpl({
+        title = pkar.config.site.title or "Pocket Archive",
+        pkar = pkar,
+        obj_idx = obj_idx,
+    })
+
+    local ofh = assert(io.open(
+            pkar.config.htmlgen.out_dir .. "/index.html", "w"))
+
+    return ofh:write(out_html)
 end
 
 

+ 16 - 10
src/model_parser.lua → src/model.lua

@@ -2,10 +2,8 @@ local string = string
 local table = table
 local io = io
 
-local lyaml = require "lyaml"
 
-
-local M = {}
+local M = {models = {}}
 
 -- Parameters that do not get inherited.
 local NO_INHERIT = {abstract = true}
@@ -13,9 +11,10 @@ local MODEL_PATH = "./config/model/typedef/"
 
 
 local function camel2snake(src)
-    return src:lower()
+    return src
         :gsub("^pas:", "")  -- Strip namespace.
         :gsub("([^^])(%u)", "%1_%2")  -- Uppercase (except initial) to _.
+        :lower()
 end
 
 
@@ -25,11 +24,8 @@ M.parse_model = function(mod_id)
     local function traverse(mod_id)
         print("traversing:", mod_id)
         local fname = camel2snake (mod_id)
-        local fh = assert(io.open(MODEL_PATH .. fname .. ".yml"), "r")
-        local yml_data = fh:read("a")
-        fh:close()
-        local model = lyaml.load(yml_data)
-        model.id = mod_id
+        local model = dofile(MODEL_PATH .. fname .. ".lua")
+        --model.id = mod_id
         --print("Model: ")
         --for k, v in pairs(model) do print (k, v) end
 
@@ -61,7 +57,7 @@ M.parse_model = function(mod_id)
             ::continue::
         end
     end
-    local config = {}
+    local config = {lineage = lineage}
 
     for _, src_config in ipairs(hierarchy) do
         merge(src_config, config)
@@ -70,4 +66,14 @@ M.parse_model = function(mod_id)
     return config
 end
 
+
+local models_mt = {
+    ["__index"] = function (t, model)
+        t[model] = M.parse_model(model)
+        return t[model]
+    end
+}
+setmetatable(M.models, models_mt)
+
+
 return M

+ 12 - 4
src/submission.lua

@@ -3,12 +3,14 @@ local io = io
 local csv = require "csv"
 local dir = require "pl.dir"
 local plpath = require "pl.path"
+local pp = require "pl.pretty"
 
 local term = require "lsup.term"
 local triple = require "lsup.triple"
 local graph = require "lsup.graph"
 
 local mc = require "pocket_archive.monocypher"
+local model = require "pocket_archive.model"
 local pkar = require "pocket_archive"
 
 
@@ -233,6 +235,9 @@ end
 
 M.update_rsrc_md = function(rsrc)
     -- TODO use a transaction when lsup_lua supports it.
+    print("Updating resource md: ")
+    pp.dump(rsrc)
+    rmod = model.parse_model(rsrc["pas:contentType"])
     triples = {}
 
     local s = term.new_iriref("par:" .. rsrc.id, pkar.nsm)
@@ -247,6 +252,13 @@ M.update_rsrc_md = function(rsrc)
             end
         else table.insert(triples, triple.new(s, p, term.new_lit(v))) end
     end
+    for i, m in ipairs(rmod.lineage) do
+        table.insert(triples, triple.new(
+            s, term.new_iriref("rdf:type", pkar.nsm),
+            term.new_iriref(m)
+        ))
+    end
+
     -- This is a full replacement.
     print("Removing triples.")
     gr:remove();
@@ -257,10 +269,6 @@ M.update_rsrc_md = function(rsrc)
 end
 
 
-M.update_md = function(sip)
-end
-
-
 M.deposit = function(sip)
     for i, rsrc in ipairs(sip) do
         -- TODO Wrap this chunk into a txn. Each row is atomic.

+ 6 - 3
templates/index.html

@@ -1,16 +1,19 @@
 <!DOCTYPE html>
 <html>
     <head>
-        <title>Pocket Archive</title>
+        <title><%= title %></title>
     </head>
     <body>
-        <header></header>
+        <header>
+            <h1><%= title %></h1>
+        </header>
         <main>
             <section id="obj_list">
+                <h2>Recent artifacts</h2>
                 <ul>
                 <% for uri, data in pairs(obj_idx) do %>
                     <li class="obj_link">
-                        <a href="<%= uri %>">
+                        <a href="<%= uri:gsub(pkar.nsm:get_ns("par"), "res/") %>.html">
                             <%= data.title.data %>
                             <%if data.title.lang then %>
                                 <span class="langtag"><%= data.title.lang %></span>

+ 24 - 0
templates/res.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title><%= title %>&emsp;:&emsp;<%= data["dc:title"].data %></title>
+    </head>
+    <body>
+        <header>
+            <h1><%= title %>&emsp;❁&emsp;<%= data["dc:title"].data %></h1>
+        </header>
+        <main>
+            <section id="res_md">
+                <h2>Metadata</h2>
+                <dl class="res_md">
+                <% for p, o in pairs(dmd) do %>
+                <dt><%= p %></dt>
+                <dd><%= o.data %></dd>
+                <% end %>
+                </dl>
+            </section>
+        </main>
+        <footer></footer>
+    </body>
+</html>
+

+ 2 - 2
test/sample_submission/postcard-bag/data/submission-v2.csv

@@ -1,4 +1,4 @@
-"path","dc:identifier","dc:type","dc:title","dc:alternative","dc:description"
+"path","dc:identifier","pas:contentType","dc:title","dc:alternative","dc:description"
 0001,0001,"Postcard","Example Postcard","This is an alternative label","Note that recto and verso representations have been named front and back, to emphasize that the ordering is not alphabetical."
 ,,,,"And this is another alternative label","Second description."
 ,,,,"Yet another alt label.",
@@ -7,4 +7,4 @@
 "0001/0001-back",0004,"Part","Verso",,
 "0001/0001-back/567890.jpg",0005,"StillImageFile",,,
 0006,0006,"StillImage","Single Image",,"Preparing kebab at Aqil's during curfew."
-0006/0685_04.jpg,0007,"StillImageFile","Nablus 2002",,
+0006/0685_04.jpg,0007,"StillImageFile","B/W scan of physical photo",,