From 463105f29f919cb53c2e35be3982c9ae6c172313 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Thu, 8 Oct 2015 13:56:31 +0200 Subject: [PATCH] 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). --- examples/demo/modules/demo/cms_demo_module.e | 6 +- examples/demo/site/config/cms.ini | 16 +- .../config/modules/feed_aggregator/feeds.json | 25 ++ .../files/css/feed_aggregator.css | 49 +++ .../files/scss/feed_aggregator.scss | 50 +++ examples/demo/site/themes/bootstrap/page.tpl | 13 +- library/configuration/src/config_reader.e | 44 ++- library/configuration/src/ini_config.e | 58 +++- library/configuration/src/json_config.e | 55 +++- .../tests/test_config_reader_set.e | 46 ++- modules/auth/cms_authentication_module.e | 39 ++- modules/feed_aggregator/feed_aggregation.e | 63 ++++ .../feed_aggregator/feed_aggregator-safe.ecf | 2 + modules/feed_aggregator/feed_aggregator_api.e | 118 ++++++- .../feed_aggregator/feed_aggregator_module.e | 299 ++++++++++++++++-- .../site/files/css/feed_aggregator.css | 49 +++ .../site/files/scss/feed_aggregator.scss | 50 +++ modules/node/cms_node_module.e | 2 +- src/cache/cms_cache.e | 94 ++++++ src/cache/cms_file_cache.e | 150 +++++++++ src/cache/cms_file_object_cache.e | 49 +++ src/cache/cms_file_string_8_cache.e | 69 ++++ src/cache/cms_memory_cache.e | 61 ++++ src/configuration/cms_default_setup.e | 12 + src/configuration/cms_setup.e | 10 + src/hooks/cms_hook_block.e | 5 + src/hooks/cms_hook_core_manager.e | 14 +- .../cms_block_expression_condition.e | 24 +- .../condition/cms_block_location_condition.e | 45 +++ src/kernel/content/cms_block.e | 13 + src/modules/cms_debug_module.e | 17 +- src/service/response/cms_response.e | 65 +++- 32 files changed, 1504 insertions(+), 108 deletions(-) create mode 100644 examples/demo/site/config/modules/feed_aggregator/feeds.json create mode 100644 examples/demo/site/modules/feed_aggregator/files/css/feed_aggregator.css create mode 100644 examples/demo/site/modules/feed_aggregator/files/scss/feed_aggregator.scss create mode 100644 modules/feed_aggregator/site/files/css/feed_aggregator.css create mode 100644 modules/feed_aggregator/site/files/scss/feed_aggregator.scss create mode 100644 src/cache/cms_cache.e create mode 100644 src/cache/cms_file_cache.e create mode 100644 src/cache/cms_file_object_cache.e create mode 100644 src/cache/cms_file_string_8_cache.e create mode 100644 src/cache/cms_memory_cache.e create mode 100644 src/kernel/content/block/condition/cms_block_location_condition.e diff --git a/examples/demo/modules/demo/cms_demo_module.e b/examples/demo/modules/demo/cms_demo_module.e index 3fb7fa6..d85bf34 100644 --- a/examples/demo/modules/demo/cms_demo_module.e +++ b/examples/demo/modules/demo/cms_demo_module.e @@ -93,7 +93,7 @@ feature -- Hooks block_list: ITERABLE [like {CMS_BLOCK}.name] do - Result := <<"demo-info">> + Result := <<"?demo-info">> end get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE) @@ -103,8 +103,8 @@ feature -- Hooks m: CMS_MENU lnk: CMS_LOCAL_LINK do - if a_block_id.is_case_insensitive_equal_general ("demo-info") then - if a_response.request.request_uri.starts_with ("/demo/") then + if a_block_id.same_string ("demo-info") then + if a_response.location.starts_with_general ("demo/") then create m.make_with_title (a_block_id, "Demo", 2) create lnk.make ("demo: abc", "demo/abc") m.extend (lnk) diff --git a/examples/demo/site/config/cms.ini b/examples/demo/site/config/cms.ini index 9a90715..ebe4208 100644 --- a/examples/demo/site/config/cms.ini +++ b/examples/demo/site/config/cms.ini @@ -20,7 +20,7 @@ smtp=localhost:25 # Default is "on" # for each module, this can be overwritten with # module_name= on or off -*=off +*=all admin=on auth=on basic_auth=on @@ -33,9 +33,17 @@ openid=on [blocks] #navigation.region=sidebar_first -feed__bertrandmeyer.region=content -feed__eiffelroom.region=content -feed__bertrandmeyer.condition=is_front +feed.eiffel.region=feed_eiffel +feed.eiffel.condition=is_front + +feed.forum.region=feed_forum +feed.forum.condition=is_front + +feed.stackoverflow.region=feed_stackoverflow +feed.stackoverflow.condition=is_front + +#management.condition=is_front +#navigation.condition=is_front [admin] # CMS Installation, are accessible by "all", "none" or uppon "permission". (default is none) diff --git a/examples/demo/site/config/modules/feed_aggregator/feeds.json b/examples/demo/site/config/modules/feed_aggregator/feeds.json new file mode 100644 index 0000000..31d1230 --- /dev/null +++ b/examples/demo/site/config/modules/feed_aggregator/feeds.json @@ -0,0 +1,25 @@ +{ + "ids": ["eiffel", "forum", "stackoverflow"], + "feeds": { + "eiffel": { + "title": "Eiffel related posts.", + "expiration": "21600", + "locations": [ + "https://bertrandmeyer.com/feed/", + "https://room.eiffel.com/blog/feed" + ] + , "categories": ["Eiffel"] + ,"option_description": "disabled" + }, + "forum": { + "title": "Eiffel Users Group", + "expiration": "43200", + "location": "https://groups.google.com/forum/feed/eiffel-users/msgs/atom.xml?num=15" + }, + "stackoverflow": { + "title": "Test", + "expiration": "3600", + "location": "http://stackoverflow.com/feeds/tag?tagnames=eiffel&sort=newest" + } + } +} diff --git a/examples/demo/site/modules/feed_aggregator/files/css/feed_aggregator.css b/examples/demo/site/modules/feed_aggregator/files/css/feed_aggregator.css new file mode 100644 index 0000000..abccac2 --- /dev/null +++ b/examples/demo/site/modules/feed_aggregator/files/css/feed_aggregator.css @@ -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: "..."; +} diff --git a/examples/demo/site/modules/feed_aggregator/files/scss/feed_aggregator.scss b/examples/demo/site/modules/feed_aggregator/files/scss/feed_aggregator.scss new file mode 100644 index 0000000..90b855f --- /dev/null +++ b/examples/demo/site/modules/feed_aggregator/files/scss/feed_aggregator.scss @@ -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: "..."; } + } +} diff --git a/examples/demo/site/themes/bootstrap/page.tpl b/examples/demo/site/themes/bootstrap/page.tpl index fc0b79b..8370b05 100644 --- a/examples/demo/site/themes/bootstrap/page.tpl +++ b/examples/demo/site/themes/bootstrap/page.tpl @@ -62,9 +62,18 @@ {unless isempty="$page_title"}

{$page_title/}

{/unless} {$page.region_content/} + {if condition="$page.is_front"} + {if isset="$page.region_feed_eiffel"} +
{$page.region_feed_eiffel_users/}
+ {/if} + {if isset="$page.region_feed_forum"} +
{$page.region_feed_forum/}
+ {/if} + {if isset="$page.region_feed_stackoverflow"} +
{$page.region_feed_stackoverflow/}
+ {/if} + {/if} - - diff --git a/library/configuration/src/config_reader.e b/library/configuration/src/config_reader.e index 8d4563f..2698b35 100644 --- a/library/configuration/src/config_reader.e +++ b/library/configuration/src/config_reader.e @@ -33,11 +33,53 @@ feature -- Query end end + resolved_text_list_item (k: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32] + -- List of String item associated with key `k', + -- and expanded values to resolved variables ${varname}. + do + if attached text_list_item (k) as lst then + from + lst.start + until + lst.after + loop + lst.replace (resolved_expression (lst.item)) + lst.forth + end + end + end + + resolved_text_table_item (k: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32] + -- Table of String item associated with key `k', + -- and expanded values to resolved variables ${varname}. + do + if attached text_table_item (k) as tb then + from + tb.start + until + tb.after + loop + tb.replace (resolved_expression (tb.item_for_iteration), tb.key_for_iteration) + tb.forth + end + end + end + text_item (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_32 -- String item associated with key `k'. deferred end + text_list_item (k: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32] + -- List of String item associated with key `k'. + deferred + end + + text_table_item (k: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32] + -- Table of String item associated with key `k'. + deferred + end + integer_item (k: READABLE_STRING_GENERAL): INTEGER -- Integer item associated with key `k'. deferred @@ -109,7 +151,7 @@ feature -- Duplication end note - copyright: "2011-2014, Jocelyn Fiat, Eiffel Software and others" + copyright: "2011-2015, Jocelyn Fiat, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/library/configuration/src/ini_config.e b/library/configuration/src/ini_config.e index 37aa75b..56abea5 100644 --- a/library/configuration/src/ini_config.e +++ b/library/configuration/src/ini_config.e @@ -119,14 +119,47 @@ feature -- Access: Config Reader text_item (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_32 -- String item associated with key `k'. - local - obj: like item do - obj := item (k) - if attached {READABLE_STRING_32} obj as s32 then - Result := s32 - elseif attached {READABLE_STRING_8} obj as s then - Result := utf.utf_8_string_8_to_escaped_string_32 (s) + Result := value_to_string_32 (item (k)) + end + + text_list_item (k: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32] + -- List of String item associated with key `k'. + do + if attached {LIST [READABLE_STRING_8]} item (k) as l_list then + create {ARRAYED_LIST [READABLE_STRING_32]} Result.make (l_list.count) + Result.compare_objects + across + l_list as ic + until + Result = Void + loop + if attached value_to_string_32 (ic.item) as s32 then + Result.force (s32) + else + Result := Void + end + end + end + end + + text_table_item (k: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32] + -- Table of String item associated with key `k'. + do + if attached {STRING_TABLE [READABLE_STRING_8]} item (k) as l_list then + create {STRING_TABLE [READABLE_STRING_32]} Result.make (l_list.count) + Result.compare_objects + across + l_list as ic + until + Result = Void + loop + if attached value_to_string_32 (ic.item) as s32 then + Result.force (s32, ic.key) + else + Result := Void + end + end end end @@ -226,6 +259,15 @@ feature -- Access feature {NONE} -- Implementation + value_to_string_32 (obj: detachable ANY): detachable STRING_32 + do + if attached {READABLE_STRING_32} obj as s32 then + Result := s32 + elseif attached {READABLE_STRING_8} obj as s then + Result := utf.utf_8_string_8_to_escaped_string_32 (s) + end + end + item_from_values (a_values: STRING_TABLE [ANY]; k: READABLE_STRING_GENERAL): detachable ANY local i,j: INTEGER @@ -460,7 +502,7 @@ feature {NONE} -- Implementation invariant note - copyright: "2011-2014, Jocelyn Fiat, Eiffel Software and others" + copyright: "2011-2015, Jocelyn Fiat, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/library/configuration/src/json_config.e b/library/configuration/src/json_config.e index 4333d26..54fe3d3 100644 --- a/library/configuration/src/json_config.e +++ b/library/configuration/src/json_config.e @@ -63,10 +63,46 @@ feature -- Access: Config Reader text_item (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_32 -- String item associated with query `k'. do - if attached {JSON_STRING} item (k) as l_string then - Result := l_string.unescaped_string_32 - elseif attached {JSON_NUMBER} item (k) as l_number then - Result := l_number.item + Result := value_to_string_32 (item (k)) + end + + text_list_item (k: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32] + -- List of String item associated with key `k'. + do + if attached {JSON_ARRAY} item (k) as l_array then + create {ARRAYED_LIST [READABLE_STRING_32]} Result.make (l_array.count) + Result.compare_objects + across + l_array as ic + until + Result = Void + loop + if attached value_to_string_32 (ic.item) as s32 then + Result.force (s32) + else + Result := Void + end + end + end + end + + text_table_item (k: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32] + -- Table of String item associated with key `k'. + do + if attached {JSON_OBJECT} item (k) as obj then + create {STRING_TABLE [READABLE_STRING_32]} Result.make (obj.count) + Result.compare_objects + across + obj as ic + until + Result = Void + loop + if attached value_to_string_32 (ic.item) as s32 then + Result.force (s32, ic.key.item) + else + Result := Void + end + end end end @@ -105,6 +141,15 @@ feature -- Access feature {NONE} -- Implementation + value_to_string_32 (v: detachable ANY): detachable STRING_32 + do + if attached {JSON_STRING} v as l_string then + Result := l_string.unescaped_string_32 + elseif attached {JSON_NUMBER} v as l_number then + Result := l_number.item + end + end + object_json_value (a_object: JSON_OBJECT; a_query: READABLE_STRING_32): detachable JSON_VALUE -- Item associated with query `a_query' from object `a_object' if any. local @@ -163,7 +208,7 @@ feature {NONE} -- JSON end note - copyright: "2011-2014, Jocelyn Fiat, Eiffel Software and others" + copyright: "2011-2015, Jocelyn Fiat, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/library/configuration/tests/test_config_reader_set.e b/library/configuration/tests/test_config_reader_set.e index b975fe5..2f90530 100644 --- a/library/configuration/tests/test_config_reader_set.e +++ b/library/configuration/tests/test_config_reader_set.e @@ -21,6 +21,18 @@ feature -- Test create {INI_CONFIG} cfg.make_from_string ("[ foo = bar + collection[] = a + collection[] = b + collection[] = c + collection[] = 1 + collection[] = 2 + collection[] = 3 + + table[a] = 1 + table[b] = 2 + table[c] = 3 + table[d] = test + [first] abc = 1 def = and so on @@ -58,6 +70,21 @@ feature -- Test assert ("has_item (second.is)", cfg.has_item ("second.is")) assert ("item (second.is)", attached cfg.text_item ("second.is") as v and then v.same_string_general ("2")) + assert ("has_item (collection)", cfg.has_item ("collection")) + assert ("item (collection)", attached cfg.text_list_item ("collection") as lst and then ( + lst.has ("a") and lst.has ("b") and lst.has ("c") and lst.has ("1") and lst.has ("2") and lst.has ("3") + ) + ) + + assert ("has_item (table)", cfg.has_item ("table")) + assert ("item (table)", attached cfg.text_table_item ("table") as tb and then ( + tb.item ("a") ~ {STRING_32} "1" and + tb.item ("b") ~ {STRING_32} "2" and + tb.item ("c") ~ {STRING_32} "3" and + tb.item ("d") ~ {STRING_32} "test" + ) + ) + if attached cfg.sub_config ("second") as cfg_second then assert ("has_item (is)", cfg_second.has_item ("is")) assert ("item (is)", attached cfg_second.text_item ("is") as v and then v.same_string_general ("2")) @@ -141,7 +168,9 @@ feature -- Test "is": 2, "the": 3, "end": 4 - } + }, + "collection": ["a", "b", "c", 1, 2, 3], + "table": { "a": 1, "b": 2, "c": 3, "d" : "test" } } ]") @@ -164,6 +193,21 @@ feature -- Test assert ("item (second.the)", attached cfg.text_item ("second.the") as v and then v.same_string_general ("3")) assert ("item (second.end)", attached cfg.text_item ("second.end") as v and then v.same_string_general ("4")) + assert ("has_item (collection)", cfg.has_item ("collection")) + assert ("item (collection)", attached cfg.text_list_item ("collection") as lst and then ( + lst.has ("a") and lst.has ("b") and lst.has ("c") and lst.has ("1") and lst.has ("2") and lst.has ("3") + ) + ) + + assert ("has_item (table)", cfg.has_item ("table")) + assert ("item (table)", attached cfg.text_table_item ("table") as tb and then ( + tb.item ("a") ~ {STRING_32} "1" and + tb.item ("b") ~ {STRING_32} "2" and + tb.item ("c") ~ {STRING_32} "3" and + tb.item ("d") ~ {STRING_32} "test" + ) + ) + if attached cfg.sub_config ("second") as cfg_second then assert ("has_item (is)", cfg_second.has_item ("is")) assert ("item (is)", attached cfg_second.text_item ("is") as v and then v.same_string_general ("2")) diff --git a/modules/auth/cms_authentication_module.e b/modules/auth/cms_authentication_module.e index 2a32c2d..21e59b1 100644 --- a/modules/auth/cms_authentication_module.e +++ b/modules/auth/cms_authentication_module.e @@ -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 diff --git a/modules/feed_aggregator/feed_aggregation.e b/modules/feed_aggregator/feed_aggregation.e index 1688089..debbad5 100644 --- a/modules/feed_aggregator/feed_aggregation.e +++ b/modules/feed_aggregator/feed_aggregation.e @@ -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 diff --git a/modules/feed_aggregator/feed_aggregator-safe.ecf b/modules/feed_aggregator/feed_aggregator-safe.ecf index 6baa236..c43b3e8 100644 --- a/modules/feed_aggregator/feed_aggregator-safe.ecf +++ b/modules/feed_aggregator/feed_aggregator-safe.ecf @@ -8,7 +8,9 @@ + + diff --git a/modules/feed_aggregator/feed_aggregator_api.e b/modules/feed_aggregator/feed_aggregator_api.e index 3928a77..a3a97d9 100644 --- a/modules/feed_aggregator/feed_aggregator_api.e +++ b/modules/feed_aggregator/feed_aggregator_api.e @@ -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 diff --git a/modules/feed_aggregator/feed_aggregator_module.e b/modules/feed_aggregator/feed_aggregator_module.e index c510cb8..2bcdce4 100644 --- a/modules/feed_aggregator/feed_aggregator_module.e +++ b/modules/feed_aggregator/feed_aggregator_module.e @@ -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) -- + 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 ("

") + s.append (r.html_encoded (l_agg.name)) + s.append ("

") + if attached l_agg.included_categories as l_categories then + s.append ("") + across + l_categories as cats_ic + loop + s.append (" [") + s.append (r.html_encoded (cats_ic.item)) + s.append ("]") + end + s.append ("") + end + if attached l_agg.description as l_desc and then l_desc.is_valid_as_string_8 then + s.append ("
") + s.append (l_desc.as_string_8) + s.append ("
") + end + s.append ("") + + 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 ("
  • ") + 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 ("") + across + l_categories as cats_ic + loop + s.append (" [") + s.append (r.html_encoded (cats_ic.item)) + s.append ("]") + end + s.append ("") + end + if attached ic.item.description as l_desc then + if l_desc.is_valid_as_string_8 then + s.append ("
    ") + s.append (l_desc.as_string_8) + s.append ("
    ") + end + end + s.append ("
  • ") + 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 ("
    ") + Result.append ("") + if with_feed_info then + if attached l_agg.description as l_desc then + Result.append ("
    ") + Result.append_string_general (l_desc) + Result.append ("
    ") + end + end + Result.append ("
      ") + 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 ("
    • ") + lnk := e.link + if attached e.date as l_date then + Result.append ("
      ") + append_date_time_to (l_date, l_today, Result) + Result.append ("
      ") + end + if lnk /= Void then + Result.append ("") + else + check has_link: False end + Result.append ("") + end + Result.append (a_response.html_encoded (e.title)) + Result.append ("") + debug + if attached e.categories as l_categories and then not l_categories.is_empty then + Result.append ("
      ") + across + l_categories as cats_ic + loop + Result.append (a_response.html_encoded (cats_ic.item)) + Result.append (" ") + end + Result.append ("
      ") + 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 ("
      ") + Result.append (l_entry_desc.as_string_8) + Result.append ("
      ") + else + check is_html: False end + end + end + Result.append ("
    • ") + end + end + end + Result.append_string ("") + Result.append_string (a_response.link ("more ...", "feed_aggregation/" + a_response.url_encoded (a_feed_id), Void)) + Result.append_string ("") + + Result.append ("
    ") + + + Result.append ("
    %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 ("[ --- --- ]", 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 diff --git a/modules/feed_aggregator/site/files/css/feed_aggregator.css b/modules/feed_aggregator/site/files/css/feed_aggregator.css new file mode 100644 index 0000000..abccac2 --- /dev/null +++ b/modules/feed_aggregator/site/files/css/feed_aggregator.css @@ -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: "..."; +} diff --git a/modules/feed_aggregator/site/files/scss/feed_aggregator.scss b/modules/feed_aggregator/site/files/scss/feed_aggregator.scss new file mode 100644 index 0000000..90b855f --- /dev/null +++ b/modules/feed_aggregator/site/files/scss/feed_aggregator.scss @@ -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: "..."; } + } +} diff --git a/modules/node/cms_node_module.e b/modules/node/cms_node_module.e index f4f6e79..1e23a28 100644 --- a/modules/node/cms_node_module.e +++ b/modules/node/cms_node_module.e @@ -244,7 +244,7 @@ feature -- Hooks block_list: ITERABLE [like {CMS_BLOCK}.name] -- do - Result := <<"node-info">> + Result := <<"?node-info">> end get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE) diff --git a/src/cache/cms_cache.e b/src/cache/cms_cache.e new file mode 100644 index 0000000..b01f6af --- /dev/null +++ b/src/cache/cms_cache.e @@ -0,0 +1,94 @@ +note + description: "Abstract interface for cache of value conforming to formal {G}." + date: "$Date: 2014-12-03 16:12:08 +0100 (mer., 03 déc. 2014) $" + revision: "$Revision: 96232 $" + +deferred class + CMS_CACHE [G -> ANY] + +feature -- Status report + + exists: BOOLEAN + -- Do associated cache file exists? + deferred + end + + expired (a_reference_date: detachable DATE_TIME; a_duration_in_seconds: INTEGER): BOOLEAN + -- Is associated cached item expired? + -- If `a_reference_date' is attached, cache is expired if `a_reference' is more recent than cached item. + local + d1, d2: DATE_TIME + do + if exists then + if + a_reference_date /= Void and then + a_reference_date > cache_date_time + then + Result := True + else + if a_duration_in_seconds = -1 then + Result := False -- Never expires + elseif a_duration_in_seconds = 0 then + Result := True -- Always expires + elseif a_duration_in_seconds > 0 then + d1 := cache_date_time + d2 := current_date_time + d2.second_add (- a_duration_in_seconds) --| do not modify `cache_date_time' + Result := d2 > d1 -- cached date + duration is older than current date + else + -- Invalid expiration value + -- thus always expired. + Result := True + end + end + else + Result := True + end + end + +feature -- Access + + item: detachable G + -- Value from the cache. + deferred + end + + cache_date_time: DATE_TIME + -- Date time for current cache if exists. + -- Note: it may be UTC or not , depending on cache type. + deferred + end + + cache_duration_in_seconds: INTEGER_64 + -- Number of seconds since cache was set. + require + exists: exists + local + d1, d2: DATE_TIME + do + d1 := cache_date_time + d2 := current_date_time + Result := d2.relative_duration (d1).seconds_count + end + + current_date_time: DATE_TIME + -- Current date time for relative duration with cache_date_time. + deferred + end + +feature -- Element change + + delete + -- Remove cache. + deferred + end + + put (g: G) + -- Put `g' into cache. + deferred + end + +note + copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/cache/cms_file_cache.e b/src/cache/cms_file_cache.e new file mode 100644 index 0000000..0ed4697 --- /dev/null +++ b/src/cache/cms_file_cache.e @@ -0,0 +1,150 @@ +note + description: "Cache using a local file." + date: "$Date: 2015-09-24 18:24:06 +0200 (jeu., 24 sept. 2015) $" + revision: "$Revision: 97926 $" + +deferred class + CMS_FILE_CACHE [G -> ANY] + +inherit + CMS_CACHE [G] + +feature {NONE} -- Initialization + + make (a_cache_filename: PATH) + do + path := a_cache_filename + end + + path: PATH + +feature -- Status report + + exists: BOOLEAN + -- Do associated cache file exists? + local + ut: FILE_UTILITIES + do + Result := ut.file_path_exists (path) + end + +feature -- Access + + cache_date_time: DATE_TIME + -- + local + f: RAW_FILE + do + create f.make_with_path (path) + if f.exists then + Result := utc_file_date_time (f) + else + create Result.make_now_utc + end + end + + current_date_time: DATE_TIME + -- + do + -- UTC, since `cache_date_time' is UTC! + create Result.make_now_utc + end + + file_size: INTEGER + -- Associated file size. + require + exists: exists + local + f: RAW_FILE + do + create f.make_with_path (path) + if f.exists and then f.is_access_readable then + Result := f.count + end + end + + item: detachable G + local + f: RAW_FILE + retried: BOOLEAN + do + if not retried then + create f.make_with_path (path) + if f.exists and then f.is_access_readable then + f.open_read + Result := file_to_item (f) + f.close + end + end + rescue + retried := True + retry + end + +feature -- Element change + + delete + -- + local + f: RAW_FILE + retried: BOOLEAN + do + if not retried then + create f.make_with_path (path) + -- Create recursively parent directory if it does not exists. + if f.exists and then f.is_access_writable then + f.delete + end + end + rescue + retried := True + retry + end + + put (g: G) + -- + local + f: RAW_FILE + d: DIRECTORY + do + create f.make_with_path (path) + -- Create recursively parent directory if it does not exists. + create d.make_with_path (path.parent) + if not d.exists then + d.recursive_create_dir + end + if not f.exists or else f.is_access_writable then + f.open_write + item_to_file (g, f) + f.close + end + end + +feature -- Helpers + + utc_file_date_time (f: FILE): DATE_TIME + -- Last change date for file `f'. + require + f.exists + do + create Result.make_from_epoch (f.date.as_integer_32) + end + +feature {NONE} -- Implementation + + file_to_item (f: FILE): detachable G + require + is_open_write: f.is_open_read + deferred + end + + item_to_file (g: G; f: FILE) + require + is_open_write: f.is_open_write + deferred + end + +note + copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/cache/cms_file_object_cache.e b/src/cache/cms_file_object_cache.e new file mode 100644 index 0000000..e32883a --- /dev/null +++ b/src/cache/cms_file_object_cache.e @@ -0,0 +1,49 @@ +note + description: "Cache for value conforming to formal {G}, and implemented using local file." + date: "$Date: 2014-10-30 12:13:25 +0100 (jeu., 30 oct. 2014) $" + revision: "$Revision: 96016 $" + +class + CMS_FILE_OBJECT_CACHE [G -> ANY] + +inherit + CMS_FILE_CACHE [G] + + SED_STORABLE_FACILITIES + +create + make + +feature {NONE} -- Implementation + + file_to_item (f: FILE): detachable G + local + retried: BOOLEAN + l_reader: SED_MEDIUM_READER_WRITER + l_void: detachable G + do + if retried then + Result := l_void + else + create l_reader.make_for_reading (f) + if attached {G} retrieved (l_reader, True) as l_data then + Result := l_data + end + end + rescue + retried := True + retry + end + + item_to_file (a_data: G; f: FILE) + local + l_writer: SED_MEDIUM_READER_WRITER + do + create l_writer.make_for_writing (f) + basic_store (a_data, l_writer, True) + end + +note + copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/cache/cms_file_string_8_cache.e b/src/cache/cms_file_string_8_cache.e new file mode 100644 index 0000000..73ee030 --- /dev/null +++ b/src/cache/cms_file_string_8_cache.e @@ -0,0 +1,69 @@ +note + description: "Cache system for STRING_8 value." + date: "$Date: 2014-10-30 12:13:25 +0100 (jeu., 30 oct. 2014) $" + revision: "$Revision: 96016 $" + +class + CMS_FILE_STRING_8_CACHE + +inherit + CMS_FILE_CACHE [STRING] + +create + make + +feature -- Access + + append_to (a_output: STRING) + -- Append `item' to `a_output'. + local + f: RAW_FILE + retried: BOOLEAN + do + if not retried then + create f.make_with_path (path) + if f.exists and then f.is_access_readable then + f.open_read + if attached file_to_item (f) as s then + a_output.append (s) + end + f.close + end + end + rescue + retried := True + retry + end + +feature {NONE} -- Implementation + + file_to_item (f: FILE): detachable STRING + local + retried: BOOLEAN + do + if retried then + Result := Void + else + from + create Result.make_empty + until + f.exhausted or f.end_of_file + loop + f.read_stream_thread_aware (1_024) + Result.append (f.last_string) + end + end + rescue + retried := True + retry + end + + item_to_file (a_data: STRING; f: FILE) + do + f.put_string (a_data) + end + +note + copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/cache/cms_memory_cache.e b/src/cache/cms_memory_cache.e new file mode 100644 index 0000000..188f3c8 --- /dev/null +++ b/src/cache/cms_memory_cache.e @@ -0,0 +1,61 @@ +note + description: "Cache relying on memory." + date: "$Date: 2014-12-03 16:57:00 +0100 (mer., 03 déc. 2014) $" + revision: "$Revision: 96234 $" + +deferred class + CMS_MEMORY_CACHE [G -> ANY] + +inherit + CMS_CACHE [G] + +feature {NONE} -- Initialization + + make + do + cache_date_time := current_date_time + end + + +feature -- Status report + + exists: BOOLEAN + -- Do associated cache memory exists? + do + Result := item /= Void + end + +feature -- Access + + cache_date_time: DATE_TIME + + current_date_time: DATE_TIME + -- + do + create Result.make_now_utc + end + + item: detachable G + +feature -- Element change + + delete + -- + local + l_default: detachable G + do + item := l_default + cache_date_time := current_date_time + end + + put (g: G) + -- + do + item := g + cache_date_time := current_date_time + end + +note + copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/configuration/cms_default_setup.e b/src/configuration/cms_default_setup.e index bdcfc4a..e15b9b8 100644 --- a/src/configuration/cms_default_setup.e +++ b/src/configuration/cms_default_setup.e @@ -68,6 +68,18 @@ feature -- Access Result := configuration.resolved_text_item (a_name) end + text_list_item (a_name: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32] + -- Configuration values associated with `a_name', if any. + do + Result := configuration.text_list_item (a_name) + end + + text_table_item (a_name: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32] + -- Configuration indexed values associated with `a_name', if any. + do + Result := configuration.text_table_item (a_name) + end + string_8_item (a_name: READABLE_STRING_GENERAL): detachable READABLE_STRING_8 -- String 8 configuration value associated with `a_name', if any. local diff --git a/src/configuration/cms_setup.e b/src/configuration/cms_setup.e index dd7b5b6..979646e 100644 --- a/src/configuration/cms_setup.e +++ b/src/configuration/cms_setup.e @@ -196,6 +196,16 @@ feature -- Query deferred end + text_list_item (a_name: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32] + -- Configuration values associated with `a_name', if any. + deferred + end + + text_table_item (a_name: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32] + -- Configuration indexed values associated with `a_name', if any. + deferred + end + text_item_or_default (a_name: READABLE_STRING_GENERAL; a_default_value: READABLE_STRING_GENERAL): READABLE_STRING_32 -- `text_item' associated with `a_name' or if none, `a_default_value'. do diff --git a/src/hooks/cms_hook_block.e b/src/hooks/cms_hook_block.e index 349f47c..cdccb6b 100644 --- a/src/hooks/cms_hook_block.e +++ b/src/hooks/cms_hook_block.e @@ -15,6 +15,8 @@ feature -- Hook block_list: ITERABLE [like {CMS_BLOCK}.name] -- List of block names, managed by current object. + -- If prefixed by "?", condition will be check + -- to determine if it should be displayed (and computed) or not. deferred end @@ -23,4 +25,7 @@ feature -- Hook deferred end +note + copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/hooks/cms_hook_core_manager.e b/src/hooks/cms_hook_core_manager.e index 285d782..2772a25 100644 --- a/src/hooks/cms_hook_core_manager.e +++ b/src/hooks/cms_hook_core_manager.e @@ -138,6 +138,9 @@ feature -- Hook: block invoke_block (a_response: CMS_RESPONSE) -- Invoke block hook for response `a_response' in order to get block from modules. + local + bl: READABLE_STRING_8 + bl_optional: BOOLEAN do if attached subscribers ({CMS_HOOK_BLOCK}) as lst then across @@ -147,7 +150,16 @@ feature -- Hook: block across h.block_list as blst loop - h.get_block_view (blst.item, a_response) + bl := blst.item + bl_optional := bl.count > 0 and bl[1] = '?' + if bl_optional then + bl := bl.substring (2, bl.count) + if a_response.is_block_included (bl, False) then + h.get_block_view (bl, a_response) + end + else + h.get_block_view (bl, a_response) + end end end end diff --git a/src/kernel/content/block/condition/cms_block_expression_condition.e b/src/kernel/content/block/condition/cms_block_expression_condition.e index 72e5be5..531fef1 100644 --- a/src/kernel/content/block/condition/cms_block_expression_condition.e +++ b/src/kernel/content/block/condition/cms_block_expression_condition.e @@ -6,11 +6,12 @@ note class CMS_BLOCK_EXPRESSION_CONDITION -inherit +inherit CMS_BLOCK_CONDITION create - make + make, + make_none feature {NONE} -- Initialization @@ -19,9 +20,14 @@ feature {NONE} -- Initialization expression := a_exp end + make_none + do + make ("") + end + feature -- Access - description: READABLE_STRING_32 + description: STRING_32 do create Result.make_from_string_general ("Expression: %"") Result.append_string_general (expression) @@ -33,10 +39,18 @@ feature -- Access feature -- Evaluation satisfied_for_response (res: CMS_RESPONSE): BOOLEAN + local + exp: like expression do - if expression.same_string ("is_front") then + exp := expression + if exp.same_string ("is_front") then Result := res.is_front - elseif expression.starts_with ("path=") then + elseif exp.same_string ("*") then + Result := True + elseif exp.same_string ("") then + Result := False + elseif exp.starts_with ("path:") then + Result := res.location.same_string (exp.substring (6, exp.count)) end end diff --git a/src/kernel/content/block/condition/cms_block_location_condition.e b/src/kernel/content/block/condition/cms_block_location_condition.e new file mode 100644 index 0000000..076392c --- /dev/null +++ b/src/kernel/content/block/condition/cms_block_location_condition.e @@ -0,0 +1,45 @@ +note + description: "Condition for block to be displayed based on location." + date: "$Date$" + revision: "$Revision$" + +class + CMS_BLOCK_LOCATION_CONDITION + +inherit + CMS_BLOCK_CONDITION + +create + make_with_location + +feature {NONE} -- Initialization + + make_with_location (a_location: READABLE_STRING_8) + do + location := a_location + end + +feature -- Access + + description: STRING_32 + do + create Result.make_from_string_general ("Location: %"") + Result.append_string_general (location) + Result.append_character ('%"') + end + + location: STRING + +feature -- Evaluation + + satisfied_for_response (res: CMS_RESPONSE): BOOLEAN + local + loc: like location + do + Result := res.location.same_string (loc) + end + +note + copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/kernel/content/cms_block.e b/src/kernel/content/cms_block.e index 0bbf1aa..e629b93 100644 --- a/src/kernel/content/cms_block.e +++ b/src/kernel/content/cms_block.e @@ -70,6 +70,19 @@ feature -- Element change opts.remove_css_class (a_class) end + add_condition (a_condition: CMS_BLOCK_CONDITION) + -- Add condition `a_condition'. + local + l_conditions: like conditions + do + l_conditions := conditions + if l_conditions = Void then + create {ARRAYED_LIST [CMS_BLOCK_CONDITION]} l_conditions.make (1) + conditions := l_conditions + end + l_conditions.force (a_condition) + end + feature -- Conversion to_html (a_theme: CMS_THEME): STRING_8 diff --git a/src/modules/cms_debug_module.e b/src/modules/cms_debug_module.e index 04c4efe..72b1cca 100644 --- a/src/modules/cms_debug_module.e +++ b/src/modules/cms_debug_module.e @@ -58,7 +58,7 @@ feature -- Hooks block_list: ITERABLE [like {CMS_BLOCK}.name] do - Result := <<"debug-info">> + Result := <<"?debug-info">> end get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE) @@ -67,14 +67,13 @@ feature -- Hooks dbg: WSF_DEBUG_INFORMATION s: STRING do - if a_response.theme.has_region ("debug") then - create dbg.make - create s.make_empty - dbg.append_information_to (a_response.request, a_response.response, s) - append_info_to ("Storage", a_response.api.storage.generator, a_response, s) - create b.make ("debug-info", "Debug", s, a_response.formats.plain_text) - a_response.add_block (b, "footer") - end + create dbg.make + create s.make_empty + dbg.append_information_to (a_response.request, a_response.response, s) + append_info_to ("Storage", a_response.api.storage.generator, a_response, s) + create b.make ("debug-info", "Debug", s, a_response.formats.plain_text) + b.add_condition (create {CMS_BLOCK_EXPRESSION_CONDITION}.make_none) + a_response.add_block (b, "footer") end feature -- Handler diff --git a/src/service/response/cms_response.e b/src/service/response/cms_response.e index 72559ce..3d026c5 100644 --- a/src/service/response/cms_response.e +++ b/src/service/response/cms_response.e @@ -423,6 +423,36 @@ feature -- Blocks initialization Result := setup.text_item_or_default ("blocks." + a_block_id + ".region", a_default_region) end + block_conditions (a_block_id: READABLE_STRING_8): detachable ARRAYED_LIST [CMS_BLOCK_EXPRESSION_CONDITION] + -- Condition associated with `a_block_id' in configuration, if any. + do + if attached setup.text_item ("blocks." + a_block_id + ".condition") as s then + create Result.make (1) + Result.force (create {CMS_BLOCK_EXPRESSION_CONDITION}.make (s)) + end + if attached setup.text_list_item ("blocks." + a_block_id + ".conditions") as lst then + if Result = Void then + create Result.make (lst.count) + across + lst as ic + loop + Result.force (create {CMS_BLOCK_EXPRESSION_CONDITION}.make (ic.item)) + end + end + end + end + + is_block_included (a_block_id: READABLE_STRING_8; dft: BOOLEAN): BOOLEAN + -- Is block `a_block_id' included in current response? + -- If no preference, return `dft'. + do + if attached block_conditions (a_block_id) as l_conditions then + Result := across l_conditions as ic some ic.item.satisfied_for_response (Current) end + else + Result := dft + end + end + feature -- Blocks regions regions: STRING_TABLE [CMS_BLOCK_REGION] @@ -457,10 +487,25 @@ feature -- Blocks regions end end -feature -- Blocks +feature -- Blocks + + put_block (b: CMS_BLOCK; a_default_region: detachable READABLE_STRING_8; is_block_included_by_default: BOOLEAN) + -- Add block `b' to associated region or `a_default_region' if provided + -- and check optional associated condition. + -- If no condition then use `is_block_included_by_default' to + -- decide if block is included or not. + local + l_region: detachable like block_region + do + if is_block_included (b.name, is_block_included_by_default) then + l_region := block_region (b, a_default_region) + l_region.extend (b) + end + end add_block (b: CMS_BLOCK; a_default_region: detachable READABLE_STRING_8) -- Add block `b' to associated region or `a_default_region' if provided. + -- WARNING: ignore any block condition! USE WITH CARE! local l_region: detachable like block_region do @@ -473,29 +518,29 @@ feature -- Blocks debug ("refactor_fixme") fixme ("find a way to have this in configuration or database, and allow different order") end - add_block (top_header_block, "top") - add_block (header_block, "header") + put_block (top_header_block, "top", True) + put_block (header_block, "header", True) if attached message_block as m then - add_block (m, "content") + put_block (m, "content", True) end if attached primary_tabs_block as m then - add_block (m, "content") + put_block (m, "content", True) end - add_block (content_block, "content") + add_block (content_block, "content") -- Can not be disabled! if attached management_menu_block as l_block then - add_block (l_block, "sidebar_first") + put_block (l_block, "sidebar_first", True) end if attached navigation_menu_block as l_block then - add_block (l_block, "sidebar_first") + put_block (l_block, "sidebar_first", True) end if attached user_menu_block as l_block then - add_block (l_block, "sidebar_second") + put_block (l_block, "sidebar_second", True) end hooks.invoke_block (Current) debug ("cms") - add_block (create {CMS_CONTENT_BLOCK}.make ("made_with", Void, "Made with EWF", Void), "footer") + put_block (create {CMS_CONTENT_BLOCK}.make ("made_with", Void, "Made with EWF", Void), "footer", True) end end