source: products/quintagroup.transmogrifier.pfm2pfg/trunk/quintagroup/transmogrifier/pfm2pfg/importing.py @ 329

Last change on this file since 329 was 329, checked in by piv, 18 years ago

realise 0.2

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