Commit 098a9c69 authored by Arnaud Fontaine's avatar Arnaud Fontaine

WIP: ZODB Components: Migrate ERP5OOo Product from filesystem.

OOoUtils + OOoTemplate
parent 421d7d98
......@@ -74,7 +74,7 @@ def getIdFromString(string):
return clean_id
def convert(self, filename, data=None):
from Products.ERP5OOo.OOoUtils import OOoParser
from erp5.component.module.OOoUtils import OOoParser
OOoParser = OOoParser()
import_file = read(self, filename, data)
......
......@@ -56,7 +56,7 @@ import ZPublisher.HTTPRequest
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import FileUpload
from Products.ERP5Type.tests.utils import DummyLocalizer
from Products.ERP5OOo.OOoUtils import OOoBuilder
from erp5.component.module.OOoUtils import OOoBuilder
from AccessControl.SecurityManagement import newSecurityManager
from AccessControl import getSecurityManager
from Products.ERP5.Document.Document import NotConvertedError
......
from Products.ERP5OOo.OOoUtils import OOoParser
from erp5.component.module.OOoUtils import OOoParser
import string
request = container.REQUEST
......
......@@ -37,31 +37,30 @@ from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from zope.tal.talinterpreter import FasterStringIO
from Products.ERP5Type import PropertySheet
from urllib import quote
from Products.ERP5Type.Globals import InitializeClass, DTMLFile, get_request
from Products.ERP5Type.Globals import InitializeClass, DTMLFile
from Acquisition import aq_base
from AccessControl import ClassSecurityInfo
from OOoUtils import OOoBuilder
from erp5.component.module.OOoUtils import OOoBuilder
from zipfile import ZipFile, ZIP_DEFLATED
from cStringIO import StringIO
import re
import itertools
try:
# pylint: disable=no-name-in-module,unused-import
from webdav.Lockable import ResourceLockedError
from webdav.WriteLockInterface import WriteLockInterface
SUPPORTS_WEBDAV_LOCKS = 1
except ImportError:
SUPPORTS_WEBDAV_LOCKS = 0
from Products.ERP5.Document.Document import ConversionError
from lxml import etree
from lxml.etree import Element
# Constructors
manage_addOOoTemplate = DTMLFile("dtml/OOoTemplate_add", globals())
def addOOoTemplate(self, id, title="", xml_file_id="content.xml", REQUEST=None):
def addOOoTemplate(self, id, title="", xml_file_id="content.xml", REQUEST=None): # pylint: disable=redefined-builtin
"""Add OOo template to folder.
id -- the id of the new OOo template to add
......@@ -76,12 +75,12 @@ def addOOoTemplate(self, id, title="", xml_file_id="content.xml", REQUEST=None):
file_ = REQUEST.form.get('file')
if file_.filename:
# Get the template in the associated context and upload the file
obj.pt_upload(REQUEST, file)
obj.pt_upload(REQUEST, file_)
# respond to the add_and_edit button if necessary
add_and_edit(self, id, REQUEST)
return ''
def add_and_edit(self, id, REQUEST):
def add_and_edit(self, id, REQUEST): # pylint: disable=redefined-builtin
"""Helper method to point to the object's management screen if
'Add and Edit' button is pressed.
id -- id of the object we just added
......@@ -97,14 +96,18 @@ def add_and_edit(self, id, REQUEST):
REQUEST.RESPONSE.redirect(u+'/manage_main')
class OOoTemplateStringIO(FasterStringIO):
# pylint: disable=method-hidden
def write(self, s):
if type(s) == unicode:
if isinstance(s, unicode):
s = s.encode('utf-8')
FasterStringIO.write(self, s)
from Products.PageTemplates.Expressions import ZopeContext, createZopeEngine
# On recent Zope, we need an engine to decode non-unicode-strings for us
#
# evaluateCode():
# pylint: disable=abstract-method
class OOoContext(ZopeContext):
""" ZopeContext variant that ALWAYS converts standard strings through utf-8,
as needed by OpenOffice, ignoring the preferred encodings in the request.
......@@ -117,9 +120,9 @@ class OOoContext(ZopeContext):
return ZopeContext._handleText(self, text, expr)
def createOOoZopeEngine():
e = createZopeEngine()
e._create_context = OOoContext
return e
e = createZopeEngine()
e._create_context = OOoContext
return e
_engine = createOOoZopeEngine()
......@@ -177,7 +180,7 @@ class OOoTemplate(Base, ZopePageTemplate):
__name__='formSettings')
formSettings._owner = None
def __init__(self, id, title='', *args, **kw):
def __init__(self, id, title='', *args, **kw): # pylint: disable=redefined-builtin
ZopePageTemplate.__init__(self, id, title, *args, **kw)
# we store the attachments of the uploaded document
self.OLE_documents_zipstring = None
......@@ -190,12 +193,12 @@ class OOoTemplate(Base, ZopePageTemplate):
def pt_getEngine(self):
return _engine
def pt_upload(self, REQUEST, file=''):
def pt_upload(self, REQUEST, file=''): # pylint: disable=redefined-builtin,arguments-differ
"""Replace the document with the text in file."""
if SUPPORTS_WEBDAV_LOCKS and self.wl_isLocked():
raise ResourceLockedError, "File is locked via WebDAV"
if type(file) is not StringType:
if not isinstance(file, StringType):
if not file: raise ValueError, 'File not specified'
file = file.read()
......@@ -217,7 +220,7 @@ class OOoTemplate(Base, ZopePageTemplate):
except RuntimeError:
zf = ZipFile(memory_file, mode='w')
for attached_file in attached_files_list:
zf.writestr(attached_file, builder.extract(attached_file) )
zf.writestr(attached_file, builder.extract(attached_file) )
zf.close()
memory_file.seek(0)
self.OLE_documents_zipstring = memory_file.read()
......@@ -225,20 +228,6 @@ class OOoTemplate(Base, ZopePageTemplate):
file = builder.prepareContentXml(self.getXmlFileId())
return ZopePageTemplate.pt_upload(self, REQUEST, file)
if 'pt_edit' not in ZopePageTemplate.__dict__:
# Override it only for 2.8 !
# ZopePageTemplate v.2.8 inherate pt_edit from
# PageTemplate. If method is defined on ZopePageTemplate
# means we are under 2.12.
# Delete me when we drop support of 2.8
security.declareProtected('Change Page Templates', 'pt_edit')
def pt_edit(self, text, content_type):
if content_type:
self.content_type = str(content_type)
if hasattr(text, 'read'):
text = text.read()
self.write(text)
security.declareProtected('Change Page Templates', 'doSettings')
def doSettings(self, REQUEST, title, xml_file_id, ooo_stylesheet, script_name=None):
"""
......@@ -262,7 +251,7 @@ class OOoTemplate(Base, ZopePageTemplate):
def renderIncludes(self, here, text, extra_context, request, sub_document=None):
attached_files_dict = {}
arguments_re = re.compile('''(\S+?)\s*=\s*('|")(.*?)\\2\s*''',re.DOTALL)
arguments_re = re.compile(r'''(\S+?)\s*=\s*('|")(.*?)\\2\s*''',re.DOTALL)
def getLengthInfos( opts_dict, opts_names ):
ret = []
for opt_name in opts_names:
......@@ -351,6 +340,7 @@ class OOoTemplate(Base, ZopePageTemplate):
if picture_type is None:
picture_type = picture.getContentType()
# pylint: disable=unbalanced-tuple-unpacking
w, h, maxwidth, maxheight = getLengthInfos(options_dict,
('width', 'height', 'maxwidth', 'maxheight'))
......@@ -425,11 +415,13 @@ class OOoTemplate(Base, ZopePageTemplate):
office_include.getparent().replace(office_include, draw_object)
text = etree.tostring(xml_doc, encoding='utf-8', xml_declaration=True,
pretty_print=False)
text = re.sub('<\s*office:include_img\s+(.*?)\s*/\s*>(?s)', replaceIncludesImg, text)
text = re.sub(r'<\s*office:include_img\s+(.*?)\s*/\s*>(?s)', replaceIncludesImg, text)
return (text, attached_files_dict)
# Proxy method to PageTemplate
def pt_render(self, source=0, extra_context={}):
def pt_render(self, source=0, extra_context=None):
if extra_context is None:
extra_context = {}
if source:
return ZopePageTemplate.pt_render(self, source=source,
extra_context=extra_context)
......@@ -448,14 +440,14 @@ class OOoTemplate(Base, ZopePageTemplate):
ooo_document = ooo_script(self.getOooStylesheet())
else:
ooo_document = getattr(here, self.getOooStylesheet())
format = request.get('format')
format_ = request.get('format')
try:
# If style is dynamic, call it
if getattr(aq_base(ooo_document), '__call__', None) is not None:
request.set('format', None)
ooo_document = ooo_document()
finally:
request.set('format', format)
request.set('format', format_)
# Create a new builder instance
ooo_builder = OOoBuilder(ooo_document)
# Pass builder instance as extra_context
......@@ -550,8 +542,8 @@ class OOoTemplate(Base, ZopePageTemplate):
filename=filename,
content_type=mimetype,)
format = opts.get('format', request.get('format', None))
if format:
format_ = opts.get('format', request.get('format', None))
if format_:
# Performance improvement:
# We already have OOo format data, so we do not need to call
# convertToBaseFormat(), but just copy it into base_data property.
......@@ -561,19 +553,19 @@ class OOoTemplate(Base, ZopePageTemplate):
if request is not None and not batch_mode and not source:
return tmp_ooo.index_html(REQUEST=request,
RESPONSE=request.RESPONSE,
format=format)
return tmp_ooo.convert(format)[1]
format=format_)
return tmp_ooo.convert(format_)[1]
def om_icons(self):
"""Return a list of icon URLs to be displayed by an ObjectManager"""
icons = ({'path': 'misc_/ERP5OOo/OOo.png',
'alt': self.meta_type, 'title': self.meta_type},)
if not self._v_cooked:
self._cook()
self._cook()
if self._v_errors:
icons = icons + ({'path': 'misc_/PageTemplates/exclamation.gif',
'alt': 'Error',
'title': 'This template has an error'},)
icons = icons + ({'path': 'misc_/PageTemplates/exclamation.gif',
'alt': 'Error',
'title': 'This template has an error'},)
return icons
def _getFileName(self):
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Module Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>default_reference</string> </key>
<value> <string>OOoTemplate</string> </value>
</item>
<item>
<key> <string>default_source_reference</string> </key>
<value> <string>Products.ERP5OOo.OOoTemplate</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>module.erp5.OOoTemplate</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Module Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -31,16 +31,12 @@
from Acquisition import Implicit
from Products.PythonScripts.Utility import allow_class
from ZPublisher.HTTPRequest import FileUpload
from xml.dom import Node
from AccessControl import ClassSecurityInfo
from Products.ERP5Type.Globals import InitializeClass, get_request
from Products.ERP5Type.Globals import get_request
from zipfile import ZipFile, ZIP_DEFLATED
from cStringIO import StringIO
import imghdr
import random
from Products.ERP5Type import Permissions
from zLOG import LOG, INFO, DEBUG
from zLOG import LOG, DEBUG
from OFS.Image import Pdata
......@@ -142,7 +138,6 @@ class OOoBuilder(Implicit):
- indent the xml
"""
content_xml = self.extract(ooo_xml_file_id)
output = StringIO()
content_doc = etree.XML(content_xml)
root = content_doc.getroottree().getroot()
#Declare zope namespaces
......@@ -160,10 +155,10 @@ class OOoBuilder(Implicit):
def addFileEntry(self, full_path, media_type, content=None):
""" Add a file entry to the manifest and possibly is content """
self.addManifest(full_path, media_type)
if content:
self.replace(full_path, content)
""" Add a file entry to the manifest and possibly is content """
self.addManifest(full_path, media_type)
if content:
self.replace(full_path, content)
def addManifest(self, full_path, media_type):
""" Add a path to the manifest """
......@@ -176,9 +171,9 @@ class OOoBuilder(Implicit):
meta_infos = self.extract(MANIFEST_FILENAME)
# prevent some duplicates
for meta_line in meta_infos.split('\n'):
for new_meta_line in self._manifest_additions_list:
if meta_line.strip() == new_meta_line:
self._manifest_additions_list.remove(new_meta_line)
for new_meta_line in self._manifest_additions_list:
if meta_line.strip() == new_meta_line:
self._manifest_additions_list.remove(new_meta_line)
# add the new lines
self._manifest_additions_list.append('</manifest:manifest>')
......@@ -186,7 +181,7 @@ class OOoBuilder(Implicit):
self.replace(MANIFEST_FILENAME, meta_infos)
self._manifest_additions_list = []
def addImage(self, image, format='png', content_type=None):
def addImage(self, image, format='png', content_type=None): # pylint: disable=redefined-builtin
"""
Add an image to the current document and return its id
"""
......@@ -334,7 +329,6 @@ class OOoParser(Implicit):
document = embedded.get('{%s}href' % embedded.nsmap['xlink'])
if document:
try:
object_content = etree.XML(self.oo_files[document[3:] + '/content.xml'])
find_path = './/{%s}table' % self.oo_content_dom.nsmap['table']
table_list = self.oo_content_dom.findall(find_path)
if table_list:
......@@ -342,7 +336,7 @@ class OOoParser(Implicit):
spreadsheets.append(table)
else: # XXX: insert the link to OLE document ?
pass
except XMLSyntaxError:
except XMLSyntaxError: # pylint: disable=catching-non-exception
pass
return spreadsheets
......@@ -383,7 +377,7 @@ class OOoParser(Implicit):
else:
lines_to_repeat = int(line_group_found)
for i in range(lines_to_repeat):
for _ in range(lines_to_repeat):
table_line = []
# Get all cells
......@@ -413,7 +407,7 @@ class OOoParser(Implicit):
cells_to_repeat = int(cell_group_found)
# Ungroup repeated cells
for j in range(cells_to_repeat):
for _ in range(cells_to_repeat):
# Get the cell content
cell_data = None
attribute_type_mapping = {'date': 'date-value',
......@@ -433,7 +427,6 @@ class OOoParser(Implicit):
# instance <text:s/> for a space (or using <text:s text:c="3"/>
# for multiple spaces) <text:tab/> for a tab and <text:line-break/>
# for new line
text_ns = cell.nsmap['text']
def format_node(node):
if node.tag == '{%s}table-cell' % node.nsmap['table']:
return "\n".join(part for part in
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Module Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>default_reference</string> </key>
<value> <string>OOoUtils</string> </value>
</item>
<item>
<key> <string>default_source_reference</string> </key>
<value> <string>Products.ERP5OOo.OOoUtils</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>module.erp5.OOoUtils</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Module Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -26,7 +26,7 @@ This scripts guarantees that the list of category info is sorted in such a
way that parent always precedes their children.
"""
from Products.ERP5Type.Message import translateString
from Products.ERP5OOo.OOoUtils import OOoParser
from erp5.component.module.OOoUtils import OOoParser
parser = OOoParser()
category_list_spreadsheet_mapping = {}
error_list = []
......
......@@ -5,5 +5,7 @@ module.erp5.GeneratedAmountList
module.erp5.Log
module.erp5.MovementCollectionDiff
module.erp5.MovementGroup
module.erp5.OOoTemplate
module.erp5.OOoUtils
module.erp5.TimeoutTransport
module.erp5.WebDAVSupport
\ No newline at end of file
......@@ -3161,7 +3161,7 @@ class TestAccountingExport(AccountingTestCase):
quantity=200),))
ods_data = accounting_transaction.Base_viewAsODS(
form_id='AccountingTransaction_view')
from Products.ERP5OOo.OOoUtils import OOoParser
from erp5.component.module.OOoUtils import OOoParser
parser = OOoParser()
parser.openFromString(ods_data)
content_xml = parser.oo_files['content.xml']
......
......@@ -826,7 +826,7 @@ AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
"output_encoding": "utf-8",
"content_type": "text/html"}
from Products.ERP5OOo.OOoTemplate import OOoTemplate
from erp5.component.module.OOoTemplate import OOoTemplate
skin_folder._setObject(OOo_template_id,
OOoTemplate(OOo_template_id, OOo_template_data, content_type=''))
......
......@@ -37,7 +37,6 @@ from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import FileUpload
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
from Products.ERP5OOo.OOoUtils import OOoParser
from AccessControl.SecurityManagement import newSecurityManager
from DateTime import DateTime
from Acquisition import aq_parent
......@@ -1712,6 +1711,7 @@ class TestInvoice(TestInvoiceMixin):
self.fail(''.join(err_list))
# the <draw:image> should not be present, because there's no logo
from erp5.component.module.OOoUtils import OOoParser
parser = OOoParser()
parser.openFromString(odt)
style_xml = parser.oo_files['styles.xml']
......
......@@ -157,7 +157,7 @@ class TestOOoChart(ERP5TypeTestCase, ZopeTestCase.Functional):
# Test Validation Relax NG
self._validate(body)
from Products.ERP5OOo.OOoUtils import OOoParser
from erp5.component.module.OOoUtils import OOoParser
parser = OOoParser()
parser.openFromString(body)
content_xml_view = parser.oo_files['content.xml']
......@@ -250,7 +250,7 @@ class TestOOoChart(ERP5TypeTestCase, ZopeTestCase.Functional):
# Test Validation Relax NG
self._validate(body)
from Products.ERP5OOo.OOoUtils import OOoParser
from erp5.component.module.OOoUtils import OOoParser
parser = OOoParser()
parser.openFromString(body)
content_xml_view = parser.oo_files['content.xml']
......
This diff is collapsed.
OOo Template
An Zope object class to generate OOo documents dynamically.
OOo Template derives from Zope Page Templates. Simply define the content.xml
part of an OOo document in the edit tab. Then create a new File and upload
an existing OOo document (ex. default_ooo_template). Then set the stylesheet
property to the id of the uploaded OOo document (ex. default_ooo_template in this case).
Rendering consists in replacing content.xml of the original OOo document
with the content.xml generated by the OOo Template.
Special tags for including content:
- <office:include>
Allow you to include another document in the current template (as an OLE attachment)
You must specify at least the path (can be either a single name or a path name using "/").
The type of document must be specified in the embedded document itself as
a MIME type. Size parameters are defined, as in any ODF file,
within the <draw:frame> tag which encloses the <office:include> tag.
TODO: make sure it is useful or useless to pass x, y params (as before)
Example:
<draw:frame draw:style-name='gr1' svg:height='18.686cm' svg:width='27.367cm' draw:layer='layout'
svg:x='0cm' svg:y='0cm'
tal:attributes="svg:height height | string:18.686cm;
svg:width width | string:27.367cm">
<office:include path="ERP5Site_viewOwnerBarChart" xlink:type='simple'
xlink:actuate='onLoad' xlink:show='embed'/>
</draw:frame>
- <office:include_img>
Not unlike <office:include>, allows you to include a picture document, refer to
the <office:include> part for details.
The optional "type" attribute specifies the picture format ; you can either
pass a full value ("image/jpeg") or the short version ("jpeg").
You can also pass position parameters with "x" and "y" attributes.
The maxwidth and maxheight parameters are useful to set constraints.
The aspect ratio information try to be kept (if you set only one size
or if a constraint is applied).
Example:
<office:include_img x="5cm" y="1cm" path="foo" />
Tips:
- it is possible to embed images by calling oo_builder.addImage(image)
where oo_builder is a handle to OOo Template internals and
addImage a method to embed an image in the resulting document.
Example:
<draw:image draw:style-name="fr1" draw:name="Image1" text:anchor-type="as-char"
draw:z-index="0" xlink:href="#Pictures/0001.png"
xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"
tal:attributes="xlink:href python:request.oo_builder.addImage(image.index_html(None,None,display='medium'))"/>
Known Issues:
- content type must be set to text/html
- remove any dtd declaration such as:
<!DOCTYPE office:document-content PUBLIC "-//OpenOffice.org//DTD OfficeDocument
1.0//EN" "office.dtd">
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment