1 | import logging |
---|
2 | import os.path |
---|
3 | from xml.dom import minidom |
---|
4 | from DateTime import DateTime |
---|
5 | |
---|
6 | from zope.interface import implements, Interface |
---|
7 | from zope.component import getUtility |
---|
8 | |
---|
9 | from ZODB.POSException import ConflictError |
---|
10 | from Acquisition import aq_inner, aq_parent |
---|
11 | |
---|
12 | from Products.Marshall.registry import getComponent |
---|
13 | from Products.Marshall.config import AT_NS |
---|
14 | from Products.CMFPlone.utils import _createObjectByType |
---|
15 | |
---|
16 | from quintagroup.transmogrifier.interfaces import IImportDataCorrector |
---|
17 | from quintagroup.transmogrifier.adapters.importing import ReferenceImporter |
---|
18 | from quintagroup.transmogrifier.xslt import XSLTSection |
---|
19 | |
---|
20 | BOOL_FIELD_PROPS = ['enabled', 'required', 'hidden'] |
---|
21 | |
---|
22 | FIELD_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 | |
---|
49 | class IXMLDemarshaller(Interface): |
---|
50 | """ |
---|
51 | """ |
---|
52 | def demarshall(obj, data): |
---|
53 | """ |
---|
54 | """ |
---|
55 | |
---|
56 | class 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 | |
---|
243 | class 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 | |
---|
321 | class 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 | |
---|
332 | class 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 | |
---|
343 | class LabelFieldDemarshaller(BaseFieldDemarshaller): |
---|
344 | """ Demarshaller of LabelField. |
---|
345 | """ |
---|
346 | |
---|
347 | def modifyData(self): |
---|
348 | self.renameEntry('default', 'fgDefault') |
---|
349 | |
---|
350 | class 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 | |
---|
365 | class 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 | |
---|
383 | class 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 | |
---|
392 | class 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 | |
---|
401 | class 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 | |
---|
415 | class 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 |
---|
426 | from xml.dom.minidom import parse, parseString, Node |
---|
427 | |
---|
428 | # an extremely simple system for loading in XML into objects |
---|
429 | |
---|
430 | class Object: |
---|
431 | pass |
---|
432 | |
---|
433 | class 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 | |
---|
447 | def 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 | |
---|
462 | def attributeToObject(parent, node): |
---|
463 | # should never be called |
---|
464 | pass |
---|
465 | |
---|
466 | def textToObject(parent, node): |
---|
467 | # add this text to parents text content |
---|
468 | parent.text += node.data |
---|
469 | |
---|
470 | def processingInstructionToObject(parent, node): |
---|
471 | # don't do anything with these |
---|
472 | pass |
---|
473 | |
---|
474 | def commentToObject(parent, node): |
---|
475 | # don't do anything with these |
---|
476 | pass |
---|
477 | |
---|
478 | def documentToObject(parent, node): |
---|
479 | elementToObject(parent, node.documentElement) |
---|
480 | |
---|
481 | def 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 | |
---|
498 | def nodeToObject(parent, node): |
---|
499 | _map[node.nodeType](parent, node) |
---|
500 | |
---|
501 | def 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 | |
---|
510 | def XMLToObjectsFromFile(path): |
---|
511 | return XMLToObjects(parse(path)) |
---|
512 | |
---|
513 | def XMLToObjectsFromString(s): |
---|
514 | return XMLToObjects(parseString(s)) |
---|
515 | |
---|
516 | def XMLToObjects(document): |
---|
517 | object = XMLObject() |
---|
518 | documentToObject(object, document) |
---|
519 | document.unlink() |
---|
520 | simplify_single_entries(object) |
---|
521 | return object |
---|
522 | |
---|