From 7176e03006113e96035c9beb13e6c187ff5d59d2 Mon Sep 17 00:00:00 2001 From: drfho Date: Mon, 9 Dec 2024 17:31:03 +0800 Subject: [PATCH 01/14] Added more transformation features to pydocx-export (#341) --- .../manage_export_pydocx.py | 124 +++++++++++++----- .../manage_export_pydocx/neon.docx | Bin 19736 -> 19797 bytes .../manage_export_pydocx/readme.md | 17 ++- .../manage_export_pydocx/styles.xml | 17 ++- .../ZMSGraphic/standard_json_docx.py | 24 ++-- .../ZMSTextarea/standard_json_docx.py | 14 +- 6 files changed, 141 insertions(+), 55 deletions(-) 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 index 3b02d273b..781bb7b70 100644 --- 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 @@ -186,6 +186,7 @@ def add_hyperlink(docx_block, link_text, url): url_base = 'http://neon/' # Omit javascript links if not url.startswith('javascript:'): + url = url.replace('mailto:', '') # 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) @@ -222,7 +223,7 @@ def add_hyperlink(docx_block, link_text, url): # ############################################# # Clean HTML -def clean_html(html): +def clean_html(html, wrap_trailling_text=False): """ Clean comments, styles, empty tags and handle special characters: left-to-right, triangle @@ -244,6 +245,9 @@ def clean_html(html): html = html.replace(left_to_right_char,'') html = html.replace('[[', triangle_char) html = html.replace(']]', '') + if wrap_trailling_text: + # Wrap untagged text following a block element into a paragraph + html = re.sub(r'(?i)(?m)(.*?<\/div>)\s*(?=\w)(.*?)', r'\g<1>\n

\g<2>

', html) return html # ADD RUNS TO DOCX-BLOCK @@ -276,6 +280,8 @@ def add_runs(docx_block, bs_element): 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.has_attr('class') and 'fa-exclamation-triangle' in elrun['class']: + docx_block.add_run(u'\U0000F045', style='Icon') elif elrun.text != '': docx_block.add_run(elrun.text).italic = True elif elrun.text != '': @@ -294,6 +300,8 @@ def add_runs(docx_block, bs_element): 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 == 'u': + docx_block.add_run(elrun.text).underline = True elif elrun.name == 'a': if elrun.has_attr('href'): add_hyperlink(docx_block = docx_block, link_text = elrun.text, url = elrun.get('href')) @@ -355,6 +363,7 @@ def add_tagged_content_as_paragraph(docx_doc, bs_element, style_name="Standard", def add_htmlblock_to_docx(zmscontext, docx_doc, htmlblock, zmsid=None, zmsmetaid=None): # Clean HTML htmlblock = clean_html(htmlblock) + htmlblock = htmlblock.strip() heading_text = '' # Apply BeautifulSoup and iterate over elements soup = BeautifulSoup(htmlblock, 'html.parser') @@ -376,20 +385,18 @@ def add_htmlblock_to_docx(zmscontext, docx_doc, htmlblock, zmsid=None, zmsmetaid prepend_bookmark(p, zmsid) else: # ############################################# - # HTML-Elements, element.name != None - # ############################################# - - # ############################################# + # BLOCK-Elements, element.name != None + # --------------------------------------------- # HEADINGS # ############################################# - if element.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']: + if element.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8']: 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'] + p.style = doc.styles['TOC-Header'] # ############################################# # PARAGRAPH # ############################################# @@ -401,11 +408,14 @@ def add_htmlblock_to_docx(zmscontext, docx_doc, htmlblock, zmsid=None, zmsmetaid # 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'] + p.style = 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] + try: + style_name = (class_name in doc.styles) and class_name or 'Normal' + except: + style_name = 'Normal' + p.style = doc.styles[style_name] add_runs(docx_block = p, bs_element = element) ## Remove empty paragraphs @@ -495,7 +505,7 @@ def add_list(docx_obj, element, level=0, c=0): # ------------------------------------------------ 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_html = clean_html(docx_cell.text, wrap_trailling_text=True) 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') @@ -509,7 +519,14 @@ def convert_cell_html_to_docx(zmscontext, docx_cell, text_style='Normal'): 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) + try: + add_htmlblock_to_docx(zmscontext, docx_cell, cl_html, zmsid=None) + except: + p.add_run('Rendering Error Table-Cell: %s'%cl.text) + # Cleaning: remove first cell paragraph if empty + if docx_cell.paragraphs[0].text == '': + first_p = docx_cell.paragraphs[0]._element + docx_cell._tc.remove(first_p) elif set([e.name for e in cl.children])==set([None]): # [B] Just text p.text = cl.text @@ -547,7 +564,7 @@ def convert_cell_html_to_docx(zmscontext, docx_cell, text_style='Normal'): img_src = zmscontext.operator_getattr(zmscontext,zmsid).attr('imghires').getHref(zmscontext.REQUEST) except: pass - img_name = img_src.split('/')[-1] + img_name = img_src.split('?')[0].split('/')[-1] if not img_src.startswith('http'): src_url0 = zmscontext.absolute_url().split('/content/')[0] src_url1 = img_src.split('/content/')[-1] @@ -583,12 +600,34 @@ def convert_cell_html_to_docx(zmscontext, docx_cell, text_style='Normal'): 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])) + zmsid = element.has_attr('id') and element['id'] or zmsid + zmscontext = zmscontext.operator_getattr(zmscontext,zmsid) 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])) + zmsid = element.has_attr('id') and element['id'] or zmsid + zmscontext = zmscontext.operator_getattr(zmscontext,zmsid) 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) + if len([e.name for e in element.children if e.name in ['ul','ol']])>0: + add_tagged_content_as_paragraph(docx_doc, element, 'Handlungsaufforderung', c, zmsid) + child_tag = [e.name for e in element.children if e.name][0] + # COPY add_list + 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.find(child_tag), level=1, c=c) + else: + 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'] \ @@ -601,14 +640,14 @@ def convert_cell_html_to_docx(zmscontext, docx_cell, text_style='Normal'): 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): + if {'em','strong','i','span','u'} & 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] + style_name = (class_name in doc.styles) and class_name or 'Normal' + p.style = doc.styles[style_name] p.add_run(element.text) elif len(element.contents) > 1: for e in element.contents: @@ -621,8 +660,15 @@ def convert_cell_html_to_docx(zmscontext, docx_cell, text_style='Normal'): # p.add_run('Routing: ') p.add_run(u'\U0000F028', style='Icon') p.add_run(' ') + elif 'fa-exclamation-triangle' in class_name: + # p.add_run('Kommentar: ') + p.add_run(u'\U0000F045', style='Icon') + p.add_run(' ') if list(e.children)!=[]: - add_runs(docx_block = p, bs_element = e) + if [ch.name for ch in e.children if ch.name in ['p', 'ol', 'ul', 'div']]: + add_htmlblock_to_docx(zmscontext, docx_doc, standard.pystr(e), zmsid) + else: + add_runs(docx_block = p, bs_element = e) else: p.add_run(standard.pystr(e.text)) elif e.name: @@ -678,8 +724,9 @@ def convert_cell_html_to_docx(zmscontext, docx_cell, text_style='Normal'): 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 + # OTHER ELEMENTS # ############################################# elif element.name == 'hr': # Omit horizontal rule @@ -687,6 +734,9 @@ def convert_cell_html_to_docx(zmscontext, docx_cell, text_style='Normal'): elif element.name == 'script': # Omit javascript pass + elif element.name == 'style': + # Omit style + pass else: try: if element.has_text: @@ -705,7 +755,7 @@ def add_breadcrumbs_as_runs(zmscontext, p): 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')) + link_text = obj.meta_id == 'ZMS' and standard.pystr(obj.attr('title')) or standard.pystr(obj.getTitlealt(zmscontext.REQUEST)) add_hyperlink(docx_block = p, link_text = link_text, url = obj.getHref2IndexHtml(zmscontext.REQUEST)) if c < len(breadcrumbs): p.add_run(' > ') @@ -755,7 +805,9 @@ def apply_standard_json_docx(self): zmscontext = self request = zmscontext.REQUEST + # For debugging use preview content # request.set('preview', 'preview') + # ################################# is_page = zmscontext.isPage() id = zmscontext.id @@ -794,7 +846,7 @@ def apply_standard_json_docx(self): 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.meta_id in [ 'LgChangeHistory','ZMSTeaserContainer'] \ and not e.isPage() ) \ or e.meta_id in [ 'ZMSLinkElement' ] ] @@ -832,7 +884,7 @@ def apply_standard_json_docx(self): 'parent_id':parent_id, 'parent_meta_id':parent_meta_id, 'docx_format':'image', - 'imgwidth': imgwidth, + 'imgwidth': imgwidth, 'imgheight':imgheight, 'content':img_url }, @@ -938,7 +990,7 @@ def apply_standard_json_docx(self): }] # Give some customizing hints for standard_html - if pageelement.meta_id in ['LgRegel','LgBedingung','LgELearningBanner','ZMSNote']: + if pageelement.meta_id in ['LgRegel','LgBedingung','LgELearningBanner','ZMSNote','ZMSTestarea']: 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; @@ -1057,6 +1109,7 @@ def add_heading(self, text, level=1): # binary data of the DOCX file. def manage_export_pydocx(self, save_file=True, file_name=None): request = self.REQUEST + request.set('lang', self.getPrimaryLanguage()) docx_creator = request.AUTHENTICATED_USER.getUserName() # PAGE_COUNTER: Counter for recursive export @@ -1207,22 +1260,21 @@ def manage_export_pydocx(self, save_file=True, file_name=None): # ############################################# # [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 + p = doc.add_paragraph(style='Caption') + if re.match(r'^\[Abb\. e\d+\] .*', v): + re_list = re.split(r'^(\[Abb. e\d+\]) (.*)',v) + v1 = re_list[1] + v2 = BeautifulSoup(re_list[2], 'html.parser').get_text() + p.add_run(v1).font.italic = False + p.add_run(' ') + p.add_run(v2) else: - p = doc.add_paragraph(style='Caption') - prepend_bookmark(p, block['id']) + p.add_run(v) + 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]: + elif v and ( block['docx_format'] in [e.name for e in doc.styles] or block['docx_format'] in [e.name.replace(' ','') for e in doc.styles] ): p = doc.add_paragraph(v, style=block['docx_format']) prepend_bookmark(p, block['id']) elif v: diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx/neon.docx b/Products/zms/conf/metacmd_manager/manage_export_pydocx/neon.docx index 57e177f41f262ce8bd16e90b95adc5c1320d91bb..0d3d8a0915094d697c1d1ec5108af63659913221 100644 GIT binary patch delta 2743 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 delta 2661 zcmV-r3YzuRngN)a0kBXlf2MtXSr|0cp3MmW0CPkD01p5F0C#V4WG{1cd2D5KE_iKh z)mz!F(=ZTy28n-&@?MgrX=xFQ;L-wOrz{e0ZsOh=(j?dk+t2XApYUUh}(?K!h~?B2Tj?dT|9FlEGZZ>!Vmx~gE;@@(Q>-d5kf*-VzIf5HOf+Q{)-d|UmF znR@rmwX3(j-ekdd2Qvj2xa{W9x~(39z`v@h z9<_byS(q`{`q7D4f5Li+o98q&E4+@xq8{`1!FAYGysn5FaG2`Rr=wHgG4IF2$n!W_ zHy>ZR9z{C`z<~8Az)!scBLIl)Sr70Yg-*bD5AUKcY23wok}m!^Zfra^V9M8AJ|QI8_v@i&QiRP&j=ci6woH7y94x+v11)W3|e~Q}W1sGl00EULCL)=f5nMTLV zZ9qNuvNCPfF>QVH4~P*~nP_%Qd=-KiDzoOQCShsH8}80KNtLIB1%m39R&*iI2fay5 zhB?7^pNstfwrBx7kvE9!Q@Dbd@G*Fbdpm}LACqkqHW_)g z^lbOZe~cW&EOlHNeZgXuC8hdjn3V9uFk!KJs5VzETd6+1ODiAM4o zxI=)(3pXCk#Hpoo@wcEn+9pv$PNM#OTQtNff4m^@jtL8%mx<88esG40kSuI=C1RW7 zQ4sv}#)IXJEbI%OEm+JJ85U71!@@B2IxJFV7N|bRAw%(rgEixeBmmReq1 zGCt1@eZtf&%Cek9nIcuH=1_B>&+?uj>a)W%b`R4!CCZ}k85LH z8=g#8r<)(FgWCq+Vq^e~r96PMF#sgFe~%Ze7Fd0JVCSvC&d(3*ybIVHV*{(p18a5z zn~#Z^Q?3+{&Jg$(tp*I*+?dc>wG zaH~~kWO1UairT?}POCe1KaUkZ#oORXph>nl@eb{QV?!?M?Ut6Q@pWF^vL5 z$|n5%6X<$X!(s*RU%o7?v1xTlA->0gPJ%U$V}N#yH_}$Z2f}XbJ$#Qre+su)3Q((* zlw2NylyH?tUlU)@l4&%Y(}9Uhb+k2J-i!X~y@NTHfi~-!xH0)h&@5v#HzhENVUetj z{R^SLEH?m%;kvF&Gm5GOP1o8JHtc9Tt~b3=;m zhOv~PGiJIeMYmk)GITR@F0~I+pBWO)COvxQDt^4x|H@1<^Q>l_O^V%|oa;CA%_g5{ z_O<}oJU!LlX3Lm}@ge84lEp~d$_a^oRs0eOe=P8ps$K@)_~kxvfAwD#zZmJ`ndB=) zoO4gdm4I<_B_=+?C}$V5tnz0d$$OBl0vIL=MAh4*&|M&-8%#V7*?fH)`QD zdOp!VwrX=GF)&2d0|eSbZmHg;F(`9Mg*S!oAqv^>&lE$vXe@Rm?yK0Fxv;Ev=p}p$ z77sA9C}~P#lkvZof6D&FM;z%z{rKs?JxY*Md|>))t2`sFb?_2|6SBPZ%B0R`P=e?Q z*<=e3n;IVYWaDxuR#UJ-24+g$p36wQM#6vn94O?|d>SlK$jLChW|4lAPAeN%e-F>L&%J>BsKGfKJO$;z z^SdElJP%d1L1}$ZP%|v|8VKdziDbAslpivGe97EwARu5sfUOgKqdz?4g zRMicSqLnrur*PzMEMmeo(Hl2X(MK)0(44xf>@w3>&IZ29Z;Gjhi4BgP1(yT`jZPR@;pvS#snA1z#_-w^uY2U`ct*jyxYjh*jOqXovCdU zpgN_^Mti8O%z=Wr3>dh1z)u z%j;lvODnB3?dpkKid77fOQEFDrnAaJO6&%eEg~)tXa%PGQ3A;%qDVfU*GtY*E{4c* zkUg4|c}T_&Sr)`)py`jsVDj#CCbzUaPF?z!1hzQkAG3Wo*%J<|d zM3ZDoF9z(0Q%Y?@lbK6A0YH=1OF{uzlNn4w0e6#IOi2PDK9hMqACr1a5R=tR8UYuR T@Jv7f9+NRm9tKH900000Mz0+L diff --git a/Products/zms/conf/metacmd_manager/manage_export_pydocx/readme.md b/Products/zms/conf/metacmd_manager/manage_export_pydocx/readme.md index 255a4bdcd..85ca1862b 100644 --- a/Products/zms/conf/metacmd_manager/manage_export_pydocx/readme.md +++ b/Products/zms/conf/metacmd_manager/manage_export_pydocx/readme.md @@ -17,6 +17,19 @@ 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 +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: + +```py +# 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") +``` + +You may prefer to export not the committed but the working content, so set the REQUEST-variable: + +```py +# For debugging use preview content +request.set('preview', 'preview') +``` + +Furthermore 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 index b83c53633..981f0c4e9 100644 --- a/Products/zms/conf/metacmd_manager/manage_export_pydocx/styles.xml +++ b/Products/zms/conf/metacmd_manager/manage_export_pydocx/styles.xml @@ -108,6 +108,8 @@ + + @@ -126,6 +128,8 @@ + + @@ -145,6 +149,8 @@ + + @@ -166,11 +172,15 @@ + + - + + + @@ -190,9 +200,10 @@ - + + - + diff --git a/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSGraphic/standard_json_docx.py b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSGraphic/standard_json_docx.py index 823ba0e4e..a3b44bdcb 100644 --- a/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSGraphic/standard_json_docx.py +++ b/Products/zms/conf/metaobj_manager/com.zms.foundation/ZMSGraphic/standard_json_docx.py @@ -8,6 +8,12 @@ ##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 @@ -17,27 +23,29 @@ parent_meta_id = zmscontext.getParentNode().meta_id text = zmscontext.attr('text') img = zmscontext.attr('imghires') or zmscontext.attr('img') -img_url = img.getHref(request) +img_url = '%s/%s'%(zmscontext.absolute_url(),img.getHref(request).split('/')[-1]) imgwidth = img and int(img.getWidth()) or 0; imgheight = img and int(img.getHeight()) or 0; blocks = [ { - 'id':id, + 'id':'%s_img'%(id), 'meta_id':meta_id, 'parent_id':parent_id, 'parent_meta_id':parent_meta_id, - 'docx_format':'Caption', - 'content':text + 'docx_format':'image', + 'imgwidth':imgwidth, + 'imgheight':imgheight, + 'content':img_url }, { - 'id':'%s_1'%(id), + 'id':id, 'meta_id':meta_id, 'parent_id':parent_id, 'parent_meta_id':parent_meta_id, - 'docx_format':'html', - 'content':''%(img_url, imgwidth, imgheight) - } + 'docx_format':'Caption', + 'content':'[Abb. %s] %s'%(id, text) + }, ] return blocks 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 index 2928fddae..b9a114f8d 100644 --- 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 @@ -28,12 +28,14 @@ "body" : "Normal", "blockquote" : "Quote", "caption" : "Caption", - "headline_1" : "Heading1", - "headline_2" : "Heading2", - "headline_3" : "Heading3", - "headline_4" : "Heading4", - "headline_5" : "Heading5", - "headline_6" : "Heading6", + "headline_1" : "Heading 1", + "headline_2" : "Heading 2", + "headline_3" : "Heading 3", + "headline_4" : "Heading 4", + "headline_5" : "Heading 5", + "headline_6" : "Heading 6", + "headline_7" : "Heading 7", + "headline_8" : "Heading 8", "ordered_list" : "ListBullet", "unordered_list" : "ListBullet", "plain_html": "html", From b4393bc70211cb6ffc43aa26eec5b96a15915948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= Date: Tue, 10 Dec 2024 13:13:26 +0100 Subject: [PATCH 02/14] Clearer formatting for script --- bin/run_tests_in_docker | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/run_tests_in_docker b/bin/run_tests_in_docker index 9eb48d489..891694efc 100755 --- a/bin/run_tests_in_docker +++ b/bin/run_tests_in_docker @@ -1,5 +1,5 @@ #!/bin/sh exec docker compose run --rm zope \ - watching_testrunner --basepath Products --basepath tests \ - -- pytest --tb=short $@ + watching_testrunner --basepath Products --basepath tests -- \ + pytest --tb=short $@ From 67185c8da3430f04823d3545228a5a4c5accb4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= Date: Tue, 10 Dec 2024 13:36:43 +0100 Subject: [PATCH 03/14] Modify built in systemd file templates --- docker/systemd/zeo.service | 25 +++++++++++++++++++++++++ docker/systemd/zms-restart.service | 15 ++++++--------- docker/systemd/zms@.service | 19 ++++++++----------- 3 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 docker/systemd/zeo.service diff --git a/docker/systemd/zeo.service b/docker/systemd/zeo.service new file mode 100644 index 000000000..3fd5cac3d --- /dev/null +++ b/docker/systemd/zeo.service @@ -0,0 +1,25 @@ +[Unit] +Description=Zope ZEO Server + +# After=docker.service +# Requires=docker.service + +[Service] +User=zope +Group=zope +Restart=always + +Environment="INSTANCE_HOME=/path/to/instance_home" +Environment="DOCKER_IMAGE=localhost/zms DOCKER_TAG=latest" + +TimeoutStartSec=0 +PrivateTmp=true + +ExecStartPre=-/usr/bin/podman stop $DOCKER_IMAGE +# TODO add zeo run command +# TODO find a way to parametrize the zeo port or use a file based socket +ExecStart=/usr/bin/podman run --rm --name $DOCKER_IMAGE $DOCKER_IMAGE:$DOCKER_TAG + +[Install] +WantedBy=multi-user.target +WantedBy=zms.target diff --git a/docker/systemd/zms-restart.service b/docker/systemd/zms-restart.service index 9bd909726..66665fba9 100644 --- a/docker/systemd/zms-restart.service +++ b/docker/systemd/zms-restart.service @@ -1,25 +1,22 @@ [Unit] Description=ZMS/Zope rebuilder and restarter -After=docker.service -Requires=docker.service - +# After=docker.service +# Requires=docker.service [Service] -#User=zope -#Group=zope +User=zope +Group=zope Restart=always -## FIXME /path/to/oidc_client/ needs to point to the root folder of the source code of the oidc_client -## so docker can actually build the image -## TODO probably best to point to a rebuild script? Environment="INSTANCE_HOME=/path/to/instance_home" Environment="DOCKER_IMAGE=localhost/zms DOCKER_TAG=latest" TimeoutStartSec=0 PrivateTmp=true -ExecStartPre=/usr/bin/docker build --no-cache --pull --tag $DOCKER_IMAGE:$DOCKER_TAG $INSTANCE_HOME +# If the image is locally built and prepared for it, then this can rebuild the image and thus apply operating system updates +#ExecStartPre=/usr/bin/podman build --no-cache --pull --tag $DOCKER_IMAGE:$DOCKER_TAG $INSTANCE_HOME ExecStart=/usr/bin/systemd restart zms@*.service [Install] diff --git a/docker/systemd/zms@.service b/docker/systemd/zms@.service index aeae6fb3f..194b2422b 100644 --- a/docker/systemd/zms@.service +++ b/docker/systemd/zms@.service @@ -1,19 +1,16 @@ [Unit] Description=ZMS/Zope -After=docker.service -Requires=docker.service - Requires=memcached.service After=memcached.service - -Requires=mariadb.service -After=mariadb.service - +# After=docker.service +# Requires=docker.service +# Requires=mariadb.service +# After=mariadb.service [Service] -#User=zope -#Group=zope +User=zope +Group=zope Restart=always Environment="ZOPE_PUBLIC_PORT=%i" @@ -22,8 +19,8 @@ Environment="DOCKER_IMAGE=localhost/zms DOCKER_TAG=latest" TimeoutStartSec=0 PrivateTmp=true -ExecStartPre=-/usr/bin/docker stop $DOCKER_IMAGE -ExecStart=/usr/bin/docker run --rm --publish 127.0.0.1:$ZOPE_PUBLIC_PORT:80 --name $DOCKER_IMAGE $DOCKER_IMAGE:$DOCKER_TAG +ExecStartPre=-/usr/bin/podman stop $DOCKER_IMAGE +ExecStart=/usr/bin/podman run --rm --publish 127.0.0.1:$ZOPE_PUBLIC_PORT:80 --name $DOCKER_IMAGE $DOCKER_IMAGE:$DOCKER_TAG [Install] WantedBy=multi-user.target From eb23d56c812628855cdef74757bda6240110ddae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= Date: Tue, 10 Dec 2024 13:37:42 +0100 Subject: [PATCH 04/14] Todo --- docker/TODO.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker/TODO.md b/docker/TODO.md index 9e40b314b..172e9c84f 100755 --- a/docker/TODO.md +++ b/docker/TODO.md @@ -9,8 +9,7 @@ - [x] Allow working on zms inside the container - [x] example systemd files to run everything - [x] this should show how automated container updates are done! -- [ ] example nginx config so you get the same experience as on the server -- [ ] Full development experience with all dependennt services locally (mariadb, memcached, …) +- [ ] Full development experience with all dependent services locally (mariadb, memcached, …) # TODOs @@ -22,3 +21,7 @@ - [x] remove debug mode from zope Dockerfile - [x] add script to run tests in docker - [ ] add nginx, mariadb, memcached to docker-compose for a fully featured development environment, that can run production like configs + +# Later + +- [ ] example nginx config so you get the same experience as on the server From 6d1d1ded806d637b259a84d0b397c9a116f3011a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= Date: Tue, 10 Dec 2024 15:16:11 +0100 Subject: [PATCH 05/14] Switch to json syntax to fix buildx warning. By eliminating the shell parent process this makes runwsgi pid 1 and therefore the direct receiver of all signals sent to the container. --- docker/base/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index 072919890..54e807e69 100755 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -82,4 +82,4 @@ COPY --chown=zope:zope Products Products # locally it is mapped into the container via bindmounts # COPY --chown=zope:zope selenium_tests test_output tests ./ -CMD runwsgi --verbose etc/zope.ini http_port=80 +CMD ["runwsgi", "--verbose", "etc/zope.ini", "http_port=80"] From cbb42a893578781920d03b5fac9bd1bfcb80b67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= Date: Tue, 10 Dec 2024 15:16:46 +0100 Subject: [PATCH 06/14] Small improvements to systemd templates --- docker/systemd/{zeo.service => zeo@.service} | 3 ++- docker/systemd/zms@.service | 1 + docker/zope/etc/zope.ini | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) rename docker/systemd/{zeo.service => zeo@.service} (75%) diff --git a/docker/systemd/zeo.service b/docker/systemd/zeo@.service similarity index 75% rename from docker/systemd/zeo.service rename to docker/systemd/zeo@.service index 3fd5cac3d..aef56827a 100644 --- a/docker/systemd/zeo.service +++ b/docker/systemd/zeo@.service @@ -9,6 +9,7 @@ User=zope Group=zope Restart=always +Environment="ZEO_PUBLIC_PORT=%i" Environment="INSTANCE_HOME=/path/to/instance_home" Environment="DOCKER_IMAGE=localhost/zms DOCKER_TAG=latest" @@ -18,7 +19,7 @@ PrivateTmp=true ExecStartPre=-/usr/bin/podman stop $DOCKER_IMAGE # TODO add zeo run command # TODO find a way to parametrize the zeo port or use a file based socket -ExecStart=/usr/bin/podman run --rm --name $DOCKER_IMAGE $DOCKER_IMAGE:$DOCKER_TAG +ExecStart=/usr/bin/podman run --rm --publish 127.0.0.1:$ZEO_PUBLIC_PORT:80 --name $DOCKER_IMAGE $DOCKER_IMAGE:$DOCKER_TAG [Install] WantedBy=multi-user.target diff --git a/docker/systemd/zms@.service b/docker/systemd/zms@.service index 194b2422b..65984c44d 100644 --- a/docker/systemd/zms@.service +++ b/docker/systemd/zms@.service @@ -14,6 +14,7 @@ Group=zope Restart=always Environment="ZOPE_PUBLIC_PORT=%i" +Environment="INSTANCE_HOME=/path/to/instance_home" Environment="DOCKER_IMAGE=localhost/zms DOCKER_TAG=latest" TimeoutStartSec=0 diff --git a/docker/zope/etc/zope.ini b/docker/zope/etc/zope.ini index 1c4048e81..f7c5e4d0b 100755 --- a/docker/zope/etc/zope.ini +++ b/docker/zope/etc/zope.ini @@ -4,8 +4,13 @@ zope_conf = %(here)s/zope.conf [server:main] use = egg:waitress#main +# If deployed on bare metal # host 127.0.0.1 +# If deployed in a container host = 0.0.0.0 +# If deployed on bare metal, need to configure the port +# port = %(http_port)s +# If deployed in a container port = 80 [filter:translogger] From 0dd274d1a4cb655ba0e81c363f547110437f3d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= Date: Tue, 10 Dec 2024 15:16:59 +0100 Subject: [PATCH 07/14] Rename built docker image for consistency --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b77d04c34..b995cc1a9 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: build: context: . dockerfile: ./docker/base/Dockerfile - image: localhost/zms:latest + image: localhost/zope:${ZOPE_TAG:-latest} depends_on: - zeo stop_grace_period: 1s # SIGKILL after 1s, as zope is always taking the full 10 seconds @@ -49,7 +49,7 @@ services: build: context: . dockerfile: ./docker/base/Dockerfile - image: localhost/zms:latest + image: localhost/zope:latest command: runzeo --configure etc/zeo.conf volumes: - ./docker/zeo/etc/:/home/zope/etc/ From b022ce3b8fd17e230365902da0f4ca2b668b01f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= Date: Tue, 10 Dec 2024 15:17:12 +0100 Subject: [PATCH 08/14] Add build script that builds a docker container into a file. --- bin/build_docker_to_file | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100755 bin/build_docker_to_file diff --git a/bin/build_docker_to_file b/bin/build_docker_to_file new file mode 100755 index 000000000..d5e47da51 --- /dev/null +++ b/bin/build_docker_to_file @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +set -xe + +cd "$(dirname "$0")" + +export ZOPE_TAG=latest + +cd .. +docker buildx bake --set '*.platform=linux/amd64' zope + +cd dist +docker image save localhost/zope:$ZOPE_TAG > zope:latest.tar From 947880fc6577b77395458735fb40c4d00e905060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= Date: Tue, 10 Dec 2024 19:31:16 +0100 Subject: [PATCH 09/14] Add script to upload docker container --- bin/build_docker_to_file | 2 +- bin/upload_docker | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100755 bin/upload_docker diff --git a/bin/build_docker_to_file b/bin/build_docker_to_file index d5e47da51..5fa45dc03 100755 --- a/bin/build_docker_to_file +++ b/bin/build_docker_to_file @@ -9,4 +9,4 @@ cd .. docker buildx bake --set '*.platform=linux/amd64' zope cd dist -docker image save localhost/zope:$ZOPE_TAG > zope:latest.tar +docker image save localhost/zope:$ZOPE_TAG > zope.latest.tar diff --git a/bin/upload_docker b/bin/upload_docker new file mode 100755 index 000000000..5e765c58a --- /dev/null +++ b/bin/upload_docker @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -exo pipefail + +TARGET_HOST=${1:-new.dosis-jena.de} +DIST_FILE=${2:-zope.latest.tar} + +cd "$(dirname "$0")/../dist" + +rsync --checksum --partial --archive --itemize-changes --compress --progress $DIST_FILE $TARGET_HOST: +ssh $TARGET_HOST "cat $DIST_FILE | sudo -iu zope podman load" From 5471a1cfae7827b3731501512c0100426213cc3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= Date: Tue, 10 Dec 2024 19:31:39 +0100 Subject: [PATCH 10/14] Add zeo.conf to docker container to make it easier to run zeo from the same container. --- docker/base/Dockerfile | 1 + docker/base/zeo.conf | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100755 docker/base/zeo.conf diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index 54e807e69..c80100cdf 100755 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -74,6 +74,7 @@ RUN mkwsgiinstance --dir . --user admin:admin ARG BUILD_DIR=docker/base COPY --chown=zope:zope $BUILD_DIR/zope.ini etc/zope.ini COPY --chown=zope:zope $BUILD_DIR/zope.conf etc/zope.conf +COPY --chown=zope:zope $BUILD_DIR/zeo.conf etc/zeo.conf RUN mkdir cache Extensions COPY --chown=zope:zope Products Products diff --git a/docker/base/zeo.conf b/docker/base/zeo.conf new file mode 100755 index 000000000..226e8ae58 --- /dev/null +++ b/docker/base/zeo.conf @@ -0,0 +1,20 @@ +%define INSTANCE /home/zope/ + + + address 0.0.0.0:8090 + + + + + path /dev/stdout + format %(asctime)s %(message)s + + + + + path $INSTANCE/var/Data.fs + + + + path $INSTANCE/var/temporary.fs + From da9c5acc712c5ce626937183474b2049780cd686 Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 11 Dec 2024 20:10:30 +0800 Subject: [PATCH 11/14] prevent import block on obj-event trigger code (#342) --- Products/zms/_importable.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Products/zms/_importable.py b/Products/zms/_importable.py index 248fd9b8e..f1fee72ef 100644 --- a/Products/zms/_importable.py +++ b/Products/zms/_importable.py @@ -74,8 +74,11 @@ def recurse_importContent(self, folder): self.setObjProperty(key, blob, lang) # Commit object. - self.onChangeObj( self.REQUEST, forced=1) - + try: + self.onChangeObj( self.REQUEST, forced=1) + except: + standard.writeBlock( self, '[recurse_importContent]: %s commitObject failed'%(self.getId())) + # Process children. for ob in self.getChildNodes(): recurse_importContent(ob, folder) From 6a041ba8caa327faf68acc2c1260323e31fa1c14 Mon Sep 17 00:00:00 2001 From: zmsdev Date: Wed, 11 Dec 2024 13:12:35 +0100 Subject: [PATCH 12/14] workflow: CopyError during publishing #795 (#340) --- Products/zms/zmscontainerobject.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Products/zms/zmscontainerobject.py b/Products/zms/zmscontainerobject.py index 94df6e6a5..2e0ca9b9e 100644 --- a/Products/zms/zmscontainerobject.py +++ b/Products/zms/zmscontainerobject.py @@ -247,30 +247,28 @@ def moveObjsToTrashcan(self, ids, REQUEST): if self.meta_id == 'ZMSTrashcan': return trashcan = self.getTrashcan() - # Set deletion-date. - ids_copy = [] + nodes = [] for id in ids: try: context = getattr(self, id) context.del_uid = str(REQUEST.get('AUTHENTICATED_USER', None)) context.del_dt = standard.getDateTime( time.time()) - ids_copy.append(id) + # Move (Cut & Paste). + children = [context] + [standard.triggerEvent(child,'beforeDeleteObjsEvt') for child in children] + cb_copy_data = _cb_decode(self.manage_cutObjects([id])) + trashcan.manage_pasteObjects(cb_copy_data=_cb_encode(cb_copy_data)) + [standard.triggerEvent(child,'afterDeleteObjsEvt') for child in children] + nodes.extend(children) except: standard.writeBlock( self, "[moveObjsToTrashcan]: Attribute Error %s"%(id)) - # Use only successfully tried ids - ids = ids_copy - # Move (Cut & Paste). - children = [getattr(self,x) for x in ids] - [standard.triggerEvent(child,'beforeDeleteObjsEvt') for child in children] - cb_copy_data = _cb_decode(self.manage_cutObjects(ids)) - trashcan.manage_pasteObjects(cb_copy_data=_cb_encode(cb_copy_data)) + # Synchronize search. + self.getCatalogAdapter().unindex_nodes(nodes=nodes) + # Trashcan: Sort-IDs and Garbage-Collection, trashcan.normalizeSortIds() trashcan.run_garbage_collection(forced=1) - # Synchronize search. - self.getCatalogAdapter().unindex_nodes(nodes=children) # Sort-IDs. self.normalizeSortIds() - [standard.triggerEvent(child,'afterDeleteObjsEvt') for child in children] ############################################################################ From dd1383d94c3bea5ed85084cc798c58130249a881 Mon Sep 17 00:00:00 2001 From: zmsdev Date: Wed, 11 Dec 2024 13:52:51 +0100 Subject: [PATCH 13/14] refact versioning (#329) * fix import of workflow * versioning markdown * more doc * more doc * editing * get_tags/get_tag * more doc * more editing * preview tags * diff * diff pretty json * diff pretty json and preview * fixed tests * revised wf zmi * wf-zmi: disable htmx temporarily * fix form name * fixed form names after refact * gui spacing, version pattern * manage_UndoVersionForm: spacing * added more doc * added more doc * added more doc * ensure wf-tab context after save * added preview toggle to history selector * added toggle html vs. json * added btn-func toggle_view for switching json/preview * typo * added notebook for difflib * eval htmldiff2 * added rest-api get_htmldiff * added POST method to rest-api * cleaning * prepared for genshi processing * cleaning, show primarily diff view * doc: version vector * added draft for implementation * diff json based on change_dt * fixed view toggeling * added htmldiff2 to requirements-full * get_body_content_diff * refactoring htmldiff * fix body_content_diff * diff version_container * rehderShort atoms * version-items if not version-container * added lang to ajax-request. styled zmi block links * diff: added JS-function show_old_src_images() * added style to tagdiff_replaced img * added readme files to docker examples * squash item-versions to container * fixed version over-filtering * workaround the race condition on versions loading * diff when loaded * added change*-attribute fallback * added lang-request param to get_tags: @TODO implement correct version-tags retrieval * version-tags retrieval * added lang for tags --------- Co-authored-by: drfho --- Products/zms/ZMSWorkflowProvider.py | 22 +- Products/zms/import/_language.xml | 28 +- Products/zms/import/example1.workflow.xml | 2 +- Products/zms/rest_api.py | 123 ++- Products/zms/standard.py | 24 + .../zpt/ZMSMetacmdProvider/manage_main.zpt | 2 +- .../zpt/ZMSWorkflowProvider/manage_main.zpt | 912 ++++++++++-------- .../versionmanager/manage_undoversionform.zpt | 400 +++++--- docker/alpine/Extensions/readme.md | 2 +- docker/ubuntu/Extensions/readme.md | 26 +- docs/images/admin_wf_ac.gif | Bin 0 -> 4343 bytes docs/images/admin_wf_extended.gif | Bin 0 -> 47199 bytes docs/images/admin_wf_minimal.gif | Bin 0 -> 23378 bytes docs/notebooks/snippets_12_difflib.ipynb | 295 ++++++ docs/versioning.md | 243 +++++ requirements-full.txt | 1 + 16 files changed, 1492 insertions(+), 588 deletions(-) create mode 100644 docs/images/admin_wf_ac.gif create mode 100644 docs/images/admin_wf_extended.gif create mode 100644 docs/images/admin_wf_minimal.gif create mode 100644 docs/notebooks/snippets_12_difflib.ipynb create mode 100644 docs/versioning.md diff --git a/Products/zms/ZMSWorkflowProvider.py b/Products/zms/ZMSWorkflowProvider.py index 5598c4273..c155dcc5a 100644 --- a/Products/zms/ZMSWorkflowProvider.py +++ b/Products/zms/ZMSWorkflowProvider.py @@ -316,13 +316,13 @@ def doAutocommit(self, lang, REQUEST): Change workflow. """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" - def manage_changeWorkflow(self, lang, btn='', key='properties', REQUEST=None, RESPONSE=None): + def manage_changeWorkflow(self, lang, btn='', key='workflow_properties', REQUEST=None, RESPONSE=None): """ ZMSWorkflowProvider.manage_changeWorkflow """ message = '' # Version Control. # ----------- - if key == 'history': + if key == 'workflow_versioning': old_active = self.getConfProperty('ZMS.Version.active',0) new_active = REQUEST.get('active',0) old_nodes = self.getConfProperty('ZMS.Version.nodes',['{$}']) @@ -342,10 +342,19 @@ def manage_changeWorkflow(self, lang, btn='', key='properties', REQUEST=None, RE except: message += '[%s: %s]'%(node,'No history to pack') message = self.getZMILangStr('MSG_CHANGED')+message - + + # Content Assignment. + # ----------- + elif key == 'workflow_assignment': + # Save. + # ------ + if btn == 'BTN_SAVE': + self.nodes = standard.string_list(REQUEST.get('nodes', '')) + message = self.getZMILangStr('MSG_CHANGED') + # Properties. # ----------- - elif key == 'properties': + elif key == 'workflow_properties': # Save. # ------ if btn == 'BTN_SAVE': @@ -354,7 +363,6 @@ def manage_changeWorkflow(self, lang, btn='', key='properties', REQUEST=None, RE new_autocommit = REQUEST.get('workflow', 0) == 0 self.revision = REQUEST.get('revision', '0.0.0') self.autocommit = new_autocommit - self.nodes = standard.string_list(REQUEST.get('nodes', '')) if old_autocommit == 0 and new_autocommit == 1: self.doAutocommit(lang, REQUEST) message = self.getZMILangStr('MSG_CHANGED') @@ -383,10 +391,10 @@ def manage_changeWorkflow(self, lang, btn='', key='properties', REQUEST=None, RE else: filename = REQUEST['init'] self.importConf(filename) - message = self.getZMILangStr('MSG_IMPORTED')%('%s'%f.filename) + message = self.getZMILangStr('MSG_IMPORTED')%('%s'%filename) # Return with message. message = standard.url_quote(message) - return RESPONSE.redirect('manage_main?lang=%s&key=%s&manage_tabs_message=%s#_properties'%(lang, key, message)) + return RESPONSE.redirect('manage_main?lang=%s&key=%s&manage_tabs_message=%s#%s'%(lang, key, message, key)) ################################################################################ diff --git a/Products/zms/import/_language.xml b/Products/zms/import/_language.xml index af1452fa2..595c3f7f7 100644 --- a/Products/zms/import/_language.xml +++ b/Products/zms/import/_language.xml @@ -6406,23 +6406,23 @@ TAB_WORKFLOW_ASSIGNMENT - Workflow-Aktivierung - Workflow Assignment - Workflow Assignment - Workflow Assignment - Workflow Assignment - Workflow Assignment - Workflow Assignment - Workflow Assignment - Workflow Assignment - Workflow Assignment - Workflow Assignment - Workflow Assignment - Workflow Assignment + Zuweisung + Assignment + Assignment + Assignment + Assignment + Assignment + Assignment + Assignment + Assignment + Assignment + Assignment + Assignment + Assignment TAB_WORKFLOW_MODEL - Ablauf-Modell + Workflow-Modell Workflow Model Workflow Model Workflow Model diff --git a/Products/zms/import/example1.workflow.xml b/Products/zms/import/example1.workflow.xml index b5969a9d6..07dd36bf4 100644 --- a/Products/zms/import/example1.workflow.xml +++ b/Products/zms/import/example1.workflow.xml @@ -43,7 +43,7 @@ - + diff --git a/Products/zms/rest_api.py b/Products/zms/rest_api.py index 5f0795f22..f54e736aa 100644 --- a/Products/zms/rest_api.py +++ b/Products/zms/rest_api.py @@ -170,6 +170,12 @@ def __bobo_traverse__(self, TraversalRequest, name): def __call__(self, REQUEST=None, **kw): """""" standard.writeBlock(self.context,'__call__: %s'%str(self.ids)) + if self.method == 'POST': + if self.ids == ['get_htmldiff']: + decoration, data = self.get_htmldiff(self.context, content_type=True) + return data + else: + return None if self.method == 'GET': decoration, data = {'content_type':'text/plain'}, {} if self.ids == [] and self.context.meta_type == 'ZMSIndex': @@ -190,12 +196,22 @@ def __call__(self, REQUEST=None, **kw): decoration, data = self.get_child_nodes(self.context, content_type=True) elif self.ids == ['get_tree_nodes']: decoration, data = self.get_tree_nodes(self.context, content_type=True) + elif self.ids == ['get_tags']: + decoration, data = self.get_tags(self.context, content_type=True) + elif self.ids == ['get_tag']: + decoration, data = self.get_tag(self.context, content_type=True) + elif self.ids == ['body_content']: + decoration, data = self.body_content(self.context, content_type=True) elif self.ids == [] or self.ids == ['get']: decoration, data = self.get(self.context, content_type=True) else: data = {'ERROR':'Not Found','context':str(self.context),'path_to_handle':self.path_to_handle,'ids':self.ids} - REQUEST.RESPONSE.setHeader('Content-Type',decoration['content_type']) - return json.dumps(data) + ct = decoration['content_type'] + REQUEST.RESPONSE.setHeader('Content-Type',ct) + REQUEST.RESPONSE.setHeader('Content-Disposition', 'inline;filename="%s.%s"'%((self.ids+['get'])[-1],ct.split('/')[-1])) + if ct == 'application/json': + return json.dumps(data) + return data return None @api(tag="zmsindex", pattern="/zmsindex", content_type="application/json") @@ -265,3 +281,106 @@ def get_tree_nodes(self, context): request = _get_request(context) nodes = context.getTreeNodes(request) return [get_attrs(x) for x in nodes] + + @api(tag="version", pattern="/{path}/get_tags", method="GET", content_type="application/json") + def get_tags(self, context): + request = _get_request(context) + lang = request.get('lang') + tags = [] + version_container = context.getVersionContainer() + version_items = ([version_container] + version_container.getVersionItems(request)) if context.isVersionContainer() else [context] + for version_item in version_items: + for obj_version in version_item.getObjVersions(): + request.set('ZMS_VERSION_%s'%version_item.id,obj_version.id) + change_dt = obj_version.attr('change_dt') + change_uid = obj_version.attr('change_uid') + if change_dt and change_uid: + dt = standard.getLangFmtDate(version_item,change_dt,'eng','DATETIME_FMT') + if not [1 for tag in tags if tag[0] == dt]: + tags.append( + (dt + ,'r%i.%i.%i'%(obj_version.attr('master_version'), obj_version.attr('major_version'), obj_version.attr('minor_version')) + ,'/'.join(version_item.getPhysicalPath()) + )) + tags = sorted(list(set(tags)),key=lambda x:x[0]) + tags.reverse() + physical_path = '/'.join(context.getPhysicalPath()) + if context.isVersionContainer(): + physical_path = '/'.join(version_container.getPhysicalPath()) + rtn = [] + for i in range(len(tags)): + tag = list(tags[i]) + if i == 0 and not tag[2] == physical_path: + dt = tag[0] + rtn.append( + [dt + ,'r*.*.*' + , physical_path + ]) + if tag[2] == physical_path: + rtn.append(tag) + return [(x[0],x[1]) for x in rtn] + + @api(tag="version", pattern="/{path}/get_tag", method="GET", content_type="application/json") + def get_tag(self, context): + request = _get_request(context) + lang = request.get('lang') + tag = request.get('tag').split(",") + dt = tag[0] + data = [] + version_container = context.getVersionContainer() + version_items = ([version_container] + version_container.getVersionItems(request)) if context.isVersionContainer() else [context] + for version_item in version_items: + d = {} + for obj_version in version_item.getObjVersions(): + request.set('ZMS_VERSION_%s'%version_item.id,obj_version.id) + change_dt = obj_version.attr('change_dt') or obj_version.attr('created_dt') + change_uid = obj_version.attr('change_uid') or obj_version.attr('created_uid') + if change_dt and change_uid: + d[standard.getLangFmtDate(version_item,change_dt,'eng','DATETIME_FMT')] = obj_version.id + tags = list(reversed(sorted(list(d.keys())))) + tags = [x for x in tags if x <= dt] + if tags: + request.set('ZMS_VERSION_%s'%version_item.id,d[tags[0]]) + attrs = get_attrs(version_item) + data.append(attrs) + return data + + @api(tag="version", pattern="/{path}/body_content", method="GET", content_type="text/html") + def body_content(self, context): + request = _get_request(context) + lang = request.get('lang') + tag = request.get('tag').split(",") + dt = tag[0] + html = [] + version_container = context.getVersionContainer() + version_items = ([version_container] + version_container.getVersionItems(request)) if context.isVersionContainer() else [context] + for version_item in version_items: + d = {} + for obj_version in version_item.getObjVersions(): + request.set('ZMS_VERSION_%s'%version_item.id,obj_version.id) + change_dt = obj_version.attr('change_dt') or obj_version.attr('created_dt') + change_uid = obj_version.attr('change_uid') or obj_version.attr('created_uid') + if change_dt and change_uid: + d[standard.getLangFmtDate(version_item,change_dt,'eng','DATETIME_FMT')] = obj_version.id + tags = list(reversed(sorted(list(d.keys())))) + tags = [x for x in tags if x <= dt] + if tags: + request.set('ZMS_VERSION_%s'%version_item.id,d[tags[0]]) + html.append('

%s
'%(version_item.absolute_url(),'/'.join(version_item.getPhysicalPath()))) + if version_item == version_container: + html.append('

%s%s

'%(version_item.meta_id,version_item.getTitle(request),version_item.getDCDescription(request))) + else: + html.append(version_item.renderShort(request)) + return '\n'.join(html) + + @api(tag="standard", pattern="/{path}/get_htmldiff", method="POST", content_type="text/html") + def get_htmldiff(self, context): + decoration, data = {'content_type':'text/html'}, {} + request = _get_request(context) + original = request.get('original','
original
') + changed = request.get('changed','
changed
') + data = standard.htmldiff(original, changed) + ct = decoration['content_type'] + request.RESPONSE.setHeader('Content-Type',ct) + return data \ No newline at end of file diff --git a/Products/zms/standard.py b/Products/zms/standard.py index 15c0a578f..403c259c5 100644 --- a/Products/zms/standard.py +++ b/Products/zms/standard.py @@ -2241,6 +2241,30 @@ def processData(context, processId, data, trans=None): return _filtermanager.processData(context, processId, data, trans) +security.declarePublic('htmldiff') +def htmldiff(original, changed): + """ + Wrapper for htmldiff2.render_html_diff. + @param original: html-file-0 + @type context: C{str} + @param changed: html-file-1 + @type changed: C{str} + """ + try: + from htmldiff2 import render_html_diff + def remove_curly_braces(s): + return re.sub(r'/[\{\}]', '', s, flags=re.IGNORECASE) + def remove_html_comments(s): + return re.sub(r'', '', s, flags=re.DOTALL) + # Remove html comments for processing with htmldiff2/genshi. + original = remove_html_comments(remove_curly_braces(original)) + changed = remove_html_comments(remove_curly_braces(changed)) + diff = render_html_diff(original,changed) + except: + diff = '
ERROR: Cannot load or work with htmldiff2
' + return diff + + ############################################################################ # #{ Executable diff --git a/Products/zms/zpt/ZMSMetacmdProvider/manage_main.zpt b/Products/zms/zpt/ZMSMetacmdProvider/manage_main.zpt index c310992c9..9b91b8ddb 100644 --- a/Products/zms/zpt/ZMSMetacmdProvider/manage_main.zpt +++ b/Products/zms/zpt/ZMSMetacmdProvider/manage_main.zpt @@ -23,7 +23,7 @@
-
+
diff --git a/Products/zms/zpt/ZMSWorkflowProvider/manage_main.zpt b/Products/zms/zpt/ZMSWorkflowProvider/manage_main.zpt index 0a62bc967..4e5d453a7 100644 --- a/Products/zms/zpt/ZMSWorkflowProvider/manage_main.zpt +++ b/Products/zms/zpt/ZMSWorkflowProvider/manage_main.zpt @@ -8,195 +8,303 @@ zmi_html_head - + +
zmi_body_header
zmi_breadcrumbs - - +
+
+ + + + + + + + + + BTN_CLEAR + +
+
+
+ + + + + + +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+   Activities +
+
+ +
+ +
+
+
 
+
+ +
+ +
+ + + + + + + + + + + + + + + + +
+
+ +
+   Transitions +
+
+ +
+
+
+
+ + + + + + + + +
+ + + +
+ + name +
+
+
+
+
+
+ + + the name< + +
    +
  • performer
  • +
+
+
+ + + +
+ + name +
+
+
+
+
+
+
+
+ + +
+
+ + + +
+
+ +
+
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + + + + + + +
+
+
+
+ +
+
+
+ +
+ +
@@ -208,7 +316,9 @@
- +
- +
- +
- +
- +
@@ -270,7 +388,7 @@ - +
@@ -287,7 +405,9 @@
- +
- +
- +
- +
- +
@@ -380,15 +508,21 @@
- +
- +
- +
- -
- -
- -
-
-
- -
-
-
- -
-
- -
-
-
-
-
-
-
- - - - -
-
- -
- -
- - -
- -
-
-
- -
-
- -
-
-
-
-
-
-
- -
-
-
-
+ .zmi.workflow_manager_main #zmi-tab nav { + margin: 0 1.25rem; + } + .zmi.workflow_manager_main #zmi-tab nav ul { + margin: 0 0 -1px -1px; + overflow: hidden; + white-space: nowrap; + border: none; + z-index: 1; + } + .zmi.workflow_manager_main #zmi-tab form nav, + .zmi.workflow_manager_main #zmi-tab nav ul li { + margin-left:0 !important; + } + .zmi.workflow_manager_main #zmi-tab nav .nav-tabs .nav-link.active { + background-color:#fff; + } + .zmi.workflow_manager_main #zmi-tab #properties.active.show .form-horizontal { + border-top-left-radius: 0; + } + .zmi.workflow_manager_main form.card { + box-shadow: 0px 0px 1px #888; + border: 0; + padding: 2rem 0.5rem .5rem !important; + } + .ace_editor { + border: 1px solid #e4e4e4; + border-radius: 4px; + } +/*-->*/ + -
+ diff --git a/Products/zms/zpt/versionmanager/manage_undoversionform.zpt b/Products/zms/zpt/versionmanager/manage_undoversionform.zpt index 226a9c6c6..52e76fdf6 100644 --- a/Products/zms/zpt/versionmanager/manage_undoversionform.zpt +++ b/Products/zms/zpt/versionmanager/manage_undoversionform.zpt @@ -6,151 +6,228 @@
zmi_breadcrumbs -
- History - - - - -
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  - v.0.0.0 -
the key - -
the original-value
-
- -
the original-value
-
the changed-value
-
-
-
-
-
-
-
- - - -
-
- - - - zmi_pagination - -
-
-
    - - -
  • -
    - the rendered child-node -
    -
    - - version_nr - -
    - version_dt -
    -
    -
    -
  • -
    -
    -
-
-
-
-
-
- - +
+
+
+
+
+ Version +
+
+
+

+		
+
+
+
+ Version +
+ +
+ + +
+
+
+

+		
+
+
+

 		
+
+
+ + - @@ -192,7 +269,52 @@ li.even.zmi-item .center { background: #EDF5FE !important; } - + button#toggle_splitview, + button#toggle_datatype { + width: 2.5rem; + } + button#toggle_splitview.btn-secondary i.fas.fa-columns { + opacity: .5; + } + .diff { + background-color: unset; + } + .diff span.d-none { + display:block !important; + } + .diff span.d-none.diff-unchanged { + display:none !important; + } + + /* Manage Links */ + .diff-container div.preview div > a:has(small) { + display:block; + font-family:monospace; + border-bottom:1px dotted #ccc; + margin:1.5rem 0 0 0; + color:#999; + text-align:left + } + .diff-container div.preview div > a:has(small):hover { + color:#008ac7; + text-decoration:none; + border-bottom:1px dotted #008ac7; + } + .diff-container .diff a.tagdiff_replaced { + background-color:#fea; + } + .diff-container .diff img.old-src { + max-width:80px; + max-height:80px; + position:absolute; + margin:-.3rem 0 0 -.3rem; + border:1px solid red; + box-shadow: 0 0 .25rem white; + } + .diff-container .diff img.tagdiff_replaced { + border: 1px solid #155724; + box-shadow: 0 0 0 .25rem #d4edda; + }
zmi_body_footer
diff --git a/docker/alpine/Extensions/readme.md b/docker/alpine/Extensions/readme.md index 3cddeefe7..b5aa25a8e 100755 --- a/docker/alpine/Extensions/readme.md +++ b/docker/alpine/Extensions/readme.md @@ -4,7 +4,7 @@ Hint: Mounting the folder ./Extensions keeps the external functions synchronous to all all ZEO clients and Docker containers. Hint: if the docker container cannot write to the ./Extensions or ./var folder, -on a dev system you can simply set the permissions to 777. +on a dev system you can simply set the permissions to 777. Important:this not recommended for production! ```bash diff --git a/docker/ubuntu/Extensions/readme.md b/docker/ubuntu/Extensions/readme.md index 4ed7ecf43..b5aa25a8e 100755 --- a/docker/ubuntu/Extensions/readme.md +++ b/docker/ubuntu/Extensions/readme.md @@ -1,13 +1,13 @@ -# Externalizing Extensions for Docker - -Hint: Mounting the folder ./Extensions keeps the external functions -synchronous to all all ZEO clients and Docker containers. - -Hint: if the docker container cannot write to the ./Extensions or ./var folder, -on a dev system you can simply set the permissions to 777. -Important:this not recommended for production! - -```bash -chmod -R 777 Extensions -chmod -R 777 var -``` \ No newline at end of file +# Externalizing Extensions for Docker + +Hint: Mounting the folder ./Extensions keeps the external functions +synchronous to all all ZEO clients and Docker containers. + +Hint: if the docker container cannot write to the ./Extensions or ./var folder, +on a dev system you can simply set the permissions to 777. +Important:this not recommended for production! + +```bash +chmod -R 777 Extensions +chmod -R 777 var +``` diff --git a/docs/images/admin_wf_ac.gif b/docs/images/admin_wf_ac.gif new file mode 100644 index 0000000000000000000000000000000000000000..0d10c94653e6b6bc247f1bc949fbec1a68c717ba GIT binary patch literal 4343 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π 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 tdg-R0j(X~\n", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
f1f1
2  <html>2  <html>
3    <body>3    <body>
t4      <h1>Title</h1>t4      <h1>Big Title</h1>
5      <p>Lorem ipsuor dilluting est</p>5      <p>Lorem ipsum <i>dilletandi</i> est</p>
6      <p>Non sensium</p>
7    </body>6    </body>
8  </html>7  </html>
\n", + " \n", + " \n", + " \n", + " \n", + "
Legends
\n", + " \n", + " \n", + " \n", + " \n", + "
Colors
 Added 
Changed
Deleted
\n", + " \n", + " \n", + " \n", + " \n", + "
Links
(f)irst change
(n)ext change
(t)op
\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from difflib import *\n", + "from IPython.display import display, HTML\n", + "\n", + "# HTML Example\n", + "original = '''\n", + " \n", + " \n", + "

Title

\n", + "

Lorem ipsuor dilluting est

\n", + "

Non sensium

\n", + " \n", + " \n", + "'''\n", + "\n", + "changed = '''\n", + " \n", + " \n", + "

Big Title

\n", + "

Lorem ipsum dilletandi est

\n", + " \n", + " \n", + "'''\n", + "\n", + "diff = HtmlDiff().make_file(original.splitlines(), changed.splitlines())\n", + "# print(diff)\n", + "\n", + "display(HTML(diff))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
f1f1
22
33
t4Titlet4Big Title
5Lorem ipsuor dilluting est5Lorem ipsum dilletandi est
6Non sensium
76
87
\n", + " \n", + " \n", + " \n", + " \n", + "
Legends
\n", + " \n", + " \n", + " \n", + " \n", + "
Colors
 Added 
Changed
Deleted
\n", + " \n", + " \n", + " \n", + " \n", + "
Links
(f)irst change
(n)ext change
(t)op
\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from bs4 import BeautifulSoup\n", + "from difflib import HtmlDiff\n", + "from IPython.display import display, HTML\n", + "\n", + "# Extract text content from HTML\n", + "soup_original = BeautifulSoup(original, 'html.parser')\n", + "soup_changed = BeautifulSoup(changed, 'html.parser')\n", + "\n", + "text_original = soup_original.get_text()\n", + "text_changed = soup_changed.get_text()\n", + "\n", + "# Compare the text content\n", + "diff_text = HtmlDiff().make_file(text_original.splitlines(), text_changed.splitlines())\n", + "\n", + "# Display the diff\n", + "display(HTML(diff_text))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. htmldiff2\n", + "\n", + "* Source: https://github.com/edsu/htmldiff2\n", + "* Install: `./pip install htmldiff2`" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from htmldiff2 import render_html_diff\n", + "import re\n", + "from IPython.display import display, HTML\n", + "\n", + "original = ''''

First Document

\\n\\n\\n\\n\\n\\t\\n\\t\\t\\n\\n

Lorem ipsum 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\\n

Lorem ipsum dolor 

\\n\\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

vulputate velit  molestie consequat

\\n\\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

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\n\\n\\n\\t\\n\\t\\n\\n\\n\\n'''\n", + "changed = ''''

Second Document

\\n\\n\\n\\n\\n\\t\\n\\t\\t\\n\\n

Lorem ipsum 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\\n

Lorem ipsum dolor 

\\n\\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

vulputate velit  molestie consequat

\\n\\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

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\\n\\n\\n\\t\\n\\t\\n\\n\\n\\n'''\n", + "# Remove the HTML comments\n", + "original = re.sub(r'', '', original, flags=re.DOTALL)\n", + "changed = re.sub(r'', '', changed, flags=re.DOTALL)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
'

FirstSecond Document

\n", + "

Lorem ipsum 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", + "

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\n", + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "diff = render_html_diff(original,changed)\n", + "#print(diff)\n", + "display(HTML(diff))" + ] + } + ], + "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": 2 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 000000000..115a4142e --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,243 @@ +# Workflow + +## Introduction + +The key tools for content quality assurance are _workflow_ and _versioning_; +both approaches go hand in hand in content production: + +* **workflow** ensures that the right person edits or reviews content at the right time, while +* **versioning** ensures that the history of editing steps remains transparent and traceable. + +A workflow requires at least two _versions_ of a document: + +1. _Working version_ that is being edited, and +2. _Published version_ (aka. _live version_). + +When a document enters the workflow, a copy of the currently published document version is created and serves as the _working version_. And when the document is published, the working version becomes the current _live version_ by irrevocably overwriting the former live version. + +To model a workflow ZMS allows you to define state names for the content and programm the transitions between these states. The following general principles apply: + +1. A workflow is the sequence of predefined state _transitions_ in a logical order - with the goal of document release. +2. A workflow step requires a transition from one active workflow state (activity state) of the content object to another. +3. Starting from the basic state, the workflow always starts automatically with the `TR_ENTER` transition, i.e. with the transfer of the object to the active workflow-specific initial state _Changed_. +4. The workflow always ends with the `TR_LEAVE` transition to the target state `AC_COMMITTED` _Commmitted_ (corresponds to the empty basic state `None`). + + +## Content States + +### Basic States (STATE) + +When an editor makes a content change and clicks the save button, the system records this change by assigning four basic states to the object. +Moreover there is the possibility that no state is assigned. So these basic states are: + +1. `STATE_NEW` +2. `STATE_MODIFIED` +3. `STATE_DELETED` +4. `None` (no state assigned, means _committed_/published) + + +These states are fundamental and operate independently of any activated workflow. Once the workflow is activated, _transitions_ become relevant to add more, workflow specific state values to the content: if a content object is assigned one of basic states it automatically triggers a virtual transition to enter the workflow process, specifically the transition (tr) `TR_ENTER` for the PAGE container of the edited content object. +As a result, the PAGE container, along with the affected content object, is assigned the initial workflow status, which is labeled as _changed_ by an activity (ac) status `AC_CHANGED`. + + +### Activity States (AC) + +Activity states are induced by specific workflow transitions; so any _activity state_ can get changed to another activity state by a _transition_ method that will exactly perform this specific action. + +![Activity States](images/admin_wf_ac.gif) + +The workflow model above starts implicitly with the basic state "Changed" and will be left implicitly with the activity state "committed"; besides the implicit initial activity state `AC_CHANGED` (as a result of the basic state settings like new, modified or deleted) the use performed _activity states_ of the workflow are: + +1. Commit requested +2. Committed + +To perform the changes of the activity states two _transitions_ are needed: + +1. Request commit +2. Commit + +## Transitions (TR) + +A _transition_ is the change of a document state from one to another executed by a transaction and fully decribed by these three elements: + +``` +State-A -----Transaction-----> State-B +``` +To ensure a logical _flow_ of transitions any ending state shall be the starting starting state of another transition. Otherwise the workflow may end prematurely and document changes cannot not be published. + +``` +State-A -----Transaction-AB-----> State-B +State-B -----Transaction-BC-----> State-C +``` + +This is a linear flow from _State-A_ to _State-C_. But how does a document get to _State-A_? And when does the flow end, when will the document be published? That is why two preset transitions for _entering_ (TR_ENTER) and _leaving_ (TR_LEAVE) the workflow are needed: + +``` + Enter Workflow-----> State-A +State-A -----Transaction-AB-----> State-B +State-B -----Transaction-BC-----> State-C +State-C -----Leave Workflow +``` + +The ZMS-UI allows to model this stepwise: First you define a set of activity states beginning with `AC_CHANGED` (_Changed_). Then you add a set of _transitions_ starting with `TR_ENTER` and ending with `TR_LEAVE`. Any transition has one or more states where it can start from and exactly one state where it ends to. +The visualisation of a very simple workflow may look like this: + +![Simple Workfow Model](images/admin_wf_minimal.gif) + +_Screen image: Simple workflow with two major transitions: 1. requesting a commit and 2. committing_ + +Besides the transitions from one state to another, the screen image shows two more important aspects for designing a workflow: + +1. A transition can get started from more than one state, e.g. "Commit" can be performed from "Changed" (like a shortcut for faster publication) and from "Commit requested" (to get the approval first) +2. A transition can be performed by certain user roles + +This very simple workflow can be made more flexible by adding more _transitions_, e.g. a transition for rejecting a request for document commit or a transition for rolling back all document changes. A _rollback_ would leave the workflow as well as a _commit_. And you can add a "Express Commit" transition for instant publishing. + +![Extended Workfow Model](images/admin_wf_extended.gif) + +_Screen image: The simple workflow has got some more transitions to cover variants in the workflow and to make it more flexible_ + + +## Selective workflow + +The content nodes that follow the workflow on publishing a document can be assigned individually. The assignment works recursively. So if the just the root-node is assigned, the workflow is set to the whole content tree. + +# Versioning + +## Introduction + +Each content block object (being a set of attributes) can be stored in its own version. +This object has a unique id and this id is referenced by the `ZMSCustom`-container. +Any object is designed to exist in two versions; its container-object aggregates these two versions by id-linking to the corresponding content object: +* `version_live_id` for the current published live-version +* `version_work_id` for the current version in progress + +Because document a massively fragmented into small block objects, a useful aggregate is the committable container-object. Thus committing a container-object (document) will be equivalent to tagging a changeset. + +The following example shows that if only two blocks are versioned atomically, it cannot be resolved if there is no change documentation for the container document, i.e., the sum of the blocks: + +``` +document-e1: 0.0.1 + |---block-e2: 0.0.1 + |---block-e3: 0.0.2 + |---block-e4: 0.0.1 + +document-e1: 0.0.1 + |---block-e2: 0.0.1 + |---block-e3: 0.0.3 + |---block-e4: 0.0.3 +``` + +The document container must keep a log and increment its version with each child change, as if it were changed itself. This is the only way to historically trace the changes. + + +``` +document-e1: - 0.0.1 {e2:0.0.1, e3:0.0.1, e4:0.0.1} + | - 0.0.2 {e2:0.0.1, e3:0.0.2, e4:0.0.1} + | - 0.0.3 {e2:0.0.1, e3:0.0.3, e4:0.0.1} + | - 0.0.4 {e2:0.0.1, e3:0.0.3, e4:0.0.2} + | - 0.0.5 {e2:0.0.1, e3:0.0.3, e4:0.0.3} + |---block-e2: [0.0.1] + |---block-e3: [0.0.1, 0.0.2, 0.0.3] + |---block-e4: [0.0.1, 0.0.2, 0.0.3] +``` + +### Implementation (DRAFT) + +To implement the versioning system for a container object that contains an arbitrary number of sub-objects, each individually versioned on any changes, consider implementing a versioning vector that captures the state of the entire container and its sub-objects. Here is a proposed solution: + +1. **Version Vector Structure**: Use a version vector that includes the version of the container object itself and the versions of all its sub-objects. This vector should be updated whenever any sub-object or the container object changes. + +2. **Composite Versioning**: Maintain a composite version for the container object that reflects the versions of all its sub-objects. This composite version can be a hash or a concatenation of the individual versions. + +3. **Change Log**: Keep a detailed change log for the container object that records changes to both the container and its sub-objects. This log should include timestamps and the specific changes made. + +4. **Incremental Updates**: Increment the version of the container object whenever any sub-object changes. This ensures that the container's version always reflects the latest state of its contents. + +5. **Efficient Storage**: Store the version vector in a way that minimizes redundancy and allows for efficient retrieval and comparison of versions. + + +_**Example Implementation:**_ + +```py +class VersionedObject: + def __init__(self, id): + self.id = id + self.version = 0 + self.sub_objects = {} + self.change_log = [] + + def add_sub_object(self, sub_object): + self.sub_objects[sub_object.id] = sub_object + self.update_version() + + def update_sub_object(self, sub_object_id, new_version): + if sub_object_id in self.sub_objects: + self.sub_objects[sub_object_id].version = new_version + self.update_version() + + def update_version(self): + self.version += 1 + self.change_log.append(self.get_version_vector()) + + def get_version_vector(self): + version_vector = {self.id: self.version} + for sub_object_id, sub_object in self.sub_objects.items(): + version_vector[sub_object_id] = sub_object.version + return version_vector + +class SubObject: + def __init__(self, id): + self.id = id + self.version = 0 + +# Example usage +container = VersionedObject('container') +block1 = SubObject('block1') +block2 = SubObject('block2') + +container.add_sub_object(block1) +container.add_sub_object(block2) + +container.update_sub_object('block1', 1) +container.update_sub_object('block2', 2) + +print(container.get_version_vector()) +``` + +_**Explanation:**_ + +* **VersionedObject Class**: Represents the container object. It maintains a version, a dictionary of sub-objects, and a change log. +* **SubObject Class**: Represents a sub-object with its own version. +* **add_sub_object Method**: Adds a sub-object to the container and updates the container's version. +* **update_sub_object Method**: Updates the version of a sub-object and increments the container's version. +* **update_version Method**: Increments the container's version and logs the current version vector. +get_version_vector Method: Returns the current version vector, which includes the versions of the container and all its sub-objects. + +This approach ensures that the container's version always reflects the latest state of its sub-objects, providing a clear and traceable version history. + + + +## Numbering + +The version numbering follows the scheme: + +``` +major.minor.patch +``` + +* **major**: Significant changes, possibly incompatible with previous versions (aka. _master_ version). +* **minor**: Minor feature additions, backward-compatible. +* **patch**: Bug fixes and minor changes, backward-compatible. + +## Versioning with activated workflow + +When the workflow is activated, versioning integrates seamlessly with the workflow states and transitions. Each state change or transition can trigger the creation of a new _patch_ version, ensuring that every step in the workflow is documented and traceable. Any committing of a document creates a new _minor_ version and all atomic _patch_ versions during the workflow cycle are ommited. +This integration provides a robust mechanism for content management, combining the benefits of both versioning and workflow to maintain high content quality and accountability. + +## Versioning without workflow + +In scenarios where the workflow is not activated, versioning still ensures that changes are tracked and can be reverted if necessary. Each save action creates a new _minor_ version of the content. Users can manually switch between versions or restore previous versions as needed. + +In both cases ZMS does not implicitly create a _major_ version (like _patch_ and _minor_), but it has to be done explicity by a user interaction ("Create Major Version"). Important note: Creating a _major_ versions omits all _minor_ and _patch_ versions, and thus helps to reduce the amount of data. + diff --git a/requirements-full.txt b/requirements-full.txt index b1d7da163..8a3e8ffad 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -23,6 +23,7 @@ Markdown pyScss ftfy pdfminer.six +htmldiff2 # OpenSearch opensearch-py From 5d01b1fa5c7807e7abca498810bd5408caeadd3e Mon Sep 17 00:00:00 2001 From: drfho Date: Wed, 11 Dec 2024 21:13:29 +0800 Subject: [PATCH 14/14] added compatibility to obsolete py2-function pybytes() (#344) After update old ZMS4/Py2 templates/py-script string processing errors with pybytes() may occur. This former charset fixing function does not exist anymore. For a smother updating it is synonymized with pystr() now. --- Products/zms/standard.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Products/zms/standard.py b/Products/zms/standard.py index 403c259c5..22e6fd37b 100644 --- a/Products/zms/standard.py +++ b/Products/zms/standard.py @@ -72,6 +72,9 @@ def pystr(v, encoding='utf-8', errors='strict'): v = str(v) return v +security.declarePublic('pybytes') +# Just for compatibility of old ZMS4 templates. +pybytes = pystr # Umlauts umlaut_map = {