From 1a62c96ee3c5fca2558a6ad0eb7a6c8553276820 Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 4 Sep 2024 13:42:18 +0200 Subject: [PATCH 001/135] fixed sitemap for proxy nodes (KKI) --- Products/zms/rest_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Products/zms/rest_api.py b/Products/zms/rest_api.py index 349072524..391091f0d 100644 --- a/Products/zms/rest_api.py +++ b/Products/zms/rest_api.py @@ -122,7 +122,11 @@ def get_attrs(node): data['has_portal_clients'] = node.getPortalClients() != [] general_keys = data.keys() obj_attrs = node.getObjAttrs() - metaobj_attrs = node.getMetaobjManager().getMetaobjAttrs(node.meta_id) + try: + metaobj_attrs = node.getMetaobjManager().getMetaobjAttrs(node.meta_id) + except: + # In case node is a ZMSProxy object: + metaobj_attrs = node.proxy.getMetaobjManager().getMetaobjAttrs(node.meta_id) for metaobj_attr in metaobj_attrs: id = metaobj_attr['id'] if id in obj_attrs and not id in general_keys: From afcf6960d9753c937bab101ae1f54926995ba08d Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 4 Sep 2024 17:44:17 +0200 Subject: [PATCH 002/135] fixed sitemap for proxy nodes (KKI, 2) --- Products/zms/rest_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Products/zms/rest_api.py b/Products/zms/rest_api.py index 391091f0d..5f0795f22 100644 --- a/Products/zms/rest_api.py +++ b/Products/zms/rest_api.py @@ -122,11 +122,11 @@ def get_attrs(node): data['has_portal_clients'] = node.getPortalClients() != [] general_keys = data.keys() obj_attrs = node.getObjAttrs() - try: - metaobj_attrs = node.getMetaobjManager().getMetaobjAttrs(node.meta_id) - except: - # In case node is a ZMSProxy object: + if hasattr(node,'proxy'): metaobj_attrs = node.proxy.getMetaobjManager().getMetaobjAttrs(node.meta_id) + data['meta_id'] = 'ZMSLinkElement' + else: + metaobj_attrs = node.getMetaobjManager().getMetaobjAttrs(node.meta_id) for metaobj_attr in metaobj_attrs: id = metaobj_attr['id'] if id in obj_attrs and not id in general_keys: From 3d9b963473c385fd6af45594d10123154dce91ba Mon Sep 17 00:00:00 2001 From: drfho Date: Thu, 5 Sep 2024 14:24:00 +0200 Subject: [PATCH 003/135] exclude Managers from login logging Ref: maintenance mode --- Products/zms/zpt/common/zmi_html_head.zpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Products/zms/zpt/common/zmi_html_head.zpt b/Products/zms/zpt/common/zmi_html_head.zpt index 8d56ea31f..179e76241 100644 --- a/Products/zms/zpt/common/zmi_html_head.zpt +++ b/Products/zms/zpt/common/zmi_html_head.zpt @@ -38,7 +38,7 @@ added_js_zmi_href python:added_js_zmi.replace('$ZMS_HOME/',ZMS_HOME).replace('$ZMS_THEME/',ZMS_THEME);" tal:condition="python:added_js_zmi.find('.js')>-1" tal:attributes="src added_js_zmi_href"> - + From 76a0c1823533605417ff3482953aa5a033eeed60 Mon Sep 17 00:00:00 2001 From: drfho Date: Thu, 5 Sep 2024 17:46:19 +0200 Subject: [PATCH 004/135] gui: handle missing lastlogin data --- Products/zms/plugins/www/zmi.core.css | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Products/zms/plugins/www/zmi.core.css b/Products/zms/plugins/www/zmi.core.css index fa2582961..9fca5a6df 100644 --- a/Products/zms/plugins/www/zmi.core.css +++ b/Products/zms/plugins/www/zmi.core.css @@ -2387,17 +2387,21 @@ body.lazy_select_form footer .controls { border:1px dotted #607D8B; } .zmi.config.users .list_users table.show_lastlogin tr.userName td:before { - content:attr(data-lastlogin); - display:block; + content: "0000-00-00"; + display: inline-block; right:0; color:white; font-family:monospace; - float:right; font-size:11px; position:absolute; - background:#333333; - padding:.1rem .5rem;; + background: #4950573d; + padding:.1rem .5rem; margin-top:-.5rem; + margin-top:-.5rem; +} +.zmi.config.users .list_users table.show_lastlogin tr.userName td[data-lastlogin]:before { + background:#333333; + content:attr(data-lastlogin); } /* ZMSLinkElement */ .zmi #tr_attr_ref #tr_attr_ref_type select#attr_type { From 23c6852ee264716a7ac4f3ff632141198194ba71 Mon Sep 17 00:00:00 2001 From: drfho Date: Fri, 6 Sep 2024 16:12:20 +0200 Subject: [PATCH 005/135] zmi: enable right click new-win on tabs-nav Ref: https://github.com/zms-publishing/ZMS/commit/27f18f235778641bf504f37d1eb95402023c908b --- Products/zms/zpt/common/zmi_tabs.zpt | 35 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Products/zms/zpt/common/zmi_tabs.zpt b/Products/zms/zpt/common/zmi_tabs.zpt index 4546758b2..6abd79693 100644 --- a/Products/zms/zpt/common/zmi_tabs.zpt +++ b/Products/zms/zpt/common/zmi_tabs.zpt @@ -1,19 +1,28 @@
-
 
+ +
+
+ + +
+
+ + +
+
+
+
+
@@ -102,25 +115,25 @@ function onExportFormatChange(el) {
-
-
- - Download to local machine +
+
+ +
-
- - Save to file on server +
+ +
-
- - Preview +
+ +
From 6eeae4f3313a1fbf2a229c48cc94804aec14a6bb Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 11 Sep 2024 14:40:56 +0200 Subject: [PATCH 009/135] zcatalog: added multisite-search (zhref) --- .../com.zms.catalog.zcatalog/__init__.py | 2 +- .../zcatalog_connector/__init__.py | 2 +- .../zcatalog_connector/zcatalog_query.py | 24 +++++++++++++-- .../zcatalog_page/__init__.py | 29 ++++++++++++++++++- .../zcatalog_page/script.js | 8 +++-- .../zcatalog_page/standard_html.zpt | 6 ++++ .../zcatalog_page/style.css | 29 +++++-------------- 7 files changed, 71 insertions(+), 29 deletions(-) diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/__init__.py index 71ed9b7a3..f4390c968 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/__init__.py @@ -22,7 +22,7 @@ class com_zms_catalog_zcatalog: package = "" # Revision - revision = "1.0.4" + revision = "1.1.0" # Type type = "ZMSPackage" diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_connector/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_connector/__init__.py index 5d2907434..8f2713037 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_connector/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_connector/__init__.py @@ -26,7 +26,7 @@ class zcatalog_connector: package = "com.zms.catalog.zcatalog" # Revision - revision = "1.0.4" + revision = "1.1.0" # Type type = "ZMSLibrary" diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_connector/zcatalog_query.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_connector/zcatalog_query.py index 97bcaafae..f99001968 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_connector/zcatalog_query.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_connector/zcatalog_query.py @@ -19,9 +19,17 @@ def zcatalog_query( self, REQUEST=None): q = request.get('q','') fq = request.get('fq','') lang = standard.nvl(request.get('lang'), self.getPrimaryLanguage()) + home_id = request.get('home_id', '') + multisite_search = int(request.get('multisite_search', 1)) + multisite_exclusions = request.get('multisite_exclusions', '').split(',') root = self.getRootElement() zcatalog = getattr(root, 'catalog_%s'%lang) - + + if multisite_search==0 and len(home_id) > 0: + if fq: + fq += ',' + fq += 'home_id_s:'+home_id + # Find search-results. items = [] prototype = {} @@ -34,6 +42,17 @@ def zcatalog_query( self, REQUEST=None): fqv = fqs[fqs.find(':')+1:] fqv = umlaut_quote(self, fqv) prototype[fqk] = fqv + + # @TODO: Implement multisite-search + # # Multisite-search: show results of all ZMS-clients except the ones in multisite_exclusions + # if multisite_search==1 and multisite_exclusions: + # prototype['zcat_index_home_id'] = {'query':'', 'operator':'AND'} + # exclusion_string = '' + # for exclusion in multisite_exclusions: + # exclusion_string += exclusion and '-%s '%exclusion or '' + # if exclusion_string: + # prototype['zcat_index_home_id']['query'] = exclusion_string + for index in zcatalog.indexes(): if index.find('zcat_index_')==0: query = copy.deepcopy(prototype) @@ -89,5 +108,4 @@ def zcatalog_query( self, REQUEST=None): docs_list.append(doc_item) response = {'status':0, 'numFound':num_found, 'start': start, 'docs':docs_list} - return json.dumps(response) - + return json.dumps(response) \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/__init__.py index 92c145824..d537168d3 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/__init__.py @@ -32,7 +32,7 @@ class zcatalog_page: package = "com.zms.catalog.zcatalog" # Revision - revision = "1.0.4" + revision = "1.1.0" # Type type = "ZMSDocument" @@ -67,6 +67,33 @@ class Attrs: ,"repetitive":0 ,"type":"title"} + multisite_search = {"default":"0" + ,"id":"multisite_search" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Multisite-Search" + ,"repetitive":0 + ,"type":"boolean"} + + multisite_exclusions = {"default":"" + ,"id":"multisite_exclusions" + ,"keys":["##" + ,"master = context.getPortalMaster()" + ,"zmsclientids = []" + ,"def getZMSPortalClients(zmsclient):" + ," zmsclientids.append(zmsclient.getHome().id)" + ," for zmsclientid in zmsclient.getPortalClients():" + ," getZMSPortalClients(zmsclientid)" + ," zmsclientids.sort()" + ," return list(zmsclientids)" + ,"return [(id,id) for id in getZMSPortalClients(zmsclient=master)]"] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Multisite-Exclusions" + ,"repetitive":0 + ,"type":"multiautocomplete"} + scriptjs = {"default":"" ,"id":"script.js" ,"keys":[] diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/script.js b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/script.js index 3b5053b96..e97a8c0c8 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/script.js +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/script.js @@ -15,7 +15,11 @@ $(function() { $('.search-results').removeClass('not-launched'); $('.search-results').html(hb_spinner_tmpl(q)); // debugger; - const qurl = `${root_url}/zcatalog_query?q=${q}&pageIndex:int=${pageIndex}`; + var q = decodeURI($('#site-search-content input[name="q"]').val()); + const home_id = $('#home_id').attr('value') || ''; + const multisite_search = $('#multisite_search').attr('value') || 1; + const multisite_exclusions = $('#multisite_exclusions').attr('value') || ''; + const qurl = `${root_url}/zcatalog_query?q=${q}&pageIndex:int=${pageIndex}&home_id=${home_id}&multisite_search=${multisite_search}&multisite_exclusions=${multisite_exclusions}`; const response = await fetch(qurl); const res = await response.json(); const res_processed = postprocess_results(q, res); @@ -82,7 +86,7 @@ $(function() { //# Execute on submit event $('.search-form form').submit(function() { - var q = $('input',this).val(); + var q = decodeURI($('input[name="q"]',this).val()); winloc.searchParams.set('q', q); history.pushState({}, '', winloc); show_results(q, 0); diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/standard_html.zpt b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/standard_html.zpt index f8ed3abdc..be93ac14b 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/standard_html.zpt +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/standard_html.zpt @@ -43,6 +43,12 @@
+ + +
diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/style.css b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/style.css index 23e0a2c40..46727b209 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/style.css +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/style.css @@ -30,7 +30,10 @@ padding-left: 0; margin-left: 0; font-size: .7em; - margin:2em 0 0.1em 0 + margin:2em 0 0.1em 0; + display: flex; + white-space:nowrap; + overflow:hidden; } .search-results .path li:before { line-height: .5; @@ -39,6 +42,10 @@ .search-results .path li a { color:#aaa; } +.search-results .path li a:before { + padding: 0 .25em 0 .5em; + content:"/ " +} .search-results .hit em { font-style: normal; font-weight: initial !important; @@ -69,26 +76,6 @@ -webkit-transform: rotate(360deg); } } -.pagination i.fas.fa-chevron-left:after, -.pagination i.fas.fa-chevron-right:after { - font-family: unibe-iconset; - speak: none; - font-size:200%; - font-style: normal; - font-weight: 400; - font-variant: normal; - text-transform: none; - line-height: .75em; - vertical-align: -.25em; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -body:not(.zms.web) .pagination i.fas.fa-chevron-left:after { - content: "\e603"; -} -body:not(.zms.web) .pagination i.fas.fa-chevron-right:after { - content: "\e602"; -} .pagination > li > a, .pagination > li > span { color: #d6002b; From 7c0b1bac4aef9b32f4c9151302c82e7c1bf53f32 Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 11 Sep 2024 14:50:46 +0200 Subject: [PATCH 010/135] zcatalog: added multisite-search (zhref) (2) --- .../com.zms.catalog.zcatalog/zcatalog_page/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/script.js b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/script.js index e97a8c0c8..8bcb3719a 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/script.js +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.zcatalog/zcatalog_page/script.js @@ -26,7 +26,7 @@ $(function() { var total = res_processed.total; var hb_results_html = hb_results_tmpl(res_processed); $('.search-results').html( hb_results_html ); - $('html, body').animate({scrollTop: $("#search_results").offset().top }, 1000); + $('html, body').animate({scrollTop: $("#site-search-content").offset().top }, 1000); //# Add pagination ################### var fn = (pageIndex) => { From 89654110e5d7eac6ca4f9f0b07b41d2bca1dcd5c Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 11 Sep 2024 15:37:19 +0200 Subject: [PATCH 011/135] feature(maintenance): fitted response content-type --- Products/zms/zms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Products/zms/zms.py b/Products/zms/zms.py index 51f197eb3..c7bd7b94d 100644 --- a/Products/zms/zms.py +++ b/Products/zms/zms.py @@ -562,6 +562,10 @@ def maintenance_hook(): # https://github.com/zopefoundation/transaction/blob/6d4785159c277067f2ec95158884870a92660220/src/transaction/_transaction.py#L421 for resource in t._resources: if isinstance(resource, ZODB.Connection.Connection): + # Reset response content type (WGSIPublisher sets it to "text/plain" per default if unset) + # and lock status (the zException does not get mapped correctly) + request.response.setHeader('Content-Type', 'text/html;charset=utf-8') + request.response.setStatus(503, lock=True) raise zExceptions.HTTPServiceUnavailable('Maintenance') t.addBeforeCommitHook(maintenance_hook) From 640bbb2424b2b5a5c101d1ef6436f50cc2dedf55 Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 11 Sep 2024 16:53:49 +0200 Subject: [PATCH 012/135] fix(default-cmd): added metacmd id --- .../zms/conf/metacmd_manager/manage_tab_langdict/__init__.py | 2 +- .../metacmd_manager/manage_tab_langdict/manage_tab_langdict.py | 1 + Products/zms/conf/metacmd_manager/manage_tab_search/__init__.py | 2 +- .../metacmd_manager/manage_tab_search/manage_tab_search.zpt | 1 + Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py | 2 +- .../conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt | 1 + 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Products/zms/conf/metacmd_manager/manage_tab_langdict/__init__.py b/Products/zms/conf/metacmd_manager/manage_tab_langdict/__init__.py index 8a9de0c59..efb7a88d0 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_langdict/__init__.py +++ b/Products/zms/conf/metacmd_manager/manage_tab_langdict/__init__.py @@ -34,7 +34,7 @@ class manage_tab_langdict: package = "com.zms.foundation.metacmd.tabs" # Revision - revision = "0.0.0" + revision = "5.0.1" # Roles roles = ["*"] diff --git a/Products/zms/conf/metacmd_manager/manage_tab_langdict/manage_tab_langdict.py b/Products/zms/conf/metacmd_manager/manage_tab_langdict/manage_tab_langdict.py index c798c78bd..7ad2d32cf 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_langdict/manage_tab_langdict.py +++ b/Products/zms/conf/metacmd_manager/manage_tab_langdict/manage_tab_langdict.py @@ -25,6 +25,7 @@ prt.append('
') prt.append(context.zmi_breadcrumbs(context,request)) prt.append('') +prt.append('') prt.append(''%request['lang']) prt.append('%s'%context.getZMILangStr('ATTR_DICTIONARY')) prt.append('
') diff --git a/Products/zms/conf/metacmd_manager/manage_tab_search/__init__.py b/Products/zms/conf/metacmd_manager/manage_tab_search/__init__.py index 838e41742..dabb330dc 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_search/__init__.py +++ b/Products/zms/conf/metacmd_manager/manage_tab_search/__init__.py @@ -34,7 +34,7 @@ class manage_tab_search: package = "com.zms.foundation.metacmd.tabs" # Revision - revision = "5.0.0" + revision = "5.0.1" # Roles roles = ["*"] diff --git a/Products/zms/conf/metacmd_manager/manage_tab_search/manage_tab_search.zpt b/Products/zms/conf/metacmd_manager/manage_tab_search/manage_tab_search.zpt index 7a1698e24..13e83c5de 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_search/manage_tab_search.zpt +++ b/Products/zms/conf/metacmd_manager/manage_tab_search/manage_tab_search.zpt @@ -7,6 +7,7 @@ zmi_breadcrumbs + Search header diff --git a/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py b/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py index 5d6c29af0..f698749fa 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py +++ b/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py @@ -34,7 +34,7 @@ class manage_tab_tasks: package = "com.zms.foundation.metacmd.tabs" # Revision - revision = "5.0.0" + revision = "5.0.1" # Roles roles = ["*"] diff --git a/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt b/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt index 9d4979a6b..1028064b1 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt +++ b/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt @@ -11,6 +11,7 @@ global obs python:[]"> + the task-type
From ad8652c6e87aad51061b08ade1c86fee13656f57 Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 11 Sep 2024 21:22:45 +0200 Subject: [PATCH 013/135] ElasticSearch connector: Configurable Index Name (#301) --- .../com.zms.catalog.elasticsearch/__init__.py | 2 +- .../elasticsearch_connector/__init__.py | 4 +- .../elasticsearch_query.py | 11 +++--- .../elasticsearch_suggest.py | 3 +- .../manage_elasticsearch_destroy.py | 2 +- .../manage_elasticsearch_init.py | 2 +- .../manage_elasticsearch_objects_add.py | 2 +- .../manage_elasticsearch_objects_clear.py | 3 +- .../manage_elasticsearch_objects_remove.py | 2 +- .../elasticsearch_connector/readme | 37 +++++++++++++++++-- .../elasticsearch_page/__init__.py | 2 +- .../elasticsearch_page/standard_html.zpt | 2 +- .../opensearch_connector/readme | 17 ++++++++- 13 files changed, 68 insertions(+), 21 deletions(-) diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/__init__.py index 5efb0d532..7d785b0e6 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/__init__.py @@ -16,7 +16,7 @@ class com_zms_catalog_elasticsearch: package = "" # Revision - revision = "1.5.0" + revision = "1.8.0" # Type type = "ZMSPackage" diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/__init__.py index 6f6e9dd83..dbde4c981 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/__init__.py @@ -28,14 +28,14 @@ class elasticsearch_connector: package = "com.zms.catalog.elasticsearch" # Revision - revision = "1.7.0" + revision = "1.8.0" # Type type = "ZMSLibrary" # Attrs class Attrs: - properties = {"custom":"[\r\n {\r\n \"id\": \"elasticsearch.url\",\r\n \"type\": \"string\",\r\n \"label\": \"URL\",\r\n \"default_value\": \"https://localhost:9200\",\r\n \"is_target_of\": \"\"\r\n },\r\n {\r\n \"id\": \"elasticsearch.username\",\r\n \"type\": \"string\",\r\n \"label\": \"Username\",\r\n \"default_value\": \"admin\",\r\n \"is_target_of\": \"\"\r\n },\r\n {\r\n \"id\": \"elasticsearch.password\",\r\n \"type\": \"password\",\r\n \"label\": \"Password\",\r\n \"default_value\": \"admin\",\r\n \"is_target_of\": \"\"\r\n },\r\n {\r\n \"id\": \"elasticsearch.schema\",\r\n \"type\": \"text\",\r\n \"label\": \"Schema\",\r\n \"default_value\": \"{}\",\r\n \"is_target_of\": \"schematize\"\r\n },\r\n {\r\n \"id\": \"elasticsearch.parser\",\r\n \"type\": \"string\",\r\n \"label\": \"Parser\",\r\n \"default_value\": \"http://localhost:9998/tika\",\r\n \"is_target_of\": \"\"\r\n }\r\n]" + properties = {"custom":"[\r\n {\r\n \"id\": \"elasticsearch.url\",\r\n \"type\": \"string\",\r\n \"label\": \"URL\",\r\n \"default_value\": \"https://localhost:9200\",\r\n \"is_target_of\": \"\"\r\n },\r\n {\r\n \"id\": \"elasticsearch.username\",\r\n \"type\": \"string\",\r\n \"label\": \"Username\",\r\n \"default_value\": \"admin\",\r\n \"is_target_of\": \"\"\r\n },\r\n {\r\n \"id\": \"elasticsearch.password\",\r\n \"type\": \"password\",\r\n \"label\": \"Password\",\r\n \"default_value\": \"admin\",\r\n \"is_target_of\": \"\"\r\n },\r\n {\r\n \"id\": \"elasticsearch.index_name\",\r\n \"type\": \"string\",\r\n \"label\": \"Index Name\",\r\n \"default_value\": \"\",\r\n \"is_target_of\": \"\"\r\n },\r\n {\r\n \"id\": \"elasticsearch.schema\",\r\n \"type\": \"text\",\r\n \"label\": \"Schema\",\r\n \"default_value\": \"{}\",\r\n \"is_target_of\": \"schematize\"\r\n },\r\n {\r\n \"id\": \"elasticsearch.parser\",\r\n \"type\": \"string\",\r\n \"label\": \"Parser\",\r\n \"default_value\": \"http://localhost:9998/tika\",\r\n \"is_target_of\": \"\"\r\n }\r\n]" ,"default":"" ,"id":"properties" ,"keys":[] diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/elasticsearch_query.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/elasticsearch_query.py index 317a5e87f..54879fc4f 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/elasticsearch_query.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/elasticsearch_query.py @@ -55,12 +55,11 @@ def elasticsearch_query( self, REQUEST=None): index_names.append(request.get('facet')) else: # Search in all configured indexes - root_name = self.getRootElement().getHome().id - index_names.append(root_name) + index_name = self.getConfProperty('elasticsearch.index_name', self.getRootElement().getHome().id ) # suggest.fields may be configured explicitly for any index, like 'elasticsearch.suggest.fields.persons = ['lastname','firstname']' - for index_name in [k.split('.')[-1] for k in list(self.getConfProperties(inherited=True)) if k.lower().startswith('elasticsearch.suggest.fields.')]: - if index_name != root_name: - index_names.append(index_name) + index_names = [k.split('.')[-1] for k in list(self.getConfProperties(inherited=True)) if k.lower().startswith('elasticsearch.suggest.fields.')] + if index_name not in index_names: + index_names.append(index_name) # Refs: query on multiple indexes and composite aggregation # https://discuss.elastic.co/t/query-multiple-indexes-but-apply-queries-to-specific-index/127858 @@ -146,4 +145,4 @@ def elasticsearch_query( self, REQUEST=None): except opensearchpy.exceptions.RequestError as e: resp_text = '//%s'%(e.error) - return resp_text + return resp_text \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/elasticsearch_suggest.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/elasticsearch_suggest.py index 98b0d8d25..5149c43ec 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/elasticsearch_suggest.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/elasticsearch_suggest.py @@ -66,7 +66,8 @@ def get_suggest_fieldsets(self): key_prefix = 'elasticsearch.suggest.fields.' default = '["title","attr_dc_subject","attr_dc_description"]' # Define default fieldset for index_name = zms root element id - fieldsets = { self.getRootElement().getHome().id : json.loads(default) } + index_name = self.getConfProperty('elasticsearch.index_name', self.getRootElement().getHome().id ) + fieldsets = { index_name : json.loads(default) } # Get all configured fieldsets (maybe overwrite default fieldset) property_names = [] try: diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_destroy.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_destroy.py index 3e14e2c50..0cc12c9ea 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_destroy.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_destroy.py @@ -43,7 +43,7 @@ def get_elasticsearch_client(self): return client def manage_elasticsearch_destroy( self): - index_name = self.getRootElement().getHome().id + index_name = self.getConfProperty('elasticsearch.index_name', self.getRootElement().getHome().id ) client = get_elasticsearch_client(self) resp_text = '//RESPONSE\n' try: diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_init.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_init.py index d0826d713..88736a1ea 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_init.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_init.py @@ -36,7 +36,7 @@ def get_elasticsearch_client(self): return client def manage_elasticsearch_init( self): - index_name = self.getRootElement().getHome().id + index_name = self.getConfProperty('elasticsearch.index_name', self.getRootElement().getHome().id ) schema = self.getConfProperty('elasticsearch.schema','{}') resp_text = '//RESPONSE\n' client = get_elasticsearch_client(self) diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_add.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_add.py index c1711d930..9c3293adc 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_add.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_add.py @@ -44,7 +44,7 @@ def get_elasticsearch_client(self): def bulk_elasticsearch_index(self, sources): client = get_elasticsearch_client(self) - index_name = self.getRootElement().getHome().id + index_name = self.getConfProperty('elasticsearch.index_name', self.getRootElement().getHome().id ) actions = [] # Name adaption to elasticsearch schema for x in sources: diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_clear.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_clear.py index 19da4522d..d23add501 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_clear.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_clear.py @@ -50,7 +50,8 @@ def bulk_elasticsearch_delete(self, actions): def manage_elasticsearch_objects_clear( self, home_id): index_names = [] - index_names.append(self.getRootElement().getHome().id) + index_name = self.getConfProperty('elasticsearch.index_name', self.getRootElement().getHome().id ) + index_names.append(index_name) query = { "query": { "query_string": { diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_remove.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_remove.py index 7416a818e..e50318d07 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_remove.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/manage_elasticsearch_objects_remove.py @@ -45,7 +45,7 @@ def get_elasticsearch_client(self): def bulk_elasticsearch_delete(self, sources): client = get_elasticsearch_client(self) - index_name = self.getRootElement().getHome().id + index_name = self.getConfProperty('elasticsearch.index_name', self.getRootElement().getHome().id ) actions = [] # Name adaption to elasticsearch schema for x in sources: diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/readme b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/readme index d20e60649..8a7a2dfd1 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/readme +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_connector/readme @@ -1,5 +1,7 @@ # Managing ZMS-Content-Index with Elasticsearch -ZMS offers an [Elasticsearch](https://elasticsearch.org/) connector for the crucial tasks of index management. +ZMS offers an [Elasticsearch](https://elasticsearch.org/) connector for the crucial tasks of index management. + +###### Define the Content Schema to be indexed Before letting Elasticsearch create an index, Elasticsearch needs to know how to index the content. Therefore Elasticsearch needs to get the content "schema" (as a JSON-based list of typed fields). By listing 1. all ZMS content types and @@ -7,9 +9,38 @@ Before letting Elasticsearch create an index, Elasticsearch needs to know how to the _ZMS Catalog Adapter GUI_ helps creating that schema: any content types (usually _pages_) and attributes can be selected to get indexed. Usually the _standard_html_-attribute provides the full text whereas other attributes can be added specifically like _title_, _titlealt_, _description_. Their content may be applied when search results are listed. -ZMS will send to the index only the content of the by _ZMS Catalog Adapter_ selected contenttypes and its selected attributes. So before starting, please make sure that the selections in the _ZMS Catalog Adapter GUI_ is correct. +ZMS will send to the index only the content of the by _ZMS Catalog Adapter_ selected contenttypes and its selected attributes. So before starting, please make sure that the selections in the _ZMS Catalog Adapter GUI_ is correct and take a careful look at the _Custom Filter-Function_ for selecting the documents to be indexed. Especially `breadcrumbs_obj_path()`-expressions may result in unexpected exclusions. Example: + +
+## filter excludes the following objects:
+## - inactive
+## - redirects
+## - robots = noindex, nofollow
+return (context.meta_id in meta_ids) \
+    and len([ob for ob in context.breadcrumbs_obj_path() if not ob.isActive(context.REQUEST)])==0  \
+    and not context.meta_id == 'ZMSFormulator' \
+    and not context.attr('attr_dc_identifier_url_redirect') \
+    and ('noindex' not in context.attr('attr_robots')) \
+    and ('none' not in context.attr('attr_robots')) \
+    and len( [ ob for ob in context.breadcrumbs_obj_path() if ( ( 'nofollow' in ob.attr('attr_robots') ) ) ] )==0 \
+    and context.isVisible(context.REQUEST)
+
+ + +###### Configuration Properties +For communicating with the Elasticsearch server over REST API, the connector needs some properties: + +1. *URL*: the URL(s) of the Elasticsearch server (comma-separated) +2. *Username*: the username for the Elasticsearch server +3. *Password*: the password for the Elasticsearch server +4. *Index Name*: the name of the index to be created. If it is empty, the ZMS root folder name will be used as a default index name (recommended) +5. *Schema*: the data schema of the index in JSON format; it is defined by the general search adapter GUI and created wirh the "Create Schema" action +6. *Parser*: the URL of the parsing application (e.g. Apache Tika) for extracting content from any binary files + +_Hint:_ All these properties are stored in the ZMS configuration and can be managed as well with the ZMS configuration GUI. + -###### Start with _Create Schema_ ! +###### Before writing data to Index a schema is needed: _Create Schema_ ! So the first step to prepare Elasticsearch is _Create Schema_: based on the _ZMS Catalog Adapter_ selections ZMS will save a small JSON file as a configuration parameter. (Note: if Elasticsearch gets no, incomplete or wrong schema, it will guess one from the the transferred content; usually Elasticsearch is not able to guess the keywords types correctly; so this will end up in defect result responses.) diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_page/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_page/__init__.py index 8a9068ec2..982d8028f 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_page/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_page/__init__.py @@ -32,7 +32,7 @@ class elasticsearch_page: package = "com.zms.catalog.elasticsearch" # Revision - revision = "1.5.1" + revision = "1.8.0" # Type type = "ZMSDocument" diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_page/standard_html.zpt b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_page/standard_html.zpt index cf02b731b..69cf74d85 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_page/standard_html.zpt +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.elasticsearch/elasticsearch_page/standard_html.zpt @@ -96,4 +96,4 @@ - + \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.catalog.opensearch/opensearch_connector/readme b/Products/zms/conf/metaobj_manager/com.zms.catalog.opensearch/opensearch_connector/readme index af3f576a7..91616be8f 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.catalog.opensearch/opensearch_connector/readme +++ b/Products/zms/conf/metaobj_manager/com.zms.catalog.opensearch/opensearch_connector/readme @@ -1,5 +1,7 @@ # Managing ZMS-Content-Index with Opensearch ZMS offers an [Opensearch](https://opensearch.org/) connector for the crucial tasks of index management. + +###### Define the Content Schema to be indexed Before letting Opensearch create an index, Opensearch needs to know how to index the content. Therefore Opensearch needs to get the content "schema" (as a JSON-based list of typed fields). By listing 1. all ZMS content types and @@ -25,8 +27,21 @@ return (context.meta_id in meta_ids) \ -###### Start with _Create Schema_ ! +###### Configuration Properties +For communicating with the Elasticsearch server over REST API, the connector needs some properties: + +1. *URL*: the URL(s) of the Elasticsearch server (comma-separated) +2. *Username*: the username for the Elasticsearch server +3. *Password*: the password for the Elasticsearch server +4. *Schema*: the data schema of the index in JSON format; it is defined by the general search adapter GUI and created wirh the "Create Schema" action +5. *Parser*: the URL of the parsing application (e.g. Apache Tika) for extracting content from any binary files +6. Score-Script (Painless): a script for boosting the relevance of certain fields in the search results +7. Suggest-Fields: indexname-specific fieldnames (as a list) for autocomplete suggestions + +_Hint:_ All these properties are stored in the ZMS configuration and can be managed as well with the ZMS configuration GUI. + +###### Before writing data to Index a schema is needed: _Create Schema_ ! So the first step to prepare Opensearch is _Create Schema_: based on the _ZMS Catalog Adapter_ selections ZMS will save a small JSON file as a configuration parameter. (Note: if Opensearch gets no, incomplete or wrong schema, it will guess one from the the transferred content; usually Opensearch is not able to guess the keywords types correctly; so this will end up in defect result responses.) The second step is _Initialisation_, which leads to an empty index based on that content schema and named according to the ZMS root name. After this is done the content can be primarily indexed with "Refresh". From 9d306a7cb1ad7072909b5b56b160fa302f36afd2 Mon Sep 17 00:00:00 2001 From: drfho Date: Thu, 12 Sep 2024 11:55:41 +0200 Subject: [PATCH 014/135] added Products.mcdutils to requirements-full --- requirements-full.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements-full.txt b/requirements-full.txt index eea124611..8a320d6c2 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -33,3 +33,6 @@ asyncio Products.PluggableAuthService Products.PluginRegistry # -e git+https://github.com/sntl-projects/Products.zmsPluggableAuthService.git#egg=Products.zmsPluggableAuthService + +# MemCacheD to replace mappingstorage +Products.mcdutils>=3.2 \ No newline at end of file From 21914644b9c831fb7ae0e12661a745a01334a438 Mon Sep 17 00:00:00 2001 From: drfho Date: Tue, 24 Sep 2024 22:07:39 +0200 Subject: [PATCH 015/135] updated docker data.fs dummy to latest zope --- docker/var/Data.fs | Bin 16094 -> 14666 bytes docker/var/Data.fs.index | Bin 128 -> 9 bytes docker/var/Data.fs.tmp | Bin 5923 -> 5422 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docker/var/Data.fs b/docker/var/Data.fs index 11da5dd03959c8b469cc985ce9dc3e46635c81a4..a108ea618636e8a2540b4314a97d8eccfcc0a4b6 100755 GIT binary patch literal 14666 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 QuIkZM-8yQz%k${J0p}#eQvd(} diff --git a/docker/var/Data.fs.index b/docker/var/Data.fs.index index 3a4b0fe5fefd37b0ac3254c1098922c84e641654..6504fe5750178f1e8fbc68d7864054d8195d897f 100755 GIT binary patch literal 9 QcmZo*_GZy*VD{4k01AHs00000 literal 128 zcmZo*_Wh)+*TC$|#sCJ+89<7Wfr){cfrWvUfsKKkfrEjQfs28gfd{N$FNhTX4kDRi uL8MYNh-C2sk!)aDo)sWA_azW1_#Q-xtAR*a0T8KZ3nG>Nw*hVS(*poIVhz3k diff --git a/docker/var/Data.fs.tmp b/docker/var/Data.fs.tmp index 788eac99cda310218b362fac82cb9b39910ad238..b64576ad0d3ea4c5fe30eca6f2f134e2526b54b7 100755 GIT binary patch literal 5422 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 Date: Wed, 25 Sep 2024 18:06:18 +0200 Subject: [PATCH 016/135] fix(zmi): for zmsindex sitemap limit selection toggle to form context --- .../zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js | 2 +- Products/zms/zpt/ZMSIndex/manage_main.zpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js b/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js index 2c52a83b9..4bb6215e0 100644 --- a/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js +++ b/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js @@ -1755,7 +1755,7 @@ function zmiActionButtonsRefresh(sender,evt) { * @param v Boolean value for new (un-)checked state. */ function zmiToggleSelectionButtonClick(sender,v) { - var $fm = $(sender).parents('form,.zmi-form-container'); + var $fm = $(sender).closest('form,.zmi-form-container'); var $inputs = $('input:checkbox:not([id~="active"]):not([id~="attr_dc_coverage"])',$fm); if (typeof v == "undefined") { v = !$inputs.prop('checked'); diff --git a/Products/zms/zpt/ZMSIndex/manage_main.zpt b/Products/zms/zpt/ZMSIndex/manage_main.zpt index 03b855174..f38e5d01c 100644 --- a/Products/zms/zpt/ZMSIndex/manage_main.zpt +++ b/Products/zms/zpt/ZMSIndex/manage_main.zpt @@ -35,7 +35,7 @@ function openWindow(url) { ZMSIndex
-
+
From 3ac7588de42c93a004dbfea1e8fa66abe303f959 Mon Sep 17 00:00:00 2001 From: drfho Date: Thu, 26 Sep 2024 15:59:49 +0200 Subject: [PATCH 017/135] fix: objattr None-type, updated examplDb --- Products/zms/_objattrs.py | 7 +++++-- Products/zms/import/exampledb.metaobj.xml | 16 ++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Products/zms/_objattrs.py b/Products/zms/_objattrs.py index 392d2d178..624a41350 100644 --- a/Products/zms/_objattrs.py +++ b/Products/zms/_objattrs.py @@ -723,14 +723,17 @@ def isActive(self, REQUEST): def formatObjAttrValue(self, obj_attr, v, lang=None): #-- DATATYPE - datatype = obj_attr.get('datatype_key', _globals.DT_UNKNOWN) + try: + datatype = obj_attr.get('datatype_key', _globals.DT_UNKNOWN) + except: + datatype = _globals.DT_UNKNOWN #-- VALUE if isinstance(v, str): chars = ''.join([ x for x in string.whitespace if x != '\t']) v = v.strip(chars) # Retrieve v from options. - if 'options' in obj_attr: + if obj_attr is not None and 'options' in obj_attr: options = obj_attr['options'] try: i = options.index(int(v)) diff --git a/Products/zms/import/exampledb.metaobj.xml b/Products/zms/import/exampledb.metaobj.xml index 456800ad8..50f1840c3 100644 --- a/Products/zms/import/exampledb.metaobj.xml +++ b/Products/zms/import/exampledb.metaobj.xml @@ -14,7 +14,7 @@ - + @@ -96,7 +96,7 @@ records python:zmscontext.attr(metaObjAttrIds[0]); metaObjAttrs python:[x for x in metaObj['attrs'][1:] if x['id'] not in ['sort_id'] - and x['custom'] + and x.get('custom') and x['type'] in zmscontext.metaobj_manager.valid_types and not x['type'] in ['password'] ]"> @@ -108,7 +108,7 @@
- +
@@ -159,7 +159,7 @@ - + @@ -301,7 +301,7 @@ records python:zmscontext.attr(metaObjAttrIds[0]); metaObjAttrs python:[x for x in metaObj['attrs'][1:] if x['id'] not in ['sort_id'] - and x['custom'] + and x.get('custom') and x['type'] in zmscontext.metaobj_manager.valid_types and not x['type'] in ['password'] ]"> @@ -313,7 +313,7 @@
- +
@@ -364,7 +364,7 @@ - + @@ -480,7 +480,7 @@ - + From 8945bffff55b8436802812089e61aea7047ba732 Mon Sep 17 00:00:00 2001 From: drfho Date: Sat, 28 Sep 2024 01:27:02 +0200 Subject: [PATCH 018/135] zmi: menulock as clickable thumbtack icon --- .../bootstrap/plugin/bootstrap.plugin.zmi.js | 2 +- Products/zms/plugins/www/zmi.core.css | 3 +++ Products/zms/zpt/ZMSObject/input_fields.zpt | 19 ++++++++++++----- Products/zms/zpt/ZMSSqlDb/input_form.zpt | 21 +++++++++++++------ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js b/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js index 4bb6215e0..59a624dba 100644 --- a/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js +++ b/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js @@ -550,7 +550,7 @@ $ZMI.registerReady(function(){ $ZMI.setCursorAuto("EO bootstrap.plugin.zmi"); // Set Save-Button Behaviour: Menu Lock - $('#menulock').prop('checked', JSON.parse($ZMILocalStorageAPI.get('ZMS.menulock',false))); + $('#menulock').val(JSON.parse($ZMILocalStorageAPI.get('ZMS.menulock',0))); // ZMSLightbox $('a.zmslightbox, a.fancybox') diff --git a/Products/zms/plugins/www/zmi.core.css b/Products/zms/plugins/www/zmi.core.css index 9fca5a6df..291eef2e6 100644 --- a/Products/zms/plugins/www/zmi.core.css +++ b/Products/zms/plugins/www/zmi.core.css @@ -1128,6 +1128,9 @@ input.btn.btn-file { text-align:right; width:100%; } +.zmi .controls.save #menulock[value='0'] + #menulock_icon { + color:#bfc8cf +} .zmi .single-line { width:100%; flex-wrap:nowrap; diff --git a/Products/zms/zpt/ZMSObject/input_fields.zpt b/Products/zms/zpt/ZMSObject/input_fields.zpt index eec556574..f6632f3a3 100644 --- a/Products/zms/zpt/ZMSObject/input_fields.zpt +++ b/Products/zms/zpt/ZMSObject/input_fields.zpt @@ -208,12 +208,21 @@
-
- +
+ +
- - + +
diff --git a/Products/zms/zpt/ZMSSqlDb/input_form.zpt b/Products/zms/zpt/ZMSSqlDb/input_form.zpt index 64cf76e06..d02022627 100644 --- a/Products/zms/zpt/ZMSSqlDb/input_form.zpt +++ b/Products/zms/zpt/ZMSSqlDb/input_form.zpt @@ -216,13 +216,22 @@
-
- +
+ +
- - + +
From 7fd9466159a830ffb189d6a834f41961ab81ab4b Mon Sep 17 00:00:00 2001 From: drfho Date: Sat, 28 Sep 2024 18:58:02 +0200 Subject: [PATCH 019/135] zmi: menulock as dialog-element to react on focus-event --- Products/zms/plugins/www/zmi.core.css | 12 ++++++++++-- Products/zms/zpt/ZMSObject/input_fields.zpt | 4 ++-- Products/zms/zpt/ZMSSqlDb/input_form.zpt | 6 +++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Products/zms/plugins/www/zmi.core.css b/Products/zms/plugins/www/zmi.core.css index 291eef2e6..a77c23822 100644 --- a/Products/zms/plugins/www/zmi.core.css +++ b/Products/zms/plugins/www/zmi.core.css @@ -1128,8 +1128,16 @@ input.btn.btn-file { text-align:right; width:100%; } -.zmi .controls.save #menulock[value='0'] + #menulock_icon { - color:#bfc8cf +.zmi .controls.save .btn:not(:last-child) { + margin-right:0.25rem; +} +.zmi .controls.save dialog#menulock_btn { + /* Hint: is dialog element for react on focus/click event */ + display:inline-block; + position: relative; +} +.zmi .controls.save dialog#menulock_btn #menulock[value='0'] + #menulock_icon { + color:#bfc8cf; } .zmi .single-line { width:100%; diff --git a/Products/zms/zpt/ZMSObject/input_fields.zpt b/Products/zms/zpt/ZMSObject/input_fields.zpt index f6632f3a3..aad4d08a9 100644 --- a/Products/zms/zpt/ZMSObject/input_fields.zpt +++ b/Products/zms/zpt/ZMSObject/input_fields.zpt @@ -208,11 +208,11 @@
- + - + +    - +
diff --git a/Products/zms/zpt/ZMSFilterManager/manage_main.zpt b/Products/zms/zpt/ZMSFilterManager/manage_main.zpt index 145d63fd8..24815d6aa 100644 --- a/Products/zms/zpt/ZMSFilterManager/manage_main.zpt +++ b/Products/zms/zpt/ZMSFilterManager/manage_main.zpt @@ -159,7 +159,7 @@ - + @@ -568,9 +568,9 @@ let table_id = $(this).closest('table').attr('id'); // meta_properties or meta_languages let new_row_name = `new_row_${table_id}_${new_row_counter}`; let new_btn_html = ` - - + + `; diff --git a/Products/zms/zpt/ZMSFormatProvider/manage_charformats.zpt b/Products/zms/zpt/ZMSFormatProvider/manage_charformats.zpt index 825179154..fa84a3b8f 100644 --- a/Products/zms/zpt/ZMSFormatProvider/manage_charformats.zpt +++ b/Products/zms/zpt/ZMSFormatProvider/manage_charformats.zpt @@ -54,7 +54,7 @@
- +
diff --git a/Products/zms/zpt/ZMSFormatProvider/manage_textformats.zpt b/Products/zms/zpt/ZMSFormatProvider/manage_textformats.zpt index 95e58e2c1..b28ee810e 100644 --- a/Products/zms/zpt/ZMSFormatProvider/manage_textformats.zpt +++ b/Products/zms/zpt/ZMSFormatProvider/manage_textformats.zpt @@ -63,7 +63,7 @@
- +
diff --git a/Products/zms/zpt/ZMSIndex/manage_main.zpt b/Products/zms/zpt/ZMSIndex/manage_main.zpt index f38e5d01c..83f2db1f6 100644 --- a/Products/zms/zpt/ZMSIndex/manage_main.zpt +++ b/Products/zms/zpt/ZMSIndex/manage_main.zpt @@ -35,7 +35,7 @@ function openWindow(url) { ZMSIndex
-
+
@@ -149,9 +149,9 @@ function openWindow(url) { tal:attributes="value python:zmscontext.operator_getattr(zmscontext,'index_names','')"/>
diff --git a/Products/zms/zpt/ZMSMetamodelProvider/manage_main.zpt b/Products/zms/zpt/ZMSMetamodelProvider/manage_main.zpt index e55d46c5d..3dc02908d 100644 --- a/Products/zms/zpt/ZMSMetamodelProvider/manage_main.zpt +++ b/Products/zms/zpt/ZMSMetamodelProvider/manage_main.zpt @@ -8,7 +8,7 @@ > zmi_html_head - +
zmi_body_header
- + Back
@@ -201,8 +201,8 @@ - + @@ -331,8 +331,8 @@ - - + + @@ -386,7 +386,8 @@ - + - + - + @@ -589,6 +590,21 @@
zmi_body_footer
+ +
zmi_body_footer
\ No newline at end of file diff --git a/Products/zms/zpt/ZMSObject/input_fields.zpt b/Products/zms/zpt/ZMSObject/input_fields.zpt index aad4d08a9..faec558fb 100644 --- a/Products/zms/zpt/ZMSObject/input_fields.zpt +++ b/Products/zms/zpt/ZMSObject/input_fields.zpt @@ -213,10 +213,10 @@ - - + +
diff --git a/Products/zms/zpt/ZMSRecordSet/input_fields.zpt b/Products/zms/zpt/ZMSRecordSet/input_fields.zpt index 58934f6e9..dbaf6343e 100644 --- a/Products/zms/zpt/ZMSRecordSet/input_fields.zpt +++ b/Products/zms/zpt/ZMSRecordSet/input_fields.zpt @@ -111,9 +111,15 @@
- - + +
diff --git a/Products/zms/zpt/ZMSRecordSet/main.zpt b/Products/zms/zpt/ZMSRecordSet/main.zpt index 726a63152..eb679ed9a 100644 --- a/Products/zms/zpt/ZMSRecordSet/main.zpt +++ b/Products/zms/zpt/ZMSRecordSet/main.zpt @@ -74,7 +74,7 @@ recordSet_Interface*:interface will only be display for grid-view (UzK)
-
+
-
+
- diff --git a/Products/zms/zpt/common/zmi_manage_tabs_message.zpt b/Products/zms/zpt/common/zmi_manage_tabs_message.zpt index f6b2f8504..47532991c 100644 --- a/Products/zms/zpt/common/zmi_manage_tabs_message.zpt +++ b/Products/zms/zpt/common/zmi_manage_tabs_message.zpt @@ -1,42 +1,39 @@ - -
- × - manage_tabs_message - (ZopeTime) - - +
+
+ × + manage_tabs_message + (ZopeTime) + + +
+
+ × + manage_tabs_warning_message + (ZopeTime) + + +
+
+ × + manage_tabs_danger_message + (ZopeTime) + + +
+
+ × + manage_tabs_error_message + (ZopeTime) + +
-
- × - manage_tabs_warning_message - (ZopeTime) - - -
- -
- × - manage_tabs_danger_message - (ZopeTime) - - -
- -
- × - manage_tabs_error_message - (ZopeTime) - -
- - \ No newline at end of file diff --git a/docker/alpine/docker-compose.yml b/docker/alpine/docker-compose.yml new file mode 100755 index 000000000..d6b62e674 --- /dev/null +++ b/docker/alpine/docker-compose.yml @@ -0,0 +1,23 @@ +version: "3.7" +services: + zms5: + build: . + image: zms5:latest + ports: + - 8085:8085 + - 8086:8086 + - 8080:8080 + - 5678:5678 + environment: + - PYTHONUNBUFFERED="1" + - CONFIG_FILE="/home/zope/venv/instance/zms5/etc/zope.ini" + - INSTANCE_HOME="/home/zope/venv/instance/zms5" + - CLIENT_HOME="/home/zope/venv/instance/zms5" + - PYTHON="/home/zope/venv/bin/python" + - SOFTWARE_HOME="/home/zope/venv/bin" + volumes: + - ./etc/:/home/zope/venv/instance/zms5/etc/ + - ./Extensions/:/home/zope/venv/instance/zms5/Extensions/:rw + - ./var/:/home/zope/venv/instance/zms5/var/ + + # command: /home/zope/venv/instance/zms5/etc/start.sh diff --git a/docker/alpine/dockerfile b/docker/alpine/dockerfile new file mode 100755 index 000000000..666b1a056 --- /dev/null +++ b/docker/alpine/dockerfile @@ -0,0 +1,37 @@ +FROM alpine + +EXPOSE 8085 +EXPOSE 8086 +EXPOSE 8080 +EXPOSE 5678 + +# Install Zope/ZEO Dependencies +RUN apk add python3 make gcc g++ git +RUN apk update && apk add python3-dev git mariadb-dev openldap-dev curl bash + +# https://stackoverflow.com/questions/49955097/how-do-i-add-a-user-when-im-using-alpine-as-a-base-image +RUN addgroup -S zope && adduser --disabled-password -S zope -G zope +USER zope +WORKDIR /home/zope/ +ENV VIRTUAL_ENV=/home/zope/venv + +RUN python3 -m venv venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN pip install -U pip wheel setuptools +RUN pip install -U -e git+https://github.com/zms-publishing/ZMS.git@main#egg=ZMS +RUN pip install -r https://raw.githubusercontent.com/zms-publishing/ZMS5/master/requirements-full.txt +RUN pip install ZEO +RUN pip install itsdangerous +RUN pip install debugpy + +# Create Zope Instance +RUN mkwsgiinstance -d venv/instance/zms5 -u admin:admin + +COPY ./etc venv/instance/zms5/etc +COPY ./var venv/instance/zms5/var +COPY ./Extensions venv/instance/zms5/Extensions + + +# Finally Start ZEO/Zope by Script +# ENTRYPOINT ["/bin/sh -c"] +CMD ["/home/zope/venv/instance/zms5/etc/start.sh"] \ No newline at end of file diff --git a/docker/alpine/etc/Extensions/readme.md b/docker/alpine/etc/Extensions/readme.md new file mode 100755 index 000000000..820867944 --- /dev/null +++ b/docker/alpine/etc/Extensions/readme.md @@ -0,0 +1 @@ +# Externalizing Extensions for Docker \ No newline at end of file diff --git a/docker/alpine/etc/start.sh b/docker/alpine/etc/start.sh new file mode 100755 index 000000000..96924caeb --- /dev/null +++ b/docker/alpine/etc/start.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# The ZEO/Zope start script works in two steps: +# 1. ZEO server is started silently (nohub) +# 2. Zope instance it started on parameter defined port:8085 +# Sending Zope's output not to dev/null but the console maintains +# docker running + +instance_dir="/home/zope/venv/instance/zms5" +venv_bin_dir="/home/zope/venv/bin" + +echo "Step-1: Starting ZEO" +nohup $venv_bin_dir/runzeo --configure $instance_dir/etc/zeo.conf 1>/dev/null 2>/dev/null & + +echo "Step-2: Starting ZOPE 8085" +$venv_bin_dir/runwsgi --debug --verbose $instance_dir/etc/zope.ini debug-mode=on http_port=8085 + + diff --git a/docker/alpine/etc/zeo.conf b/docker/alpine/etc/zeo.conf new file mode 100755 index 000000000..92edb2d77 --- /dev/null +++ b/docker/alpine/etc/zeo.conf @@ -0,0 +1,16 @@ +%define INSTANCE /home/zope/venv/instance/zms5 + + + address $INSTANCE/var/zeosocket + + + + + path $INSTANCE/var/log/zeo.log + format %(asctime)s %(message)s + + + + + path $INSTANCE/var/Data.fs + \ No newline at end of file diff --git a/docker/alpine/etc/zope.conf b/docker/alpine/etc/zope.conf new file mode 100755 index 000000000..2dc653171 --- /dev/null +++ b/docker/alpine/etc/zope.conf @@ -0,0 +1,266 @@ +%define INSTANCE /home/zope/venv/instance/zms5 + +instancehome $INSTANCE + +%import ZEO + + + # Main FileStorage database + + server $INSTANCE/var/zeosocket + storage main + name zeostorage Data.fs + client-label zms5 8085 + + mount-point / + + + +# +# +# path $INSTANCE/var/Data.fs +# +# mount-point / +# + + +# Uncomment this if you use Products.Sessions and Products.TemporaryFolder +# +# +# name Temporary database (for sessions) +# +# mount-point /temp_folder +# container-class Products.TemporaryFolder.TemporaryContainer +# + + +# Directive: locale +# +# Description: +# Overwrite the locale settings found in the environment by supplying a +# locale name to be used. See your operating system documentation for +# locale information specific to your system. If the requested locale is +# not supported by your system, an error will be raised and Zope will not +# start. +# +# Default: unset +# +# Example: +# +# locale fr_FR + + +# Directive: environment +# +# Description: +# A section which can be used to define arbitrary key-value pairs +# for use as environment variables during Zope's run cycle. It +# is not recommended to set system-related environment variables such as +# PYTHONPATH within this section. +# +# Default: unset +# +# Example: +# +# +# MY_PRODUCT_ENVVAR foobar +# + + CHAMELEON_CACHE $INSTANCE/var/cache + + + +# Directive: debug-mode +# +# Description: +# A switch which controls several aspects of Zope operation useful for +# developing under Zope. When debug mode is on: +# +# - The process will not detach from the controlling terminal +# +# - Errors in product initialization will cause startup to fail +# (instead of writing error messages to the event log file). +# +# - Filesystem-based scripts such as skins, PageTemplateFiles, and +# DTMLFiles can be edited while the server is running and the server +# will detect these changes in real time. When this switch is +# off, you must restart the server to see the changes. +# +# Setting this to 'off' when Zope is in a production environment is +# encouraged, as it speeds execution (sometimes dramatically). +# +# Default: off +# +# Example: +# +debug-mode on + + +# Directive: debug-exceptions +# +# Description: +# This switch controls how exceptions are handled. If it is set to +# "off" (the default), Zope's own exception handling is active. +# Exception views or a standard_error_message are used to handle them. +# +# If set to "on", exceptions are not handled by Zope and can propagate +# into the WSGI pipeline, where they may be handled by debugging +# middleware. +# +# This setting should always be "off" in production. It is useful for +# developers and while debugging site issues. +# +# Default: off +# +# Example: +# +# debug-exceptions on + + +# Directive: http-realm +# +# Description: +# The HTTP "Realm" header value sent by this Zope instance. This value +# often shows up in basic authentication dialogs. +# +# Default: Zope +# +# Example: +# +# http-realm Slipknot + + +# Directive: webdav-source-port +# +# Description: +# This value designates a network port number as WebDAV source port. +# +# If this value is set to a positive integer, any GET request coming into +# Zope via the designated port will be marked up to signal that this is a +# WebDAV request. This request markup resembles what ZServer did for +# requests coming though its designated WebDAV source server port, so it is +# backwards-compatible for existing code that offers WebDAV handling under +# ZServer. +# +# Please note that Zope itself has no server capabilities and cannot open +# network ports. You need to configure your WSGI server to listen on the +# designated port. +# +# Default: Off +# +# Example: +# +# webdav-source-port 9800 + + +# Directive: zmi-bookmarkable-urls +# +# Description: +# Set this directive to 'on' to cause Zope to show the ZMI right hand +# frame's URL in the browser navigation bar as opposed to the static +# '/manage'. The default is 'on'. To restore the behavior of Zope 2 +# where the URL was always static unless you opened the right-hand frame in +# its own browser window, set this to off. +# +# Default: on +# +# Example: +# +# zmi-bookmarkable-urls off + + +# Directive: pid-filename +# +# Description: +# The path to the file in which the Zope process id(s) will be written. +# This defaults to client-home/Z4.pid. +# +# Default: CLIENT_HOME/Z4.pid +# +# Example: +# +# pid-filename /home/chrism/projects/sessions/var/Z4.pid + + +# Directive: trusted-proxy +# +# Description: +# Define one or more 'trusted-proxies' directives, each of which is a +# hostname or an IP address. The set of definitions comprises a list +# of front-end proxies that are trusted to supply an accurate +# X-Forwarded-For header to Zope. If a connection comes from +# a trusted proxy, Zope will trust any X-Forwarded header to contain +# the user's real IP address for the purposes of address-based +# authentication restriction. +# +# Default: unset +# +# Example: +# +# trusted-proxy www.example.com +# trusted-proxy 192.168.1.1 + + +# Directive: security-policy-implementation +# +# Description: +# The default Zope security machinery is implemented in C. Change +# this to "python" to use the Python version of the Zope security +# machinery. This setting may impact performance but is useful +# for debugging purposes. See also the "verbose-security" option +# below. +# +# Default: C +# +# Example: +# +# security-policy-implementation python + + +# Directive: skip-authentication-checking +# +# Description: +# Set this directive to 'on' to cause Zope to skip checks related +# to authentication, for servers which serve only anonymous content. +# Only works if security-policy-implementation is 'C'. +# +# Default: off +# +# Example: +# +# skip-authentication-checking on + + +# Directive: skip-ownership-checking +# +# Description: +# Set this directive to 'on' to cause Zope to ignore ownership checking +# when attempting to execute "through the web" code. By default, this +# directive is on in order to prevent 'trojan horse' security problems +# whereby a user with less privilege can cause a user with more +# privilege to execute dangerous code. +# +# Default: off +# +# Example: +# +# skip-ownership-checking on + + +# Directive: verbose-security +# +# Description: +# By default, Zope reports authorization failures in a terse manner in +# order to avoid revealing unnecessary information. This option +# modifies the Zope security policy to report more information about +# the reason for authorization failures. It's designed for debugging. +# If you enable this option, you must also set the +# 'security-policy-implementation' to 'python'. +# +# Default: off +# +# Example: +# +# security-policy-implementation python +# verbose-security on + diff --git a/docker/alpine/etc/zope.ini b/docker/alpine/etc/zope.ini new file mode 100755 index 000000000..7fdb1f707 --- /dev/null +++ b/docker/alpine/etc/zope.ini @@ -0,0 +1,75 @@ +[app:zope] +use = egg:Zope#main +zope_conf = %(here)s/zope.conf + +[server:main] +use = egg:waitress#main +# host 127.0.0.1 +host = 0.0.0.0 +# port = 8080 +port = %(http_port)s + +[filter:translogger] +use = egg:Paste#translogger +setup_console_handler = False + +[pipeline:main] +pipeline = + egg:Zope#httpexceptions + translogger + zope + +[loggers] +keys = root, waitress.queue, waitress, wsgi + +[handlers] +keys = console, accesslog, eventlog + +[formatters] +keys = generic, message + +[formatter_generic] +format = %(asctime)s %(levelname)s [%(name)s:%(lineno)s][%(threadName)s] %(message)s +datefmt = %Y-%m-%d %H:%M:%S + +[formatter_message] +format = %(message)s + +[logger_root] +level = INFO +handlers = console, eventlog + +[logger_waitress.queue] +level = INFO +handlers = eventlog +qualname = waitress.queue +propagate = 0 + +[logger_waitress] +level = INFO +handlers = eventlog +qualname = waitress + +[logger_wsgi] +level = WARN +handlers = accesslog +qualname = wsgi +propagate = 0 + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[handler_accesslog] +class = FileHandler +args = ('/home/zope/venv/instance/zms5/var/log/Z4.log','a') +level = INFO +formatter = message + +[handler_eventlog] +class = FileHandler +args = ('/home/zope/venv/instance/zms5/var/log/event.log', 'a') +level = INFO +formatter = generic diff --git a/docker/alpine/readme.md b/docker/alpine/readme.md new file mode 100755 index 000000000..c60894988 --- /dev/null +++ b/docker/alpine/readme.md @@ -0,0 +1,71 @@ +# Running ZMS in a Docker container with Alpine Linux + +Important: *The here presented Docker environment is not recommended for production, just for testing and exploration.* + +The ZMS source folder `./docker` contains two minimalistic Docker files: +1. the [dockerfile](https://github.com/zms-publishing/ZMS/blob/main/docker/dockerfile) for creating a Docker *image* and +2. the [docker-compose](https://github.com/zms-publishing/ZMS/blob/main/docker/docker-compose.yml) file for building a Docker *container*. + +The image utilizes a minimal *alpine*-Linux with a fresh compiled Python3 and some additional software packages (like mariadb and openldap). The ZMS installation happens with pip in a successively created virtual python environment (`/home/zope/venv`) and provides the ZMS code in the pip-"editable" mode. Both the ZMS source code (`/home/zope/venv/src/ZMS/.git`) and the Zope instance are placed in the virtual python environment folder (`/home/zope/venv/instance/zms5`) + +To make Zope running there are some crucial config files needed which usually (created by `mkwsgiinstance`) are set on default values. In a Docker environment these defaults must be modified; moreover the setup contains a ZEO-server for running multiple Zope processes in parallel (e.g. for debugging). That is why a small set of config files is provided as presets via the the source-folders +1. ./docker/var +2. ./docker/etc +3. ./docker/Extensions + +These sources will be copied into the *image* (on building) +```yaml +# dockerfile +COPY ./etc venv/instance/zms5/etc +COPY ./var venv/instance/zms5/var +COPY ./Extensions venv/instance/zms5/Extensions +``` +or referenced as *volume mounts* from the *container* (on composing): +```yaml +# docker-compose + volumes: + - ./etc/:/home/zope/venv/instance/zms5/etc/ + - ./var/:/home/zope/venv/instance/zms5/var/ + - ./Extensions/: /home/zope/venv/instance/zms5/Extensions +``` + + +## Overview of Docker- and all Zope config-files + +*Hint: to ease the file access from the container the config files are not restricted:* `chmod -r 777` +``` +$ tree -p +. +├── [-rw-r--r--] docker-compose.yml +├── [-rw-r--r--] dockerfile +├── [drwxrwxrwx] Extensions +├── [drwxrwxrwx] etc +│ ├── [-rwxrwxrwx] start.sh +│ ├── [-rwxrwxrwx] zeo.conf +│ ├── [-rwxrwxrwx] zope.conf +│ └── [-rwxrwxrwx] zope.ini +└── [drwxrwxrwx] var + ├── [-rwxrwxrwx] Data.fs + ├── [-rwxrwxrwx] Data.fs.index + ├── [-rwxrwxrwx] Data.fs.lock + ├── [-rwxrwxrwx] Data.fs.tmp + ├── [-rwxrwxrwx] Z4.pid + ├── [drwxrwxrwx] cache + ├── [drwxrwxrwx] log + │ ├── [-rwxrwxrwx] Z4.log + │ ├── [-rwxrwxrwx] event.log + │ └── [-rwxrwxrwx] zeo.log + └── [srwxrwxrwx] zeosocket +``` + +## Running the ZMS Container with VSCode + +The VSCode Docker Extension [ms-azuretools.vscode-docker](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker) is a perfect tool for handling containers. A right mouse click on the file ´docker-compose.yaml´ starts composing the container. Initially ZEO will be started and Zope will run on port 8085. + +![Running the ZMS Container with VSCode](../docs/images/admin_docker_run.gif) + +## Attach VSCode to the ZMS Container +Another right click on the running container-ID allows to intrude the container with VSCode and launch a new Zope instance in debugging mode. +Hint: For this purpose the docker-container folder `/home/zope/venv/src/zms/docker/.vscode/` contains a prepared VSCode-workspace file and a launch file for starting Zope in debug-mode within the container [launch.json](https://github.com/zms-publishing/ZMS/blob/main/docker/.vscode/launch.json). The thus launched Zope instance will run port 8087. + +![Attach VSCode to the ZMS Container](../docs/images/admin_docker_debug_zeo.gif) \ No newline at end of file diff --git a/docker/alpine/var/Data.fs b/docker/alpine/var/Data.fs new file mode 100755 index 0000000000000000000000000000000000000000..f4a46beaec72f117070a1f5fdb9f0afed102906c GIT binary patch literal 17825 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 HlR){uUQIx! literal 0 HcmV?d00001 diff --git a/docker/alpine/var/Data.fs.index b/docker/alpine/var/Data.fs.index new file mode 100755 index 0000000000000000000000000000000000000000..a71f453e1f63f3b61aa5ca2557c9f4c6129a8b8c GIT binary patch literal 419 zcmW;JJ1+zQ6o%nAPUH_=Z4Y%%LvwG&(c4)yRfk%=y+DXw(nhaTT5{@1)S_spuyy_!vPQD%S4Yjej;%glp$ETfC&|6?}dKc?9x;s5{u literal 0 HcmV?d00001 diff --git a/docker/alpine/var/Data.fs.lock b/docker/alpine/var/Data.fs.lock new file mode 100755 index 000000000..c7c9ea733 --- /dev/null +++ b/docker/alpine/var/Data.fs.lock @@ -0,0 +1 @@ + 7 diff --git a/docker/alpine/var/Data.fs.tmp b/docker/alpine/var/Data.fs.tmp new file mode 100755 index 0000000000000000000000000000000000000000..1e8336e3688c491a97496d39170ca8e71678eb05 GIT binary patch literal 399 zcmZ{f&rX9t5XKP-R8BsIC(^A8EXRgRFHJPYgo`2jkA_5+4oefGA?ZcmMc=|>PdY#uX?wlS@@`*VUw4-46;N-5^sHiIilWaQLdY|C(1?N)htWjOt+=W|hGJ+e^ z1Mr@~ztTGVdHk1gR4wa>@y#&)Z+E~tAGj2)CD=z$PHr2=qA+yJ$Lr-{9>qQ3ahk+3 zVi86&j3OpvIwMSAAtj1L$xxiol#}G5fE87A WEnD!PU^)n*scEG&oZdpP4Zi_=%4ov? literal 0 HcmV?d00001 diff --git a/docker/alpine/var/Z4.pid b/docker/alpine/var/Z4.pid new file mode 100755 index 000000000..301160a93 --- /dev/null +++ b/docker/alpine/var/Z4.pid @@ -0,0 +1 @@ +8 \ No newline at end of file diff --git a/docker/alpine/var/log/Z4.log b/docker/alpine/var/log/Z4.log new file mode 100755 index 000000000..e69de29bb diff --git a/docker/alpine/var/log/event.log b/docker/alpine/var/log/event.log new file mode 100755 index 000000000..e69de29bb diff --git a/docker/alpine/var/log/zeo.log b/docker/alpine/var/log/zeo.log new file mode 100755 index 000000000..e69de29bb From fb26173e1389deac0ea7f9eee64949d608467982 Mon Sep 17 00:00:00 2001 From: drfho Date: Fri, 4 Oct 2024 15:19:15 +0200 Subject: [PATCH 067/135] zmi(metas): faster by JS-populating select-lists (#318) Montonized existing JS function zmiPopulateTypeSelect() for both metas and content-models from tr.row_insert. --- .../zpt/ZMSMetamodelProvider/manage_main.zpt | 702 +++++++++--------- .../zpt/ZMSMetamodelProvider/manage_metas.zpt | 88 ++- 2 files changed, 415 insertions(+), 375 deletions(-) diff --git a/Products/zms/zpt/ZMSMetamodelProvider/manage_main.zpt b/Products/zms/zpt/ZMSMetamodelProvider/manage_main.zpt index 3dc02908d..f4890671e 100644 --- a/Products/zms/zpt/ZMSMetamodelProvider/manage_main.zpt +++ b/Products/zms/zpt/ZMSMetamodelProvider/manage_main.zpt @@ -61,35 +61,6 @@ - -
From 7646692976f0d18c355a1e7419b04178963fefbc Mon Sep 17 00:00:00 2001 From: drfho Date: Sun, 20 Oct 2024 18:02:48 +0200 Subject: [PATCH 101/135] readme-attr: show simple constant/string-attr additionally to markdown/file-resource --- Products/zms/zpt/ZMSObject/input_fields.zpt | 40 +++++++++++++-------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/Products/zms/zpt/ZMSObject/input_fields.zpt b/Products/zms/zpt/ZMSObject/input_fields.zpt index faec558fb..229cc3609 100644 --- a/Products/zms/zpt/ZMSObject/input_fields.zpt +++ b/Products/zms/zpt/ZMSObject/input_fields.zpt @@ -312,26 +312,36 @@
- From 8fb5e6496a4e46a65f0f5186a62bf09d30eec421 Mon Sep 17 00:00:00 2001 From: drfho Date: Sun, 20 Oct 2024 18:16:15 +0200 Subject: [PATCH 102/135] fix: bt_link_list sorting --- .../com.zms.foundation.bootstrap/bt_link_list/__init__.py | 2 +- .../bt_link_list/interface0.zpt | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/__init__.py index 03882e8cc..eea284765 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/__init__.py @@ -26,7 +26,7 @@ class bt_link_list: package = "com.zms.foundation.bootstrap" # Revision - revision = "5.0.7" + revision = "5.0.8" # Type type = "ZMSResource" diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/interface0.zpt b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/interface0.zpt index 24159903d..47a30b01e 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/interface0.zpt +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/interface0.zpt @@ -116,6 +116,10 @@ refreshView(); $ZMI.initUrlInput($("tr.url-inputs",$table)); }); + + // Execute Creating GUI + refreshView(); + // ###################################### // HELPER: Sort Button // ###################################### @@ -135,8 +139,6 @@ refreshLinks(); }) - // Execute Creating GUI - refreshView(); } // ###################################### From 3901241d2c2fc6644be1f0dfe6fa3854c8d21488 Mon Sep 17 00:00:00 2001 From: drfho Date: Sun, 20 Oct 2024 22:34:09 +0200 Subject: [PATCH 103/135] zmi: minor fix of action manage_tab_tasks shifted spinner-style to onclick-attribute --- .../manage_tab_tasks/__init__.py | 2 +- .../manage_tab_tasks/manage_tab_tasks.zpt | 45 ++++++++++--------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py b/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py index f698749fa..833b21843 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py +++ b/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py @@ -34,7 +34,7 @@ class manage_tab_tasks: package = "com.zms.foundation.metacmd.tabs" # Revision - revision = "5.0.1" + revision = "5.0.2" # Roles roles = ["*"] diff --git a/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt b/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt index 1028064b1..01c3765e8 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt +++ b/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt @@ -10,16 +10,20 @@ task_ids python:['TASK_ZMSNOTE', 'TASK_UNTRANSLATED', 'TASK_CHANGED_BY_DATE','TASK_INACTIVE_NODES']; global obs python:[]"> - + the task-type
- +
- @@ -59,9 +63,9 @@ + obj_tuples python:[ ( x.attr('change_dt'), x ) for x in [here]+here.getTreeNodes(request) ]; + obj_tuples python:sorted(obj_tuples, key=lambda obj: obj[0], reverse=True); + global obs python:[obj[1] for obj in obj_tuples]"> @@ -76,7 +80,7 @@ - zmi_manage_main_grid @@ -87,26 +91,27 @@
zmi_body_footer
- \ No newline at end of file From 8036fdeab144efa1c573568e8ae8d59829c39bd5 Mon Sep 17 00:00:00 2001 From: drfho Date: Mon, 21 Oct 2024 12:37:27 +0200 Subject: [PATCH 104/135] zmi: minor fix of action manage_tab_tasks / wf (minerva) --- .../manage_tab_tasks/__init__.py | 2 +- .../manage_tab_tasks/manage_tab_tasks.zpt | 26 +++++++++++-------- Products/zms/import/example1.workflow.xml | 8 +++--- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py b/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py index 833b21843..9afd8ac9a 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py +++ b/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py @@ -34,7 +34,7 @@ class manage_tab_tasks: package = "com.zms.foundation.metacmd.tabs" # Revision - revision = "5.0.2" + revision = "5.0.3" # Roles roles = ["*"] diff --git a/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt b/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt index 01c3765e8..7d81decbe 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt +++ b/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt @@ -10,24 +10,28 @@ task_ids python:['TASK_ZMSNOTE', 'TASK_UNTRANSLATED', 'TASK_CHANGED_BY_DATE','TASK_INACTIVE_NODES']; global obs python:[]"> - + the task-type
-
-
- -
-
- +
+ +
diff --git a/Products/zms/import/example1.workflow.xml b/Products/zms/import/example1.workflow.xml index 3241b052f..b5969a9d6 100644 --- a/Products/zms/import/example1.workflow.xml +++ b/Products/zms/import/example1.workflow.xml @@ -106,12 +106,12 @@
-

+
- +
@@ -191,12 +191,12 @@
-

+
- +
From f45079e3a048308c1463369695b776af94ecfa5b Mon Sep 17 00:00:00 2001 From: drfho Date: Mon, 21 Oct 2024 12:57:06 +0200 Subject: [PATCH 105/135] fix(zmi): hilighting action nav tabs --- Products/zms/zpt/common/zmi_tabs.zpt | 1 + 1 file changed, 1 insertion(+) diff --git a/Products/zms/zpt/common/zmi_tabs.zpt b/Products/zms/zpt/common/zmi_tabs.zpt index 6abd79693..22803527e 100644 --- a/Products/zms/zpt/common/zmi_tabs.zpt +++ b/Products/zms/zpt/common/zmi_tabs.zpt @@ -6,6 +6,7 @@ active python:[ (x==current and not here.id+'/'+current in actions) or (x==here.id+'/'+current) or + (x==request.get('id')) or (current not in actions and current.startswith(x)) for x in actions]; noactive python:len([x for x in active if x])<1;" > Date: Mon, 21 Oct 2024 13:07:33 +0200 Subject: [PATCH 106/135] zmi: minor css-fix of action manage_tab_tasks (minerva) --- Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py | 2 +- .../conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py b/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py index 9afd8ac9a..2856b5143 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py +++ b/Products/zms/conf/metacmd_manager/manage_tab_tasks/__init__.py @@ -34,7 +34,7 @@ class manage_tab_tasks: package = "com.zms.foundation.metacmd.tabs" # Revision - revision = "5.0.3" + revision = "5.0.4" # Roles roles = ["*"] diff --git a/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt b/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt index 7d81decbe..bed2f17ed 100644 --- a/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt +++ b/Products/zms/conf/metacmd_manager/manage_tab_tasks/manage_tab_tasks.zpt @@ -115,6 +115,9 @@ form.task_list .zmi-manage-main-change i { line-height: 1.25; } + .form-group.task_type .btn { + min-width: 8rem; + } /* --> */ From 8d042f1944c4439c2dbe037d0d3f573e24ab23bc Mon Sep 17 00:00:00 2001 From: drfho Date: Mon, 21 Oct 2024 14:53:06 +0200 Subject: [PATCH 107/135] added zms-favicon to www-plugins --- Products/zms/plugins/www/img/favicon.ico | Bin 0 -> 31646 bytes Products/zms/plugins/www/img/svg/zmslogo.svg | 52 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 Products/zms/plugins/www/img/favicon.ico create mode 100644 Products/zms/plugins/www/img/svg/zmslogo.svg diff --git a/Products/zms/plugins/www/img/favicon.ico b/Products/zms/plugins/www/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2583636c38d89d1a4de24ea8b30865b8fd41047c GIT binary patch literal 31646 zcmeHP3vg8Dbv_aT34_GS(@w}^-5pXp&eUK{Q`bowoLk3sXv#z5h6za!-kHSUI0ZDW zZQ{m@H|^NQsf~A%n#a^&ji;#}HCRm=+leiCjYaCm6$p@cU$Gz%l0g@lhX8}ve&?M3 zzk62_%w*!WX?>4<|HpZJ=ltjY|9k(vT9&AUDkv6K0dL>aNqy4M@gy`O{ z5M2$QF%8Pm4sN+_StYv)5OOR7ssVzKI232{w@I;MSags*jVj6@$%l`voG z7%Kn=1A+X%i8xOaan;}o=jkf+`Ld2X{;hz#D&Ob?wn z{4(wQ*+e?{_FOu9vXlJ1PmuT8b;#=l>e+G+o$22Uqz{M6s;fVBc}ZsfqetdasrT=f zcg|>7_vL;2sA6HRv~x}Sy4GjC1U1!f#-=05Pc%zpi8tYB>I-9&(_Fyqpc(;06`@Gh@UYe-sey@Fvw9Ec=;nQBj4DYYJ zJyfxJ^{Um0M^>lbPJB3n`X4~ydFE&xT;X~C9Qu5P=lST)f293yew7CNcd2{Ro#g#; z3ccU;9Cf`lht8krrCmS&Bze;$P_D^Ti=jkNY!T&lPxa+Qa4&;8E`0+jT znHv#J-1FMw+0?GO_Mdp(7tr56>B@L4zO}D?#`eLzM3Z-BD4w0#Uf15*Ie81wDy*OQ z$Gt6m=%>&h=fx;~xwj4Sjwa}rde2eZ^}N3J!%MxbSUZn(Qhf7nZ*n{JKox6d3&orE zPHk!*y>1U@Befeb?1+2t|4Z(#@e*;bbB5QR+Ie&)`s2K8+}kzTYj53~+JSz#*AVw= z`q1CO{jV?aI^$k!74)|?!C%Vrw#GeYl-It(^Uf08I%(2p4@}BbO}df}FJ=6XHt{;= zdsz}!c%4s2pRb~Ij{DjE4PT*wzPD%(?q`Qu7tr|+j$nO%oQ@xS0r#>!w6FQksdwAo zVXd!}_0P|W6Ng^`Qbd=|z!IkNCG}K}|CeB0-{i2tXusQE)cw=$B&}UY7rGlN*ZRG+ z;8*%g>-+^}{t>$1H#DR7H_C7Iyqo=%rG6Lq|Kj^!@Vgs`F7y2?TP+yp`}NEH=l$Nh zeZLI`YyAc6W#uT}Z@Az;?RS+g68=2@fauSJ+6CwzD_8yX{(jM4@B59*MPK=2{O)?- zubk)mHH>o%5i&mfC4is2;QNOW&+NK;ecu5e0mV3;MZSL!@l*K?7zn7G0~+lw@Vgcr zVEzNX|1k5Z*?$lEPxyVy_k&-)+&@+?{PnOK?f3fcHFq!a8@})Rr-e=TA|TGv!A=wn z^Q<}o>TVbf*2?uLo>}CteExYcKtPSGpPh-~dEeimaT4;elJ#rvL-DkjIAU;x{~qfn zCMbW2(Wfc?A=Y=e0M1Q|d=7N>>}J0UEDZ{NANPB~zO1|37k&?C#BbnW`@k=o4_Z8W z{`}je*--kB-<1HLWSyV{CYr%IVNpa;27X5~z|R>6R~lFNIdePue8q4v>ACqK%Dr*3 zK4)IJTF%V;eCckz1J9aI$vNuG@g2lxD1N^5Y`vEbzV!_}XI@f}sEjf&ECXr7d1SRv z=8@H_sBHBbT&rVL(R-Lv||0r;P@h;m{!IOjfHFUq|x^J<4SY82nLgX#nY_aww#-5FmY7bL)gRyV4 z_;geQneMi*t2eAY;uHTgT?YXF6Ci)n`UkrOutXTZ+k)SMVY%+#1MZRUu9)_M!pZ

Y3(1PR|DS09Sb1KJF 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 literal 0 HcmV?d00001 diff --git a/Products/zms/plugins/www/img/svg/zmslogo.svg b/Products/zms/plugins/www/img/svg/zmslogo.svg new file mode 100644 index 000000000..c050b756c --- /dev/null +++ b/Products/zms/plugins/www/img/svg/zmslogo.svg @@ -0,0 +1,52 @@ + + + + + + + + + + From 76926f210b2d5172a13f8e6412bd813d65f290a9 Mon Sep 17 00:00:00 2001 From: drfho Date: Mon, 21 Oct 2024 22:08:13 +0200 Subject: [PATCH 108/135] bt_link_list: simplyfied by using css for hiding xml-textarea --- .../bt_link_list/__init__.py | 2 +- .../bt_link_list/interface0.zpt | 34 ++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/__init__.py index eea284765..528772e45 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/__init__.py @@ -26,7 +26,7 @@ class bt_link_list: package = "com.zms.foundation.bootstrap" # Revision - revision = "5.0.8" + revision = "5.0.9" # Type type = "ZMSResource" diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/interface0.zpt b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/interface0.zpt index 47a30b01e..29e0beecf 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/interface0.zpt +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_link_list/interface0.zpt @@ -14,12 +14,21 @@

+ From 2d0fec3c598e745a6b832ed9a5c1c916284be3b1 Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 23 Oct 2024 13:04:08 +0200 Subject: [PATCH 109/135] fix(htmx): restored all body attrs on htmx:afterRequest, whitespace Background: history-array etc. depends on body data-attrs --- .../bootstrap/plugin/bootstrap.plugin.zmi.js | 22 ++++++++--------- Products/zms/plugins/www/zmi.js | 24 ++++++++++++++----- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js b/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js index 9f5e8f1a9..a156f6491 100644 --- a/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js +++ b/Products/zms/plugins/www/bootstrap/plugin/bootstrap.plugin.zmi.js @@ -27,13 +27,12 @@ $ZMI.registerReady(function(){ $ZMILocalStorageAPI.replace(key,bookmarks); var frames = window.parent.frames; try { - for (var i = 0; i < frames.length; i++) { - if (frames[i] != window && typeof frames[i].zmiBookmarksChanged == "function") { - frames[i].zmiBookmarksChanged(); + for (var i = 0; i < frames.length; i++) { + if (frames[i] != window && typeof frames[i].zmiBookmarksChanged == "function") { + frames[i].zmiBookmarksChanged(); + } } - } - } - catch (e) { + } catch (e) { } }); var index = bookmarks.indexOf(data_path); @@ -58,13 +57,12 @@ $ZMI.registerReady(function(){ $ZMILocalStorageAPI.replace(key,history); var frames = window.parent.frames; try { - for (var i = 0; i < frames.length; i++) { - if (frames[i] != window && typeof frames[i].zmiHistoryChanged == "function") { - frames[i].zmiHistoryChanged(); + for (var i = 0; i < frames.length; i++) { + if (frames[i] != window && typeof frames[i].zmiHistoryChanged == "function") { + frames[i].zmiHistoryChanged(); + } } - } - } - catch (e) { + } catch (e) { } } }); diff --git a/Products/zms/plugins/www/zmi.js b/Products/zms/plugins/www/zmi.js index 01bd5d7df..9cddff9eb 100644 --- a/Products/zms/plugins/www/zmi.js +++ b/Products/zms/plugins/www/zmi.js @@ -38,12 +38,24 @@ if (typeof htmx != "undefined") { body.classList.add('loading'); }); document.addEventListener('htmx:afterRequest', (evt) => { - var bodyClass = evt.detail.xhr.responseText; - bodyClass = bodyClass.substr(bodyClass.indexOf(" -1 ) { - bodyClass = bodyClass.substr(bodyClass.indexOf("class=\"")+"class=\"".length); - bodyClass = bodyClass.substr(0,bodyClass.indexOf("\"")); - document.querySelector('body').classList = bodyClass; + var resp_text = evt.detail.xhr.responseText; + var parser = new DOMParser(); + var resp_doc = parser.parseFromString(resp_text , 'text/html'); + var newBody = resp_doc.querySelector('body'); + // Check if response is a full HTML page or just a message + if ( resp_text.indexOf(" -1 && newBody.childNodes[0].id!='zmi_manage_tabs_message') { + var currentBody = document.querySelector('body'); + // Copy all attributes from newBody to currentBody + Array.from(newBody.attributes).forEach(attr => { + currentBody.setAttribute(attr.name, attr.value); + }); + // Remove attributes that are not in newBody + Array.from(currentBody.attributes).forEach(attr => { + if (!newBody.hasAttribute(attr.name)) { + currentBody.removeAttribute(attr.name); + } + }); + // Trigger Ready Event $ZMI.runReady(); }; // Remove form-modified class from From 74cd44d161e0c8aae94ed7a6aab1a7050ae54631 Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 23 Oct 2024 16:59:49 +0200 Subject: [PATCH 110/135] history/bookmarks: hilight icon on change --- Products/zms/plugins/www/object/manage_menu.js | 3 +++ Products/zms/plugins/www/zmi.core.css | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/Products/zms/plugins/www/object/manage_menu.js b/Products/zms/plugins/www/object/manage_menu.js index c3d861470..2f0884c82 100644 --- a/Products/zms/plugins/www/object/manage_menu.js +++ b/Products/zms/plugins/www/object/manage_menu.js @@ -77,6 +77,8 @@ function zmiBookmarksChanged() { html += '
'; } $("#zmi-bookmarks").html(html); + $("#zmi-bookmarks").removeClass('has_changed').addClass('has_changed'); + $("#zmi-bookmarks .close").click(function(event) { event.preventDefault(); event.stopPropagation(); @@ -129,6 +131,7 @@ function zmiHistoryChanged() { html += '
'; } $("#zmi-history").html(html); + $("#zmi-history").removeClass('has_changed').addClass('has_changed'); }); } diff --git a/Products/zms/plugins/www/zmi.core.css b/Products/zms/plugins/www/zmi.core.css index 02941e419..326fd200a 100644 --- a/Products/zms/plugins/www/zmi.core.css +++ b/Products/zms/plugins/www/zmi.core.css @@ -103,6 +103,20 @@ header .navbar-brand { color: silver; text-decoration:none; } +@keyframes highlight_icon { + 0% { + color: #96ebff; + } + 68% { + color: #96ebff; + } + 100% { + color: inherit; + } +} +.navbar-nav > ul > li.has_changed > a > i { + animation: highlight_icon .75s +} .navbar-nav > ul > li > a:active, .navbar-nav > ul > li > a:hover, .navbar-nav a.navbar-brand:hover { From 9962594c2d1cc97d2c44a97a16921c237d9d9b13 Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 23 Oct 2024 17:02:33 +0200 Subject: [PATCH 111/135] typo --- Products/zms/plugins/www/zmi.core.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Products/zms/plugins/www/zmi.core.css b/Products/zms/plugins/www/zmi.core.css index 326fd200a..c57a97057 100644 --- a/Products/zms/plugins/www/zmi.core.css +++ b/Products/zms/plugins/www/zmi.core.css @@ -103,7 +103,7 @@ header .navbar-brand { color: silver; text-decoration:none; } -@keyframes highlight_icon { +@keyframes icon_blink { 0% { color: #96ebff; } @@ -115,7 +115,7 @@ header .navbar-brand { } } .navbar-nav > ul > li.has_changed > a > i { - animation: highlight_icon .75s + animation: icon_blink .75s } .navbar-nav > ul > li > a:active, .navbar-nav > ul > li > a:hover, From 33e20550d6220240d1c2299060c37019375b2797 Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 23 Oct 2024 21:40:15 +0200 Subject: [PATCH 112/135] Word/DOCX-Eport as ZMS-Action applying python-docx (#288) Hint: needs https://pypi.org/project/python-docx/ The new ZMS action manage_export_docx uses a simple JSON represetation of any ZMS-content object and utilized a prepared DOCX-File as a master-template containing the styles/numberings definitions given as DOCX xml code to create the output as a DOCX file.. --- .gitignore | 5 +- .../manage_export_pydocx/__init__.py | 51 + .../manage_export_pydocx.py | 1295 +++++++++++++++++ .../manage_export_pydocx/neon.docx | Bin 0 -> 19805 bytes .../manage_export_pydocx/numbering.xml | 401 +++++ .../manage_export_pydocx/readme.md | 22 + .../manage_export_pydocx/styles.xml | 631 ++++++++ .../__init__.py | 51 + .../manage_export_pydocx_recursive.py | 15 + .../bt_carousel/__init__.py | 11 +- .../bt_carousel/standard_json_docx.py | 60 + .../bt_jumbotron/__init__.py | 2 +- .../bt_jumbotron/standard_html.zpt | 2 +- .../com.zms.foundation/ZMSGraphic/__init__.py | 13 +- .../ZMSGraphic/standard_json_docx.py | 44 + .../com.zms.foundation/ZMSNote/__init__.py | 13 +- .../ZMSNote/standard_json_docx.py | 43 + .../ZMSTextarea/__init__.py | 11 +- .../ZMSTextarea/standard_json_docx.py | 55 + docs/notebooks/snippets_08_pythondocx.ipynb | 1013 +++++++++++++ docs/notebooks/snippets_09_pythondocx.ipynb | 242 +++ docs/notebooks/snippets_10_pythondocx.ipynb | 170 +++ .../snippets_11_pythondocx_table .ipynb | 175 +++ docs/notebooks/tbl_preview.png | Bin 0 -> 13790 bytes docs/notebooks/test.docx | Bin 0 -> 36817 bytes 25 files changed, 4314 insertions(+), 11 deletions(-) create mode 100644 Products/zms/conf/metacmd_manager/manage_export_pydocx/__init__.py create mode 100644 Products/zms/conf/metacmd_manager/manage_export_pydocx/manage_export_pydocx.py create mode 100644 Products/zms/conf/metacmd_manager/manage_export_pydocx/neon.docx create mode 100644 Products/zms/conf/metacmd_manager/manage_export_pydocx/numbering.xml create mode 100644 Products/zms/conf/metacmd_manager/manage_export_pydocx/readme.md create mode 100644 Products/zms/conf/metacmd_manager/manage_export_pydocx/styles.xml create mode 100644 Products/zms/conf/metacmd_manager/manage_export_pydocx_recursive/__init__.py create mode 100644 Products/zms/conf/metacmd_manager/manage_export_pydocx_recursive/manage_export_pydocx_recursive.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_carousel/standard_json_docx.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSGraphic/standard_json_docx.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSNote/standard_json_docx.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSTextarea/standard_json_docx.py create mode 100644 docs/notebooks/snippets_08_pythondocx.ipynb create mode 100644 docs/notebooks/snippets_09_pythondocx.ipynb create mode 100644 docs/notebooks/snippets_10_pythondocx.ipynb create mode 100644 docs/notebooks/snippets_11_pythondocx_table .ipynb create mode 100644 docs/notebooks/tbl_preview.png create mode 100644 docs/notebooks/test.docx diff --git a/.gitignore b/.gitignore index e611332c9..410739dbd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ Products/zms/overrides.zcml dist/ build/ -docker/var/* -docker/var/log/ +docker/**/var/* +docker/**/Extensions/* /revisions.txt + diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx/__init__.py b/Products/zms/conf/metacmd_manager/manage_export_pydocx/__init__.py new file mode 100644 index 000000000..170ba132b --- /dev/null +++ b/Products/zms/conf/metacmd_manager/manage_export_pydocx/__init__.py @@ -0,0 +1,51 @@ +class manage_export_pydocx: + """ + python-representation of manage_export_pydocx + """ + + # Acquired + acquired = 0 + + # Action + action = "%smanage_executeMetacmd?id=manage_export_pydocx" + + # Description + description = "" + + # Execution + execution = 0 + + # Icon_clazz + icon_clazz = "fas fa-download text-primary" + + # Id + id = "manage_export_pydocx" + + # Meta_types + meta_types = ["type(ZMSDocument)"] + + # Name + name = "Py-DOCX Export" + + # Nodes + nodes = "{$}" + + # Package + package = "com.zms.foundation.export" + + # Revision + revision = "5.0.0" + + # Roles + roles = ["ZMSAdministrator"] + + # Stereotype + stereotype = "" + + # Title + title = "Py-DOCX Export" + + # Impl + class Impl: + manage_export_pydocx = {"id":"manage_export_pydocx" + ,"type":"External Method"} diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx/manage_export_pydocx.py b/Products/zms/conf/metacmd_manager/manage_export_pydocx/manage_export_pydocx.py new file mode 100644 index 000000000..3b02d273b --- /dev/null +++ b/Products/zms/conf/metacmd_manager/manage_export_pydocx/manage_export_pydocx.py @@ -0,0 +1,1295 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# IMPORT GENERAL LIBRARIES +import os +import re +import shutil +import tempfile +import urllib +import json +import requests +from io import BytesIO +import sys +import datetime + +# IMPORT ZMS LIBRARIES +from Products.zms import standard +# from Products.zms import rest_api + +# X/HTML LIBRARIES +from bs4 import BeautifulSoup, NavigableString +from lxml import etree + +# IMPORT DOCX LIBRARIES +import docx +from docx.oxml import OxmlElement, ns, parse_xml +from docx.oxml.ns import nsdecls +from docx.text.paragraph import Paragraph +from docx.shared import Pt +from docx.shared import Emu +from docx.shared import Cm +from docx.enum.table import WD_TABLE_ALIGNMENT +from docx.enum.text import WD_TAB_ALIGNMENT +from docx.enum.style import WD_STYLE_TYPE +from docx.enum.text import WD_COLOR_INDEX +from docx.enum.section import WD_SECTION_START + + +# ############################################# +# GLOBALS +# ############################################# +# ZMS-Context will be set in main function manage_export_pydocx() +zmscontext = None +# DOCX-Document will be set in main function manage_export_pydocx() +doc = None +# Set local path for docx-template +docx_tmpl = open("/home/zope/src/zms-publishing/ZMS5/Products/zms/conf/metacmd_manager/manage_export_pydocx/neon.docx", "rb") +# Set initial numbering.num_id for restarting decimal num-lists +num_id = 5 + + +# ############################################# +# Helper Functions 1: DOCX-XML +# e.g. `add_page_number(run)` : add page number to text-run (e.g. footer) +# Hint: the docx API does not support the page counter directly. +# We have to create a custom footer with a page counter. +# ############################################# +# Reference: DocumentFormat.OpenXml +# https://learn.microsoft.com/en-us/dotnet/api/overview/openxml/?view=openxml-3.0.1 + + +# XML-Helpers +def create_element(name): + return OxmlElement(name) + +def create_attribute(element, name, value): + element.set(ns.qn(name), value) + + +def get_normalized_image_width(w, h, max_w = 460): + if w: + if h > w: + scale = h>max_w and float(h)/float(max_w) or 1 + else: + scale = w>max_w and float(w)/float(max_w) or 1 + w = int(w/scale) + else: + w = max_w + return w + +# ############################################# + +# ADD DATA FIELD: eg PAGE, SAVEDATE +def add_field(paragraph, field_code="PAGE"): + fldChar1 = create_element('w:fldChar') + create_attribute(fldChar1, 'w:fldCharType', 'begin') + instrText = create_element('w:instrText') + create_attribute(instrText, 'xml:space', 'preserve') + instrText.text = field_code + fldChar2 = create_element('w:fldChar') + create_attribute(fldChar2, 'w:fldCharType', 'end') + run = paragraph.add_run() + run._r.append(fldChar1) + run._r.append(instrText) + run._r.append(fldChar2) + + +# BOOKMARK ZMS-ID +def prepend_bookmark(docx_block, bookmark_id): + bookmark_start = create_element('w:bookmarkStart') + create_attribute(bookmark_start, 'w:id', bookmark_id) + create_attribute(bookmark_start, 'w:name', bookmark_id) + bookmark_end = create_element('w:bookmarkEnd') + create_attribute(bookmark_end, 'w:id', bookmark_id) + try: + docx_block._element.insert(0, bookmark_end) + docx_block._element.insert(0, bookmark_start) + except: + pass + + +def add_num_element(document=doc, abstractNum_id=3, num_id=5): + """ + Add a num element to the numbering part of a document + referencing an abstract numbering definition identified by *abstractNum_id*. + """ + # Create a new num element + num = create_element('w:num') + create_attribute(num, 'w:numId', str(num_id)) + abstractNumId = create_element('w:abstractNumId') + create_attribute(abstractNumId, 'w:val', str(abstractNum_id)) + lvlOverride = create_element('w:lvlOverride') + create_attribute(lvlOverride, 'w:ilvl', '0') + startOverride = create_element('w:startOverride') + create_attribute(startOverride, 'w:val', '1') + lvlOverride.append(startOverride) + num.append(abstractNumId) + num.append(lvlOverride) + # Add the num element to the numbering part + numbering_part = document.part.numbering_part + numbering_part.element.append(num) + + +def set_block_as_listitem(docx_block, list_type='ul', level=0, i=0): + """ + Set list properties to docx-block + ul = List Bullet => numId=2 + ol = List Number => numId=3 + level: 0-8 + i: enumeration index of list item (py2 0-based) + Hints: + 1. ul/ol/li are handled as blocks in add_htmlblock_to_docx.add_list + 2. docx-numbering.xml needs suitable list style definitions with numId=2 and 3 + Example xml code for a list item paragraph: + + + + + + + + + + + + + + 1st level + + + """ + if list_type=='ol' and i==0: + list_type = 'ol_restart' + global num_id + num_id += 1 + add_num_element(document=doc, abstractNum_id=3, num_id=num_id) + + pPr = create_element('w:pPr') + numPr = create_element('w:numPr') + ilvl = create_element('w:ilvl') + create_attribute(ilvl, 'w:val', str(level)) + numId = create_element('w:numId') + create_attribute(numId, 'w:val', {'ul':'2', 'ol':'3','ol_restart':str(num_id)}[list_type]) + numPr.append(ilvl) + numPr.append(numId) + pPr.append(numPr) + if docx_block._element.pPr is not None: + docx_block._element.remove(docx_block._element.pPr) + docx_block._element.append(pPr) + return docx_block + + +# ADD HYPERLINK +def add_hyperlink(docx_block, link_text, url): + # url_base = 'http://127.0.0.1:8080/' + url_base = 'http://neon/' + # Omit javascript links + if not url.startswith('javascript:'): + # Fix missing domain name + url = ('http' in url) and url.replace('http:///', url_base) or (url_base + (url.startswith('/') and url[1:] or url)) + r_id = docx_block.part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True) + hyper_link = create_element('w:hyperlink') + create_attribute(hyper_link, 'r:id', r_id) + hyper_link_run = create_element('w:r') + hyper_link_run_prop = create_element('w:rPr') + hyper_link_run_prop_style = create_element('w:rStyle') + create_attribute(hyper_link_run_prop_style, 'w:val', 'Hyperlink') + hyper_link_run_prop.append(hyper_link_run_prop_style) + hyper_link_run.append(hyper_link_run_prop) + hyper_link_text = create_element('w:t') + hyper_link_text.text = link_text + hyper_link_run.append(hyper_link_text) + hyper_link.append(hyper_link_run) + + ## DEBUG: Show docx xml + # etree.tostring(hyper_link, pretty_print=True) + + if isinstance(docx_block, type(docx.Document())): + # Add hyperlink as a new block element + p = docx_block.add_paragraph() + p._p.append(hyper_link) + else: + # Add hyperlink as inline element + docx_block._p.append(hyper_link) + else: + docx_block.add_run(link_text) + return docx_block + + +# ############################################# +# Helper Functions 2: HTML/Richtext-Processing +# ############################################# + +# Clean HTML +def clean_html(html): + """ + Clean comments, styles, empty tags + and handle special characters: left-to-right, triangle + """ + left_to_right_char = '\u200e' + triangle_char = '\U0001F806' + + html = re.sub(r'','', html) + html = re.sub(r'','', html) + html = standard.re_sub(r'\n|\t', ' ', html) + html = standard.re_sub(r'\s\s', ' ', html) + html = html.replace('
','') + html = html.replace('','') + html = html.replace('">\n','">') + html = re.sub(r'(?i)(?m)((ol\>) )', r'\g<2>', html) + html = re.sub(r'(?i)(?m)((ul\>) )', r'\g<2>', html) + html = re.sub(r'(?i)(?m)((li\>) )', r'\g<2>', html) + # refGlossary + html = html.replace(left_to_right_char,'') + html = html.replace('[[', triangle_char) + html = html.replace(']]', '') + return html + +# ADD RUNS TO DOCX-BLOCK +def add_runs(docx_block, bs_element): + """ + Adding a minimum set of inline runs + any BeautifulSoup block element may contain + to the docx-block, e.g. , , + """ + if bs_element.children: + c = 0 + # Hint: ul/ol/li are handled as blocks in add_htmlblock_to_docx.add_list + elruns = [elrun for elrun in bs_element.children if elrun.name not in ['ul', 'ol', 'li', 'img', 'figure']] + for elrun in list(elruns): + c += 1 + if elrun.name == None: + s = standard.pystr(elrun) + # Remove trailing spaces on first text element of a new line or block + if c == 1: + s = s.lstrip() + elif elruns[c-2].name == 'br': + s = s.lstrip() + docx_block.add_run(s) + elif elrun.name == 'br': + docx_block.add_run('\n') + elif elrun.name != None and elrun.text == '↵': + pass + elif elrun.name == 'i': + if elrun.has_attr('class') and 'fa-pencil-alt' in elrun['class']: + docx_block.add_run(u'\U0000F021', style='Icon') + elif elrun.has_attr('class') and 'fa-phone' in elrun['class']: + docx_block.add_run(u'\U0000F028', style='Icon') + elif elrun.text != '': + docx_block.add_run(elrun.text).italic = True + elif elrun.text != '': + if elrun.name == 'strong' or elrun.name == 'b': + docx_block.add_run(elrun.text).bold = True + elif elrun.name == 'q' or elrun.name == 'quote': + docx_block.add_run('"%s"'%(elrun.text), style='Quote-Inline') + elif elrun.name == 'em': + docx_block.add_run(elrun.text).italic = True + elif elrun.name in ['samp', 'code', 'tt', 'var', 'pre']: + docx_block.add_run(elrun.text, style='Code-Inline') + elif elrun.name == 'kbd': + docx_block.add_run(elrun.text, style='Keyboard') + # r.font.highlight_color = WD_COLOR_INDEX.BLACK + elif elrun.name == 'sub': + docx_block.add_run(elrun.text).font.subscript = True + elif elrun.name == 'sup': + docx_block.add_run(elrun.text).font.superscript = True + elif elrun.name == 'a': + if elrun.has_attr('href'): + add_hyperlink(docx_block = docx_block, link_text = elrun.text, url = elrun.get('href')) + docx_block.add_run(' ') + else: + # elrun.encode_contents() => html + docx_block.add_run(elrun.text) + elif elrun.name == 'span': + if elrun.has_attr('class'): + class_name = elrun['class'][0] + style_name = (class_name in doc.styles) and class_name or 'Default Paragraph Font' + docx_block.add_run(elrun.text, style=style_name) + else: + docx_block.add_run(elrun.text) + + # ############################################# + ## TO-DO: Add inline image to docx + ## Error: adding image as a block element + ## may result in content replication + # ############################################# + # elif elrun.name == 'img': + # add_htmlblock_to_docx(zmscontext, doc, str(elrun), zmsid=None) + # ############################################# + elif elrun.name == 'p': + add_runs(docx_block = docx_block, bs_element = elrun) + # ############################################# + elif bs_element.text != '': + docx_block.text(standard.pystr(bs_element.text)) + # ############################################# + # CAVE: + # Empty runs may cause rendering errors, + # so remove empty runs from docx-block + # ############################################ + for r in docx_block.runs: + if r.text == '' and r.style.name != "Icon": + docx_block._p.remove(r._r) + + +# ADD CLASS-NAMED HTML-BLOCK AS PARAGRAPH +def add_tagged_content_as_paragraph(docx_doc, bs_element, style_name="Standard", c=0, zmsid=None): + """ + Add content of a BeautifulSoup element as a paragraph + to the docx document (self) + """ + if docx_doc.paragraphs and docx_doc.paragraphs[-1].text == '': + p = docx_doc.paragraphs[-1] + p.style = style_name + else: + p = docx_doc.add_paragraph(style=style_name) + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + el = BeautifulSoup(standard.pystr(bs_element), 'html.parser') + el_tag = el.div + el_tag.unwrap() + add_runs(docx_block = p, bs_element = el) + + +# ADD HTML-BLOCK TO DOCX +def add_htmlblock_to_docx(zmscontext, docx_doc, htmlblock, zmsid=None, zmsmetaid=None): + # Clean HTML + htmlblock = clean_html(htmlblock) + heading_text = '' + # Apply BeautifulSoup and iterate over elements + soup = BeautifulSoup(htmlblock, 'html.parser') + + # Counter for html elements: set bookmark before first element + c = 0 + + # Iterate over elements + for element in soup.children: + # Skip empty elements + if element not in ['\n',' ']: + c+=1 + if isinstance(element, NavigableString): + # ############################################# + # Text only + # ############################################# + p = docx_doc.add_paragraph(standard.pystr(element).strip()) + if c==0 and zmsid: + prepend_bookmark(p, zmsid) + else: + # ############################################# + # HTML-Elements, element.name != None + # ############################################# + + # ############################################# + # HEADINGS + # ############################################# + if element.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: + heading_level = int(element.name[1]) + heading_text = standard.pystr(element.text).strip() + p = add_heading(docx_doc, heading_text, level=heading_level) + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + if element.text == 'Inhaltsverzeichnis': + p.style = docx_doc.styles['TOC-Header'] + # ############################################# + # PARAGRAPH + # ############################################# + elif (element.name == 'p' and element.text != '' and element.text != ' ' ) \ + or ( element.name == 'p' and ('i' in [e.name for e in element.children]) ): + p = docx_doc.add_paragraph() + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + # htmlblock.__contains__('ZMSTable') or htmlblock.__contains__('img') + if element.has_attr('class'): + if 'caption' in element['class'] and zmsmetaid in ['ZMSGraphic', 'ZMSTable']: + p.style = docx_doc.styles['caption'] + else: + class_name = element['class'][0] + style_name = (class_name in docx_doc.styles) and class_name or 'Normal' + p.style = docx_doc.styles[style_name] + add_runs(docx_block = p, bs_element = element) + + ## Remove empty paragraphs + if element.text == '' or element.text == ' ': + if 'Icon' not in [r.style.name for r in docx_doc.paragraphs[-1].runs]: + last_paragraph = docx_doc.paragraphs[-1]._element + last_paragraph.getparent().remove(last_paragraph) + + # ############################################# + # LIST + # ############################################# + elif element.name in ['ul','ol']: + def add_list(docx_obj, element, level=0, c=0): + for i, li in enumerate(element.find_all('li', recursive=False)): + if docx_obj.paragraphs and docx_obj.paragraphs[-1].text == '': + p = docx_obj.paragraphs[-1] + else: + p = docx_obj.add_paragraph() + p = set_block_as_listitem(p, list_type=element.name, level=level, i=i) + add_runs(docx_block = p, bs_element = li) + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + for ul in li.find_all(['ul','ol'], recursive=False): + add_list(docx_doc, ul, level+1) + add_list(docx_doc, element, level=0, c=c) + + # ############################################# + # TABLE + # ############################################# + elif element.name == 'table': + table = element + # Add caption if available + caption = table.find('caption') + caption_text = caption and caption.text or '' + p = docx_doc.add_paragraph(standard.pystr(caption_text), style='Table-Caption') + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + + # Create a 2D list to represent the table + rows = table.find_all('tr') + cols_len = max([len(row.find_all(['td','th'])) for row in rows]) + table_list = [[None] * cols_len for _ in range(len(rows))] + + # Set table style + text_style = 'Normal' + if len(rows) > 16: + text_style = 'Table-Small' + + # ------------------------------------------------ + # Fill a declarative 2D list with the cell contents + # ------------------------------------------------ + for i, row in enumerate(rows): + for j, cell in enumerate(row.find_all(['td', 'th'])): + # Find the first None element in the 2D list + while table_list[i][j] is not None: + j += 1 + # Set the appropriate number of elements to the cell content + for k in range(i, i + int(cell.get('rowspan', 1))): + for l in range(j, j + int(cell.get('colspan', 1))): + table_list[k][l] = '[%s:%s:%s] %s'%(cell.name, i,j,cell.decode_contents() if cell.contents else '') + + # Create DOCX table + docx_table = docx_doc.add_table(rows=len(rows), cols=cols_len) + docx_table.style = 'Table Grid' + docx_table.alignment = WD_TABLE_ALIGNMENT.CENTER + + # ------------------------------------------------ + # Fill the docx table with the cell contents and merge cells + # ------------------------------------------------ + merge_log = [] # Keep track of merged cells that finally should be empty + for i, row in enumerate(table_list): + for j, cell in enumerate(row): + docx_table.cell(i, j).text = cell + # Merge cells if they're the same as the previous cell + if i > 0 and cell == table_list[i - 1][j]: + docx_table.cell(i, j).text = '' + docx_table.cell(i - 1, j).merge(docx_table.cell(i, j)) + merge_log.append((i, j)) + if j > 0 and cell == row[j - 1]: + docx_table.cell(i, j).text = '' + docx_table.cell(i, j - 1).merge(docx_table.cell(i, j)) + merge_log.append((i, j)) + + + # ------------------------------------------------ + # HELPER FUNCTION: Transform table cells html-content to docx + # ------------------------------------------------ + def convert_cell_html_to_docx(zmscontext, docx_cell, text_style='Normal'): + '''Convert cell html to docx''' + cl_html = clean_html(docx_cell.text) + cl_type = cl_html.startswith('[th:') and 'th' or 'td' + cl_html = re.sub(r'\[(th|td):\d:\d\] ','',cl_html) + cl = BeautifulSoup(cl_html, 'html.parser') + + # Clear docx_cell content + docx_cell._tc.clear_content() + p = docx_cell.add_paragraph() + p.style = cl_type == 'th' and 'Tableheader' or text_style + + # Add structured content to cell + try: + if {'div','ol','ul','table','p'} & set([e.name for e in cl.children]): + # [A] Block elements + add_htmlblock_to_docx(zmscontext, docx_cell, cl_html, zmsid=None) + elif set([e.name for e in cl.children])==set([None]): + # [B] Just text + p.text = cl.text + else: + # [C] Inline elements + add_runs(p, cl) + except: + p.add_run('Rendering Error Table-Cell: %s'%cl.text) + + # ------------------------------------------------ + # Iterate each cell for executing html conversion + # ------------------------------------------------ + for i, row in enumerate(docx_table.rows): + for j, cell in enumerate(row.cells): + # Skip merged cells + if (i, j) not in merge_log: + convert_cell_html_to_docx(zmscontext, cell, text_style='Normal') + + # ------------------------------------------------ + # Add linebreak or pagebreak after table + # ------------------------------------------------ + p = docx_doc.add_paragraph() + + # ############################################# + # IMAGE + # ############################################# + elif element.name == 'img' or element.name == 'figure': + if element.name == 'figure': + element = element.find('img') + if element.has_attr('src'): + img_src = element['src'] + try: + if zmscontext.operator_getattr(zmscontext,zmsid).attr('imghires'): + # Use high resolution image + img_src = zmscontext.operator_getattr(zmscontext,zmsid).attr('imghires').getHref(zmscontext.REQUEST) + except: + pass + img_name = img_src.split('/')[-1] + if not img_src.startswith('http'): + src_url0 = zmscontext.absolute_url().split('/content/')[0] + src_url1 = img_src.split('/content/')[-1] + if src_url1.startswith('/'): + # eg. ZMS assets starting with /++resource++zms_ + src_url1 = src_url1[1:] + element['src'] = '%s/content/%s'%(src_url0, src_url1) + + # Normalize image size to 460px + imgheight = element.has_attr('height') and int(float(element['height'])) or None + imgwidth = element.has_attr('width') and int(float(element['width'])) or None + imgwidth = get_normalized_image_width(w = imgwidth, h = imgheight, max_w = 460) + + try: + response = requests.get(element['src'], verify=False) + with open(img_name, 'wb') as f: + f.write(response.content) + if src_url1.startswith('++resource++zms_'): + docx_doc.add_picture(img_name) + else: + docx_doc.add_picture(img_name, width=Emu(imgwidth*9525)) + os.remove(img_name) + if c==1 and zmsid: + try: + prepend_bookmark(docx_doc.paragraphs[-1], zmsid) + except: + pass + except: + pass + # ############################################# + # DIV + # ############################################# + elif element.name == 'div': + if element.has_attr('class') and (('ZMSGraphic' in element['class']) or ('graphic' in element['class'])): + ZMSGraphic_html = standard.pystr(''.join([str(e) for e in element.children])) + add_htmlblock_to_docx(zmscontext, docx_doc, ZMSGraphic_html, zmsid, zmsmetaid='ZMSGraphic') + elif element.has_attr('class') and ('ZMSTextarea' in element['class']): + ZMSTextarea_html = standard.pystr(''.join([str(e) for e in element.children])) + add_htmlblock_to_docx(zmscontext, docx_doc, ZMSTextarea_html, zmsid, zmsmetaid='ZMSTextarea') + elif element.has_attr('class') and 'handlungsaufforderung' in element['class']: + add_tagged_content_as_paragraph(docx_doc, element, 'Handlungsaufforderung', c, zmsid) + elif element.has_attr('class') and 'grundsatz' in element['class']: + add_tagged_content_as_paragraph(docx_doc, element, 'Grundsatz', c, zmsid) + elif element.has_attr('style') and 'background: rgb(238, 238, 238)' in element['style'] \ + and heading_text != 'Inhaltsverzeichnis': + add_tagged_content_as_paragraph(docx_doc, element, 'Hinweis', c, zmsid) + elif element.has_attr('class') and 'text' in element['class'] and zmsmetaid in ['ZMSGraphic', 'ZMSTable']: + p = docx_doc.add_paragraph(style='Caption') + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + add_runs(docx_block = p, bs_element = element) + else: + child_tags = [e.name for e in element.children if e.name] + if {'em','strong','i', 'span'} & set(child_tags): + p = docx_doc.add_paragraph() + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + if len(element.contents) == 1: + if element.has_attr('class'): + style_name = (class_name in docx_doc.styles) and class_name or 'Normal' + p.style = docx_doc.styles[style_name] + p.add_run(element.text) + elif len(element.contents) > 1: + for e in element.contents: + if e.name and e.has_attr('class'): + class_name = e['class'] + if 'fa-pencil-alt' in class_name: + # p.add_run('Bearbeitung: ') + p.add_run(u'\U0000F021', style='Icon') + elif 'fa-phone' in class_name: + # p.add_run('Routing: ') + p.add_run(u'\U0000F028', style='Icon') + p.add_run(' ') + if list(e.children)!=[]: + add_runs(docx_block = p, bs_element = e) + else: + p.add_run(standard.pystr(e.text)) + elif e.name: + p.add_run(standard.pystr(e.text)) + else: + p.add_run(standard.pystr(e)) + else: + add_runs(docx_block = p, bs_element = element) + else: + div_html = standard.pystr(''.join([standard.pystr(e) for e in element.children])) + add_htmlblock_to_docx(zmscontext, docx_doc, div_html, zmsid) + + # ############################################# + # Link/A containing text or block elements + # ############################################# + elif element.name == 'a': + # Hyperlink just containing text + if element.children and list(element.children)[0] == element.text: + try: + add_hyperlink(docx_block = docx_doc, link_text = element.text, url = element.get('href')) + except: + p = docx_doc.add_paragraph() + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + add_runs(docx_block = p, bs_element = element) + # Hyperlink containing a block element + elif {'div','p','table','img'} & set([e.name for e in element.children]): + # Acquire highres image from fancybox link + if 'img' in [e.name for e in element.children] and \ + element.has_attr('class') and 'fancybox' in element['class'] and element.has_attr('href'): + try: + element.img['src'] = element['href'] + except: + pass + div_html = ''.join([str(e) for e in element.children]) + add_htmlblock_to_docx(zmscontext, docx_doc, div_html, zmsid) + # Hyperlink containing inline elements + else: + p = docx_doc.add_paragraph() + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + add_runs(docx_block = p, bs_element = element) + + # ############################################# + # FORM + # ############################################# + elif element.name == 'form': + p = docx_doc.add_paragraph(style='macro') + p.add_run('\n').font.bold = True + if c==1: + prepend_bookmark(p, zmsid) + input_field_count = 0 + for input_field in element.find_all('input', recursive=True): + input_field_count += 1 + p.add_run('%s. : %s\n'%(input_field_count, input_field.get('name',''))) + # ############################################# + # OTHERS + # ############################################# + elif element.name == 'hr': + # Omit horizontal rule + pass + elif element.name == 'script': + # Omit javascript + pass + else: + try: + if element.has_text: + p = docx_doc.add_paragraph(standard.pystr(element.text)) + if c==1 and zmsid: + prepend_bookmark(p, zmsid) + except: + docx_doc.add_paragraph(str(element)) + + return docx_doc + + +# ADD BREADCRUMBS AS RUNS TO PARAGRAPH +def add_breadcrumbs_as_runs(zmscontext, p): + breadcrumbs = zmscontext.breadcrumbs_obj_path() + c = 0 + for obj in breadcrumbs: + c += 1 + link_text = obj.meta_id == 'ZMS' and standard.pystr(obj.attr('title')) or standard.pystr(obj.attr('titlealt')) + add_hyperlink(docx_block = p, link_text = link_text, url = obj.getHref2IndexHtml(zmscontext.REQUEST)) + if c < len(breadcrumbs): + p.add_run(' > ') + return p + + +# ############################################# +# Helper Functions 4: GET DOCX NORMALIZED JSON +# ############################################# + +def apply_standard_json_docx(self): + """ + The function creates a normalized JSON stream of + a PAGE-like ZMS node. This JSON stream is used for + transforming the content to DOCX. + It is a list of dicts (key/value-pairs), where the + first dict is representing the container meta data + and the following blocks are representing the PAGEELEMENTS + of the document. + Each object dictionary has the following keys: + - id: the id of the node + - meta_id: the meta_id of the node + - parent_id: the id of the parent node + - parent_meta_id: the meta_id of the parent node + - title: the title of the node + - description: the description of the node + - last_change_dt: the last change date of the node + - docx_format: the format of the content (html/xml/image + or text-stylename e.g.'Normal') + - content: the content of the node + + Any PAGEELEMENT-node may have a specific 'standard_json_docx' + attribute which preprocesses it's ZMS content model close to + the translation into the DOCX model. The key 'docx_format' + is used to determine the style of the content block. + + If this attribute method (py-primtive) is not available, + the object's class standard_html-method is used to get the + content, so that the (maybe not optimum) html will be + transformed to DOCX. + + Depending on the complexity of the content it's JSON + representation may consist of ore or multiple key/value- + sequences. Any of these blocks will create a new block + element (e.g. paragraph) in the DOCX document. + """ + + zmscontext = self + request = zmscontext.REQUEST + # request.set('preview', 'preview') + is_page = zmscontext.isPage() + + id = zmscontext.id + meta_id = zmscontext.meta_id + parent_id = zmscontext.id + parent_meta_id = zmscontext.meta_id + if zmscontext.meta_id == 'LgRegel': + title = zmscontext.attr('titlealt') + else: + title = zmscontext.attr('title') or zmscontext.getTitle(request) + description = zmscontext.attr('attr_dc_description') + last_change_dt = zmscontext.attr('change_dt') or zmscontext.attr('created_dt') + userid = zmscontext.attr('change_uid') + url = zmscontext.getHref2IndexHtml(request) + + # Meta data as 1st block + blocks = [ + { + 'id':id, + 'url':url, + 'meta_id':meta_id, + 'parent_id':parent_id, + 'parent_meta_id':parent_meta_id, + 'title':title, + 'description':description, + 'last_change_dt':last_change_dt, + 'userid':userid, + } + ] + + if not is_page: + pageelements = [zmscontext] + else: + # Sequence all pageelements including ZMSNote + # Ref: ZMSObject.isPageElement + pageelements = [ \ + e for e in zmscontext.getChildNodes(request) \ + if ( ( e.getType() in [ 'ZMSObject', 'ZMSRecordSet'] ) \ + and not e.meta_id in [ 'LgChangeHistory','ZMSTeaserContainer','LgELearningBanner'] \ + and not e.isPage() ) \ + or e.meta_id in [ 'ZMSLinkElement' ] + ] + # if zmscontext.meta_id == 'LgRegel': + # pageelements = [zmscontext] + + for pageelement in pageelements: + + json_block = [] + + # Check for standard_json_docx attribute + json_block = pageelement.attr('standard_json_docx') + + if not json_block: + + # ############################################# + # ZMSGraphic + # ############################################# + if pageelement.meta_id == 'ZMSGraphic': + id = pageelement.id + meta_id = pageelement.meta_id + parent_id = pageelement.getParentNode().id + parent_meta_id = pageelement.getParentNode().meta_id + text = BeautifulSoup(pageelement.attr('text'), 'html.parser').get_text() + img = pageelement.attr('imghires') or pageelement.attr('img') + # img_url = img.getHref(request) + img_url = '%s/%s'%(pageelement.absolute_url(),img.getHref(request).split('/')[-1]) + imgwidth = img and int(img.getWidth()) or 0 + imgheight = img and int(img.getHeight()) or 0 + + json_block = [ + { + 'id':'%s_img'%(id), + 'meta_id':meta_id, + 'parent_id':parent_id, + 'parent_meta_id':parent_meta_id, + 'docx_format':'image', + 'imgwidth': imgwidth, + 'imgheight':imgheight, + 'content':img_url + }, + { + 'id':id, + 'meta_id':meta_id, + 'parent_id':parent_id, + 'parent_meta_id':parent_meta_id, + 'docx_format':'Caption', + 'content':'[Abb. %s] %s'%(id, text) + }, + + ] + # ############################################# + # ZMSLinkElement + # ############################################# + elif pageelement.meta_id == 'ZMSLinkElement': + # and pageelement.attr('attr_type') in ['','replace','new']: + id = pageelement.id + meta_id = pageelement.meta_id + parent_id = pageelement.getParentNode().id + parent_meta_id = pageelement.getParentNode().meta_id + icon = '\U0001F517' + title = standard.pystr(pageelement.attr('title')) + text = standard.pystr(pageelement.attr('attr_dc_description')) + try: + href = zmscontext.getLinkObj(pageelement.attr('attr_url'),request).getHref2IndexHtml(request) + except: + href = '#' + + json_block = [ { + 'id':id, + 'meta_id':meta_id, + 'parent_id':parent_id, + 'parent_meta_id':parent_meta_id, + 'docx_format':'html', + 'content':'

%s %s
%s

'%(icon, href, title, text) + } + ] + # ############################################# + # ZMSFile + # ############################################# + elif (pageelement.meta_id == 'ZMSFile' or pageelement.meta_id == 'downloadfile') and pageelement.attr('file'): + id = pageelement.id + meta_id = pageelement.meta_id + parent_id = pageelement.getParentNode().id + parent_meta_id = pageelement.getParentNode().meta_id + icon = u'\U0001F87E' + title = standard.pystr(pageelement.attr('title')) + text = standard.pystr(pageelement.attr('attr_dc_description')) + try: + href = pageelement.getHref2IndexHtml(request) + except: + href = '#' + + json_block = [ { + 'id':id, + 'meta_id':meta_id, + 'parent_id':parent_id, + 'parent_meta_id':parent_meta_id, + 'docx_format':'html', + 'content':'

%s %s%s

'%(icon, href, title, text) + } + ] + + # ############################################# + # CAVE: Do not apply renderShort! + # e.g. LgBedingung, ZMSNote + # ############################################# + elif 'renderShort' in pageelement.getMetaobjAttrIds(pageelement.meta_id): + json_block = [{ + 'id': pageelement.id, + 'meta_id': pageelement.meta_id, + 'parent_id': pageelement.getParentNode().id, + 'parent_meta_id': pageelement.getParentNode().meta_id, + 'docx_format': 'html', + 'content': pageelement.attr('standard_html') + }] + + # ############################################# + else: + html = '' + # Get standard content of pageelement + try: + html = pageelement.getBodyContent(request) + # Clean html data + html = clean_html(html) + except: + html = '' + html += '' % pageelement.meta_id + attrs = [d['id'] for d in zmscontext.getMetaobjAttrs(pageelement.meta_id) if d['type'] not in ['dtml','zpt','py','constant','resource','interface']] + for attr in attrs: + html += '' % (attr, pageelement.attr(attr)) + html += '
Rendering Error: %s
%s%s
' + # Create a json block + json_block = [{ + 'id': pageelement.id, + 'meta_id': pageelement.meta_id, + 'parent_id': pageelement.getParentNode().id, + 'parent_meta_id': pageelement.getParentNode().meta_id, + 'docx_format': 'html', + 'content': html + }] + + # Give some customizing hints for standard_html + if pageelement.meta_id in ['LgRegel','LgBedingung','LgELearningBanner','ZMSNote']: + standard.writeStdout(None, 'IMPORTANT NOTE: %s.standard_html needs to be customized!'%(pageelement.meta_id)) + # %<---- CUSTOMIZE LIKE THIS --------------------- + # zmi python:request['URL'].find('/manage')>0 and not request['URL'].find('pydocx')>0; + # %<---- /CUSTOMIZE LIKE THIS --------------------- + + blocks.extend(json_block) + + # Check for newer content + if pageelement.attr('change_dt') and pageelement.attr('change_dt') >= last_change_dt: + # Update editorial data + last_change_dt = pageelement.attr('change_dt') + if pageelement.attr('change_uid'): + userid = pageelement.attr('change_uid') + + # Finally set editorial data + blocks[0]['last_change_dt'] = last_change_dt + blocks[0]['userid'] = userid + + return blocks + +# ############################################# +# Helper Functions 5: READ HEADLINE LEVELS +# ############################################# +# Based on number-prefix of headlines +def get_headline_levels_from_numbering(headline_paragraphs=[]): + list2 = [ \ + ( + len(p.text.split(' ')[0].rstrip('.').split('.'))+1 \ + if '.' in p.text.split(' ')[0] else 1 \ + ) \ + for p in headline_paragraphs + ] + + return list2 + + +# ############################################# +# Helper Functions 6: NORMALIZE HEADLINE LEVELS +# ############################################# +def normalize_headline_levels(list1): + """ + Normalize headline levels + expects a list of headline levels as integer values + """ + s = [] + list2 = [1] + for i in list1[1:]: + i1 = i + 1 + if s and s[-1] == i1: + pass + elif not s or s[-1] < i1: + s.append(i1) + elif s: + while len(s) > 1 and s[-1] > i1: + s = s[:-1] + list2.append(len(s) + 1) + return list2 + +# ############################################# +# Helper Functions 7: GET PARAGRAPHS OF SECTION +# ############################################# +def get_headings_of_section(doc): + # Initialize an empty list to hold the sections + sections = [] + # Initialize an empty list to hold the current section + section = [] + + # Iterate over the elements in the document body + for element in doc._body._element: + # If the element is a paragraph + if element.tag == '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p': + # Create a Paragraph object from the element + paragraph = Paragraph(element, doc._body) + # If the paragraph has a heading style + if 'Heading' in paragraph.style.name: + # Add the paragraph to the current section + section.append(paragraph) + # If the element is a section property + elif element.tag == '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}sectPr': + # Add the current section to the list of sections + sections.append(section) + # Start a new section + section = [] + + # Add the last section to the list of sections + sections.append(section) + sections[:] = [section for section in sections if section != []] + + # Return the list of sections + return sections + +# Overwrite site-packages/docx/document.py +def add_heading(self, text, level=1): + """Return a heading paragraph newly added to the end of the document. + + The heading paragraph will contain `text` and have its paragraph style + determined by `level`. If `level` is 0, the style is set to `Title`. If `level` + is 1 (or omitted), `Heading 1` is used. Otherwise the style is set to `Heading + {level}`. Raises |ValueError| if `level` is outside the range 0-9. + """ + if not 0 <= level <= 9: + raise ValueError("level must be in range 0-9, got %d" % level) + style = "Title" if level == 0 else "heading %d" % level + return self.add_paragraph(text, style) + + +# ############################################# +# MAIN function for DOCX-Generation +# ############################################# +# The function `manage_export_pydocx` may be called +# recursively to create a DOCX document from a +# document tree. The function is called with the +# `do_return` parameter set to `True` on the last +# node of the tree. The `filename` parameter is used +# to name the DOCX file. The function returns the +# binary data of the DOCX file. +def manage_export_pydocx(self, save_file=True, file_name=None): + request = self.REQUEST + docx_creator = request.AUTHENTICATED_USER.getUserName() + + # PAGE_COUNTER: Counter for recursive export + page_counter = request.get('page_counter',0) + 1 + request.set('page_counter', page_counter) + + global zmscontext + zmscontext = self + is_page = zmscontext.isPage() + + # Write to stdout + standard.writeStdout(None, 'DOCX-INFO-%s: Writing ZMS node %s' %(page_counter, zmscontext.id)) + + # INITIALIZE DOCX Document + # and preserve it while exporting recursively + if page_counter == 1: + + # ############################################# + # DOCX-Document with custom style-set + # Usage: docx.Document('template.docx') + # ############################################# + global doc + doc = docx.Document(docx_tmpl) + # Remove eventual example content/paragraphs + for p in doc.paragraphs: + e = p._element + e.getparent().remove(e) + # set_docx_styles(doc) + + # https://python-docx.readthedocs.io/en/latest/dev/analysis/features/coreprops.html + doc.core_properties.author = docx_creator + doc.core_properties.title = standard.pystr(zmscontext.attr('title')) + doc.core_properties.created = datetime.datetime.now() + doc.core_properties.modified = datetime.datetime.now() + doc.core_properties.category = 'ZMS-Export' + doc.core_properties.comments = 'Generated by ZMS / python-docx' + + # GET PAGE CONTENT (JSON) + zmsdoc = apply_standard_json_docx(self) + heading = zmsdoc[0] + blocks = zmsdoc[1:] + + # [A] CREATE SECTION HEADER/FOOTER + # On recursive export change header for any new page (self) + # while preserving footer all the same (zmscontext) + + dt = standard.getLangFmtDate(self, heading.get('last_change_dt',''), 'eng', '%Y-%m-%d') + userid = heading.get('userid','') + url = heading.get('url','').replace('nohost','localhost') + url = heading.get('url','').replace('http:///','http://localhost/') + url = len(url)>124 and url[:124]+'...' or url + tabs = len(heading.get('title',''))>68 and '\t' or '\t\t' + header_text = '%s%sOnline Edition %s\nURL: %s'%(standard.pystr(heading.get('title','')), tabs, dt, url) + + if page_counter == 1: + header_p = doc.sections[0].header.paragraphs[0] + header_p.clear() + header_p.text = header_text + footer_p = doc.sections[0].footer.paragraphs[0] + footer_p.clear() + footer_p.add_run('Seite ') + add_field(footer_p, field_code='PAGE') + footer_p.add_run('\t') + footer_p.add_run('Dateiname: ') + add_field(footer_p, field_code='FILENAME') + footer_p.add_run(' / Stand: ') + add_field(footer_p, field_code='SAVEDATE \@ "yyyy-MM-dd" \* MERGEFORMAT') + else: + new_section = doc.add_section(WD_SECTION_START.NEW_PAGE) + new_section.header.is_linked_to_previous = False # ESSENTIAL FOR CHANGING HEADER!!! + header_paragraph = new_section.header.paragraphs[0] if new_section.header.paragraphs else new_section.header.add_paragraph() + header_paragraph.text = header_text + # add_breadcrumbs_as_runs(zmscontext, header_paragraph) + # new_section.header.first_page_header = True + # new_section.footer.first_page_footer = True + + # [B] CONTENT HEADINGS + if is_page: + # [1] CREATE INFO-PAGE + doc_title = add_heading(doc, standard.pystr(heading.get('title','')), level=0) + p = doc.add_paragraph() + add_breadcrumbs_as_runs(zmscontext, p) + + # Document Protocol Table + v = ''' + + + + + + + + + +
Dokumenthistorie
DatumBearbeitungsstadium*Bearbeiter
%sZMS-Edition%s
%sZMS-Export%s
'''%(dt, userid, str(doc.core_properties.created).split(' ')[0], docx_creator) + add_htmlblock_to_docx(zmscontext=zmscontext, docx_doc=doc, htmlblock=v, zmsid=None) + p = doc.add_paragraph(style='Table-Small') + p.add_run(standard.pystr('* mögliche Bearbeitungsstadien siehe Dokument NEON Blueprints: How To')).font.italic = True + + p.add_run().add_break(docx.enum.text.WD_BREAK.PAGE) + + # [2] DOCUMENT TITLE AND DESCRIPTION + add_heading(doc, standard.pystr(heading.get('title','')), level=1) + prepend_bookmark(doc.paragraphs[-1], heading.get('id','')) + if heading.get('description','')!='': + doc.add_paragraph(standard.pystr(heading.get('description','')), style='Description') + + # [C] CREATE PAGE CONTENT-BLOCKS + for block in blocks: + v = standard.pystr(block['content']) + # ############################################# + # [1] HTML-BLOCK (e.g. richtext with inline styles, just a minimum set of inline elements) + if v and block['docx_format'] == 'html': + try: + add_htmlblock_to_docx(zmscontext=zmscontext, docx_doc=doc, htmlblock=v, zmsid=block['id'], zmsmetaid=block['meta_id']) + except: + p = doc.add_paragraph() + p.add_run('Rendering Error: %s'%block['meta_id']) + # ############################################# + # [2] XML-BLOCK + elif v and block['docx_format'] == 'xml': + # Create a paragraph object from parsed xml + parsed_xml = parse_xml(v) + doc.add_paragraph() + # Replace last paragraph with parsed xml + doc.element.body[-1] = parsed_xml + # ############################################# + # [3] IMAGE-BLOCK + elif v and block['docx_format'] == 'image': + # Add image to document + image_url = v + resp = requests.get(image_url, verify=False) + image_file = BytesIO(resp.content) + # Add a paragraph containing the image as run + # see: /python-docx/src/docx/document.py + p = doc.add_paragraph() + r = p.add_run() + # Normalize image width to 460px + # Get image width and height from 'image' data block + imgwidth = block.get('imgwidth',0) + imgheight = block.get('imgheight',0) + try: + imgwidth = get_normalized_image_width(w = imgwidth, h = imgheight, max_w = 460) + except: + imgwidth = 460 + r.add_picture(image_file, width=Emu(imgwidth*9525)) + prepend_bookmark(p, block['id']) + # ############################################# + # [4] CAPTION TEXT-BLOCK + elif v and block['docx_format']=='Caption': + if re.match(r'^\[Abb. e\d+\] .*', v): + capt_list = re.split(r'^\[Abb. e\d+\] ', v) + if len(capt_list) > 1 and len(capt_list[1]) > 0: + p = doc.add_paragraph(style='Caption') + prepend_bookmark(p, block['id']) + p.add_run('Abb. %s: '%block['id']).font.italic = False + p.add_run(capt_list[1]) + elif re.match(r'^\[Abb. e\d+\] ', v): + # Omit caption with empty text + pass + else: + p = doc.add_paragraph(style='Caption') + prepend_bookmark(p, block['id']) + # ############################################# + # [5] TEXT-BLOCK with given block format (style) + elif v and block['docx_format'] in [e.name for e in doc.styles]: + p = doc.add_paragraph(v, style=block['docx_format']) + prepend_bookmark(p, block['id']) + elif v: + p = doc.add_paragraph(v) + prepend_bookmark(p, block['id']) + + + # ############################################# + # Normalize Headline Hierarchy + # using function normalize_headline_levels + # up-levelling systematic gaps in headline levels + # --------------------------------------------- + # TODO: Make it work with recursive export + # ############################################# + if is_page: + # Get all headline paragraphs for any page / section + section_list = get_headings_of_section(doc) + for headline_paragraphs in section_list: + # 1. Get headline levels on HTML/stylenames + # and its normalization with function normalize_headline_levels() + headline_levels_on_stylenames = [int(p.style.name[-1]) for p in headline_paragraphs] + headline_levels_normalized = normalize_headline_levels(headline_levels_on_stylenames) + + # 2. Get headline levels on its numbering-prefixes + headline_levels_on_numbering = get_headline_levels_from_numbering(headline_paragraphs) + + # 3. Which method for getting the normalized levels is more reliable? + # Sum up prefix-based level-numbers and divide by number of levels + # Approach: if there are no numbers, all levels are considered as 1 + # what is supposed to be unreliable + if sum(headline_levels_on_numbering)/len(headline_levels_on_numbering) > 1.25: + headline_levels_normalized = headline_levels_on_numbering + + # 4. Reset levels of headline-paragraphs + for i, p in enumerate(headline_paragraphs): + p.style = doc.styles['heading %s'%headline_levels_normalized[i]] + + + + # ############################################# + # [d] SAVE DOCX-FILE + # ############################################# + + if save_file: + # Save document in temporary directory + fn = '%s.docx'%(file_name and file_name or zmscontext.id_quote(zmscontext.getTitlealt(request))) + tempfolder = tempfile.mkdtemp() + docx_file_name = os.path.join(tempfolder, fn) + doc.save(docx_file_name) + + # Read the docx file + with open(docx_file_name, 'rb') as f: + docx_file_data = f.read() + + # Remove the temporary folder + shutil.rmtree(tempfolder) + + # Set the HTTP response headers + request.RESPONSE.setHeader('Content-Disposition', 'inline;filename=%s'%fn) + request.RESPONSE.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') + + # msg = 'DOCX-Export erfolgreich: %s'%fn + # request.response.redirect(standard.url_append_params('%s/manage_main'%self.absolute_url(),{'lang':request['lang'],'manage_tabs_message':msg})) + + # Return the data of the docx file + # on single page export or on last page of recursive export + return docx_file_data + else: + # Proceed with recursive export + pass \ No newline at end of file diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx/neon.docx b/Products/zms/conf/metacmd_manager/manage_export_pydocx/neon.docx new file mode 100644 index 0000000000000000000000000000000000000000..44770a8f280c6242aace895a282991ad39325eec GIT binary patch literal 19805 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 PfPX1~PxazI{`da@%U!TY literal 0 HcmV?d00001 diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx/numbering.xml b/Products/zms/conf/metacmd_manager/manage_export_pydocx/numbering.xml new file mode 100644 index 000000000..9e7030258 --- /dev/null +++ b/Products/zms/conf/metacmd_manager/manage_export_pydocx/numbering.xml @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx/readme.md b/Products/zms/conf/metacmd_manager/manage_export_pydocx/readme.md new file mode 100644 index 000000000..255a4bdcd --- /dev/null +++ b/Products/zms/conf/metacmd_manager/manage_export_pydocx/readme.md @@ -0,0 +1,22 @@ +# Word/DOCX Export + +## Overview + +This script, `manage_export_pydoc.py`, is designed to export content to a Word/DOCX file using the `python-docx` library. It is intended to be used as a ZMS action. + +## Prerequisites + +- Python 3.x +- `python-docx` library + +You can install the required library using pip: + +```sh +pip install python-docx +``` + +## Configuration and Customization + +Ensure that the script is configured correctly with the necessary parameters for your specific use case (especially the global variable `docx_tmpl` as filesystem path to the DOCX file that is used as a template). You may need to modify the script to fit your data source and desired output format. +Some (complex) ZMS content objects may need another template `standard_json_docx` (Python script) to generate a normalized JSON representation of the object's content. The standard content model contains some examples of the script. For further details, please refer to the docstring of +`manage_export_pydocx.apply_standard_json_docx()`. diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx/styles.xml b/Products/zms/conf/metacmd_manager/manage_export_pydocx/styles.xml new file mode 100644 index 000000000..2f7d1fd40 --- /dev/null +++ b/Products/zms/conf/metacmd_manager/manage_export_pydocx/styles.xml @@ -0,0 +1,631 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx_recursive/__init__.py b/Products/zms/conf/metacmd_manager/manage_export_pydocx_recursive/__init__.py new file mode 100644 index 000000000..3906474cf --- /dev/null +++ b/Products/zms/conf/metacmd_manager/manage_export_pydocx_recursive/__init__.py @@ -0,0 +1,51 @@ +class manage_export_pydocx_recursive: + """ + python-representation of manage_export_pydocx_recursive + """ + + # Acquired + acquired = 0 + + # Action + action = "%smanage_executeMetacmd?id=manage_export_pydocx_recursive" + + # Description + description = "" + + # Execution + execution = 0 + + # Icon_clazz + icon_clazz = "fas fa-download text-danger" + + # Id + id = "manage_export_pydocx_recursive" + + # Meta_types + meta_types = ["type(ZMSDocument)"] + + # Name + name = "Py-DOCX Export (Recursive)" + + # Nodes + nodes = "{$}" + + # Package + package = "com.zms.foundation.export" + + # Revision + revision = "5.0.0" + + # Roles + roles = ["ZMSAdministrator"] + + # Stereotype + stereotype = "" + + # Title + title = "Py-DOCX Export (Recursive)" + + # Impl + class Impl: + manage_export_pydocx_recursive = {"id":"manage_export_pydocx_recursive" + ,"type":"External Method"} diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx_recursive/manage_export_pydocx_recursive.py b/Products/zms/conf/metacmd_manager/manage_export_pydocx_recursive/manage_export_pydocx_recursive.py new file mode 100644 index 000000000..0f2f437b9 --- /dev/null +++ b/Products/zms/conf/metacmd_manager/manage_export_pydocx_recursive/manage_export_pydocx_recursive.py @@ -0,0 +1,15 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +def manage_export_pydocx_recursive(self): + request = self.REQUEST + file_name = self.id_quote(self.getTitlealt(request)) + zmsdocs = [] + zmsdocs.append(self) + zmsdocs.extend(self.filteredTreeNodes(request, self.PAGES)) + docx_file_data = None + for zmsdoc in zmsdocs: + # Do return data on last zmsdoc + save_file = zmsdoc == zmsdocs[-1] + docx_file_data = zmsdoc.manage_export_pydocx(save_file = save_file, file_name = file_name) + return docx_file_data \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_carousel/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_carousel/__init__.py index ceb63246b..93960a73a 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_carousel/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_carousel/__init__.py @@ -26,7 +26,7 @@ class bt_carousel: package = "com.zms.foundation.bootstrap" # Revision - revision = "4.1.0" + revision = "5.0.0" # Type type = "ZMSObject" @@ -105,3 +105,12 @@ class Attrs: ,"name":"Template: Carousel" ,"repetitive":0 ,"type":"zpt"} + + standard_json_docx = {"default":"" + ,"id":"standard_json_docx" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"JSON-DOCX Template" + ,"repetitive":0 + ,"type":"py"} diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_carousel/standard_json_docx.py b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_carousel/standard_json_docx.py new file mode 100644 index 000000000..09172bf2c --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_carousel/standard_json_docx.py @@ -0,0 +1,60 @@ +## Script (Python) "bt_carousel.standard_json_docx" +##bind container=container +##bind context=context +##bind namespace= +##bind script=script +##bind subpath=traverse_subpath +##parameters=zmscontext=None,options=None +##title=py: JSON-DOCX Template +## +# --// standard_json_docx //-- +# This Python script is used to generate a normalized JSON representation +# of the object's content, which is then used by ZMS-action manage_export_pydocx +# for exporting its content to a Word file. +# For further details, please refer to the docstring of +# manage_export_pydocx.apply_standard_json_docx(). +# +from Products.zms import standard +request = zmscontext.REQUEST +url_base = request.get('BASE0','') + +blocks = [] + +for slide in zmscontext.filteredObjChildren('slides',request): + id = slide.id + title = slide.attr('title') + text = slide.attr('text') + url = slide.attr('url') + img = slide.attr('image') + src = img.getHref(request) + + docxml = f''' + + + + {f'{title}' if title else ''} + {text} + {f'{url}' if url else ''} + + ''' + blocks.append( { + 'id': id, + 'meta_id': slide.meta_id, + 'parent_id': slide.getParentNode().id, + 'parent_meta_id': slide.getParentNode().meta_id, + 'docx_format': 'xml', + 'content': docxml + } ) + + blocks.append( { + 'id': '%s_1'%(id), + 'meta_id': slide.meta_id, + 'parent_id': slide.getParentNode().id, + 'parent_meta_id': slide.getParentNode().meta_id, + 'docx_format': 'image', + 'content': '//' in src and src or '%s%s'%(url_base, src) + } ) + +return blocks + +# --// /standard_json_docx //-- diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_jumbotron/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_jumbotron/__init__.py index 872572716..e1f9d42d1 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_jumbotron/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_jumbotron/__init__.py @@ -26,7 +26,7 @@ class bt_jumbotron: package = "com.zms.foundation.bootstrap" # Revision - revision = "4.1.0" + revision = "5.0.0" # Type type = "ZMSObject" diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_jumbotron/standard_html.zpt b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_jumbotron/standard_html.zpt index b67b7adaf..d5a58f9c8 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_jumbotron/standard_html.zpt +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation.bootstrap/bt_jumbotron/standard_html.zpt @@ -10,7 +10,7 @@

Jumbotron title

Jumbotron subtitle


-

Some quick example text to build on the card title and make up the bulk of the card's content.

+

Some quick example text to build on the card title and make up the bulk of the card's content.

'%(img_url, imgwidth, imgheight) + } +] + +return blocks +# --// standard_json_docx //-- diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSNote/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSNote/__init__.py index 19f748b2f..88a1de477 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSNote/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSNote/__init__.py @@ -6,12 +6,10 @@ class ZMSNote: # Access access = {"delete_custom":"" ,"delete_deny":["" - ,"" ,"" ,""] ,"insert_custom":"{$}" ,"insert_deny":["" - ,"" ,"" ,""]} @@ -28,7 +26,7 @@ class ZMSNote: package = "com.zms.foundation" # Revision - revision = "5.1.0" + revision = "5.2.0" # Type type = "ZMSObject" @@ -80,3 +78,12 @@ class Attrs: ,"name":"Event: onChangeObj" ,"repetitive":0 ,"type":"py"} + + standard_json_docx = {"default":"" + ,"id":"standard_json_docx" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"JSON-DOCX Template" + ,"repetitive":0 + ,"type":"py"} diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSNote/standard_json_docx.py b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSNote/standard_json_docx.py new file mode 100644 index 000000000..384afe598 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSNote/standard_json_docx.py @@ -0,0 +1,43 @@ +## Script (Python) "ZMSNote.standard_json_docx" +##bind container=container +##bind context=context +##bind namespace= +##bind script=script +##bind subpath=traverse_subpath +##parameters=zmscontext=None,options=None +##title=py: JSON-DOCX Template +## +# --// standard_json_docx //-- +# This Python script is used to generate a normalized JSON representation +# of the object's content, which is then used by ZMS-action manage_export_pydocx +# for exporting its content to a Word file. +# For further details, please refer to the docstring of +# manage_export_pydocx.apply_standard_json_docx(). +# +from Products.zms import standard +request = zmscontext.REQUEST +id = zmscontext.id +text = zmscontext.attr('text') +author = zmscontext.attr('change_uid') +# date = '2024-01-01T00:00:00Z' +date = '%sZ'%standard.format_datetime_iso(zmscontext.attr('change_dt'))[0:-6] +zmsnote_langstr = zmscontext.getZMILangStr('TYPE_ZMSNOTE') + +blocks = [] + +text = '%s: %s, %s\n%s'%(zmsnote_langstr, author, date, text) + +blocks.append( + { + 'id': id, + 'meta_id': zmscontext.meta_id, + 'parent_id': zmscontext.getParentNode().id, + 'parent_meta_id': zmscontext.getParentNode().meta_id, + 'docx_format': 'ZMSNotiz', + 'content': text + } +) + +return blocks +# --// /standard_json_docx //-- + diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSTextarea/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSTextarea/__init__.py index 53749ac91..2078937ef 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSTextarea/__init__.py +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSTextarea/__init__.py @@ -26,7 +26,7 @@ class ZMSTextarea: package = "com.zms.foundation" # Revision - revision = "5.1.0" + revision = "5.2.0" # Type type = "ZMSObject" @@ -96,3 +96,12 @@ class Attrs: ,"name":"Template: ZMSTextarea" ,"repetitive":0 ,"type":"zpt"} + + standard_json_docx = {"default":"" + ,"id":"standard_json_docx" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"DOCX-JSON Template: ZMSTextarea" + ,"repetitive":0 + ,"type":"py"} diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSTextarea/standard_json_docx.py b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSTextarea/standard_json_docx.py new file mode 100644 index 000000000..2928fddae --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSTextarea/standard_json_docx.py @@ -0,0 +1,55 @@ +## Script (Python) "ZMSTextarea.standard_json_docx" +##bind container=container +##bind context=context +##bind namespace= +##bind script=script +##bind subpath=traverse_subpath +##parameters=zmscontext=None,options=None +##title=JSON +## +# --// standard_json_docx //-- +# This Python script is used to generate a normalized JSON representation +# of the object's content, which is then used by ZMS-action manage_export_pydocx +# for exporting its content to a Word file. +# For further details, please refer to the docstring of +# manage_export_pydocx.apply_standard_json_docx(). +# +from Products.zms import standard +request = zmscontext.REQUEST + +id = zmscontext.id +meta_id = zmscontext.meta_id +parent_id = zmscontext.getParentNode().id +parent_meta_id = zmscontext.getParentNode().meta_id +format = zmscontext.attr('format') +text = zmscontext.attr('text') + +format_docx_map = { + "body" : "Normal", + "blockquote" : "Quote", + "caption" : "Caption", + "headline_1" : "Heading1", + "headline_2" : "Heading2", + "headline_3" : "Heading3", + "headline_4" : "Heading4", + "headline_5" : "Heading5", + "headline_6" : "Heading6", + "ordered_list" : "ListBullet", + "unordered_list" : "ListBullet", + "plain_html": "html", + "wysiwyg" : "html", +} + +blocks = [ + { + 'id':id, + 'meta_id':meta_id, + 'parent_id':parent_id, + 'parent_meta_id':parent_meta_id, + "docx_format":format_docx_map.get(format,'Normal'), + 'content':text + } +] + +return blocks +# --// standard_json_docx //-- diff --git a/docs/notebooks/snippets_08_pythondocx.ipynb b/docs/notebooks/snippets_08_pythondocx.ipynb new file mode 100644 index 000000000..ac2314576 --- /dev/null +++ b/docs/notebooks/snippets_08_pythondocx.ipynb @@ -0,0 +1,1013 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Applying python-docx to ZMS content\n", + "\n", + "Hint: The _jupyter notebook_ shows how to use the python-docx library to extract and convert content from ZMS. This code works with direct access to the ZODB database, so that images are not extracted (images need a running Zope server to be accessed).\n", + "For production needs ZMS contains a (customizable) command (aka \"action\") _manage_export_pydocx_ \"Export to python-docx\" that is based on this notebook code and exports a ZMS page content to a .docx file.\n", + "\n", + "\n", + "For the action _manage_export_pydocx_ the function `get_docx_normalized_json`creates a normalized JSON stream of a PAGE-like ZMS node. This JSON stream is used for transforming the content to DOCX. It is a list of dicts (key/value-pairs), where the first dict is representing the container meta data and the following blocks are representing the PAGEELEMENTS of the document. Each object dictionary has the following keys:\n", + "- id: the id of the node\n", + "- meta_id: the meta_id of the node\n", + "- parent_id: the id of the parent node\n", + "- parent_meta_id: the meta_id of the parent node\n", + "- title: the title of the node\n", + "- description: the description of the node\n", + "- last_change_dt: the last change date of the node\n", + "- docx_format: the format of the content ('html' or e.g.'Normal')\n", + "- content: the content of the node\n", + "\n", + "Any PAGEELEMENT-node may have a specific 'standard_json_docx' attribute which preprocesses it's ZMS content model close to the translation into the DOCX model. The key 'docx_format' is used to determine the style of the content block.\n", + "If this attribute method (py-primtive) is not available, the object's class standard_html-method is used to get the content, so that the (maybe not optimum) html will be transformed to DOCX.\n", + "\n", + "Depending on the complexity of the content it's JSON representation may consist of ore or multiple key/value-sequences. Any of these blocks will create a new block element (e.g. paragraph) in the DOCX document." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step-1: Load basic libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "__REGISTRY__['confdict'] None\n" + ] + } + ], + "source": [ + "import ZODB\n", + "from Products.Five.browser.tests.pages import SimpleView\n", + "from Testing.makerequest import makerequest \t\t\t\t\t\t\t# makerequest(context)\n", + "from Testing.ZopeTestCase.testZODBCompat import make_request_response \t# make_request_response()[1]\n", + "from Acquisition import aq_get\n", + "\n", + "from Products.zms import standard\n", + "from Products.zms import rest_api\n", + "\n", + "import os\n", + "import re\n", + "import shutil\n", + "import tempfile\n", + "import urllib\n", + "import json\n", + "import requests\n", + "\n", + "import docx\n", + "from docx.shared import Pt\n", + "from docx.enum.table import WD_TABLE_ALIGNMENT\n", + "from docx.enum.style import WD_STYLE_TYPE\n", + "from docx.oxml import OxmlElement, ns\n", + "from docx.shared import Emu\n", + "\n", + "from bs4 import BeautifulSoup" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step-2: Open ZODB" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a ZODB connection to an existing ZODB database file\n", + "# Important: Fit working directory to your Zope instance\n", + "try:\n", + "\twd = '/home/zope/instance/zms5_dev/var/'\n", + "\tdb = ZODB.DB(os.path.join(wd, 'Data.fs'))\n", + "\tconn = db.open()\n", + "\troot = conn.root\n", + "\t###{'Application': }\n", + "except:\n", + "\tdb.close()\n", + "\tprint('Error: Database connection had to be closed before reopened.')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step-3: Get ZODB Context" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# ZMS-Node /myzms2/content\n", + "# ##############################################\n", + "context = root.Application.myzms2.content \n", + "# ##############################################\n", + "\n", + "# Add REQUEST to zmscontext object\n", + "context = makerequest(context)\n", + "# Add REQUEST vars\n", + "context.REQUEST.environ.setdefault('SERVER_NAME','localhost')\n", + "context.REQUEST.environ.setdefault('SERVER_PORT', '8087')\n", + "context.REQUEST['URL' ]= 'http://localhost:8087'\n", + "context.REQUEST.set('lang','ger')\n", + "context.REQUEST.set('path_to_handle','')\n", + "# Add RESPONSE\n", + "context.REQUEST.set('RESPONSE', make_request_response()[1])\n", + "\n", + "zmscontext = context.e5\n", + "request = rest_api._get_request(zmscontext)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Test: Some example API calls for extracting content from ZMS objects" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"id\": \"e12\",\n", + " \"meta_id\": \"ZMSFolder\",\n", + " \"uid\": \"uid:38335f90-dea3-459b-b5fb-8566e66b065e\",\n", + " \"getPath\": \"/myzms2/content/e12\",\n", + " \"active\": 1,\n", + " \"title\": \"Details about the ZMS concept\",\n", + " \"titlealt\": \"Concept\",\n", + " \"is_page\": true,\n", + " \"is_page_element\": false,\n", + " \"index_html\": \"http://nohost/myzms2/content/e12/\",\n", + " \"parent_uid\": \"uid:4b46b796-d146-43c1-8eca-954d1ba2aafc\",\n", + " \"home_id\": \"myzms2\",\n", + " \"level\": 1,\n", + " \"restricted\": false,\n", + " \"titleimage\": null,\n", + " \"levelnfc\": \"\",\n", + " \"attr_dc_description\": \"Aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsu\",\n", + " \"attr_dc_subject\": \"\",\n", + " \"attr_dc_type\": \"\",\n", + " \"attr_dc_creator\": \"\"\n", + "}\n", + "{\n", + " \"id\": \"e4\",\n", + " \"meta_id\": \"ZMSTextarea\",\n", + " \"uid\": \"uid:68eeb9a5-c69e-4d0f-8869-b07f07e18d1a\",\n", + " \"getPath\": \"/myzms2/content/e4\",\n", + " \"active\": 1,\n", + " \"title\": \"ZMSTextarea\",\n", + " \"titlealt\": \"ZMSTextarea\",\n", + " \"is_page\": false,\n", + " \"is_page_element\": true,\n", + " \"index_html\": \"http://nohost/myzms2/content/#e4\",\n", + " \"parent_uid\": \"uid:4b46b796-d146-43c1-8eca-954d1ba2aafc\",\n", + " \"home_id\": \"myzms2\",\n", + " \"level\": 1,\n", + " \"restricted\": false,\n", + " \"format\": \"wysiwyg\",\n", + " \"text\": \"

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.   

\\n

Lorem ipsum dolor 

\\n

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.  Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.   

\\n

vulputate velit  molestie consequat

\\n

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.  Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.   

\"\n", + "}\n", + "[\n", + " {\n", + " \"id\": \"e4\",\n", + " \"meta_id\": \"ZMSTextarea\",\n", + " \"parent_id\": \"content\",\n", + " \"parent_meta_id\": \"ZMS\",\n", + " \"docx_format\": \"html\",\n", + " \"content\": \"

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.   

\\n

Lorem ipsum dolor 

\\n

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.  Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.   

\\n

vulputate velit  molestie consequat

\\n

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.  Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.   

\"\n", + " }\n", + "]\n" + ] + } + ], + "source": [ + "# tree_nodes = rest_api.RestApiController(context,request).list_tree_nodes(zmscontext)[0:1]\n", + "\n", + "node = context.e12\n", + "print(json.dumps(rest_api.get_attrs(node),indent=2))\n", + "\n", + "node = context.e4\n", + "print(json.dumps(rest_api.get_attrs(node),indent=2))\n", + "\n", + "## Test Python-Script\n", + "# a_pyscript = makerequest(root.Application.myzmsx.a_pyscript)\n", + "# print(a_pyscript.read())\n", + "\n", + "# Get a custom py methods standard_json_docx of a node (zpt does not work!)\n", + "print(json.dumps(rest_api.get_attr(node,'standard_json_docx'),indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper Functions 1: DOCX-XML\n", + "\n", + "1. `add_page_number(run)` : add page number to text-run (e.g. footer)\n", + "2. `add_bottom_border(style)` : adds border-properties to paragraph-style-object\n", + "\n", + "_Hint: the docx API does not support the page counter directly. We have to create a custom footer with a page counter._" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# #############################################\n", + "# Helper Functions 1: DOCX-XML\n", + "# 1. `add_page_number(run)` : add page number to text-run (e.g. footer)\n", + "# 2. `add_bottom_border(style)` : adds border-properties to paragraph-style-object\n", + "# Hint: the docx API does not support the page counter directly. \n", + "# We have to create a custom footer with a page counter.\n", + "# #############################################\n", + "\n", + "# XML-Helpers\n", + "def create_element(name):\n", + "\treturn OxmlElement(name)\n", + "\n", + "def create_attribute(element, name, value):\n", + "\telement.set(ns.qn(name), value)\n", + "\n", + "\n", + "def get_normalized_image_width(w, h, max_w = 460):\n", + "\tif w:\n", + "\t\tif h > w:\n", + "\t\t\tscale = h>max_w and float(h)/float(max_w) or 1\n", + "\t\telse:\n", + "\t\t\tscale = w>max_w and float(w)/float(max_w) or 1\n", + "\t\tw = int(w/scale)\n", + "\telse:\n", + "\t\tw = max_w\n", + "\treturn w\n", + "\n", + "# #############################################\n", + "\n", + "# ADD DATA FIELD: eg PAGE, SAVEDATE\n", + "def add_field(paragraph, field_code=\"PAGE\"):\n", + "\tfldChar1 = create_element('w:fldChar')\n", + "\tcreate_attribute(fldChar1, 'w:fldCharType', 'begin')\n", + "\tinstrText = create_element('w:instrText')\n", + "\tcreate_attribute(instrText, 'xml:space', 'preserve')\n", + "\tinstrText.text = field_code\n", + "\tfldChar2 = create_element('w:fldChar')\n", + "\tcreate_attribute(fldChar2, 'w:fldCharType', 'end')\n", + "\trun = paragraph.add_run()\n", + "\trun._r.append(fldChar1)\n", + "\trun._r.append(instrText)\n", + "\trun._r.append(fldChar2)\n", + "\n", + "\n", + "# BOOKMARK ZMS-ID\n", + "def prepend_bookmark(docx_block, bookmark_id):\n", + "\tbookmark_start = create_element('w:bookmarkStart')\n", + "\tcreate_attribute(bookmark_start, 'w:id', bookmark_id)\n", + "\tcreate_attribute(bookmark_start, 'w:name', bookmark_id)\n", + "\tbookmark_end = create_element('w:bookmarkEnd')\n", + "\tcreate_attribute(bookmark_end, 'w:id', bookmark_id)\n", + "\ttry:\n", + "\t\tdocx_block._element.insert(0, bookmark_end)\n", + "\t\tdocx_block._element.insert(0, bookmark_start)\n", + "\texcept:\n", + "\t\tpass\n", + "\n", + "def add_hyperlink(docx_block, link_text, url):\n", + "\t# url_base = 'http://127.0.0.1:8080/'\n", + "\turl_base = 'http://neon/'\n", + "\t# Omit javascript links\n", + "\tif not url.startswith('javascript:'):\n", + "\t\t# Fix missing domain name\n", + "\t\turl = ('http' in url) and url.replace('http:///', url_base) or (url_base + (url.startswith('/') and url[1:] or url))\n", + "\t\tr_id = docx_block.part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True)\n", + "\t\thyper_link = create_element('w:hyperlink')\n", + "\t\tcreate_attribute(hyper_link, 'r:id', r_id)\n", + "\t\thyper_link_run = create_element('w:r')\n", + "\t\thyper_link_run_prop = create_element('w:rPr')\n", + "\t\thyper_link_run_prop_style = create_element('w:rStyle')\n", + "\t\tcreate_attribute(hyper_link_run_prop_style, 'w:val', 'Hyperlink')\n", + "\t\thyper_link_run_prop.append(hyper_link_run_prop_style)\n", + "\t\thyper_link_run.append(hyper_link_run_prop)\n", + "\t\thyper_link_text = create_element('w:t')\n", + "\t\thyper_link_text.text = link_text\n", + "\t\thyper_link_run.append(hyper_link_text)\n", + "\t\thyper_link.append(hyper_link_run)\n", + "\t\tdocx_block._p.append(hyper_link)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper Functions 2: HTML/Richtext-Processing " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# #############################################\n", + "# ADD RUNS\n", + "# #############################################\n", + "def add_runs(docx_block, bs_element):\n", + "\t# #########################################\n", + "\t# Adding a minimum set of inline runs\n", + "\t# any BeautifulSoup block element may contain\n", + "\t# to the docx-block, e.g. , ,
\n", + "\t# #########################################\n", + "\tif bs_element.children:\n", + "\t\tfor elrun in bs_element.children:\n", + "\t\t\tif elrun.name == 'strong':\n", + "\t\t\t\tdocx_block.add_run(elrun.text).bold = True\n", + "\t\t\telif elrun.name == 'em':\n", + "\t\t\t\tdocx_block.add_run(elrun.text).italic = True\n", + "\t\t\telif elrun.name == 'a':\n", + "\t\t\t\tadd_hyperlink(block = docx_block, link_text = elrun.text, url = elrun.get('href'))\n", + "\t\t\t\tdocx_block.add_run(' ')\n", + "\t\t\telse:\n", + "\t\t\t\tdocx_block.add_run(str(elrun))\n", + "\telse:\n", + "\t\tdocx_block.text(bs_element.text)\n", + "\n", + "\n", + "# #############################################\n", + "# ADD HTML-BLOCK TO DOCX\n", + "# #############################################\n", + "def add_htmlblock_to_docx(zmscontext, docx_doc, htmlblock, zmsid=None):\n", + "\t# Clean HTML\n", + "\thtmlblock = clean_html(htmlblock)\n", + "\n", + "\t# Apply BeautifulSoup and iterate over elements\n", + "\tsoup = BeautifulSoup(htmlblock, 'html.parser')\n", + "\n", + "\t# Counter for html elements: set bookmark before first element\n", + "\tc = 0\n", + "\n", + "\t# Iterate over elements\n", + "\tfor element in soup.children:\n", + "\t\tif element.name != None and element not in ['\\n']:\n", + "\t\t\tc+=1\n", + "\t\t\tif element.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:\n", + "\t\t\t\theading_level = int(element.name[1])\n", + "\t\t\t\tp = docx_doc.add_heading(element.text, level=heading_level)\n", + "\t\t\t\tif c==1: \n", + "\t\t\t\t\tprepend_bookmark(p, zmsid)\n", + "\n", + "\t\t\telif element.name == 'p':\n", + "\t\t\t\tp = docx_doc.add_paragraph()\n", + "\t\t\t\tif c==1: \n", + "\t\t\t\t\tprepend_bookmark(p, zmsid)\n", + "\t\t\t\tif element.has_attr('class'):\n", + "\t\t\t\t\tif 'caption' in element['class']:\n", + "\t\t\t\t\t\tp.style = docx_doc.styles['Caption']\n", + "\t\t\t\t\telse:\n", + "\t\t\t\t\t\tclass_name = element['class'][0]\n", + "\t\t\t\t\t\tstyle_name = (class_name in docx_doc.styles) and class_name or 'Normal'\n", + "\t\t\t\t\t\tp.style = docx_doc.styles[style_name]\n", + "\t\t\t\tadd_runs(docx_block = p, bs_element = element)\n", + "\n", + "\t\t\telif element.name in ['ul','ol']:\n", + "\t\t\t\tdef add_list(docx_doc, element, level=0, c=0):\n", + "\t\t\t\t\tli_styles = {'ul':'ListBullet', 'ol':'ListNumber'}\n", + "\t\t\t\t\tlevel_suffix = level!=0 and str(level+1) or ''\n", + "\t\t\t\t\tfor li in element.find_all('li', recursive=False):\n", + "\t\t\t\t\t\tp = docx_doc.add_paragraph(li.contents[0].strip(), style='%s%s'%(li_styles[element.name], level_suffix))\n", + "\t\t\t\t\t\tif c==1: \n", + "\t\t\t\t\t\t\tprepend_bookmark(p, zmsid)\n", + "\t\t\t\t\t\tfor ul in li.find_all(['ul','ol'], recursive=False):\n", + "\t\t\t\t\t\t\tadd_list(docx_doc, ul, level+1)\n", + "\t\t\t\tadd_list(docx_doc, element, level=0, c=c)\n", + "\n", + "\t\t\telif element.name == 'table':\n", + "\t\t\t\tcaption = element.find('caption')\n", + "\t\t\t\tif caption:\n", + "\t\t\t\t\tp = docx_doc.add_paragraph(caption.text, style='Caption')\n", + "\t\t\t\t\tif c==1: \n", + "\t\t\t\t\t\tprepend_bookmark(p, zmsid)\n", + "\t\t\t\trows = element.find_all('tr')\n", + "\t\t\t\tcols = rows[0].find_all(['td','th'])\n", + "\t\t\t\ttable = docx_doc.add_table(rows=len(rows), cols=len(cols))\n", + "\t\t\t\ttable.style = 'Table Grid'\n", + "\t\t\t\ttable.alignment = WD_TABLE_ALIGNMENT.CENTER\n", + "\t\t\t\tr=-1\n", + "\t\t\t\tfor row in rows:\n", + "\t\t\t\t\tr+=1\n", + "\t\t\t\t\tcells = row.find_all(['td','th'])\n", + "\t\t\t\t\tfor i, cl in enumerate(cells):\n", + "\t\t\t\t\t\ttable.cell(r,i).text = cl.text\n", + "\t\t\t\t\t\tif cl.name == 'th':\n", + "\t\t\t\t\t\t\ttable.cell(r,i).paragraphs[0].runs[0].bold = True\n", + "\t\t\t\tif not caption and c==1:\n", + "\t\t\t\t\tprepend_bookmark(table, zmsid)\n", + "\n", + "\n", + "\t\t\telif element.name == 'img' or element.name == 'figure':\n", + "\t\t\t\tif element.name == 'figure':\n", + "\t\t\t\t\telement = element.find('img')\n", + "\t\t\t\tif element.has_attr('src'):\n", + "\t\t\t\t\timg_name = element['src'].split('/')[-1]\n", + "\t\t\t\t\tif not element['src'].startswith('http'):\n", + "\t\t\t\t\t\tsrc_url0 = zmscontext.absolute_url().split('/content/')[0]\n", + "\t\t\t\t\t\tsrc_url1 = element['src'].split('/content/')[-1]\n", + "\t\t\t\t\t\telement['src'] = '%s/content/%s'%(src_url0, src_url1)\n", + "\n", + "\t\t\t\tmaxwidth = 460\n", + "\t\t\t\timgwidth = element.has_attr('width') and int(float(element['width'])) or None\n", + "\t\t\t\tif imgwidth:\n", + "\t\t\t\t\tscale = imgwidth>maxwidth and imgwidth/maxwidth or 1\n", + "\t\t\t\t\timgwidth = imgwidth/scale\n", + "\t\t\t\telse:\n", + "\t\t\t\t\timgwidth = maxwidth\n", + "\n", + "\t\t\t\ttry:\n", + "\t\t\t\t\tresponse = requests.get(element['src'])\n", + "\t\t\t\t\twith open(img_name, 'wb') as f:\n", + "\t\t\t\t\t\tf.write(response.content)\n", + "\t\t\t\t\tdocx_doc.add_picture(img_name, width=Emu(imgwidth*9525))\n", + "\t\t\t\t\tif c==1:\n", + "\t\t\t\t\t\tprepend_bookmark(docx_doc.paragraphs[-1], zmsid)\n", + "\t\t\t\texcept:\n", + "\t\t\t\t\tpass\n", + "\n", + "\t\t\telif element.name == 'div':\n", + "\t\t\t\tchild_tags = [e.name for e in element.children if e.name]\n", + "\t\t\t\tif 'em' in child_tags or 'strong' in child_tags:\n", + "\t\t\t\t\tp = docx_doc.add_paragraph()\n", + "\t\t\t\t\tif c==1: \n", + "\t\t\t\t\t\tprepend_bookmark(p, zmsid)\n", + "\t\t\t\t\tif element.has_attr('class'):\n", + "\t\t\t\t\t\tclass_name = element['class'][0]\n", + "\t\t\t\t\t\tstyle_name = (class_name in docx_doc.styles) and class_name or 'Normal'\n", + "\t\t\t\t\t\tp.style = docx_doc.styles[style_name]\n", + "\t\t\t\t\tadd_runs(docx_block = p, bs_element = element)\n", + "\t\t\t\telse:\n", + "\t\t\t\t\tdiv_html = ''.join([str(e) for e in element.children])\n", + "\t\t\t\t\tadd_htmlblock_to_docx(zmscontext, docx_doc, div_html, zmsid)\n", + "\n", + "\t\t\telif element.name == 'a':\n", + "\t\t\t\t# Hyperlink containing a block element \n", + "\t\t\t\tdiv_html = ''.join([str(e) for e in element.children])\n", + "\t\t\t\tadd_htmlblock_to_docx(zmscontext, docx_doc, div_html, zmsid)\n", + "\n", + "\t\t\telif element.name == 'hr':\n", + "\t\t\t\t# ignore horizontal rule\n", + "\t\t\t\tpass\n", + "\n", + "\t\t\telse:\n", + "\t\t\t\tp = docx_doc.add_paragraph(str(element))\n", + "\t\t\t\tif c==1: \n", + "\t\t\t\t\tprepend_bookmark(p, zmsid)\n", + "\n", + "\treturn docx_doc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper Functions 3: Set Docx-Styles" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# #############################################\n", + "# Helper Function for Style Definitions\n", + "# #############################################\n", + "\n", + "# BORDER BOTTOM\n", + "def add_bottom_border(style):\n", + "\tborder = create_element('w:pBdr') # pBdr = Paragraph border\n", + "\tbottom = create_element('w:bottom')\n", + "\tcreate_attribute(bottom, 'w:val', 'single')\n", + "\tcreate_attribute(bottom, 'w:sz', '2')\n", + "\tcreate_attribute(bottom, 'w:space', '9')\n", + "\tcreate_attribute(bottom, 'w:color', '017D87')\n", + "\tborder.append(bottom)\n", + "\ttry:\n", + "\t\tstyle.element.pPr.append(border) # pPr = Paragraph properties\n", + "\texcept:\n", + "\t\tstandard.writeStdout(None, 'Error: Could not add bottom border to style %s' % style.name)\n", + "\n", + "def add_listbullet_arrow(style):\n", + "\t# Add arrow list character to paragraph properties\n", + "\tnumPr = create_element('w:numPr') # numPr = Numbering properties\n", + "\tilvl = create_element('w:ilvl')\n", + "\tcreate_attribute(ilvl, 'w:val', '1')\n", + "\tnumId = create_element('w:numId')\n", + "\tcreate_attribute(numId, 'w:val', '1')\n", + "\tnumPr.append(ilvl)\n", + "\tnumPr.append(numId)\n", + "\tstyle.element.pPr.append(numPr)\n", + "\tjc = create_element('w:jc')\n", + "\tcreate_attribute(jc, 'w:val', 'left')\n", + "\tstyle.element.pPr.append(jc)\n", + "\toutlineLvl = create_element('w:outlineLvl')\n", + "\tcreate_attribute(outlineLvl, 'w:val', '0')\n", + "\tstyle.element.pPr.append(outlineLvl)\n", + "\n", + "\n", + "def add_paragraph_bgcolor(style, color):\n", + "\t\"\"\"\n", + "\tAdd shadow and borders to paragraph properties\n", + "\tParameters:\n", + "\t\tstyle = styles['ZMSNotiz']\n", + "\t\tcolor = 'fff5ce'\n", + "\t\"\"\"\n", + "\tshading = create_element('w:shd') # shd = Shading\n", + "\tcreate_attribute(shading, 'w:val', 'clear')\n", + "\tcreate_attribute(shading, 'w:color', 'auto')\n", + "\tcreate_attribute(shading, 'w:fill', color)\n", + "\tstyle.element.pPr.append(shading)\n", + "\tborder = create_element('w:pBdr') # pBdr = Paragraph border\n", + "\tfor side in ['left', 'right', 'top', 'bottom']:\n", + "\t\tborder_side = create_element('w:%s' % side)\n", + "\t\tcreate_attribute(border_side, 'w:val', 'single')\n", + "\t\tcreate_attribute(border_side, 'w:sz', '4')\n", + "\t\tcreate_attribute(border_side, 'w:space', '5')\n", + "\t\tcreate_attribute(border_side, 'w:color', color)\n", + "\t\tborder.append(border_side)\n", + "\tstyle.element.pPr.append(border)\n", + "\n", + "def add_table_bgcolor(style, color):\n", + "\t\"\"\"\n", + "\tAdd shadow and borders to table properties\n", + "\tParameters:\n", + "\t\tstyle = styles['Normal Table']\n", + "\t\tcolor = 'fff5ce'\n", + "\t\"\"\"\n", + "\tshading = create_element('w:shd') # shd = Shading\n", + "\tcreate_attribute(shading, 'w:val', 'clear')\n", + "\tcreate_attribute(shading, 'w:color', 'auto')\n", + "\tcreate_attribute(shading, 'w:fill', color)\n", + "\tstyle.element.tblPr.append(shading)\n", + "\tborder = create_element('w:tblBorders') # tblBorders = Table borders\n", + "\tcreate_attribute(border, 'w:val', 'single')\n", + "\tcreate_attribute(border, 'w:sz', '4')\n", + "\tcreate_attribute(border, 'w:space', '5')\n", + "\tcreate_attribute(border, 'w:color', color)\n", + "\tstyle.element.tblPr.append(border)\n", + "\n", + "def add_character_bgcolor(style, color):\n", + "\t\"\"\"\n", + "\tAdd shadow and borders to run properties\n", + "\tParameters:\n", + "\t\tstyle = styles['Macro Text Char']\n", + "\t\tcolor = '017D87'\n", + "\t\"\"\"\n", + "\tshading = create_element('w:shd') # shd = Shading\n", + "\tcreate_attribute(shading, 'w:val', 'clear')\n", + "\tcreate_attribute(shading, 'w:color', 'auto')\n", + "\tcreate_attribute(shading, 'w:fill', color)\n", + "\tstyle.element.rPr.append(shading)\n", + "\tborder = create_element('w:bdr') # bdr = run border\n", + "\tcreate_attribute(border, 'w:val', 'single')\n", + "\tcreate_attribute(border, 'w:sz', '12')\n", + "\tcreate_attribute(border, 'w:space', '1')\n", + "\tcreate_attribute(border, 'w:color', color)\n", + "\tstyle.element.rPr.append(border)\n", + "\n", + "# #############################################\n", + "# Helper Functions 3: Set Docx-Styles\n", + "# #############################################\n", + "# List of all style names:\n", + "# print('\\n'.join([s.name for s in doc.styles]))\n", + "# List of all style IDs:\n", + "# print('\\n'.join([s.style_id for s in doc.styles]))\n", + "\n", + "# OBSOLETE: \n", + "# Function is not used in the current version\n", + "# Styles are provided in the docx-template\n", + "def set_docx_styles(doc):\n", + "\tstyles = doc.styles\n", + "\n", + "\t# Custom color 1: #017D87 dark turquoise\n", + "\tcolor_turquoise = docx.shared.RGBColor(1, 125, 135)\n", + "\t# Custom color 2: #AAAAAA light grey\n", + "\tcolor_lightgrey = docx.shared.RGBColor(170, 170, 170)\n", + "\t# Custom color 3: #333333 dark grey\n", + "\tcolor_darkgrey = docx.shared.RGBColor(51, 51, 51)\n", + "\t# Custom color 4: #FFFFFF white\n", + "\tcolor_white = docx.shared.RGBColor(255, 255, 255)\n", + "\t# Custom color 5: #0070FF blue\n", + "\tcolor_blue = docx.shared.RGBColor(0, 112, 255)\n", + "\n", + "\t# Page margins\n", + "\tdoc.sections[0].top_margin = Emu(120*9525)\n", + "\t# Normal\n", + "\tstyles['Normal'].font.name = 'Arial'\n", + "\tstyles['Normal'].font.size = Pt(9)\n", + "\tstyles['Normal'].paragraph_format.space_after = Pt(6)\n", + "\tstyles['Normal'].paragraph_format.space_before = Pt(6)\n", + "\tstyles['Normal'].paragraph_format.line_spacing = 1.35\n", + "\n", + "\t# Headlines derived from Normal\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['Heading 1'].basedOn = doc.styles['Normal']\n", + "\tstyles['Heading 1'].font.name = 'Arial'\n", + "\tstyles['Heading 1'].font.size = Pt(24)\n", + "\tstyles['Heading 1'].font.bold = False\n", + "\tstyles['Heading 1'].paragraph_format.line_spacing = 1\n", + "\tstyles['Heading 1'].paragraph_format.space_before = Pt(18)\n", + "\tstyles['Heading 1'].paragraph_format.space_after = Pt(18)\n", + "\tstyles['Heading 1'].font.color.rgb = color_turquoise\n", + "\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['Title'].basedOn = doc.styles['Heading 1']\n", + "\tstyles['Title'].font.name = 'Arial'\n", + "\tstyles['Title'].font.size = Pt(24)\n", + "\tstyles['Title'].font.bold = False\n", + "\tstyles['Title'].paragraph_format.line_spacing = 1\n", + "\tstyles['Title'].paragraph_format.space_before = Pt(18)\n", + "\tstyles['Title'].paragraph_format.space_after = Pt(18)\n", + "\tstyles['Title'].font.color.rgb = color_turquoise\n", + "\tadd_bottom_border(styles['Title'])\n", + "\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['Heading 2'].basedOn = doc.styles['Normal']\n", + "\tstyles['Heading 2'].font.name = 'Arial'\n", + "\tstyles['Heading 2'].font.size = Pt(18)\n", + "\tstyles['Heading 2'].font.bold = False\n", + "\tstyles['Heading 2'].paragraph_format.line_spacing = 1.2\n", + "\tstyles['Heading 2'].paragraph_format.space_before = Pt(24)\n", + "\tstyles['Heading 2'].font.color.rgb = color_turquoise\n", + "\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['Heading 3'].basedOn = doc.styles['Normal']\n", + "\tstyles['Heading 3'].font.name = 'Arial'\n", + "\tstyles['Heading 3'].font.size = Pt(13)\n", + "\tstyles['Heading 3'].font.bold = True\n", + "\tstyles['Heading 3'].paragraph_format.space_before = Pt(22)\n", + "\tstyles['Heading 3'].font.color.rgb = color_turquoise\n", + "\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['Heading 4'].basedOn = doc.styles['Normal']\n", + "\tstyles['Heading 4'].font.name = 'Arial'\n", + "\tstyles['Heading 4'].font.size = Pt(10)\n", + "\tstyles['Heading 4'].paragraph_format.space_before = Pt(14)\n", + "\tstyles['Heading 4'].font.bold = False\n", + "\tstyles['Heading 4'].font.bold = True\n", + "\tstyles['Heading 4'].font.color.rgb = color_turquoise\n", + "\n", + "\n", + "\t# More styles derived from Normal\n", + "\tstyles.add_style('Description', WD_STYLE_TYPE.PARAGRAPH)\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['Description'].basedOn = doc.styles['Normal']\n", + "\tstyles['Description'].font.name = 'Arial'\n", + "\tstyles['Description'].font.size = Pt(9)\n", + "\tstyles['Description'].font.italic = True\n", + "\tstyles['Description'].font.color.rgb = color_turquoise\n", + "\tstyles['Description'].paragraph_format.space_after = Pt(18)\n", + "\tstyles['Description'].paragraph_format.line_spacing = 1.35\n", + "\tadd_bottom_border(styles['Description'])\n", + "\n", + "\tstyles['Caption'].font.name = 'Arial'\n", + "\tstyles['Caption'].font.size = Pt(8)\n", + "\tstyles['Caption'].font.italic = True\n", + "\tstyles['Caption'].font.color.rgb = color_turquoise\n", + "\tstyles['Caption'].paragraph_format.space_before = Pt(6)\n", + "\tstyles['Caption'].paragraph_format.space_after = Pt(24)\n", + "\tstyles['Caption'].paragraph_format.keep_with_next = True\n", + "\n", + "\tstyles['Quote Char'].font.color.rgb = color_lightgrey\n", + "\tstyles['Quote Char'].font.bold = True\n", + "\tstyles['Quote Char'].font.italic = True\n", + "\n", + "\tstyles.add_style('Hyperlink', WD_STYLE_TYPE.CHARACTER)\n", + "\tstyles['Hyperlink'].font.color.rgb = color_turquoise\n", + "\tstyles['Hyperlink'].font.underline = True\n", + "\n", + "\tstyles.add_style('refGlossary', WD_STYLE_TYPE.CHARACTER)\n", + "\tstyles['refGlossary'].font.color.rgb = color_turquoise\n", + "\tstyles['refGlossary'].font.italic = False\n", + "\n", + "\tstyles['macro'].font.size = Pt(9)\n", + "\tstyles['macro'].paragraph_format.space_before = Pt(12)\n", + "\tstyles['macro'].paragraph_format.space_after = Pt(12)\n", + "\tstyles['macro'].paragraph_format.line_spacing = 1.35\n", + "\n", + "\tstyles['Macro Text Char'].font.bold = True\n", + "\tstyles['Macro Text Char'].font.color.rgb = color_blue\n", + "\n", + "\tstyles.add_style('Keyboard', WD_STYLE_TYPE.CHARACTER)\n", + "\tstyles['Keyboard'].font.name = 'Courier New'\n", + "\tstyles['Keyboard'].font.size = Pt(8)\n", + "\tstyles['Keyboard'].font.bold = True\n", + "\tstyles['Keyboard'].font.color.rgb = color_white\n", + "\tadd_character_bgcolor(styles['Keyboard'], '000000')\n", + "\n", + "\tstyles['header'].font.size = Pt(7)\n", + "\tstyles['header'].font.color.rgb = color_lightgrey\n", + "\tstyles['header'].paragraph_format.space_before = Pt(0)\n", + "\tstyles['header'].paragraph_format.line_spacing = 1.5\n", + "\tstyles['footer'].font.size = Pt(7)\n", + "\tstyles['footer'].font.color.rgb = color_lightgrey\n", + "\t# Remove default tabstops\n", + "\tstyles['footer'].paragraph_format.tab_stops.clear_all()\n", + "\t# Set new tabstop for right-aligned text\n", + "\tstyles['footer'].paragraph_format.tab_stops.add_tab_stop(Cm(16.5), WD_TAB_ALIGNMENT.RIGHT)\n", + "\n", + "\t# Table small\n", + "\tstyles.add_style('Table-Small', WD_STYLE_TYPE.PARAGRAPH)\n", + "\tstyles['Table-Small'].font.name = 'Arial'\n", + "\tstyles['Table-Small'].font.size = Pt(7)\n", + "\tstyles['Table-Small'].paragraph_format.space_after = Pt(2)\n", + "\tstyles['Table-Small'].paragraph_format.space_before = Pt(2)\n", + "\tstyles['Table-Small'].paragraph_format.line_spacing = 1.2\n", + "\n", + "\tstyles.add_style('Table-Caption', WD_STYLE_TYPE.PARAGRAPH)\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['Table-Caption'].basedOn = doc.styles['Normal']\n", + "\tstyles['Table-Caption'].font.name = 'Arial'\n", + "\tstyles['Table-Caption'].font.italic = True\n", + "\tstyles['Table-Caption'].font.color.rgb = color_turquoise\n", + "\tstyles['Table-Caption'].paragraph_format.space_before = Pt(24)\n", + "\tstyles['Table-Caption'].paragraph_format.space_after = Pt(3)\n", + "\tstyles['Table-Caption'].paragraph_format.keep_with_next = True\n", + "\n", + "\t# Inhaltsverzeichnis\n", + "\tstyles.add_style('TOC-Header', WD_STYLE_TYPE.PARAGRAPH)\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['TOC-Header'].basedOn = doc.styles['Heading 2']\n", + "\tstyles['TOC-Header'].font.name = 'Arial'\n", + "\tstyles['TOC-Header'].font.size = Pt(12)\n", + "\tstyles['TOC-Header'].font.bold = True\n", + "\tstyles['TOC-Header'].font.color.rgb = color_lightgrey\n", + "\tstyles['TOC-Header'].paragraph_format.space_before = Pt(12)\n", + "\tadd_bottom_border(styles['TOC-Header'])\n", + "\n", + "\t# Notiz\n", + "\tstyles.add_style('ZMSNotiz', WD_STYLE_TYPE.PARAGRAPH)\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['ZMSNotiz'].basedOn = doc.styles['Normal']\n", + "\tstyles['ZMSNotiz'].font.name = 'Arial'\n", + "\tstyles['ZMSNotiz'].font.size = Pt(8)\n", + "\tstyles['ZMSNotiz'].paragraph_format.space_before = Pt(12)\n", + "\tstyles['ZMSNotiz'].paragraph_format.space_after = Pt(12)\n", + "\tstyles['ZMSNotiz'].paragraph_format.line_spacing = 1.5\n", + "\t# Add background color\n", + "\tadd_paragraph_bgcolor(styles['ZMSNotiz'], 'fff5ce')\n", + "\n", + "\t# Merksatz\n", + "\tstyles.add_style('emphasis', WD_STYLE_TYPE.PARAGRAPH)\n", + "\tif sys.version_info[0] > 2:\n", + "\t\tstyles['emphasis'].basedOn = doc.styles['Normal']\n", + "\tstyles['emphasis'].font.name = 'Arial'\n", + "\tstyles['emphasis'].font.size = Pt(9)\n", + "\tstyles['emphasis'].font.bold = False\n", + "\tstyles['emphasis'].font.italic = True\n", + "\tstyles['emphasis'].paragraph_format.space_before = Pt(12)\n", + "\tstyles['emphasis'].paragraph_format.space_after = Pt(12)\n", + "\tstyles['emphasis'].paragraph_format.line_spacing = 1.5\n", + "\t# Add background color\n", + "\tadd_paragraph_bgcolor(styles['emphasis'], 'f0f8ff')\n", + "\n", + "\treturn doc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## HINT: standard_json_docx\n", + "\n", + "PAGEELEMENTS may have a 'standard_json_docx' to create a docx-like representation of the content.\n", + "PAGE objects will iterate over all PAGEELEMENTS and create a JSON stream of the page content. The action 'manage_export_pydocx' contains this method a function; if you want to use it in a notebook, you have to copy the code into the model of the PAGE-like content classes.\n", + "\n", + "```python\n", + "## Script (Python) \"ZMSDocument.standard_json_docx\"\n", + "##bind container=container\n", + "##bind context=context\n", + "##bind namespace=\n", + "##bind script=script\n", + "##bind subpath=traverse_subpath\n", + "##parameters=zmscontext=None,options=None\n", + "##title=py: JSON Template: ZMSDocument\n", + "##\n", + "# --// standard_json //--\n", + "from Products.zms import standard\n", + "request = zmscontext.REQUEST\n", + "\n", + "id = zmscontext.id\n", + "meta_id = zmscontext.meta_id\n", + "parent_id = zmscontext.getParentNode().id\n", + "parent_meta_id = zmscontext.getParentNode().meta_id\n", + "title = zmscontext.attr('title')\n", + "descripton = zmscontext.attr('attr_dc_descripton')\n", + "last_change_dt = zmscontext.attr('change_dt') or zmscontext.attr('created_dt')\n", + "url = zmscontext.getHref2IndexHtml(request)\n", + "\n", + "# 1st block is container meta data\n", + "blocks = [\n", + "\t{\n", + "\t\t'id':id,\n", + "\t\t'url':url,\n", + "\t\t'meta_id':meta_id,\n", + "\t\t'parent_id':parent_id,\n", + "\t\t'parent_meta_id':parent_meta_id,\n", + "\t\t'title':title,\n", + "\t\t'descripton':descripton,\n", + "\t\t'last_change_dt':last_change_dt\n", + "\t}\n", + "]\n", + "\n", + "# Sequence all pageelements\n", + "for pageelement in zmscontext.filteredChildNodes(request,zmscontext.PAGEELEMENTS):\n", + "\tif pageelement.attr('change_dt') and pageelement.attr('change_dt') >= last_change_dt:\n", + "\t\tlast_change_dt = pageelement.attr('change_dt')\n", + "\tjson_block = []\n", + "\tjson_block = pageelement.attr('standard_json')\n", + "\tif not json_block:\n", + "\t\thtml = ''\n", + "\t\ttry:\n", + "\t\t\thtml = pageelement.getBodyContent(request)\n", + "\t\t\t# Clean html data\n", + "\t\t\thtml = standard.re_sub(r'', '', html)\n", + "\t\t\thtml = standard.re_sub(r'\\n|\\t|\\s\\s', '', html)\n", + "\t\texcept:\n", + "\t\t\thtml = ''\n", + "\t\t\thtml += '' % pageelement.meta_id\n", + "\t\t\tattrs = [d['id'] for d in zmscontext.getMetaobjAttrs(pageelement.meta_id) if d['type'] not in ['dtml','zpt','py','constant','resource','interface']]\n", + "\t\t\tfor attr in attrs:\n", + "\t\t\t\thtml += '' % (attr, pageelement.attr(attr))\n", + "\t\t\thtml += '
Rendering Error: %s
%s%s
'\n", + "\t\t# Create a json block\n", + "\t\tjson_block = [{\n", + "\t\t\t'id': pageelement.id,\n", + "\t\t\t'meta_id': pageelement.meta_id,\n", + "\t\t\t'parent_id': pageelement.getParentNode().id,\n", + "\t\t\t'parent_meta_id': pageelement.getParentNode().meta_id,\n", + "\t\t\t'docx_format': 'html',\n", + "\t\t\t'content': html\n", + "\t\t}]\n", + "\tblocks.extend(json_block)\n", + "\n", + "# Update last_change_dt\n", + "blocks[0]['last_change_dt'] = last_change_dt\n", + "\n", + "return blocks\n", + "\n", + "# --// /standard_json //--\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MAIN function for DOCX-Generation" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def manage_export_pydocx(self):\n", + "\trequest = self.REQUEST\n", + "\n", + "\t# #############################################\n", + "\t# 1. INIT DOCUMENT\n", + "\t# #############################################\n", + "\tdoc = docx.Document()\t# Hint: may use template like docx.Document('template.docx')\n", + "\n", + "\t# #############################################\n", + "\t# 2. SET DOCX STYLES\n", + "\t# #############################################\n", + "\tdoc = set_docx_styles(doc)\n", + "\n", + "\t# #############################################\n", + "\t# 3. ITERATE JSON CONTENT TO DOCX\n", + "\t# #############################################\n", + "\tzmsdoc = self.attr('standard_json_docx')\n", + "\theading = zmsdoc[0]\n", + "\tblocks = zmsdoc[1:]\n", + "\n", + "\tdt = standard.getLangFmtDate(self, heading.get('last_change_dt',''), 'eng', '%Y-%m-%d')\n", + "\turl = heading.get('url','').replace('nohost','localhost')\n", + "\tdoc.sections[0].header.paragraphs[0].text = '%s\\t\\t%s\\nURL: %s'%(heading.get('title',''), dt, url)\n", + "\tadd_page_number(doc.sections[0].footer.paragraphs[0].add_run('Seite '))\n", + "\t\n", + "\tdoc.add_heading(heading.get('title',''), level=1)\n", + "\tprepend_bookmark(doc.paragraphs[-1], heading.get('id',''))\n", + "\t\n", + "\tif heading.get('description','')!='':\n", + "\t\tdescr = doc.add_paragraph(heading.get('description',''))\n", + "\t\tdescr.style = 'Description'\n", + "\t\n", + "\tfor block in blocks:\n", + "\t\tv = block['content']\n", + "\t\tif v and block['docx_format'] == 'html':\n", + "\t\t\tadd_htmlblock_to_docx(zmscontext=self, docx_doc=doc, htmlblock=v, zmsid=block['id'])\n", + "\t\telse:\n", + "\t\t\tp = doc.add_paragraph(v, style=block['docx_format'])\n", + "\t\t\tprepend_bookmark(p, block['id'])\n", + "\n", + "\n", + "\t# Save document in temporary directory\n", + "\tfn = '%s.docx'%(self.id_quote(self.getTitlealt(request)))\n", + "\ttempfolder = tempfile.mkdtemp()\n", + "\tdocx_filename = os.path.join(tempfolder, fn)\n", + "\tdoc.save(docx_filename)\n", + "\t\n", + "\t# Read the docx file\n", + "\twith open(docx_filename, 'rb') as f:\n", + "\t\tdata = f.read()\n", + "\n", + "\t# Remove the temporary folder\n", + "\tshutil.rmtree(tempfolder)\n", + "\n", + "\t# Set the HTTP response headers\n", + "\trequest.RESPONSE.setHeader('Content-Disposition', f'inline;filename={fn}')\n", + "\trequest.RESPONSE.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')\n", + "\n", + "\t# Return the data of the docx file\n", + "\treturn data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example: Generate DOCX from ZMS-Content" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "zmsdoc = context.e34.e36\n", + "data = manage_export_pydocx(zmsdoc)\n", + "# Write the data to a file\n", + "with open('test.docx', 'wb') as f:\n", + "\tf.write(data)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# # Finally close ZODB connection\n", + "db.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "vpy38", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/snippets_09_pythondocx.ipynb b/docs/notebooks/snippets_09_pythondocx.ipynb new file mode 100644 index 000000000..de7aaec97 --- /dev/null +++ b/docs/notebooks/snippets_09_pythondocx.ipynb @@ -0,0 +1,242 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating DOCX objects from XML fragments" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import docx\n", + "from docx.oxml import parse_xml\n", + "from docx.oxml.ns import nsdecls\n", + "from lxml import etree\n", + "\n", + "doc = docx.Document()\n", + "# Add a heading\n", + "doc.add_heading('Test Document', level=1)\n", + "\n", + "\n", + "# Add a paragraph\n", + "# This XML string is a paragraph with two runs. The second run has a style applied.\n", + "# Examples: https://github.com/python-openxml/python-docx/tree/master/docs/dev/analysis/features\n", + "\n", + "xml_string = \"\"\"\n", + "\t\n", + "\t\t\n", + "\t\t\tHello, \n", + "\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\t\n", + "\t\t\n", + "\t\t\tworld!\n", + "\t\t\n", + "\t\n", + "\"\"\"\n", + "\n", + "# Parse the XML string\n", + "root = parse_xml(xml_string)\n", + "\n", + "# Append the root to the element of the document\n", + "doc.element.body.append(root)\n", + "\n", + "# Save the document\n", + "doc.save('test.docx')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Cellpadding" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Rowspan\n", + "\n", + "In python-docx, you can merge cells vertically to simulate the effect of rowspan in HTML. Here's how you can do it:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import docx\n", + "\n", + "# Create a new Document\n", + "doc = docx.Document()\n", + "\n", + "# Add a table\n", + "table = doc.add_table(rows=3, cols=3)\n", + "\n", + "# Merge cells\n", + "cell_1 = table.cell(0, 0)\n", + "cell_2 = table.cell(2, 0)\n", + "cell_1.merge(cell_2)\n", + "\n", + "# Save the document\n", + "doc.save('test.docx')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Colspan" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this code, `table.cell(0, 0)` and `table.cell(2, 0)` are used to get the first cell of the first and third `rows. cell_1.merge(cell_2)` is then used to merge these cells, effectively creating a cell with rowspan=\"3\".\n", + "\n", + "Please note that this will only merge the cells vertically. If you want to merge cells horizontally (i.e., create a cell with colspan), you can use the same method but with cells from the same row.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import docx \n", + "# Create a new Document\n", + "doc = docx.Document()\n", + "\n", + "# Add a table\n", + "table = doc.add_table(rows=3, cols=3)\n", + "\n", + "# Merge cells\n", + "cell_1 = table.cell(0, 0)\n", + "cell_2 = table.cell(0, 2)\n", + "cell_1.merge(cell_2)\n", + "\n", + "# Save the document\n", + "doc.save('test.docx')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import docx\n", + "from docx.enum.table import WD_TABLE_ALIGNMENT\n", + "from bs4 import BeautifulSoup, NavigableString\n", + "from lxml import etree\n", + "\n", + "docx_doc = docx.Document()\n", + "\n", + "htbl = '''\n", + "\n", + "\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\n", + "\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\n", + "\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\n", + "\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\n", + "
Row 1, Column 1Row 1, Column 2Row 1, Column 3Row 1, Column 4
Row 2, Column 1Row 2, Column 2Row 2, Column 4
Row 3, Column 1Row 3, Column 4
Row 4, Column 1Row 4, Column 3Row 4, Column 4
\n", + "'''\n", + "\n", + "element = BeautifulSoup(htbl, 'html.parser')\n", + "\n", + "rows = element.find_all('tr')\n", + "cols = rows[0].find_all(['td','th'])\n", + "table = docx_doc.add_table(rows=len(rows), cols=len(cols))\n", + "table.style = 'Table Grid'\n", + "table.alignment = docx.enum.table.WD_TABLE_ALIGNMENT.CENTER\n", + "\n", + "# [A] Filling Cells with data\n", + "r=-1\n", + "for row in rows:\n", + "\tr+=1\n", + "\tcells = row.find_all(['td','th'])\n", + "\tfor i, cl in enumerate(cells):\n", + "\t\ttable.cell(r,i).text = cl.text\n", + "\t\tif cl.name == 'th':\n", + "\t\t\ttable.cell(r,i).paragraphs[0].runs[0].bold = True\n", + "\n", + "# [B] Merge Cells with rowspan and colspan\n", + "r=-1\n", + "rspn = 0\n", + "clspn = 0\n", + "for row in rows:\n", + "\tr+=1\n", + "\tcells = row.find_all(['td','th'])\n", + "\tfor i, cl in enumerate(cells):\n", + "\t\t# Merge cells if rowspan or colspan is set\n", + "\t\tif cl.has_attr('rowspan'):\n", + "\t\t\trspn = int(cl['rowspan'])\n", + "\t\t\ttable.cell(r,i).merge(table.cell(r,i+rspn-1))\n", + "\t\tif cl.has_attr('colspan'):\n", + "\t\t\tclspn = int(cl['colspan'])\n", + "\t\t\ttable.cell(r,i).merge(table.cell(r+clspn-1,i))\n", + "\n", + "# Save the document\n", + "docx_doc.save('test.docx')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "HINT: You can use the `cell.merge()` method to merge cells horizontally. For example, `table.cell(0, 0).merge(table.cell(0, 2))` will merge the first three cells of the first row, creating a cell with colspan=\"3\".\n", + "\n", + "![tbl.png](./tbl_preview.png)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "vpy38", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/snippets_10_pythondocx.ipynb b/docs/notebooks/snippets_10_pythondocx.ipynb new file mode 100644 index 000000000..c88df324d --- /dev/null +++ b/docs/notebooks/snippets_10_pythondocx.ipynb @@ -0,0 +1,170 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Normalize Headline Leveling\n", + "\n", + "Headline level supposed to be stricly hierarchical, but in practice, they are not always used that way. The following algorithm aims to normalize the headline levels for a correctly structured text.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Example lists of headline levels:\n", + "example_lists = [\n", + "\t[1,3,3,3,4,4,3,3,3,3,3,4,4,3,3,3,3,3],\n", + "\t[1,3,3,4,4,1,3,4,3,1,2],\n", + "\t[1,1,3,2,4,1,2,4,3,5,2,2],\n", + "\t[2,1,3,3,5,3],\n", + "\t[2,1,5],\n", + "\t[3,2]\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1, 3, 3, 3, 4, 4, 3, 3, 3, 3, 3, 4, 4, 3, 3, 3, 3, 3] ==>\n", + "[1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 3, 3, 3, 3, 3]\n", + "\n", + "[1, 3, 3, 4, 4, 1, 3, 4, 3, 1, 2] ==>\n", + "[1, 2, 2, 3, 3, 1, 2, 3, 3, 1, 2]\n", + "\n", + "[1, 1, 3, 2, 4, 1, 2, 4, 3, 5, 2, 2] ==>\n", + "[1, 2, 2, 2, 3, 1, 2, 3, 3, 4, 2, 2]\n", + "\n", + "[2, 1, 3, 3, 5, 3] ==>\n", + "[1, 1, 2, 2, 3, 3]\n", + "\n", + "[2, 1, 5] ==>\n", + "[1, 1, 2]\n", + "\n", + "[3, 2] ==>\n", + "[1, 2]\n", + "\n" + ] + } + ], + "source": [ + "# Variant-1\n", + "# Better for very volatile headline jumping\n", + "def normalize_headline_levels(list1):\n", + "\tlist2 = list1.copy() # Create a copy of list1\n", + "\tl = len(list2)\n", + "\ti = 0\n", + "\tn = 0\n", + "\t# Start with headline level 1\n", + "\tlist2[0] = 1\n", + "\twhile i < l:\n", + "\t\ti = (n == 0 or i > n) and i+1 or n + 1\n", + "\t\tn = 0\n", + "\t\tif i >= l:\n", + "\t\t\tbreak\n", + "\t\tv = list2[i]\n", + "\t\tif v == list1[i-1]:\n", + "\t\t\tcontinue\n", + "\t\tif v - list1[i-1] > 1 or v - list2[i-1] > 1:\n", + "\t\t\tlist2[i] = list1[i-1] + 1\n", + "\t\t\tif v - list2[i-1] > 1:\n", + "\t\t\t\tlist2[i] = list2[i-1] + 1\n", + "\t\t\tn = i\n", + "\t\tif n + 1 >= l:\n", + "\t\t\tbreak\n", + "\t\twhile list1[n+1] == list1[n]:\n", + "\t\t\tn += 1\n", + "\t\t\tif n + 1 >= l:\n", + "\t\t\t\tbreak\n", + "\t\t\tlist2[n] = list2[i]\n", + "\treturn list2\n", + "\n", + "for i in range(0, len(example_lists)):\n", + "\tprint('%s ==>\\n%s\\n'%(example_lists[i], normalize_headline_levels(example_lists[i])))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1, 3, 3, 3, 4, 4, 3, 3, 3, 3, 3, 4, 4, 3, 3, 3, 3, 3] ==>\n", + "[1, 2, 2, 2, 3, 3, 2, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2, 2]\n", + "\n", + "[1, 3, 3, 4, 4, 1, 3, 4, 3, 1, 2] ==>\n", + "[1, 2, 2, 3, 3, 2, 2, 3, 2, 2, 2]\n", + "\n", + "[1, 1, 3, 2, 4, 1, 2, 4, 3, 5, 2, 2] ==>\n", + "[1, 2, 3, 2, 3, 2, 3, 4, 3, 4, 3, 3]\n", + "\n", + "[2, 1, 3, 3, 5, 3] ==>\n", + "[1, 2, 3, 3, 4, 3]\n", + "\n", + "[2, 1, 5] ==>\n", + "[1, 2, 3]\n", + "\n", + "[3, 2] ==>\n", + "[1, 2]\n", + "\n" + ] + } + ], + "source": [ + "# Variant-2:\n", + "# Better for systmatical headline jumps\n", + "# e.g. omitted h2 (see Test-Case 1)\n", + "def normalize_headline_levels2(list1):\n", + "\ts = []\n", + "\tlist2 = [1]\n", + "\tfor i in list1[1:]:\n", + "\t\ti1 = i + 1\n", + "\t\tif s and s[-1] == i1:\n", + "\t\t\tpass\n", + "\t\telif not s or s[-1] < i1:\n", + "\t\t\ts.append(i1)\n", + "\t\telif s:\n", + "\t\t\twhile len(s) > 1 and s[-1] > i1:\n", + "\t\t\t\ts = s[:-1]\n", + "\t\tlist2.append(len(s) + 1)\n", + "\treturn list2\n", + "\n", + "for i in range(0, len(example_lists)):\n", + "\tprint('%s ==>\\n%s\\n'%(example_lists[i], normalize_headline_levels2(example_lists[i])))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "vpy38", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/snippets_11_pythondocx_table .ipynb b/docs/notebooks/snippets_11_pythondocx_table .ipynb new file mode 100644 index 000000000..dc91e8c6a --- /dev/null +++ b/docs/notebooks/snippets_11_pythondocx_table .ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Complex Tables in python-docx\n", + "\n", + "How to transform html table with arbitrary colspans and rowspans into python-docx table?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Transforming an HTML table with arbitrary colspans and rowspans into a python-docx table is a complex task. Here's a step-by-step plan:\n", + "\n", + "1. Parse the HTML table using a library like BeautifulSoup.\n", + "2. Create a 2D list (list of lists) to represent the table. Each sublist represents a row, and each element in the sublist represents a cell. Initialize all cells with `None`.\n", + "3. Iterate over the rows and cells in the HTML table. For each cell, find the first `None` element in the 2D list and set it to the cell content. If the cell has a colspan or rowspan, set the appropriate number of elements in the 2D list to the cell content.\n", + "4. Create a python-docx table with the same number of rows and columns as the 2D list.\n", + "5. Iterate over the cells in the 2D list. For each cell, if it's the same as the previous cell in the same row or column, merge the corresponding cells in the python-docx table.\n", + "\n", + "Here's a simplified version of the code:\n", + "\n", + "_Please note that this code assumes that all rows in the HTML table have the same number of cells, and that the colspan and rowspan attributes are used correctly. If this is not the case, you'll need to modify the code accordingly. Also, this code does not handle cell formatting. If you want to preserve the formatting of the cells, you'll need to add code to copy the formatting from the HTML table to the python-docx table._" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "html = '''\n", + "\n", + "\t\n", + "\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\n", + "\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\t\n", + "\t\t\n", + "\t\n", + "
testtest  
  test test
   test
    
    
\n", + "'''\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from bs4 import BeautifulSoup\n", + "from docx import Document\n", + "import re\n", + "\n", + "# Parse the HTML table\n", + "soup = BeautifulSoup(html, 'html.parser')\n", + "table = soup.find('table')\n", + "\n", + "# Create a 2D list to represent the table\n", + "rows = table.find_all('tr')\n", + "cols_len = max([len(row.find_all(['td','th'])) for row in rows])\n", + "table_list = [[None] * cols_len for _ in range(len(rows))]\n", + "\n", + "# Fill the 2D list with the cell contents\n", + "for i, row in enumerate(rows):\n", + " for j, cell in enumerate(row.find_all(['td', 'th'])):\n", + " # Find the first None element in the 2D list\n", + " while table_list[i][j] is not None:\n", + " j += 1\n", + " # Set the appropriate number of elements to the cell content\n", + " for k in range(i, i + int(cell.get('rowspan', 1))):\n", + " for l in range(j, j + int(cell.get('colspan', 1))):\n", + " table_list[k][l] = '[%s,%s] %s'%(i,j,cell.text)\n", + "\n", + "# Create a python-docx table\n", + "doc = Document()\n", + "doc_table = doc.add_table(rows=len(table_list), cols=len(table_list[0]))\n", + "\n", + "# Fill the python-docx table with the cell contents and merge cells\n", + "for i, row in enumerate(table_list):\n", + " for j, cell in enumerate(row):\n", + " doc_table.cell(i, j).text = cell\n", + " # Merge cells if they're the same as the previous cell\n", + " if i > 0 and cell == table_list[i - 1][j]:\n", + " doc_table.cell(i, j).text = ''\n", + " doc_table.cell(i - 1, j).merge(doc_table.cell(i, j))\n", + " if j > 0 and cell == row[j - 1]:\n", + " doc_table.cell(i, j).text = ''\n", + " doc_table.cell(i, j - 1).merge(doc_table.cell(i, j))\n", + "\n", + " doc_table.cell(i, j).text = re.sub(r'\\[\\d,\\d\\] ','',doc_table.cell(i, j).text)\n", + "\n", + "# Save the document\n", + "doc.save('test.docx')" + ] + }, + { + "attachments": { + "image-2.png": { + "image/png": "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" + }, + "image.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![image.png](attachment:image.png)\n", + "\n", + "![image-2.png](attachment:image-2.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Comments:\n", + "\n", + "- The cell merging is done by comparing the current cell content (!) with the previous cell in the same row and column. If the cells are the same, the current cell is merged with the previous cell. If the cells have same content, there will false positive merges. Thats why I added cell coords as a prefix to the cell content to make it unique.\n", + "- So, this prefix must be removed after the merging." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "vpy38", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/notebooks/tbl_preview.png b/docs/notebooks/tbl_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8978bfd9ab30338a9cd6ea7e5a1badd9092db4 GIT binary patch literal 13790 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 Vc(Vd?5GYp(G9&`CM^rb62LS(uo4f!3 literal 0 HcmV?d00001 From e189171d1447248d5bb4226a9aed23fd5212740e Mon Sep 17 00:00:00 2001 From: drfho Date: Thu, 24 Oct 2024 17:21:16 +0200 Subject: [PATCH 113/135] added vscode workbench file "ZMS5" for down-compatibility --- .vscode/Docker.code-workspace | 28 +++++++++++++- .vscode/Native.code-workspace | 37 ++++-------------- .vscode/ZMS5.code-workspace | 70 +++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 .vscode/ZMS5.code-workspace diff --git a/.vscode/Docker.code-workspace b/.vscode/Docker.code-workspace index 4cce77ffd..f88f2218c 100755 --- a/.vscode/Docker.code-workspace +++ b/.vscode/Docker.code-workspace @@ -5,7 +5,33 @@ "path": "../.." }, ], - + "settings": { + "python.defaultInterpreterPath": "/home/zope/venv/bin/python", + "window.zoomLevel": 0, + "git.ignoreMissingGitWarning": true, + "editor.minimap.enabled": false, + "editor.renderWhitespace": "all", + "editor.renderControlCharacters": false, + "workbench.iconTheme": "vs-minimal", + "files.associations": { + "*.zpt": "html", + "*.zcml": "xml" + }, + "scm.alwaysShowActions": true, + "files.exclude": { + "*.pyc": true, + "*-all.min.*":true, + "**/cache/**": true, + "**/Data.*": true, + }, + "search.exclude": { + "**/apidocs/**": true + }, + "files.eol": "\n", + "files.autoSave": "afterDelay", + "workbench.colorTheme": "Visual Studio Light", + "python.linting.enabled": true + }, "launch": { "version": "0.2.0", "configurations": [ diff --git a/.vscode/Native.code-workspace b/.vscode/Native.code-workspace index d765cd025..65812c41b 100644 --- a/.vscode/Native.code-workspace +++ b/.vscode/Native.code-workspace @@ -3,34 +3,7 @@ { "name": "ZMS5", "path": "../" - }, - // { - // "name": "ZMS4", - // "path": "../../ZMS4" - // }, - // { - // "name": "ZMS3", - // "path": "../../ZMS3" - // }, - { - "name": "Zope 5", - "path": "/home/zope/src/zopefoundation/Zope" - }, - { - "name": "ZMS5 Dev Instance", - "path": "/home/zope/instance/zms5_dev" - }, - { - "name": "OpenSearchServer", - "path": "/home/zope/src/sntl-projects/opensearch_demo" - }, - // { - // "name": "Zope-WebDAV Access", - // "uri": "webdav://admin:admin@localhost:8091/" - // }, - // { - // "path": "../../../../vpy38/lib/python3.8/site-packages/pydoctor" - // } + } ], "settings": { "python.defaultInterpreterPath": "~/vpy38/bin/python", @@ -50,9 +23,15 @@ "*-all.min.*":true, "**/cache/**": true, "**/Data.*": true, + "**/docker/**/var/*": true }, "search.exclude": { - "**/apidocs/**": true + "**/apidocs/**": true, + "*.pyc": true, + "*-all.min.*":true, + "**/cache/**": true, + "**/Data.*": true, + "**/docker/**/var/*": true }, "files.eol": "\n", "files.autoSave": "afterDelay", diff --git a/.vscode/ZMS5.code-workspace b/.vscode/ZMS5.code-workspace new file mode 100644 index 000000000..6139bc265 --- /dev/null +++ b/.vscode/ZMS5.code-workspace @@ -0,0 +1,70 @@ +{ + // Copy of Native.code-workspace + "folders": [ + { + "name": "ZMS5", + "path": "../" + }, + // { + // "name": "ZMS4", + // "path": "../../ZMS4" + // }, + // { + // "name": "ZMS3", + // "path": "../../ZMS3" + // }, + { + "name": "Zope 5", + "path": "/home/zope/src/zopefoundation/Zope" + }, + { + "name": "ZMS5 Dev Instance", + "path": "/home/zope/instance/zms5_dev" + }, + { + "name": "OpenSearchServer", + "path": "/home/zope/src/sntl-projects/opensearch_demo" + }, + // { + // "name": "Zope-WebDAV Access", + // "uri": "webdav://admin:admin@localhost:8091/" + // }, + // { + // "path": "../../../../vpy38/lib/python3.8/site-packages/pydoctor" + // } + ], + "settings": { + "python.defaultInterpreterPath": "~/vpy38/bin/python", + "window.zoomLevel": 0, + "git.ignoreMissingGitWarning": true, + "editor.minimap.enabled": false, + "editor.renderWhitespace": "all", + "editor.renderControlCharacters": false, + "workbench.iconTheme": "vs-minimal", + "files.associations": { + "*.zpt": "html", + "*.zcml": "xml" + }, + "scm.alwaysShowActions": true, + "files.exclude": { + "*.pyc": true, + "*-all.min.*":true, + "**/cache/**": true, + "**/Data.*": true, + "**/docker/**/var/*": true + }, + "search.exclude": { + "**/apidocs/**": true, + "*.pyc": true, + "*-all.min.*":true, + "**/cache/**": true, + "**/Data.*": true, + "**/docker/**/var/*": true + }, + "files.eol": "\n", + "files.autoSave": "afterDelay", + "workbench.colorTheme": "Visual Studio Light", + "python.linting.enabled": true + }, + "remoteAuthority": "wsl+Ubuntu" +} \ No newline at end of file From fec0be1c79f8691f1c4274aaf144bf8897e4bfbc Mon Sep 17 00:00:00 2001 From: drfho Date: Thu, 24 Oct 2024 17:34:21 +0200 Subject: [PATCH 114/135] Fixed type error xml import of empty elements (#199) Ref: https://github.com/idasm-unibe-ch/unibe-cms/pull/583 --- Products/zms/_xmllib.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Products/zms/_xmllib.py b/Products/zms/_xmllib.py index f021d4abe..d7a164d3b 100644 --- a/Products/zms/_xmllib.py +++ b/Products/zms/_xmllib.py @@ -316,7 +316,13 @@ def xmlOnUnknownEndTag(self, sTagName): else: item = self.dValueStack.pop() values = self.dValueStack.pop() - values[lang] = item + try: + values[lang] = item + except: + # empty values + standard.writeBlock(self, "[values]: WARNING Importing xml may not match to ZMS client's content model - Skip lang %s for %s" %(lang, str(values))) + values = {} + values[lang] = item self.dValueStack.append(values) # -- COMF-PROPERTY -- @@ -337,7 +343,9 @@ def xmlOnUnknownEndTag(self, sTagName): # -- Multi-Language Attributes. if obj_attr['multilang']: - item = self.dValueStack.pop() + item = None + if len(self.dValueStack) > 0: + item = self.dValueStack.pop() if item is not None: if not isinstance(item, dict): item = {self.getPrimaryLanguage():item} From 20b1dcb542f7dc41490c40b9f883d8dc61d11c44 Mon Sep 17 00:00:00 2001 From: drfho Date: Thu, 24 Oct 2024 23:56:47 +0200 Subject: [PATCH 115/135] Added content model lib for SQL Mirroring (#112) 1. teaser_registry_lib: mirroring teaser elements in a SQL database 2. url_extract_lib: cache / extract external html content by given URL in a SQL database and publish it as ZMS page --- .../com.zms.mirroring/__init__.py | 22 ++++ .../teaser_registry_lib/__init__.py | 121 +++++++++++++++++ .../teaser_registry_lib/index_html.zpt | 83 ++++++++++++ .../teaser_registry_lib/readme | 49 +++++++ .../teaser_registry_lib/register_teaser.py | 33 +++++ .../teaser_registry_lib/sql_create.zsql | 15 +++ .../teaser_registry_lib/sql_delete.zsql | 6 + .../teaser_registry_lib/sql_select.zsql | 6 + .../sql_update_datetime.zsql | 6 + .../teaser_registry_lib/sql_upsert.zsql | 42 ++++++ .../teaser_registry_lib/tryout.py | 36 ++++++ .../url_extract_lib/__init__.py | 84 ++++++++++++ .../com.zms.mirroring/url_extract_lib/readme | 23 ++++ .../url_extract_lib/sql_create.zsql | 14 ++ .../url_extract_lib/sql_select.zsql | 6 + .../url_extract_lib/sql_update_datetime.zsql | 6 + .../url_extract_lib/sql_upsert.zsql | 38 ++++++ .../url_extract_lib/tryout.zpt | 6 + .../url_extract_page/__init__.py | 122 ++++++++++++++++++ .../url_extract_page/content_preview.zpt | 39 ++++++ .../url_extract_page/standard_html.zpt | 12 ++ .../url_extract_page/url_extracting.py | 79 ++++++++++++ 22 files changed, 848 insertions(+) create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/__init__.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/__init__.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/index_html.zpt create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/readme create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/register_teaser.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_create.zsql create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_delete.zsql create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_select.zsql create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_update_datetime.zsql create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_upsert.zsql create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/tryout.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/__init__.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/readme create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_create.zsql create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_select.zsql create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_update_datetime.zsql create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_upsert.zsql create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/tryout.zpt create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/__init__.py create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/content_preview.zpt create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/standard_html.zpt create mode 100644 Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/url_extracting.py diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.mirroring/__init__.py new file mode 100644 index 000000000..e61f217b6 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/__init__.py @@ -0,0 +1,22 @@ +class com_zms_mirroring: + """ + python-representation of com.zms.mirroring + """ + + # Enabled + enabled = 1 + + # Id + id = "com.zms.mirroring" + + # Name + name = "com.zms.mirroring" + + # Package + package = "" + + # Revision + revision = "5.0.0" + + # Type + type = "ZMSPackage" diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/__init__.py new file mode 100644 index 000000000..a30e2b46d --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/__init__.py @@ -0,0 +1,121 @@ +class teaser_registry_lib: + """ + python-representation of teaser_registry_lib + """ + + # Access + access = {"delete_custom":"" + ,"delete_deny":[] + ,"insert_custom":"" + ,"insert_deny":[]} + + # Enabled + enabled = 0 + + # Id + id = "teaser_registry_lib" + + # Name + name = "Teaser Registry Lib" + + # Package + package = "com.zms.mirroring" + + # Revision + revision = "5.0.0" + + # Type + type = "ZMSLibrary" + + # Attrs + class Attrs: + register_teaser = {"default":"" + ,"id":"register_teaser" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Execute Teaser Registration" + ,"repetitive":0 + ,"type":"External Method"} + + teaser_registry_sql_create = {"default":"" + ,"id":"teaser_registry/sql_create" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"teaser_registry/sql_create" + ,"repetitive":0 + ,"type":"Z SQL Method"} + + teaser_registry_sql_upsert = {"default":"" + ,"id":"teaser_registry/sql_upsert" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"teaser_registry/sql_upsert" + ,"repetitive":0 + ,"type":"Z SQL Method"} + + teaser_registry_sql_select = {"default":"" + ,"id":"teaser_registry/sql_select" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"teaser_registry/sql_select" + ,"repetitive":0 + ,"type":"Z SQL Method"} + + teaser_registry_sql_update_datetime = {"default":"" + ,"id":"teaser_registry/sql_update_datetime" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"teaser_registry/sql_update_datetime" + ,"repetitive":0 + ,"type":"Z SQL Method"} + + teaser_registry_sql_delete = {"default":"" + ,"id":"teaser_registry/sql_delete" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"teaser_registry/sql_delete" + ,"repetitive":0 + ,"type":"Z SQL Method"} + + icon_clazz = {"custom":"fas fa-flask text-success" + ,"default":"" + ,"id":"icon_clazz" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Icon-Class (CSS)" + ,"repetitive":0 + ,"type":"constant"} + + teaser_registry_tryout = {"default":"" + ,"id":"teaser_registry/tryout" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Try Out (Test)" + ,"repetitive":0 + ,"type":"Script (Python)"} + + readme = {"default":"" + ,"id":"readme" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"readme" + ,"repetitive":0 + ,"type":"resource"} + + teaser_registry_index_html = {"default":"" + ,"id":"teaser_registry/index_html" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Preview" + ,"repetitive":0 + ,"type":"Page Template"} diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/index_html.zpt b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/index_html.zpt new file mode 100644 index 000000000..c5643abef --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/index_html.zpt @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/readme b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/readme new file mode 100644 index 000000000..1a884de82 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/readme @@ -0,0 +1,49 @@ +# SQL Content Registry: Create a Teaser Database + +The content model library teaser_registry_lib is a collection of ZSQL/Py-methods for +saving ZMS content data into a SQL database. This database can work as a registry and +provides quick access to content types, in this case: teasers. Working just with +teaser elements this library is a simple example code that can be developed to more +complex needs. + + +## Installation + +First add an empty file into file system: +
$INSTANCE_HOME/var/sqlite/teasers.sqlite
+First insert a Zope-Folder 'teaser_registry' into the ZMS root folder. Here add a +ZSQLiteDA object named teasers connecting to the filesystem file. +After importing the teaser_registry_lib into ZMS, inialize the databse with the ZSQL object +teaser_registry/sql_create by clicking on the object's view' tab in Zope-ZMI. + +## Try out + +To check whether any teaser data are written into the SQL database you can +use the library's Python script teaser_registry/tryout; it +collects the existing ZMSTeaserElement objects. +Executing teaser_registry/tryout by clicking on it's view tab shall +copy the teaser's content to the SQL database and will the SQL-mirrored content with +teaser_registry/index_html. + +## Adding Primitve py-Attribute onChangeObjEvt to Teaser Model + +The library provides a method register_teaser: it will be globally available +and can be called in a python attribute-method onChangeObjEvt via the teaser +content model. So just add a py-attribute with the following code: + +
+# --// onChangeObjEvt //--
+from Products.zms import standard
+request = container.REQUEST
+RESPONSE =  request.RESPONSE
+try:
+    zmscontext.register_teaser()
+except:
+    pass
+return None
+# --// /onChangeObjEvt //--
+
+ +Thus inserting or changing of any ZMS teaser object will be mirrored into the SQL database. + + diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/register_teaser.py b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/register_teaser.py new file mode 100644 index 000000000..fe75f9fce --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/register_teaser.py @@ -0,0 +1,33 @@ +## params: self + +import hashlib +import datetime +import re + +def gt(dt_str): + ### How to convert Python's .isoformat() string back into datetime object + ### https://stackoverflow.com/questions/28331512 + dt, _, us = dt_str.partition(".") + dt = datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S") + us = int(us.rstrip("Z"), 10) + return dt + datetime.timedelta(microseconds=us) + + +def register_teaser( self ): + now = datetime.datetime.now() + now_iso = now.isoformat() + zmscontext = self + lang = self.REQUEST.get('lang','ger') + html = zmscontext.getBodyContent(self.REQUEST) + if self.isActive(self.REQUEST): + zmscontext.teaser_registry.sql_upsert( + zms_id = zmscontext.getId(), + client_id = zmscontext.getHome().getId(), + uuid = zmscontext.get_uid(), + change_dt = zmscontext.getLangFmtDate(zmscontext.attr('change_dt'), lang=lang, fmt_str='%Y-%m-%dT%H:%M:%S'), + lang = lang, + content_md5 = hashlib.md5(html.encode()).hexdigest(), + content_datetime = now_iso, + content_cache = html + ) + return html \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_create.zsql b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_create.zsql new file mode 100644 index 000000000..7e90699b5 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_create.zsql @@ -0,0 +1,15 @@ +teasers + +1000 +100 +0 +CREATE TABLE "teasers" ( + "zms_id" VARCHAR(36) PRIMARY KEY NOT NULL , + "client_id" VARCHAR(36) NOT NULL , + "uuid" VARCHAR(64) NOT NULL , + "change_dt" VARCHAR(48) , + "lang" VARCHAR(3) , + "content_md5" VARCHAR(255) , + "content_datetime" VARCHAR(48) , + "content_cache" TEXT +) \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_delete.zsql b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_delete.zsql new file mode 100644 index 000000000..5ab3c6e78 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_delete.zsql @@ -0,0 +1,6 @@ +teasers +zms_id +1000 +100 +0 +DELETE FROM teasers WHERE zms_id = \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_select.zsql b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_select.zsql new file mode 100644 index 000000000..5f0bad42d --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_select.zsql @@ -0,0 +1,6 @@ +teasers +zms_id +1000 +100 +0 +SELECT * FROM teasers WHERE zms_id = \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_update_datetime.zsql b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_update_datetime.zsql new file mode 100644 index 000000000..dadae18bc --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_update_datetime.zsql @@ -0,0 +1,6 @@ +teasers +zms_id +1000 +100 +0 +UPDATE teasers SET content_datetime = '' WHERE zms_id = \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_upsert.zsql b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_upsert.zsql new file mode 100644 index 000000000..50d3e6af9 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/sql_upsert.zsql @@ -0,0 +1,42 @@ +teasers +zms_id +client_id +uuid +change_dt +lang +content_md5 +content_datetime +content_cache +1000 +100 +0 +UPDATE OR IGNORE teasers SET + zms_id = , + client_id = , + uuid = , + change_dt = , + lang = , + content_md5 = , + content_datetime = , + content_cache = +WHERE zms_id = + +INSERT OR IGNORE INTO teasers ( + zms_id, + client_id, + uuid, + change_dt, + lang, + content_md5, + content_datetime, + content_cache +) VALUES ( + , + , + , + , + , + , + , + +) \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/tryout.py b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/tryout.py new file mode 100644 index 000000000..254a9c526 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/teaser_registry_lib/tryout.py @@ -0,0 +1,36 @@ +## Script (Python) "tryout" +##bind container=container +##bind context=context +##bind namespace= +##bind script=script +##bind subpath=traverse_subpath +##parameters= +##title=TEST +## +# --// tryout //-- +from Products.zms import standard +request = container.REQUEST +zmscontext = context.content + +lang = zmscontext.getPrimaryLanguage() +request.set('lang',lang) + +teasers = zmscontext.getTreeNodes(request,meta_types=['ZMSTeaserElement']) + +for teaser in teasers: + html = teaser.getBodyContent(request) + + context.teaser_registry.sql_upsert( + zms_id = zmscontext.getId(), + client_id = zmscontext.getHome().getId(), + uuid = zmscontext.get_uid(), + change_dt = zmscontext.attr('change_dt'), + lang = lang, + content_md5 = zmscontext.encrypt_password(html, algorithm='md5', hex=True), + content_datetime = zmscontext.attr('change_dt'), + content_cache = html + ) + +return container.index_html() + +# --// /tryout //-- diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/__init__.py new file mode 100644 index 000000000..339705efa --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/__init__.py @@ -0,0 +1,84 @@ +class url_extract_lib: + """ + python-representation of url_extract_lib + """ + + # Access + access = {"delete_custom":"" + ,"delete_deny":[] + ,"insert_custom":"" + ,"insert_deny":[]} + + # Enabled + enabled = 0 + + # Id + id = "url_extract_lib" + + # Name + name = "URL-Extract Lib" + + # Package + package = "com.zms.mirroring" + + # Revision + revision = "5.0.0" + + # Type + type = "ZMSLibrary" + + # Attrs + class Attrs: + url_extract_sql_create = {"default":"" + ,"id":"url_extract/sql_create" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"url_extract/sql_create" + ,"repetitive":0 + ,"type":"Z SQL Method"} + + url_extract_sql_upsert = {"default":"" + ,"id":"url_extract/sql_upsert" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"url_extract/sql_upsert" + ,"repetitive":0 + ,"type":"Z SQL Method"} + + url_extract_sql_select = {"default":"" + ,"id":"url_extract/sql_select" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"url_extract/sql_select" + ,"repetitive":0 + ,"type":"Z SQL Method"} + + url_extract_sql_update_datetime = {"default":"" + ,"id":"url_extract/sql_update_datetime" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"url_extract/sql_update_datetime" + ,"repetitive":0 + ,"type":"Z SQL Method"} + + url_extract_tryout = {"default":"" + ,"id":"url_extract/tryout" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Try Out" + ,"repetitive":0 + ,"type":"Page Template"} + + readme = {"default":"" + ,"id":"readme" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"readme" + ,"repetitive":0 + ,"type":"resource"} diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/readme b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/readme new file mode 100644 index 000000000..d5d64b4ea --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/readme @@ -0,0 +1,23 @@ +# Integrate External Content as ZMS-Page via SQL Cache + +The content model library url_extract_lib is a collection of ZSQL/Py-methods for +saving ZMS external content data into a SQL database. The content class url_extract_page +acts like page in ZMS and can be added to ZMS at any folderish tree node. The entered URL +will request the html stream and extract it at the CSS node selector. + +## Installation + +First add an empty file into file system: +
$INSTANCE_HOME/var/sqlite/extracts.sqlite
+First insert a Zope-Folder 'url_extrac' into the ZMS root folder. Here add a +ZSQLiteDA object named extracts connecting to the filesystem file. +After importing the url_extract_lib into ZMS, inialize the databse with the ZSQL object +url_extract/sql_create by clicking on the object's view' tab in Zope-ZMI. + +## Use + +To check whether any content data are written into the SQL database just add a new +URL-Extract Page to the content tree. Afte savnig the extracted content will be +show in ZMS and some meta-data (timestamp, checksum). + + diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_create.zsql b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_create.zsql new file mode 100644 index 000000000..b856d7553 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_create.zsql @@ -0,0 +1,14 @@ +extracts + +1000 +100 +0 +CREATE TABLE "pages" ( + "zms_id" VARCHAR(36) PRIMARY KEY NOT NULL , + "client_id" VARCHAR(36) NOT NULL , + "change_dt" VARCHAR(48) , + "lang" VARCHAR(3) , + "content_md5" VARCHAR(255) , + "content_datetime" VARCHAR(48) , + "content_cache" TEXT +) \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_select.zsql b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_select.zsql new file mode 100644 index 000000000..98f6a0abf --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_select.zsql @@ -0,0 +1,6 @@ +extracts +zms_id +1000 +100 +0 +SELECT * FROM pages WHERE zms_id = \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_update_datetime.zsql b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_update_datetime.zsql new file mode 100644 index 000000000..17e9c10d1 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_update_datetime.zsql @@ -0,0 +1,6 @@ +extracts +zms_id +1000 +100 +0 +UPDATE pages SET content_datetime = '' WHERE zms_id = \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_upsert.zsql b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_upsert.zsql new file mode 100644 index 000000000..ef6fbb99b --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/sql_upsert.zsql @@ -0,0 +1,38 @@ +extracts +zms_id +client_id +change_dt +lang +content_md5 +content_datetime +content_cache +1000 +100 +0 +UPDATE OR IGNORE pages SET + zms_id = , + client_id = , + change_dt = , + lang = , + content_md5 = , + content_datetime = , + content_cache = +WHERE zms_id = + +INSERT OR IGNORE INTO pages ( + zms_id, + client_id, + change_dt, + lang, + content_md5, + content_datetime, + content_cache +) VALUES ( + , + , + , + , + , + , + +) \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/tryout.zpt b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/tryout.zpt new file mode 100644 index 000000000..486cbc415 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_lib/tryout.zpt @@ -0,0 +1,6 @@ +
+    Extracted HTML
+
\ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/__init__.py b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/__init__.py new file mode 100644 index 000000000..6ea73eecc --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/__init__.py @@ -0,0 +1,122 @@ +class url_extract_page: + """ + python-representation of url_extract_page + """ + + # Access + access = {"delete_custom":"" + ,"delete_deny":["" + ,"" + ,"" + ,"" + ,"" + ,""] + ,"insert_custom":"{$}" + ,"insert_deny":["" + ,"" + ,"" + ,"" + ,"" + ,""]} + + # Enabled + enabled = 1 + + # Id + id = "url_extract_page" + + # Name + name = "URL-Extract Page" + + # Package + package = "com.zms.mirroring" + + # Revision + revision = "5.0.0" + + # Type + type = "ZMSDocument" + + # Attrs + class Attrs: + icon_clazz = {"custom":"fas fa-file-import text-primary" + ,"default":"" + ,"id":"icon_clazz" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Icon-Class (CSS)" + ,"repetitive":0 + ,"type":"constant"} + + titlealt = {"default":"" + ,"id":"titlealt" + ,"keys":[] + ,"mandatory":1 + ,"multilang":1 + ,"name":"DC.Title.Alt" + ,"repetitive":0 + ,"type":"titlealt"} + + title = {"default":"" + ,"id":"title" + ,"keys":[] + ,"mandatory":1 + ,"multilang":1 + ,"name":"DC.Title" + ,"repetitive":0 + ,"type":"title"} + + content_url = {"default":"https://www.uniklinikum-jena.de/Uniklinikum+Jena/Wir+%C3%BCber+uns/Portrait/Geschichte.html" + ,"id":"content_url" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Absolute URL" + ,"repetitive":0 + ,"type":"url"} + + content_node = {"default":"div.content-bottom" + ,"id":"content_node" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Node-Selector" + ,"repetitive":0 + ,"type":"string"} + + css_custom = {"default":".media {\n float: left;\n margin: .5rem 2rem 0 0;\n max-width:30%;\n}\n.media img {\n max-width: 100%;\n}\np {\n text-align: left !important;\n}\nh1.maintitle {\n display: none;\n}\nbody.zmi h1.maintitle {\n display: block;\n}" + ,"id":"css_custom" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Custom-CSS" + ,"repetitive":0 + ,"type":"text"} + + url_extracting = {"default":"" + ,"id":"url_extracting" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"URL Content Extaction" + ,"repetitive":0 + ,"type":"External Method"} + + content_preview = {"default":"" + ,"id":"content_preview" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"content_preview" + ,"repetitive":0 + ,"type":"interface"} + + standard_html = {"default":"" + ,"id":"standard_html" + ,"keys":[] + ,"mandatory":0 + ,"multilang":0 + ,"name":"Template: URL-Extract" + ,"repetitive":0 + ,"type":"zpt"} diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/content_preview.zpt b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/content_preview.zpt new file mode 100644 index 000000000..afffcbd43 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/content_preview.zpt @@ -0,0 +1,39 @@ + +
+ +
+
+ + Extracted HTML + +
+ Date:
+ MD5:
+ HTML: show
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/standard_html.zpt b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/standard_html.zpt new file mode 100644 index 000000000..d305e38c4 --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/standard_html.zpt @@ -0,0 +1,12 @@ + +
+ + Extracted HTML + +
Date: , MD5: 
+ +
+ \ No newline at end of file diff --git a/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/url_extracting.py b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/url_extracting.py new file mode 100644 index 000000000..e4f0663fc --- /dev/null +++ b/Products/zms/conf/metaobj_manager/com.zms.mirroring/url_extract_page/url_extracting.py @@ -0,0 +1,79 @@ +## params: self, content_url, content_node, force + +from bs4 import BeautifulSoup +from urllib.request import urlopen +import hashlib +import datetime +import re +import requests + +def cache_data_by_sql(self, extract={}): + # SQLITE: SAVE/CACHE ATTRIBUTE VALUES + lang = self.REQUEST.get('lang','ger') + self.url_extract.sql_upsert( + zms_id = self.getId(), + client_id = self.getHome().getId(), + change_dt = extract['content_datetime'], + lang = lang, + content_md5 = extract['content_md5'], + content_datetime = extract['content_datetime'], + content_cache = extract['content_cache'] + ) + return True + + +def gt(dt_str): + ### How to convert Python's .isoformat() string back into datetime object + ### https://stackoverflow.com/questions/28331512 + dt, _, us = dt_str.partition(".") + dt = datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S") + us = int(us.rstrip("Z"), 10) + return dt + datetime.timedelta(microseconds=us) + + +def url_extracting(self, content_url, content_node, force=False): + now = datetime.datetime.now() + now_iso = now.isoformat() + meta_type = self.meta_type + + extract = { + 'content_md5':'', + 'content_datetime':'', + 'content_cache':'' + } + + res = self.url_extract.sql_select(zms_id=self.getId()) + if len(res)>0: + extract['content_md5']=res[0]['content_md5'] + extract['content_datetime']=res[0]['content_datetime'] + extract['content_cache']=res[0]['content_cache'] + dt_cached = gt(res[0]['content_datetime']) + else: + dt_cached = now + + # Check MD5/Refresh Content Once a Day or on Force + if force or ( (now - dt_cached).days > 0 ) or len(res)==0: + baseurl = re.compile(r'(https:\/\/(.*?)\/)').match(content_url)[0] + html = requests.get(content_url).text + # html = urlopen(content_url).read() + soup = BeautifulSoup(html, features='lxml') + content = str(soup.select_one(content_node)) + md5 = hashlib.md5(content.encode()).hexdigest() + + md5_prev = extract['content_md5'] + extract['content_md5'] = md5 + extract['content_datetime'] = now_iso + if force: + extract['html'] = html + + if md5 != md5_prev: + # Fix URLs in Content + for attr in ['src','srcset','href']: + content = content.replace('%s="/'%attr,'%s="%s'%(attr,baseurl)) + extract['content_cache'] = content + # Save Cache On MD5-Diff + cache_data_by_sql(self, extract=extract) + else: + self.url_extract.sql_update_datetime(zms_id=self.getId()) + + return extract \ No newline at end of file From 0a704eca7cf2300070ccfd6448495bc32233955b Mon Sep 17 00:00:00 2001 From: drfho Date: Fri, 25 Oct 2024 01:38:19 +0200 Subject: [PATCH 116/135] cleaned minmal-theme --- .../common/webdesign/style.css | 112 +++++++++++------- 1 file changed, 70 insertions(+), 42 deletions(-) diff --git a/Products/zms/skins/skin_zms5_minimal/common/webdesign/style.css b/Products/zms/skins/skin_zms5_minimal/common/webdesign/style.css index bc45e0519..e026b002f 100644 --- a/Products/zms/skins/skin_zms5_minimal/common/webdesign/style.css +++ b/Products/zms/skins/skin_zms5_minimal/common/webdesign/style.css @@ -1,12 +1,14 @@ /* LAYOUT */ body { margin:0 auto; - font-family:Arial,sans-serif; - line-height:1.33; + font-family: Roboto, Arial,sans-serif; + line-height: 1.5; background-color: #f8f8f8; + font-size: larger; } header { background-color: white; + border-bottom: 1px solid white; } .grid { display: grid; @@ -33,18 +35,19 @@ header .logo { -ms-grid-row-align: center; } header .logo a { - display:block; - width:100%; - text-align:left; + /* display:block; */ + /* width:100%; */ + /* text-align:left; */ } header .logo a img { - display:block; - margin:auto; + /* display:block; */ + margin-left: .7em; + width: 160px; } header nav { grid-area: nav; width:100%; - align-self:end; + align-self: center; text-align:right; -ms-grid-column: 2; -ms-grid-row: 1; @@ -54,17 +57,18 @@ header nav { display:none; } #shadowbox { - height:20px; + height: 1.25em; position:absolute; z-index:101; - background: linear-gradient( rgba(0,0,0,.3),rgba(0,0,0,.15),rgba(0,0,0,.05),rgba(128,128,128,0)); + background: linear-gradient( rgba(0,0,0,.10),rgba(128,128,128,0)); width:100% } main.grid { - grid-template-areas:"leftnavigation content rightnavigation" - "footer footer footer"; - grid-template-columns: 260px 1fr 260px; + grid-template-areas: + "leftnavigation content rightnavigation" + "footer footer footer"; + grid-template-columns: 260px 1fr 260px;; grid-template-rows: 1fr; display: -ms-grid; -ms-grid-columns: 260px 1fr 260px; @@ -80,10 +84,17 @@ body.no_teasers main.grid { -ms-grid-columns: 260px auto 0px; border-right: 1px solid #eee; } +body#zmsroot main.grid { + grid-template-columns: 0px 1fr 0px; + -ms-grid-columns: 0px auto 0px; +} +body#zmsroot aside { + display: none; +} article, aside, footer{ margin:0; - padding:1em; + padding: 8.75em 1em 3em 1em; } article { z-index:10; @@ -96,7 +107,7 @@ article { aside.left { z-index:20; grid-area: leftnavigation; - background-color: black; + background-color: #ffffff; -ms-grid-column: 1; -ms-grid-row: 1; } @@ -114,7 +125,7 @@ aside.right { } footer { grid-area: footer; - background-color: rgb(68, 74, 78); + background-color: rgb(0 0 0 / 50%); padding-top: 1.5em; } @@ -126,29 +137,34 @@ header a { color:black; } aside.left a { - color:white; + color: black; } /* CONTENT TYPOGRAPHY*/ article { - padding: 1em 2rem !important; + padding: 1em 3rem 1em 2em !important; } article section { padding-top:0; } article section.titles h1 { font-weight:normal; - font-size:220%; - margin:0.5em 0 0.7em; + font-size: 225%; + margin: 0.6em 0 0.7em; line-height:1.2; } +#zmsroot article section.titles { + padding-top: 2em; +} article section.titles .description { font-weight:bold; letter-spacing:.5px; color:#666; + line-height: 1.65; + margin: 0 0 2em 0; } article a { - color: #03A9F4; + color: #07aee9; } /* FUNCTION BREADCRUMB */ @@ -156,13 +172,14 @@ article a { z-index:1; display:block; list-style-type:none; - margin: -1em -2em 0 -2em; + margin: -1em 0 0 -2em; padding: 1em 2em; width: 100%; font-size:100%; - background:#eee; + /* background:#eee; */ white-space: nowrap; overflow:hidden; + /* text-align: end; */ } body.has_teasers .breadcrumb { margin: -1em -1.1em 0 -1.1em; @@ -171,12 +188,12 @@ body.has_teasers .breadcrumb { .breadcrumb li { display:inline-block; line-height:1em; - font-size:90%; + font-size:80%; } -.breadcrumb li:after, -.breadcrumb li:first-child::before { - content:" > "; +.breadcrumb li:after { + content:">"; color:silver; + margin: 0 .25em 0 .35em; } .breadcrumb li:last-child::after { content:none; @@ -189,6 +206,7 @@ body.has_teasers .breadcrumb { text-overflow:ellipsis; text-decoration:none; border-bottom:1px solid transparent; + color: silver; } .breadcrumb li a:hover { text-decoration:none; @@ -210,9 +228,11 @@ nav ul li { nav ul li a { display:inline-block; text-decoration:none; - padding:0.5em 1em; + padding: 0.15em 1em; +} +nav ul#navigationtree li a { + padding: 0.5em 1em; } - @keyframes darken { from { @@ -224,12 +244,18 @@ nav ul li a { } header nav > ul > li { - line-height:2em; + /* line-height:2em; */ background-color:rgba(0,0,0,.0); + margin: 0; + padding: 0; } header nav ul li.active, header nav ul li:hover { - background-color:rgba(0,0,0,.3); + background-color: rgb(7 174 233); +} +header nav ul li.active a, +header nav ul li:hover a { + color: white; } header nav ul li:hover { animation-name: darken; @@ -245,7 +271,8 @@ aside nav ul li a:hover { color:#82d8ff } aside nav ul > li.active > a { - color:#82d8ff !important; + color: #07aee9 !important; + font-weight: bold; } aside nav ul li.active.current:before { content:'>'; @@ -253,7 +280,7 @@ aside nav ul li.active.current:before { display:inline-block; margin:.5em 0 0 0; position:absolute; - color:#82d8ff + color: #07aee9; } aside nav ul li ul { padding-left:1.5em; @@ -263,12 +290,12 @@ footer p { padding:0; margin:0; line-height:2; - font-size:80%; + font-size: 70%; color: white; } footer p a { text-decoration:none; - color: white; + color: inherit; } @media screen and (min-width: 1200px) { @@ -297,8 +324,8 @@ footer p a { display:block !important; } header .logo { - text-align:center; - padding:.75em 1em 0.25em; + /* text-align:center; */ + padding: 1em 0 0.35em 0.35em; } header nav { text-align:left; @@ -309,6 +336,7 @@ footer p a { header nav.expand ul li, header nav ul li.active { display:block; + margin: .5em 0; } header nav ul li.active, header nav ul li:hover { @@ -318,7 +346,7 @@ footer p a { color:white } header nav ul li a { - padding: .1em 1em; + padding: .1em 2em; } #shadowbox { display:none; @@ -356,11 +384,11 @@ footer p a { display:block; position: absolute; right: 0; - padding: 1px 3px 2px 3px; - font-size: 18px; - line-height: .8; + padding: 0 .35em; + font-size: 32px; + line-height: .5; color:black; - border: 1px solid black; + /* border: 1px solid black; */ margin: 7px; } #zmsroot .navtoggle { From 26f5953f4fed8d61ff6b1652cdb6d3c1517a20c9 Mon Sep 17 00:00:00 2001 From: drfho Date: Fri, 25 Oct 2024 14:10:31 +0200 Subject: [PATCH 117/135] cleaning --- Products/zms/conf/metaobj_manager/skin_zms5_base/__init__.py | 2 +- .../zms/conf/metaobj_manager/skin_zms5_base/standard_html.zpt | 1 + Products/zms/import/skin_zms5_base-5.0.2.metaobj.xml | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Products/zms/conf/metaobj_manager/skin_zms5_base/__init__.py b/Products/zms/conf/metaobj_manager/skin_zms5_base/__init__.py index fe13924d4..79e98f589 100644 --- a/Products/zms/conf/metaobj_manager/skin_zms5_base/__init__.py +++ b/Products/zms/conf/metaobj_manager/skin_zms5_base/__init__.py @@ -22,7 +22,7 @@ class skin_zms5_base: package = "" # Revision - revision = "5.0.1" + revision = "5.0.2" # Type type = "ZMSLibrary" diff --git a/Products/zms/conf/metaobj_manager/skin_zms5_base/standard_html.zpt b/Products/zms/conf/metaobj_manager/skin_zms5_base/standard_html.zpt index 0712b1331..3b794329d 100644 --- a/Products/zms/conf/metaobj_manager/skin_zms5_base/standard_html.zpt +++ b/Products/zms/conf/metaobj_manager/skin_zms5_base/standard_html.zpt @@ -46,6 +46,7 @@ data-path python:zmscontext.getRootElement().getRefObjPath(here); data-root python:zmscontext.getRootElement().getHome().id; data-client python:zmsclient.getHome().id; + data-level python:zmscontext.getLevel(); id python:'zmsid_%s'%(zmscontext.id); class python:'zms web %s %s'%(zmscontext.meta_id, attr_dc_type)">
+ +