Added feed aggregation module.

Redesigned the CMS_BLOCK system,
   - added condition attribute. It can be set via configuration file
     with
     [blocks]
      {blockid}.region={region_name}
      {blockid}.conditions[]=is_front
      {blockid}.conditions[]=path:location-path/foo/bar
   - For backward compatibility, the CMS will check only conditions for block name prefixed by "?".
Improved the configuration library to support list and table properties.
Updated theme for now, to include the feed examples.
Added "cache" classes, to ease caching of html output for instance. (TODO: improve by providing a cache manager).
This commit is contained in:
2015-10-08 13:56:31 +02:00
parent abebd00a4f
commit 463105f29f
32 changed files with 1504 additions and 108 deletions

View File

@@ -521,26 +521,26 @@ feature {NONE} -- Helpers
feature {NONE} -- Block views
get_block_view_login (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
local
-- vals: CMS_VALUE_TABLE
do
if attached template_block (a_block_id, a_response) as l_tpl_block then
-- create vals.make (1)
-- -- add the variable to the block
-- value_table_alter (vals, a_response)
-- across
-- vals as ic
-- loop
-- l_tpl_block.set_value (ic.item, ic.key)
-- get_block_view_login (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
-- local
---- vals: CMS_VALUE_TABLE
-- do
-- if attached template_block (a_block_id, a_response) as l_tpl_block then
---- create vals.make (1)
---- -- add the variable to the block
---- value_table_alter (vals, a_response)
---- across
---- vals as ic
---- loop
---- l_tpl_block.set_value (ic.item, ic.key)
---- end
-- a_response.put_required_block (l_tpl_block, "content")
-- else
-- debug ("cms")
-- a_response.add_warning_message ("Error with block [" + a_block_id + "]")
-- end
a_response.add_block (l_tpl_block, "content")
else
debug ("cms")
a_response.add_warning_message ("Error with block [" + a_block_id + "]")
end
end
end
-- end
-- end
get_block_view_register (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
do
@@ -579,7 +579,6 @@ feature {NONE} -- Block views
end
end
get_block_view_reactivate (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
do
if a_response.request.is_get_request_method then

View File

@@ -15,6 +15,8 @@ feature {NONE} -- Initialization
do
create name.make_from_string_general (a_name)
create {ARRAYED_LIST [READABLE_STRING_8]} locations.make (0)
expiration := 60*60
description_enabled := True
end
feature -- Access
@@ -22,12 +24,23 @@ feature -- Access
name: IMMUTABLE_STRING_32
-- Associated name.
expiration: INTEGER
-- Suggested expiration time in seconds (default: 1 hour).
-- If negative then never expires.
description: detachable IMMUTABLE_STRING_32
-- Optional description.
locations: LIST [READABLE_STRING_8]
-- List of feed location aggregated into current.
included_categories: detachable LIST [READABLE_STRING_32]
-- Optional categories to filter.
-- If Void, include any.
description_enabled: BOOLEAN
-- Display description?
feature -- Element change
set_description (a_desc: detachable READABLE_STRING_GENERAL)
@@ -39,4 +52,54 @@ feature -- Element change
end
end
set_expiration (nb_seconds: INTEGER)
-- Set `expiration' to `nb_seconds'.
do
expiration := nb_seconds
end
set_description_enabled (b: BOOLEAN)
-- Set `description_enabled' to `b'.
do
description_enabled := b
end
reset_categories
do
included_categories := Void
end
include_category (a_cat: READABLE_STRING_GENERAL)
local
lst: like included_categories
s32: STRING_32
do
lst := included_categories
if lst = Void then
create {ARRAYED_LIST [READABLE_STRING_32]} lst.make (1)
included_categories := lst
lst.compare_objects
end
s32 := a_cat.to_string_32
if not lst.has (s32) then
lst.force (s32)
end
end
feature -- Status report
is_included (e: FEED_ITEM): BOOLEAN
do
Result := True
if attached e.categories as e_cats then
if attached included_categories as lst then
Result := across lst as ic some
across e_cats as e_ic some
e_ic.item.same_string (ic.item)
end
end
end
end
end
end

View File

@@ -8,7 +8,9 @@
<library name="cms" location="..\..\cms-safe.ecf"/>
<library name="cms_model" location="..\..\library\model\cms_model-safe.ecf" readonly="false"/>
<library name="error" location="$ISE_LIBRARY\contrib\library\utility\general\error\error-safe.ecf"/>
<library name="feed" location="$ISE_LIBRARY\contrib\library\text\parser\feed\feed-safe.ecf"/>
<library name="http" location="$ISE_LIBRARY\contrib\library\network\protocol\http\http-safe.ecf"/>
<library name="http_client" location="$ISE_LIBRARY\contrib\library\network\http_client\http_client-safe.ecf"/>
<library name="wsf" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\wsf\wsf-safe.ecf"/>
<library name="wsf_extension" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\wsf\wsf_extension-safe.ecf" readonly="false"/>
<library name="wsf_html" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\wsf_html\wsf_html-safe.ecf" readonly="false"/>

View File

@@ -18,15 +18,65 @@ feature -- Access
-- List of feed aggregations.
local
agg: FEED_AGGREGATION
l_feed_id: READABLE_STRING_32
l_title: detachable READABLE_STRING_GENERAL
l_location_list: detachable LIST [READABLE_STRING_32]
utf: UTF_CONVERTER
l_table: like internal_aggregations
do
create Result.make (2)
create agg.make ("Blog from Bertrand Meyer")
agg.locations.force ("https://bertrandmeyer.com/category/computer-science/feed/")
Result.force (agg, "bertrandmeyer")
create agg.make ("Eiffel Room")
agg.locations.force ("https://room.eiffel.com/recent_changes/feed")
Result.force (agg, "eiffelroom")
l_table := internal_aggregations
if l_table /= Void then
Result := l_table
else
create Result.make (0)
internal_aggregations := Result
if attached cms_api.module_configuration_by_name ({FEED_AGGREGATOR_MODULE}.name, "feeds") as cfg then
if attached cfg.text_list_item ("ids") as l_ids then
across
l_ids as ic
loop
l_feed_id := ic.item
l_location_list := cfg.text_list_item ({STRING_32} "feeds." + l_feed_id + ".locations")
if
attached cfg.text_item ({STRING_32} "feeds." + l_feed_id + ".location") as l_location
then
if l_location_list = Void then
create {ARRAYED_LIST [READABLE_STRING_32]} l_location_list.make (1)
end
l_location_list.force (l_location)
end
if l_location_list /= Void and then not l_location_list.is_empty then
l_title := cfg.text_item ({STRING_32} "feeds." + l_feed_id + ".title")
if l_title = Void then
l_title := l_feed_id
end
create agg.make (l_title)
if attached cfg.text_item ({STRING_32} "feeds." + l_feed_id + ".expiration") as l_expiration then
if l_expiration.is_integer then
agg.set_expiration (l_expiration.to_integer)
end
end
if attached cfg.text_item ({STRING_32} "feeds." + l_feed_id + ".option_description") as l_description_opt then
agg.set_description_enabled (not l_description_opt.is_case_insensitive_equal_general ("disabled"))
end
across
l_location_list as loc_ic
loop
agg.locations.force (utf.utf_32_string_to_utf_8_string_8 (loc_ic.item))
end
Result.force (agg, l_feed_id)
if attached cfg.text_list_item ({STRING_32} "feeds." + l_feed_id + ".categories") as l_cats then
across
l_cats as cats_ic
loop
agg.include_category (cats_ic.item)
end
end
end
end
end
end
end
end
aggregation (a_name: READABLE_STRING_GENERAL): detachable FEED_AGGREGATION
@@ -36,4 +86,56 @@ feature -- Access
end
end
feature {NONE} -- Access: implementation
internal_aggregations: detachable like aggregations
-- Cache value for `aggregations'.
feature -- Operation
feed (a_location: READABLE_STRING_8): detachable FEED
local
fac: FEED_DEFAULT_PARSERS
ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT
do
create fac
if attached new_http_client_session (a_location).get ("", ctx) as res then
if attached res.body as l_content then
Result := fac.feed_from_string (l_content)
end
end
end
aggregation_feed (agg: FEED_AGGREGATION): detachable FEED
-- Feed from aggregation `agg'.
local
fac: FEED_DEFAULT_PARSERS
f: detachable FEED
do
create fac
across
agg.locations as ic
loop
if attached new_http_client_session (ic.item).get ("", Void).body as res then
f := fac.feed_from_string (res)
if Result /= Void then
if f /= Void then
Result := Result + f
end
else
Result := f
end
end
end
end
new_http_client_session (a_url: READABLE_STRING_8): HTTP_CLIENT_SESSION
local
cl: LIBCURL_HTTP_CLIENT
do
create cl.make
Result := cl.new_session (a_url)
Result.set_is_insecure (True)
end
end

View File

@@ -21,6 +21,8 @@ inherit
CMS_HOOK_RESPONSE_ALTER
CMS_HOOK_MENU_SYSTEM_ALTER
create
make
@@ -63,8 +65,112 @@ feature -- Access: router
setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
-- <Precursor>
local
h: WSF_URI_TEMPLATE_HANDLER
do
-- a_router.handle ("/admin/feed_aggregator/", create {WSF_URI_AGENT_HANDLER}.make (agent handle_feed_aggregator_admin (a_api, ?, ?)), a_router.methods_head_get_post)
a_router.handle ("/admin/feed_aggregator/", create {WSF_URI_AGENT_HANDLER}.make (agent handle_feed_aggregator_admin (a_api, ?, ?)), a_router.methods_head_get_post)
create {WSF_URI_TEMPLATE_AGENT_HANDLER} h.make (agent handle_feed_aggregation (a_api, ?, ?))
a_router.handle ("/feed_aggregation/", h, a_router.methods_head_get)
a_router.handle ("/feed_aggregation/{feed_id}", h, a_router.methods_head_get)
end
feature -- Handle
handle_feed_aggregator_admin (a_api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
local
nyi: NOT_IMPLEMENTED_ERROR_CMS_RESPONSE
do
create nyi.make (req, res, a_api)
nyi.execute
end
handle_feed_aggregation (a_api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
local
r: CMS_RESPONSE
s: STRING
nb: INTEGER
do
if attached {WSF_STRING} req.query_parameter ("size") as p_size and then p_size.is_integer then
nb := p_size.integer_value
else
nb := -1
end
create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, a_api)
if attached {WSF_STRING} req.path_parameter ("feed_id") as p_feed_id then
if attached feed_aggregation (p_feed_id.value) as l_agg then
create s.make_empty
s.append ("<h1>")
s.append (r.html_encoded (l_agg.name))
s.append ("</h1>")
if attached l_agg.included_categories as l_categories then
s.append ("<span class=%"category%">")
across
l_categories as cats_ic
loop
s.append (" [")
s.append (r.html_encoded (cats_ic.item))
s.append ("]")
end
s.append ("</span>")
end
if attached l_agg.description as l_desc and then l_desc.is_valid_as_string_8 then
s.append ("<div class=%"description%">")
s.append (l_desc.as_string_8)
s.append ("</div>")
end
s.append ("<ul>")
across
l_agg.locations as ic
loop
s.append ("<li><a href=%"")
s.append (ic.item)
s.append ("%">")
s.append (ic.item)
s.append ("</a></li>")
end
s.append ("</ul>")
if attached feed_to_html (p_feed_id.value, nb, True, r) as l_html then
s.append (l_html)
end
r.set_main_content (s)
else
create {NOT_FOUND_ERROR_CMS_RESPONSE} r.make (req, res, a_api)
end
else
if attached feed_aggregator_api as l_feed_agg_api then
create s.make_empty
across
l_feed_agg_api.aggregations as ic
loop
s.append ("<li>")
s.append (r.link (ic.key, "feed_aggregation/" + r.url_encoded (ic.key), Void))
if attached ic.item.included_categories as l_categories then
s.append ("<span class=%"category%">")
across
l_categories as cats_ic
loop
s.append (" [")
s.append (r.html_encoded (cats_ic.item))
s.append ("]")
end
s.append ("</span>")
end
if attached ic.item.description as l_desc then
if l_desc.is_valid_as_string_8 then
s.append ("<div class=%"description%">")
s.append (l_desc.as_string_8)
s.append ("</div>")
end
end
s.append ("</li>")
end
r.set_main_content (s)
else
create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, a_api)
end
end
r.execute
end
feature -- Hooks configuration
@@ -74,6 +180,7 @@ feature -- Hooks configuration
do
a_response.hooks.subscribe_to_block_hook (Current)
a_response.hooks.subscribe_to_response_alter_hook (Current)
a_response.hooks.subscribe_to_menu_system_alter_hook (Current)
end
feature -- Hook
@@ -82,14 +189,18 @@ feature -- Hook
-- List of block names, managed by current object.
local
res: ARRAYED_LIST [like {CMS_BLOCK}.name]
l_aggs: HASH_TABLE [FEED_AGGREGATION, STRING_8]
do
create res.make (5)
if attached feed_aggregator_api as l_feed_api then
l_aggs := l_feed_api.aggregations
create res.make (l_aggs.count)
across
l_feed_api.aggregations as ic
l_aggs as ic
loop
res.force ("feed__" + ic.key)
res.force ("?feed." + ic.key)
end
else
create res.make (0)
end
Result := res
end
@@ -97,50 +208,178 @@ feature -- Hook
get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
-- Get block object identified by `a_block_id' and associate with `a_response'.
local
i: INTEGER
s: READABLE_STRING_8
b: CMS_CONTENT_BLOCK
l_content: STRING
pref: STRING
do
if attached feed_aggregator_api as l_feed_api then
pref := "feed__"
pref := "feed."
if a_block_id.starts_with (pref) then
s := a_block_id.substring (pref.count + 1, a_block_id.count)
else
s := a_block_id
end
if attached l_feed_api.aggregation (s) as l_agg then
create l_content.make_empty
if attached l_agg.description as l_desc then
l_content.append_string_general (l_desc)
l_content.append_character ('%N')
l_content.append_character ('%N')
end
across
l_agg.locations as ic
loop
l_content.append ("%T-" + ic.item)
l_content.append_character ('%N')
end
create b.make (a_block_id, l_agg.name, l_content, Void)
a_response.add_block (b, Void)
if attached feed_to_html (s, 5, True, a_response) as l_content then
create b.make (a_block_id, Void, l_content, Void)
b.set_is_raw (True)
a_response.add_block (b, "feed_" + s)
end
end
end
feed_aggregation (a_feed_id: READABLE_STRING_GENERAL): detachable FEED_AGGREGATION
do
if attached feed_aggregator_api as l_feed_api then
Result := l_feed_api.aggregation (a_feed_id)
end
end
feed_to_html (a_feed_id: READABLE_STRING_GENERAL; a_count: INTEGER; with_feed_info: BOOLEAN; a_response: CMS_RESPONSE): detachable STRING
local
nb: INTEGER
i: INTEGER
e: FEED_ITEM
l_cache: CMS_FILE_STRING_8_CACHE
lnk: detachable FEED_LINK
l_today: DATE
do
if attached feed_aggregator_api as l_feed_api then
if attached l_feed_api.aggregation (a_feed_id) as l_agg then
create l_cache.make (a_response.api.files_location.extended (".cache").extended (name).extended ("feed__" + a_feed_id + "__" + a_count.out + "_" + with_feed_info.out))
Result := l_cache.item
if Result = Void or l_cache.expired (Void, l_agg.expiration) then
create l_today.make_now_utc
create Result.make_from_string ("<div class=%"feed%">")
Result.append ("<!-- ")
Result.append ("Updated: " + l_cache.cache_date_time.out)
Result.append (" -->")
if with_feed_info then
if attached l_agg.description as l_desc then
Result.append ("<div class=%"description%">")
Result.append_string_general (l_desc)
Result.append ("</div>")
end
end
Result.append ("<ul>")
if attached l_feed_api.aggregation_feed (l_agg) as l_feed then
nb := a_count
across
l_feed as f_ic
until
nb = 0 -- If `a_count' < 0 , no limit.
loop
e := f_ic.item
if l_agg.is_included (e) then
nb := nb - 1
Result.append ("<li>")
lnk := e.link
if attached e.date as l_date then
Result.append ("<div class=%"date%">")
append_date_time_to (l_date, l_today, Result)
Result.append ("</div>")
end
if lnk /= Void then
Result.append ("<a href=%"" + lnk.href + "%">")
else
check has_link: False end
Result.append ("<a href=%"#%">")
end
Result.append (a_response.html_encoded (e.title))
Result.append ("</a>")
debug
if attached e.categories as l_categories and then not l_categories.is_empty then
Result.append ("<div class=%"category%">")
across
l_categories as cats_ic
loop
Result.append (a_response.html_encoded (cats_ic.item))
Result.append (" ")
end
Result.append ("</div>")
end
end
if
l_agg.description_enabled and then
attached e.description as l_entry_desc
then
if l_entry_desc.is_valid_as_string_8 then
Result.append ("<div class=%"description%">")
Result.append (l_entry_desc.as_string_8)
Result.append ("</div>")
else
check is_html: False end
end
end
Result.append ("</li>")
end
end
end
Result.append_string ("<liv class=%"nav%">")
Result.append_string (a_response.link ("more ...", "feed_aggregation/" + a_response.url_encoded (a_feed_id), Void))
Result.append_string ("</li>")
Result.append ("</ul>")
Result.append ("</div> <!-- End of Feed -->%N")
l_cache.put (Result)
end
end
end
end
append_date_time_to (dt: DATE_TIME; a_today: DATE; a_output: STRING_GENERAL)
do
if dt.year /= a_today.year then
a_output.append (dt.year.out)
a_output.append (",")
end
a_output.append (" ")
append_month_mmm_to (dt.month, a_output)
a_output.append (" ")
if dt.day < 10 then
a_output.append ("0")
end
a_output.append (dt.day.out)
end
append_month_mmm_to (m: INTEGER; s: STRING_GENERAL)
require
1 <= m and m <= 12
do
inspect m
when 1 then s.append ("Jan")
when 2 then s.append ("Feb")
when 3 then s.append ("Mar")
when 4 then s.append ("Apr")
when 5 then s.append ("May")
when 6 then s.append ("Jun")
when 7 then s.append ("Jul")
when 8 then s.append ("Aug")
when 9 then s.append ("Sep")
when 10 then s.append ("Oct")
when 11 then s.append ("Nov")
when 12 then s.append ("Dec")
else
-- Error
end
end
feature -- Hook
response_alter (a_response: CMS_RESPONSE)
do
-- a_response.add_additional_head_line ("[
-- <style>
-- table.recent-changes th { padding: 3px; }
-- table.recent-changes td { padding: 3px; border: dotted 1px #ddd; }
-- table.recent-changes td.date { padding-left: 15px; }
-- table.recent-changes td.title { font-weight: bold; }
-- </style>
-- ]", True)
a_response.add_style (a_response.url ("/module/" + name + "/files/css/feed_aggregator.css", Void), Void)
end
menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
-- Hook execution on collection of menu contained by `a_menu_system'
-- for related response `a_response'.
do
a_menu_system.navigation_menu.extend (create {CMS_LOCAL_LINK}.make ("Feeds", "feed_aggregation/"))
if a_response.has_permission ("manage feed aggregator") then
a_menu_system.management_menu.extend (create {CMS_LOCAL_LINK}.make ("Feeds (admin)", "admin/feed_aggregator/"))
end
end
end

View File

@@ -0,0 +1,49 @@
div.feed ul {
list-style: none;
position: relative;
width: 99%;
}
div.feed li {
/* border-top: solid 1px #ddd; */
margin-bottom: 5px;
}
div.feed li a {
font-weight: bold;
}
div.feed li .date {
font-weight: bold;
font-size: small;
}
div.feed li .category {
margin-left: 20px;
font-size: 8px;
height: 9px;
overflow: hidden;
color: #999;
}
div.feed li .description {
margin-left: 20px;
font-size: small;
height: 18px;
overflow: hidden;
color: #999;
}
div.feed li:hover {
margin-bottom: 23px;
}
div.feed li:hover .description {
position: absolute;
height: auto;
overflow-y: scroll;
overflow-x: scroll;
color: #000;
background-color: #fff;
border: solid 1px #000;
z-index: 10;
}
div.feed li:hover:last-child {
margin-bottom: 28px;
}
div.feed li .description::after {
content: "...";
}

View File

@@ -0,0 +1,50 @@
div.feed {
ul {
list-style: none;
position: relative;
width: 99%;
}
li {
/* border-top: solid 1px #ddd; */
margin-bottom: 5px;
a {
font-weight: bold;
}
.date {
font-weight: bold;
font-size: small;
}
.category {
margin-left: 20px;
font-size: 8px;
height: 9px;
overflow: hidden;
color: #999;
}
.description {
margin-left: 20px;
font-size: small;
height: 18px;
overflow: hidden;
color: #999;
}
&:hover {
margin-bottom: 23px;
.description {
position: absolute;
height: auto;
overflow-y: scroll;
overflow-x: scroll;
color: #000;
background-color: #fff;
border: solid 1px #000;
z-index: 10;
}
&:last-child {
margin-bottom: 28px;
}
}
.description::after { content: "..."; }
}
}

View File

@@ -244,7 +244,7 @@ feature -- Hooks
block_list: ITERABLE [like {CMS_BLOCK}.name]
-- <Precursor>
do
Result := <<"node-info">>
Result := <<"?node-info">>
end
get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)