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

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

DEBUG mode set to false

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):
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' in self.data:
374            # convert from DateTime object to string
375            self.data['default'] = str(self.data['default'])
376            self.renameEntry('default', 'fgDefault')
377        # date_only is boolean flag
378        self.data['fgShowHM'] = not bool(self.data['date_only'])
379        if 'start_datetime' in self.data:
380            self.data['start_datetime'] = self.data['start_datetime'].year()
381            self.renameEntry('start_datetime', 'fgStartingYear')
382        if 'end_datetime' in self.data:
383            self.data['end_datetime'] = self.data['end_datetime'].year()
384            self.renameEntry('end_datetime', 'fgEndingYear')
385
386class LinesFieldDemarshaller(BaseFieldDemarshaller):
387    """ Demarshaller of LinesField.
388    """
389
390    def modifyData(self):
391        self.renameEntry('default', 'fgDefault')
392        if self.entryIsDigit('height'):
393            self.renameEntry('height', 'fgRows')
394
395class BooleanFieldDemarshaller(BaseFieldDemarshaller):
396    """ Demarshaller of BooleanField.
397    """
398
399    def modifyData(self):
400        self.data['default'] = bool(self.data['default'])
401        self.renameEntry('default', 'fgDefault')
402
403
404class SelectionFieldDemarshaller(BaseFieldDemarshaller):
405    """ Demarshaller of SelectionField.
406    """
407
408    def modifyData(self):
409        self.renameEntry('default', 'fgDefault')
410        l = []
411        for i in self.data['items']:
412            i = list(i)
413            i.reverse()
414            l.append(i)
415        self.data['items'] = ['|'.join(i) for i in l]
416        self.renameEntry('items', 'fgVocabulary')
417
418class MultiSelectFieldDemarshaller(SelectionFieldDemarshaller):
419    """ Demarshaller of MultiSelectField.
420    """
421
422    def modifyData(self):
423        super(MultiSelectFieldDemarshaller, self).modifyData()
424        if self.entryIsDigit('size'):
425            self.renameEntry('size', 'fgRows')
426
427
428# next code was stolen from Formulator product
429from xml.dom.minidom import parse, parseString, Node
430
431# an extremely simple system for loading in XML into objects
432
433class Object:
434    pass
435
436class XMLObject:
437    def __init__(self):
438        self.elements = Object()
439        self.first = Object()
440        self.attributes = {}
441        self.text = ''
442
443    def getElementNames(self):
444        return [element for element in dir(self.elements)
445                if not element.startswith('__')]
446
447    def getAttributes(self):
448        return self.attributes
449
450def elementToObject(parent, node):
451    # create an object to represent element node
452    object = XMLObject()
453    # make object attributes off node attributes
454    for key, value in node.attributes.items():
455        object.attributes[key] = value
456    # make lists of child elements (or ignore them)
457    for child in node.childNodes:
458        nodeToObject(object, child)
459    # add ourselves to parent node
460    name = str(node.nodeName)
461    l = getattr(parent.elements, name, [])
462    l.append(object)
463    setattr(parent.elements, name, l)
464
465def attributeToObject(parent, node):
466    # should never be called
467    pass
468
469def textToObject(parent, node):
470    # add this text to parents text content
471    parent.text += node.data
472
473def processingInstructionToObject(parent, node):
474    # don't do anything with these
475    pass
476
477def commentToObject(parent, node):
478    # don't do anything with these
479    pass
480
481def documentToObject(parent, node):
482    elementToObject(parent, node.documentElement)
483
484def documentTypeToObject(parent, node):
485    # don't do anything with these
486    pass
487
488_map = {
489    Node.ELEMENT_NODE: elementToObject,
490    Node.ATTRIBUTE_NODE: attributeToObject,
491    Node.TEXT_NODE: textToObject,
492 #   Node.CDATA_SECTION_NODE:
493 #   Node.ENTITY_NODE:
494    Node.PROCESSING_INSTRUCTION_NODE: processingInstructionToObject,
495    Node.COMMENT_NODE: commentToObject,
496    Node.DOCUMENT_NODE: documentToObject,
497    Node.DOCUMENT_TYPE_NODE: documentTypeToObject,
498#    Node.NOTATION_NODE:
499    }
500
501def nodeToObject(parent, node):
502    _map[node.nodeType](parent, node)
503
504def simplify_single_entries(object):
505    for name in object.getElementNames():
506        l = getattr(object.elements, name)
507        # set the first subelement (in case it's just one, this is easy)
508        setattr(object.first, name, l[0])
509        # now do the same for rest
510        for element in l:
511            simplify_single_entries(element)
512
513def XMLToObjectsFromFile(path):
514    return XMLToObjects(parse(path))
515
516def XMLToObjectsFromString(s):
517    return XMLToObjects(parseString(s))
518
519def XMLToObjects(document):
520    object = XMLObject()
521    documentToObject(object, document)
522    document.unlink()
523    simplify_single_entries(object)
524    return object
525
Note: See TracBrowser for help on using the repository browser.