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/_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) 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 57e177f41..0d3d8a091 100644 Binary files a/Products/zms/conf/metacmd_manager/manage_export_pydocx/neon.docx and b/Products/zms/conf/metacmd_manager/manage_export_pydocx/neon.docx differ 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", 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 bd0c3c880..22e6fd37b 100644 --- a/Products/zms/standard.py +++ b/Products/zms/standard.py @@ -2244,6 +2244,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/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] ############################################################################ 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/bin/build_docker_to_file b/bin/build_docker_to_file new file mode 100755 index 000000000..5fa45dc03 --- /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 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 $@ 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" 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/ 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 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/base/Dockerfile b/docker/base/Dockerfile index 072919890..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 @@ -82,4 +83,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"] 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 + diff --git a/docker/systemd/zeo@.service b/docker/systemd/zeo@.service new file mode 100644 index 000000000..aef56827a --- /dev/null +++ b/docker/systemd/zeo@.service @@ -0,0 +1,26 @@ +[Unit] +Description=Zope ZEO Server + +# After=docker.service +# Requires=docker.service + +[Service] +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" + +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 --publish 127.0.0.1:$ZEO_PUBLIC_PORT:80 --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..65984c44d 100644 --- a/docker/systemd/zms@.service +++ b/docker/systemd/zms@.service @@ -1,29 +1,27 @@ [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" +Environment="INSTANCE_HOME=/path/to/instance_home" 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 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/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] diff --git a/docs/images/admin_wf_ac.gif b/docs/images/admin_wf_ac.gif new file mode 100644 index 000000000..0d10c9465 Binary files /dev/null and b/docs/images/admin_wf_ac.gif differ diff --git a/docs/images/admin_wf_extended.gif b/docs/images/admin_wf_extended.gif new file mode 100644 index 000000000..4f2611243 Binary files /dev/null and b/docs/images/admin_wf_extended.gif differ diff --git a/docs/images/admin_wf_minimal.gif b/docs/images/admin_wf_minimal.gif new file mode 100644 index 000000000..6c8b4b215 Binary files /dev/null and b/docs/images/admin_wf_minimal.gif differ diff --git a/docs/notebooks/snippets_12_difflib.ipynb b/docs/notebooks/snippets_12_difflib.ipynb new file mode 100644 index 000000000..c45cd41dc --- /dev/null +++ b/docs/notebooks/snippets_12_difflib.ipynb @@ -0,0 +1,295 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Comparing Content Versions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. difflib" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "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
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