source: products/quintagroup.transmogrify.pfm2pfg/trunk/quintagroup/transmogrify/pfm2pfg/importing.py @ 1498

Last change on this file since 1498 was 1496, checked in by koval, 10 years ago

updated package to work with a lastest version of quintagroup.transmogrifier

File size: 18.0 KB
Line 
1import logging
2import os.path
3from xml.dom import minidom
4from DateTime import DateTime
5
6from zope.interface import implements, Interface
7from zope.component import getUtility
8
9from ZODB.POSException import ConflictError
10from Acquisition import aq_inner, aq_parent
11
12from Products.Marshall.registry import getComponent
13from Products.Marshall.config import AT_NS
14from Products.CMFPlone.utils import _createObjectByType
15
16from quintagroup.transmogrifier.interfaces import IImportDataCorrector
17from quintagroup.transmogrifier.adapters.importing import ReferenceImporter
18from quintagroup.transmogrifier.xslt import XSLTSection
19
20BOOL_FIELD_PROPS = ['enabled', 'required', 'hidden']
21
22FIELD_MAP = {
23            'StringField'   : 'FormStringField',
24            'EmailField'    : ('FormStringField', {'fgStringValidator': 'isEmail'}),
25            'LinkField'     : ('FormStringField', {'fgStringValidator': 'isURL'}),
26            'PatternField'  : 'FormStringField',
27
28            'TextAreaField' : 'FormTextField',
29            'RawTextAreaField': 'FormTextField',
30
31            'PasswordField' : 'FormPasswordField',
32            'LabelField'    : 'FormRichLabelField', #FormLabelField
33
34            'IntegerField'  : 'FormIntegerField',
35            'FloatField'    : 'FormFixedPointField',
36
37            'DateTimeField' : 'FormDateField',
38            'FileField'     : 'FormFileField',
39
40            'LinesField'    : 'FormLinesField',
41
42            'CheckBoxField' : 'FormBooleanField',
43            'ListField'     : ('FormSelectionField', {'fgFormat': 'select'}),
44            'RadioField'    : ('FormSelectionField', {'fgFormat': 'radio'}),
45            'MultiListField': ('FormMultiSelectionField', {'fgFormat': 'select'}),
46            'MultiCheckBoxField':('FormMultiSelectionField', {'fgFormat': 'checkbox'}),
47            }
48
49class IXMLDemarshaller(Interface):
50    """
51    """
52    def demarshall(obj, data):
53        """
54        """
55
56class FormFolderImporter(ReferenceImporter):
57    """ Demarshaller of PloneFormGen's FormFolder content type.
58    """
59    implements(IImportDataCorrector)
60
61    def __init__(self, context, transmogrifier):
62        self.context = context
63        self.demarshaller = getComponent("atxml")
64        self.auto_added_fgfields = ['replyto', 'topic', 'comments']
65        self.date_fields = {}
66
67    def __call__(self, data):
68        data = super(FormFolderImporter, self).__call__(data)
69        xml = data['data']
70        data['data'] = self.transformWithMinidom(xml)
71
72        # update FormMailerAdapter and FormThanksPage objects
73        cleaned_xml = self.updateMailer(xml)
74
75        self.updateResponsePage(cleaned_xml)
76
77        self.updateFormFields(xml)
78
79        return data
80
81    def transformWithMinidom(self, xml):
82        """ Do some extra transformations on xml document (that can be done by XSLT).
83        """
84        doc = minidom.parseString(xml)
85        root = doc.documentElement
86
87        # get 'modification_date' and 'creation_date' for next usage when creating fields
88        for i in root.getElementsByTagName('xmp:CreateDate'):
89            self.date_fields['creation_date'] = i.firstChild.nodeValue.strip()
90        for i in root.getElementsByTagName('xmp:ModifyDate'):
91            self.date_fields['modification_date'] = i.firstChild.nodeValue.strip()
92
93        # xxx: update button labels (those elements are now skiped by xslt)
94        # PFM has only one field 'form_buttons', but PFG has two: 'submitLabel', 'resetLabel' and
95        # field 'useCancelButton'
96
97        # add element for setting of 'thanksPage' field to 'thank-you' FormThanksPage
98        elem = doc.createElementNS(AT_NS, "field")
99        name_attr = doc.createAttribute("name")
100        name_attr.value = 'thanksPage'
101        elem.setAttributeNode(name_attr)
102        value = doc.createTextNode('thank-you')
103        elem.appendChild(value)
104        root.appendChild(elem)
105
106        # update 'thanksPageOverride' field
107        elem = [i for i in doc.getElementsByTagName('field') if i.getAttribute('name') == 'thanksPageOverride']
108        if elem:
109            elem = elem[0]
110            old_text = elem.firstChild
111            new_text = doc.createTextNode('redirect_to:' + old_text.nodeValue.strip())
112            elem.removeChild(old_text)
113            elem.appendChild(new_text)
114
115        # xxx: update 'afterValidationOverride' field
116        # 'afterValidationOverride' is TALES expression, but PFM's 'cpyaction' is name
117        # of controller python script
118        elem = [i for i in doc.getElementsByTagName('field') 
119                if i.getAttribute('name') == 'afterValidationOverride']
120        if elem:
121            elem = elem[0]
122            old_text = elem.firstChild
123            new_text = doc.createTextNode('string:' + old_text.nodeValue.strip())
124            elem.removeChild(old_text)
125            elem.appendChild(new_text)
126
127        # xxx: update 'onDisplayOverride' field (this element is now skiped by xslt)
128        # 'onDisplayOverride' is TALES expression, but PFM's 'before_script' is python script
129        # in 'scripts' subfolder
130
131        return doc.toxml('utf-8')
132
133    def updateMailer(self, xml):
134        mailer = self.context['mailer']
135        transformed = self.transformWithXSLT(xml, 'FormMailerAdapter')
136        self.demarshaller.demarshall(mailer, transformed)
137        mailer.indexObject()
138        return transformed
139
140    def updateResponsePage(self, xml):
141        page = self.context['thank-you']
142        transformed = self.transformWithXSLT(xml, 'FormThanksPage')
143        self.demarshaller.demarshall(page, transformed)
144        # override default value of description field
145        page.getField('description').getMutator(page)('')
146        page.indexObject()
147        return transformed
148
149    def transformWithXSLT(self, xml, to):
150        """ Apply XSLT transformations using XSLT transmogrifier section.
151        """
152        item = dict(
153            _from='PloneFormMailer',
154            _to=to,
155            _files=dict(
156                marshall=dict(
157                    name='.marshall.xml',
158                    data=xml
159                )
160            )
161        )
162        section = XSLTSection(object(), 'xslt', {'blueprint': ''}, iter((item,)))
163        for i in section: pass
164        return item['_files']['marshall']['data']
165
166    def updateFormFields(self, data):
167        """ Walk trough xml tree and create fields in FormFolder.
168        """
169        # delete fields that were added on FormFolder creation
170        for oid in [i for i in self.auto_added_fgfields if i in self.context]:
171            self.context._delObject(oid)
172
173        doc = minidom.parseString(data)
174        root = doc.documentElement
175        form_element = root.getElementsByTagName('form')
176        if not form_element:
177            return
178        groups = form_element[0].getElementsByTagName('group')
179        for group in groups:
180            group_title = group.getElementsByTagName('title')[0]
181            group_title = str(group_title.firstChild.nodeValue).strip()
182            if group_title == 'Default':
183                for field in group.getElementsByTagName('field'):
184                    field_id = str(field.getElementsByTagName('id')[0].firstChild.nodeValue).strip()
185                    field_type = str(field.getElementsByTagName('type')[0].firstChild.nodeValue).strip()
186                    self.createField(field_type, field_id, field, self.context)
187            else:
188                fieldset = self.createFieldset(group_title)
189                if not fieldset:
190                    return
191                for field in group.getElementsByTagName('field'):
192                    field_id = str(field.getElementsByTagName('id')[0].firstChild.nodeValue).strip()
193                    field_type = str(field.getElementsByTagName('type')[0].firstChild.nodeValue).strip()
194                    self.createField(field_type, field_id, field, fieldset)
195
196    def createField(self, type_name, field_id, field_node, context=None):
197        """ Created formfield and update it from field_node's xml tree.
198
199            Use demarshalling adapter.
200        """
201        if FIELD_MAP.get(type_name) is None:
202            return
203
204        if isinstance(FIELD_MAP[type_name], tuple):
205            type_name, options = FIELD_MAP[type_name]
206        else:
207            type_name, options = FIELD_MAP[type_name], {}
208        # update options with 'creation_date' and 'modification_date', that are
209        # the same as in FormFolder (dates on fields must be the same)
210        options.update(self.date_fields)
211
212        if field_id not in context.contentIds():
213            try:
214                field = _createObjectByType(type_name, context, field_id)
215            except ConflictError:
216                raise
217            except:
218                return
219        else:
220            field = context._getOb(field_id)
221
222        try:
223            IXMLDemarshaller(field).demarshall(field_node, **options)
224        except ConflictError:
225            raise
226
227    def createFieldset(self, title):
228        """ Create FieldsetFolder with id=title
229        """
230        if title not in self.context.contentIds():
231            try:
232                fieldset = _createObjectByType('FieldsetFolder', self.context, title)
233            except ConflictError:
234                raise
235            except:
236                return
237        else:
238            fieldset = self.context._getOb(title)
239        fieldset.Schema()['title'].getMutator(fieldset)(title)
240
241        return fieldset
242
243class BaseFieldDemarshaller(object):
244    """ Base class for demarshallers of PloneFormGen's fields from PloneFormMailer fields.
245    """
246    implements(IXMLDemarshaller)
247
248    def __init__(self, context):
249        self.context = context
250
251    def demarshall(self, node, **kw):
252        self.extractData(node)
253        # update data dictionary form keyword arguments
254        for k, v in kw.items():
255            self.data[k] = v
256        # call special hook for changing data
257        self.modifyData()
258
259        # update instance
260        schema = self.context.Schema()
261        for fname, value in self.data.items():
262            if not schema.has_key(fname):
263                continue
264            mutator = schema[fname].getMutator(self.context)
265            if not mutator:
266                continue
267            mutator(value)
268
269        self.context.indexObject()
270
271    def extractData(self, node):
272        tree = XMLObject()
273        elementToObject(tree, node)
274        simplify_single_entries(tree)
275
276        data = {}
277        values = tree.first.field.first.values
278        for name in values.getElementNames():
279            value = getattr(values.first, name)
280            if value.attributes.get('type') == 'float':
281                data[name] = float(value.text)
282            elif value.attributes.get('type') == 'int':
283                data[name] = int(value.text)
284                # boolean property is exported as int, fix that
285                if name in BOOL_FIELD_PROPS:
286                    data[name] = bool(data[name])
287            elif value.attributes.get('type') == 'list':
288                # XXX bare eval here (this may be a security leak ?)
289                data[name] = eval(str(value.text))
290            elif value.attributes.get('type') == 'datetime':
291                data[name] = DateTime(value.text)
292            else:
293                data[name] = str(value.text)
294
295        # get tales expressions
296        tales = tree.first.field.first.tales
297        for name in tales.getElementNames():
298            value = getattr(tales.first, name)
299            data["tales:"+name] = str(value.text)
300
301        self.data = data
302
303    def modifyData(self):
304        pass
305
306    def renameEntry(self, old, new):
307        if self.data.has_key(old):
308            self.data[new] = self.data.pop(old)
309
310    def entryIsDigit(self, key):
311        """ Formulator exports empty fields in xml as empty elements without
312            'type' attribute. That's is why we get in data dictionary for
313            integer fields values that are empty strings. This method is
314            used to check whether integer field has integer value.
315        """
316        if key in self.data and isinstance(self.data[key], int):
317            return True
318        else:
319            return False
320
321class StringFieldDemarshaller(BaseFieldDemarshaller):
322    """ Demarshaller of StringField and other fields of this kind.
323    """
324
325    def modifyData(self):
326        self.renameEntry('default', 'fgDefault')
327        if self.entryIsDigit('display_maxwidth'):
328            self.renameEntry('display_maxwidth', 'fgmaxlength')
329        if self.entryIsDigit('display_width'):
330            self.renameEntry('display_width', 'fgsize')
331
332class TextFieldDemarshaller(BaseFieldDemarshaller):
333    """ Demarshaller of TextField.
334    """
335
336    def modifyData(self):
337        self.renameEntry('default', 'fgDefault')
338        if self.entryIsDigit('max_length'):
339            self.renameEntry('max_length', 'fgmaxlength')
340        if self.entryIsDigit('height'):
341            self.renameEntry('height', 'fgRows')
342
343class LabelFieldDemarshaller(BaseFieldDemarshaller):
344    """ Demarshaller of  LabelField.
345    """
346
347    def modifyData(self):
348        self.renameEntry('default', 'fgDefault')
349
350class IntegerFieldDemarshaller(BaseFieldDemarshaller):
351    """ Demarshaller of IntegerField.
352    """
353
354    def modifyData(self):
355        self.renameEntry('default', 'fgDefault')
356        if self.entryIsDigit('display_maxwidth'):
357            self.renameEntry('display_maxwidth', 'fgmaxlength')
358        if self.entryIsDigit('display_width'):
359            self.renameEntry('display_width', 'fgsize')
360        if self.entryIsDigit('start'):
361            self.renameEntry('start', 'minval')
362        if self.entryIsDigit('end'):
363            self.renameEntry('end', 'maxval')
364
365class DateTimeFieldDemarshaller(BaseFieldDemarshaller):
366    """ Demarshaller of DateTimeField.
367    """
368
369    def modifyData(self):
370        if 'default' in self.data:
371            # convert from DateTime object to string
372            self.data['default'] = str(self.data['default'])
373            self.renameEntry('default', 'fgDefault')
374        # date_only is boolean flag
375        self.data['fgShowHM'] = not bool(self.data['date_only'])
376        if 'start_datetime' in self.data:
377            self.data['start_datetime'] = self.data['start_datetime'].year()
378            self.renameEntry('start_datetime', 'fgStartingYear')
379        if 'end_datetime' in self.data:
380            self.data['end_datetime'] = self.data['end_datetime'].year()
381            self.renameEntry('end_datetime', 'fgEndingYear')
382
383class LinesFieldDemarshaller(BaseFieldDemarshaller):
384    """ Demarshaller of LinesField.
385    """
386
387    def modifyData(self):
388        self.renameEntry('default', 'fgDefault')
389        if self.entryIsDigit('height'):
390            self.renameEntry('height', 'fgRows')
391
392class BooleanFieldDemarshaller(BaseFieldDemarshaller):
393    """ Demarshaller of BooleanField.
394    """
395
396    def modifyData(self):
397        self.data['default'] = bool(self.data['default'])
398        self.renameEntry('default', 'fgDefault')
399
400
401class SelectionFieldDemarshaller(BaseFieldDemarshaller):
402    """ Demarshaller of SelectionField.
403    """
404
405    def modifyData(self):
406        self.renameEntry('default', 'fgDefault')
407        l = []
408        for i in self.data['items']:
409            i = list(i)
410            i.reverse()
411            l.append(i)
412        self.data['items'] = ['|'.join(i) for i in l]
413        self.renameEntry('items', 'fgVocabulary')
414
415class MultiSelectFieldDemarshaller(SelectionFieldDemarshaller):
416    """ Demarshaller of MultiSelectField.
417    """
418
419    def modifyData(self):
420        super(MultiSelectFieldDemarshaller, self).modifyData()
421        if self.entryIsDigit('size'):
422            self.renameEntry('size', 'fgRows')
423
424
425# next code was stolen from Formulator product
426from xml.dom.minidom import parse, parseString, Node
427
428# an extremely simple system for loading in XML into objects
429
430class Object:
431    pass
432
433class XMLObject:
434    def __init__(self):
435        self.elements = Object()
436        self.first = Object()
437        self.attributes = {}
438        self.text = ''
439
440    def getElementNames(self):
441        return [element for element in dir(self.elements)
442                if not element.startswith('__')]
443
444    def getAttributes(self):
445        return self.attributes
446
447def elementToObject(parent, node):
448    # create an object to represent element node
449    object = XMLObject()
450    # make object attributes off node attributes
451    for key, value in node.attributes.items():
452        object.attributes[key] = value
453    # make lists of child elements (or ignore them)
454    for child in node.childNodes:
455        nodeToObject(object, child)
456    # add ourselves to parent node
457    name = str(node.nodeName)
458    l = getattr(parent.elements, name, [])
459    l.append(object)
460    setattr(parent.elements, name, l)
461
462def attributeToObject(parent, node):
463    # should never be called
464    pass
465
466def textToObject(parent, node):
467    # add this text to parents text content
468    parent.text += node.data
469
470def processingInstructionToObject(parent, node):
471    # don't do anything with these
472    pass
473
474def commentToObject(parent, node):
475    # don't do anything with these
476    pass
477
478def documentToObject(parent, node):
479    elementToObject(parent, node.documentElement)
480
481def documentTypeToObject(parent, node):
482    # don't do anything with these
483    pass
484
485_map = {
486    Node.ELEMENT_NODE: elementToObject,
487    Node.ATTRIBUTE_NODE: attributeToObject,
488    Node.TEXT_NODE: textToObject,
489 #   Node.CDATA_SECTION_NODE:
490 #   Node.ENTITY_NODE:
491    Node.PROCESSING_INSTRUCTION_NODE: processingInstructionToObject,
492    Node.COMMENT_NODE: commentToObject,
493    Node.DOCUMENT_NODE: documentToObject,
494    Node.DOCUMENT_TYPE_NODE: documentTypeToObject,
495#    Node.NOTATION_NODE:
496    }
497
498def nodeToObject(parent, node):
499    _map[node.nodeType](parent, node)
500
501def simplify_single_entries(object):
502    for name in object.getElementNames():
503        l = getattr(object.elements, name)
504        # set the first subelement (in case it's just one, this is easy)
505        setattr(object.first, name, l[0])
506        # now do the same for rest
507        for element in l:
508            simplify_single_entries(element)
509
510def XMLToObjectsFromFile(path):
511    return XMLToObjects(parse(path))
512
513def XMLToObjectsFromString(s):
514    return XMLToObjects(parseString(s))
515
516def XMLToObjects(document):
517    object = XMLObject()
518    documentToObject(object, document)
519    document.unlink()
520    simplify_single_entries(object)
521    return object
522
Note: See TracBrowser for help on using the repository browser.