Extracted page support from cms_node_module, and add a proper CMS_PAGE_MODULE.

- now, the CMS_PAGE_MODULE has to be declared in the related CMS_SETUP via CMS_EXECUTION.
   (See demo for example)

Improved the export facilities.
  Implemented blog and page export.
Added import facilities.
  Implemented blog and page import.

Improved node revision web interface (allow to edit a past revision, in order to restore it as latest revisionm i.e current).
Removed specific tag from blog module, and reuse the taxonomy module for that purpose.

Added WIKITEXT module that provide a WIKITEXT_FILTER, so now we can have wikitext content.
   - for now, no support for wiki links such as [[Foobar]].
This commit is contained in:
2017-01-20 16:05:40 +01:00
parent 3bcfb0a44a
commit 2d698f604b
59 changed files with 1761 additions and 679 deletions

View File

@@ -0,0 +1,95 @@
note
description: "A page node."
date: "$Date$"
revision: "$Revision$"
class
CMS_PAGE
inherit
CMS_NODE
redefine
make_empty,
import_node
end
create
make_empty,
make
feature {NONE} -- Initialization
make_empty
do
Precursor
end
feature -- Conversion
import_node (a_node: CMS_NODE)
-- <Precursor>
do
Precursor (a_node)
if attached {CMS_PAGE} a_node as l_page then
set_parent (l_page.parent)
end
end
feature -- Access
content_type: READABLE_STRING_8
once
Result := {CMS_PAGE_NODE_TYPE}.name
end
feature -- Access: content
summary: detachable READABLE_STRING_32
-- A short summary of the node.
content: detachable READABLE_STRING_32
-- Content of the node.
format: detachable READABLE_STRING_8
-- Format associated with `content' and `summary'.
-- For example: text, mediawiki, html, etc
parent: detachable CMS_PAGE
-- Eventual parent page.
--| Used to describe a book structure.
feature -- Element change
set_content (a_content: like content; a_summary: like summary; a_format: like format)
do
content := a_content
summary := a_summary
format := a_format
end
set_parent (a_page: detachable CMS_PAGE)
-- Set `parent' to `a_page'
require
Current_is_not_parent_of_a_page: not is_parent_of (a_page)
do
parent := a_page
end
feature -- Status report
is_parent_of (a_page: detachable CMS_PAGE): BOOLEAN
-- Is Current page, a parent of `a_page' ?
do
if
a_page /= Void and then
attached a_page.parent as l_parent
then
if l_parent.same_node (a_page) then
Result := True
else
Result := is_parent_of (l_parent)
end
end
end
end

View File

@@ -0,0 +1,172 @@
note
description: "API to handle nodes of type page. Extends the node API."
date: "$Date$"
revision: "$Revision$"
class
CMS_PAGE_API
inherit
CMS_MODULE_API
rename
make as make_with_cms_api
redefine
initialize
end
REFACTORING_HELPER
create
make
feature {NONE} -- Initialization
make (a_api: CMS_API; a_node_api: CMS_NODE_API)
-- (from CMS_MODULE_API)
-- (export status {NONE})
do
node_api := a_node_api
make_with_cms_api (a_api)
end
initialize
-- <Precursor>
do
Precursor
---- l_sql_node_storage.register_node_storage_extension (create {CMS_NODE_STORAGE_SQL_PAGE_EXTENSION}.make (l_node_api, l_sql_node_storage))
-- Create the node storage for type blog
if attached storage.as_sql_storage as l_storage_sql then
create {CMS_PAGE_STORAGE_SQL} page_storage.make (l_storage_sql)
else
create {CMS_PAGE_STORAGE_NULL} page_storage.make
end
initialize_node_types
end
initialize_node_types
-- Initialize content type system.
local
ct: CMS_PAGE_NODE_TYPE
do
-- Initialize node content types.
create ct
page_content_type := ct
--| For now, add all available formats to content type `ct'.
across
cms_api.formats as ic
loop
ct.extend_format (ic.item)
end
node_api.add_node_type (ct)
node_api.add_node_type_webform_manager (create {CMS_PAGE_NODE_TYPE_WEBFORM_MANAGER}.make (ct, Current))
end
feature {CMS_API_ACCESS, CMS_MODULE, CMS_API} -- Restricted access
node_api: CMS_NODE_API
page_content_type: CMS_PAGE_NODE_TYPE
feature -- Access
pages: LIST [CMS_PAGE]
-- All pages.
do
Result := nodes_to_pages (node_api.nodes_of_type (page_content_type))
end
pages_with_title (a_title: READABLE_STRING_GENERAL): LIST [CMS_PAGE]
-- List of pages with title `a_title`.
do
Result := nodes_to_pages (node_api.nodes_of_type_with_title (page_content_type, a_title))
end
feature -- Access: page/book outline
children (a_node: CMS_NODE): detachable LIST [CMS_NODE]
-- Children of node `a_node'.
-- note: this is the partial version of the nodes.
do
Result := page_storage.children (a_node)
end
available_parents_for_node (a_node: CMS_NODE): LIST [CMS_NODE]
-- Potential parent nodes for node `a_node'.
-- Ensure no cycle exists.
do
create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
across page_storage.available_parents_for_node (a_node) as ic loop
check distinct: not a_node.same_node (ic.item) end
if not is_node_a_parent_of (a_node, ic.item) then
Result.force (ic.item)
end
end
ensure
no_cycle: across Result as c all not is_node_a_parent_of (a_node, c.item) end
end
is_node_a_parent_of (a_node: CMS_NODE; a_child: CMS_NODE): BOOLEAN
-- Is `a_node' a direct or indirect parent of node `a_child'?
require
distinct_nodes: not a_node.same_node (a_child)
do
if
attached {CMS_PAGE} node_api.full_node (a_child) as l_child_page and then
attached l_child_page.parent as l_parent
then
if l_parent.same_node (a_node) then
Result := True
else
Result := is_node_a_parent_of (a_node, l_parent)
end
end
end
feature -- Conversion
full_page_node (a_page: CMS_PAGE): CMS_PAGE
-- If `a_page' is partial, return the full page node from `a_page',
-- otherwise return directly `a_page'.
require
a_page_set: a_page /= Void
do
if attached {CMS_PAGE} node_api.full_node (a_page) as l_full_page then
Result := l_full_page
else
Result := a_page
end
end
feature -- Commit
save_page (a_page: CMS_PAGE)
do
node_api.save_node (a_page)
end
feature {CMS_MODULE} -- Access nodes storage.
page_storage: CMS_PAGE_STORAGE_I
feature {NONE} -- Helpers
nodes_to_pages (a_nodes: LIST [CMS_NODE]): ARRAYED_LIST [CMS_PAGE]
-- Convert list of nodes into a list of page when possible.
do
create {ARRAYED_LIST [CMS_PAGE]} Result.make (a_nodes.count)
if attached node_api as l_node_api then
across
a_nodes as ic
loop
if attached {CMS_PAGE} l_node_api.full_node (ic.item) as l_page then
Result.force (l_page)
end
end
end
end
end

View File

@@ -0,0 +1,411 @@
note
description: "Node page implementation."
date: "$Date$"
revision: "$Revision$"
class
CMS_PAGE_MODULE
inherit
CMS_MODULE
rename
module_api as page_api
redefine
setup_hooks,
initialize,
install, is_installed,
page_api
end
CMS_HOOK_EXPORT
CMS_HOOK_IMPORT
CMS_EXPORT_NODE_UTILITIES
CMS_IMPORT_NODE_UTILITIES
create
make
feature {NONE} -- Initialization
make
do
version := "1.0"
description := "Page service"
package := "content"
add_dependency ({CMS_NODE_MODULE})
end
feature -- Access
name: STRING = "page"
feature {CMS_API} -- Module Initialization
initialize (a_api: CMS_API)
-- <Precursor>
local
p1,p2: CMS_PAGE
ct: CMS_PAGE_NODE_TYPE
do
Precursor (a_api)
if attached {CMS_NODE_API} a_api.module_api ({CMS_NODE_MODULE}) as l_node_api then
node_api := l_node_api
create page_api.make (a_api, l_node_api)
-- Add support for CMS_PAGE, which requires a storage extension to store the optional "parent" id.
-- For now, we only have extension based on SQL statement.
if attached {CMS_NODE_STORAGE_SQL} l_node_api.node_storage as l_sql_node_storage then
l_sql_node_storage.register_node_storage_extension (create {CMS_NODE_STORAGE_SQL_PAGE_EXTENSION}.make (l_node_api, l_sql_node_storage))
-- FIXME: the following code is mostly for test purpose/initialization, remove later
if l_sql_node_storage.sql_table_items_count ("page_nodes") = 0 then
if attached a_api.user_api.user_by_id (1) as u then
create ct
p1 := ct.new_node (Void)
p1.set_title ("Welcome")
p1.set_content ("Welcome, you are using the ROC Eiffel CMS", Void, Void) -- Use default format
p1.set_author (u)
l_sql_node_storage.save_node (p1)
p2 := ct.new_node (Void)
p2.set_title ("A new page example")
p2.set_content ("This is the content of a page", Void, Void) -- Use default format
p2.set_author (u)
p2.set_parent (p1)
l_sql_node_storage.save_node (p2)
end
end
else
-- FIXME: maybe provide a default solution based on file system, when no SQL storage is available.
-- IDEA: we could also have generic extension to node system, that handle generic addition field.
end
end
end
feature {CMS_API} -- Module management
is_installed (a_api: CMS_API): BOOLEAN
-- Is Current module installed?
do
Result := Precursor (a_api)
if Result and attached a_api.storage.as_sql_storage as l_sql_storage then
Result := l_sql_storage.sql_table_exists ("page_nodes")
end
end
install (a_api: CMS_API)
do
-- Schema
if attached a_api.storage.as_sql_storage as l_sql_storage then
if attached a_api.module ({CMS_NODE_MODULE}) as l_node_module then
l_sql_storage.sql_execute_file_script (a_api.module_resource_location (l_node_module, (create {PATH}.make_from_string ("scripts")).extended (name).appended_with_extension ("sql")), Void)
end
if l_sql_storage.has_error then
a_api.logger.put_error ("Could not initialize database for module [" + name + "]", generating_type)
else
Precursor {CMS_MODULE} (a_api)
end
end
end
feature {CMS_API} -- Access: API
page_api: detachable CMS_PAGE_API
-- <Precursor>
node_api: detachable CMS_NODE_API
feature -- Access: router
setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
-- <Precursor>
do
end
feature -- Hooks
setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
do
a_hooks.subscribe_to_export_hook (Current)
a_hooks.subscribe_to_import_hook (Current)
end
export_to (a_export_id_list: detachable ITERABLE [READABLE_STRING_GENERAL]; a_export_ctx: CMS_EXPORT_CONTEXT; a_response: CMS_RESPONSE)
-- Export data identified by `a_export_id_list',
-- or export all data if `a_export_id_list' is Void.
local
n: CMS_PAGE
p: PATH
d: DIRECTORY
f: PLAIN_TEXT_FILE
lst: LIST [CMS_NODE]
do
if
attached node_api as l_node_api and then
attached l_node_api.node_type ("page") as l_node_type and then
( a_export_id_list = Void
or else across a_export_id_list as ic some ic.item.same_string (l_node_type.name) end
)
then
if
a_response.has_permissions (<<"export any node", "export " + l_node_type.name>>) and then
attached page_api as l_page_api
then
lst := l_page_api.pages
a_export_ctx.log ("Exporting " + lst.count.out + " notes of type `" + l_node_type.name + "`.")
create d.make_with_path (a_export_ctx.location.extended ("nodes").extended (l_node_type.name))
if d.exists then
d.recursive_delete
end
across
lst as ic
loop
if attached {CMS_PAGE} ic.item as l_page_node then
n := l_page_api.full_page_node (l_page_node)
a_export_ctx.log (n.content_type + " #" + n.id.out)
p := a_export_ctx.location.extended ("nodes").extended (n.content_type).extended (n.id.out).appended_with_extension ("json")
create d.make_with_path (p.parent)
if not d.exists then
d.recursive_create_dir
end
create f.make_with_path (p)
if not f.exists or else f.is_access_writable then
f.open_write
f.put_string (json_to_string (page_node_to_json (n, l_page_api)))
f.close
end
-- Revisions.
if
attached l_node_api.node_revisions (n) as l_revisions and then
l_revisions.count > 1
then
a_export_ctx.log (n.content_type + " " + l_revisions.count.out + " revisions.")
p := a_export_ctx.location.extended ("nodes").extended (n.content_type).extended (n.id.out)
create d.make_with_path (p)
if not d.exists then
d.recursive_create_dir
end
across
l_revisions as revs_ic
loop
if attached {CMS_PAGE} revs_ic.item as l_rev_page then
create f.make_with_path (p.extended ("rev-" + n.revision.out).appended_with_extension ("json"))
if not f.exists or else f.is_access_writable then
f.open_write
f.put_string (json_to_string (page_node_to_json (l_rev_page, l_page_api)))
end
f.close
end
end
end
end
end
end
end
end
page_node_to_json (a_page: CMS_PAGE; a_page_api: CMS_PAGE_API): JSON_OBJECT
local
j: JSON_OBJECT
p: CMS_PAGE
do
Result := node_to_json (a_page, a_page_api.node_api)
if attached a_page.parent as l_parent_page then
p := a_page_api.full_page_node (l_parent_page)
create j.make_empty
j.put_string (p.content_type, "type")
j.put_string (p.title, "title")
j.put_integer (p.id, "nid")
if attached p.link as lnk then
j.put (link_to_json (lnk), "link")
end
Result.put (j, "parent")
end
end
import_from (a_import_id_list: detachable ITERABLE [READABLE_STRING_GENERAL]; a_import_ctx: CMS_IMPORT_CONTEXT; a_response: CMS_RESPONSE)
-- Import data identified by `a_import_id_list',
-- or import all data if `a_import_id_list' is Void.
local
p: PATH
d: DIRECTORY
f: PLAIN_TEXT_FILE
s: STRING
jp: JSON_PARSER
loc: READABLE_STRING_8
l_parentable_list: ARRAYED_LIST [TUPLE [page: CMS_PAGE; parent: CMS_PAGE]]
l_new_pages: STRING_TABLE [CMS_PAGE] -- indexed by link location, if any.
do
if
attached node_api as l_node_api and then
attached {CMS_PAGE_NODE_TYPE} l_node_api.node_type ({CMS_PAGE_NODE_TYPE}.name) as l_node_type and then
attached page_api as l_page_api and then
( a_import_id_list = Void
or else across a_import_id_list as ic some ic.item.same_string (l_node_type.name) end
)
then
if
a_response.has_permissions (<<"import any node", "import " + l_node_type.name>>)
then
p := a_import_ctx.location.extended ("nodes").extended (l_node_type.name)
create d.make_with_path (p)
if d.exists and then d.is_readable then
create l_parentable_list.make (0)
create l_new_pages.make (0)
a_import_ctx.log ("Importing [" + l_node_type.name + "] items ..")
across
d.entries as ic
loop
if attached ic.item.extension as ext and then ext.same_string_general ("json") then
create f.make_with_path (p.extended_path (ic.item))
if f.exists and then f.is_access_readable then
f.open_read
from
create s.make (0)
until
f.exhausted or f.end_of_file
loop
f.read_stream (1_024)
s.append (f.last_string)
end
f.close
create jp.make_with_string (s)
jp.parse_content
if jp.is_valid and then attached jp.parsed_json_object as j then
if
attached json_string_item (j, "type") as l_type and then
l_type.same_string_general (l_node_type.name)
then
if attached json_to_node_page (l_node_type, j, l_node_api) as l_page then
if l_page.is_published then
if l_page.author = Void then
-- FIXME!!!
l_page.set_author (l_page_api.cms_api.user)
a_import_ctx.log (l_node_type.name + " %"" + f.path.utf_8_name + "%" WARNING (Author is unknown!)")
end
if attached l_page.author as l_author then
if
attached l_page_api.pages_with_title (l_page.title) as l_pages and then
not l_pages.is_empty
then
-- Page Already exists!
-- FIXME/TODO
a_import_ctx.log (l_node_type.name + " %"" + f.path.utf_8_name + "%" skipped (already exists for user #" + l_author.id.out + ")!")
else
if
attached l_page.parent as l_parent and then
not l_parent.has_id
then
l_parentable_list.extend ([l_page, l_parent])
l_page.set_parent (Void)
end
l_page_api.save_page (l_page)
l_new_pages.force (l_page, l_node_api.node_path (l_page))
a_import_ctx.log (l_node_type.name + " %"" + f.path.utf_8_name + "%" imported as "+ l_page.id.out +" for user #" + l_author.id.out + ".")
if attached {CMS_LOCAL_LINK} l_page.link as l_link then
loc := l_node_api.node_path (l_page)
if not l_link.location.starts_with_general ("node/") then
l_page_api.cms_api.set_path_alias (loc, l_link.location, False)
l_new_pages.force (l_page, l_link.location)
end
end
end
else
a_import_ctx.log (l_node_type.name + " %"" + f.path.utf_8_name + "%" skipped (Author is unknown!)")
end
else
a_import_ctx.log (l_node_type.name + " %"" + f.path.utf_8_name + "%" skipped (Status is Not Published!)")
end
end
end
end
end
end
end
across
l_parentable_list as ic
loop
if attached ic.item.page as l_page then
update_page_parent (l_page, ic.item.parent, l_new_pages)
if attached l_page.parent as l_parent then
a_import_ctx.log (l_node_type.name + " #" + l_page.id.out + " assigned to parent #" + l_parent.id.out)
l_page_api.save_page (l_page)
else
a_import_ctx.log (l_node_type.name + " #" + l_page.id.out + " : unable to find parent!")
end
end
end
end
end
end
end
json_to_node_page (a_node_type: CMS_PAGE_NODE_TYPE; j: JSON_OBJECT; a_node_api: CMS_NODE_API): detachable CMS_PAGE
local
p: detachable CMS_PAGE
do
if attached json_to_node (a_node_type, j, a_node_api) as l_node then
Result := a_node_type.new_node (l_node)
if
attached {JSON_OBJECT} j.item ("parent") as j_parent and then
attached json_string_item (j_parent, "title") as l_title
then
p := a_node_type.new_node_with_title (l_title, Void)
if
attached {JSON_OBJECT} j_parent.item ("link") as j_link and then
attached json_to_local_link (j_link) as l_parent_lnk and then
not l_parent_lnk.location.starts_with ("node/")
then
p.set_link (l_parent_lnk)
else
-- No link ..
end
if not Result.is_parent_of (p) then
Result.set_parent (p)
end
end
end
end
update_page_parent (a_page: CMS_PAGE; a_parent: CMS_PAGE; a_new_pages: STRING_TABLE [CMS_PAGE])
require
no_parent_set: a_page.parent = Void
local
lst: detachable LIST [CMS_PAGE]
p: detachable CMS_PAGE
do
p := a_parent
if attached page_api as l_page_api then
if attached a_parent.link as l_parent_lnk then
lst := l_page_api.pages_with_title (a_parent.title)
across
lst as ic
until
p.has_id
loop
if
attached ic.item.link as lnk and then
lnk.location.same_string_general (l_parent_lnk.location)
then
p := ic.item
end
end
else
lst := l_page_api.pages_with_title (a_parent.title)
if lst.count = 1 then
p := lst.first
end
end
end
if p.has_id then
a_page.set_parent (p)
end
end
end

View File

@@ -0,0 +1,46 @@
note
description: "[
Interface defining a CMS page type.
]"
date: "$Date$"
revision: "$Revision$"
class
CMS_PAGE_NODE_TYPE
inherit
CMS_NODE_TYPE [CMS_PAGE]
feature -- Access
name: STRING = "page"
-- Internal name.
title: STRING_32 = "Page"
-- Human readable name.
description: STRING_32 = "Use basic pages for your content, such as an 'About us' page."
-- Optional description
feature -- Factory
new_node_with_title (a_title: READABLE_STRING_32; a_partial_node: detachable CMS_NODE): like new_node
-- New node with `a_title' and fill from partial `a_partial_node' if set.
do
create Result.make (a_title)
if a_partial_node /= Void then
Result.import_node (a_partial_node)
Result.set_title (a_title)
end
end
new_node (a_partial_node: detachable CMS_NODE): CMS_PAGE
-- New node based on partial `a_partial_node', or from none.
do
create Result.make_empty
if a_partial_node /= Void then
Result.import_node (a_partial_node)
end
end
end

View File

@@ -0,0 +1,31 @@
note
description: "Summary description for {CMS_PAGE_STORAGE_I}."
date: "$Date$"
revision: "$Revision$"
deferred class
CMS_PAGE_STORAGE_I
feature -- Error Handling
error_handler: ERROR_HANDLER
-- Error handler.
deferred
end
feature -- Access
children (a_node: CMS_NODE): detachable LIST [CMS_NODE]
-- Children of node `a_node'.
-- note: this is the partial version of the nodes.
deferred
end
available_parents_for_node (a_node: CMS_NODE): LIST [CMS_NODE]
-- Given the node `a_node', return the list of possible parent nodes id
deferred
ensure
a_node_excluded: across Result as ic all not a_node.same_node (ic.item) end
end
end

View File

@@ -0,0 +1,31 @@
note
description: "Summary description for {CMS_PAGE_STORAGE_NULL}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
CMS_PAGE_STORAGE_NULL
inherit
CMS_NODE_STORAGE_NULL
CMS_PAGE_STORAGE_I
create
make
feature -- Access
children (a_node: CMS_NODE): detachable LIST [CMS_NODE]
-- <Precursor>
do
end
available_parents_for_node (a_node: CMS_NODE): LIST [CMS_NODE]
-- <Precursor>
do
create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
end
end

View File

@@ -0,0 +1,86 @@
note
description: "Access to the sql database for the page module"
date: "$Date$"
revision: "$Revision$"
class
CMS_PAGE_STORAGE_SQL
inherit
CMS_NODE_STORAGE_SQL
CMS_PAGE_STORAGE_I
create
make
feature -- Access
children (a_node: CMS_NODE): detachable LIST [CMS_NODE]
-- <Precursor>
local
l_parameters: STRING_TABLE [detachable ANY]
do
create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
error_handler.reset
write_information_log (generator + ".children")
from
create l_parameters.make (1)
l_parameters.put (a_node.id, "nid")
sql_query (sql_select_children_of_node, l_parameters)
sql_start
until
sql_after
loop
if attached fetch_node as l_node then
Result.force (l_node)
end
sql_forth
end
sql_finalize
end
available_parents_for_node (a_node: CMS_NODE): LIST [CMS_NODE]
-- <Precursor>
local
l_parameters: STRING_TABLE [detachable ANY]
do
create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
error_handler.reset
write_information_log (generator + ".available_parents_for_node")
from
create l_parameters.make (1)
l_parameters.put (a_node.id, "nid")
sql_query (sql_select_available_parents_for_node, l_parameters)
sql_start
until
sql_after
loop
if attached fetch_node as l_node then
Result.force (l_node)
end
sql_forth
end
sql_finalize
end
feature {NONE} -- Queries
sql_select_available_parents_for_node : STRING = "[
SELECT node.nid, node.revision, node.type, title, summary, content, format, author, publish, created, changed, status
FROM nodes node LEFT JOIN page_nodes pn ON node.nid = pn.nid AND node.nid != :nid
WHERE node.nid != :nid AND pn.parent != :nid AND node.status != -1 GROUP BY node.nid, node.revision;
]"
sql_select_children_of_node: STRING = "[
SELECT node.nid, node.revision, node.type, title, summary, content, format, author, publish, created, changed, status
FROM nodes node LEFT JOIN page_nodes pn ON node.nid = pn.nid
WHERE pn.parent = :nid AND node.status != -1 GROUP BY node.nid, node.revision;
]"
end

View File

@@ -0,0 +1,17 @@
note
description: "Summary description for {CMS_PARTIAL_PAGE}."
author: ""
date: "$Date$"
revision: "$Revision$"
class
CMS_PARTIAL_PAGE
inherit
CMS_PAGE
create
make_empty,
make
end