Commit 807e8ee9 authored by Arnaud Fontaine's avatar Arnaud Fontaine

ZODB Components: Use pylint to report source code errors/warnings each time it is modified.

Before, 'exec' was used but it is not safe as it executes the source code and
also not so useful as it stops at the first error. So, use pylint instead:

* Report errors and warnings (naming conventions could later be checked too).
* Allow to jump directly in Ace Editor to the message line/column by clicking on it.
* Define properly dynamic packages by setting __path__, otherwise pylint fails
  when a Component imports another one.

Also, split up consistency checking done in validated/modified state and
checking source code everytime the source code is modified:

  Source code error message was previously stored as a workflow variable,
  which didn't make any sense because user would only get error messages on
  Workflow transition, thus use existing Interaction Workflow to check source
  code and set newly defined error/warning properties everytime the source
  code is modified.
parent d8289e10
...@@ -86,6 +86,7 @@ ...@@ -86,6 +86,7 @@
position: absolute;\n position: absolute;\n
bottom: 0;\n bottom: 0;\n
right: 20px;\n right: 20px;\n
width: 40%;\n
z-index: 424242;\n z-index: 424242;\n
padding: 20px;\n padding: 20px;\n
background-color: #DAE6F6;\n background-color: #DAE6F6;\n
...@@ -273,7 +274,6 @@ ...@@ -273,7 +274,6 @@
{duration: 3000, queue: false});\n {duration: 3000, queue: false});\n
\n \n
var maximize_fullscreen_message = data;\n var maximize_fullscreen_message = data;\n
var error_arr = [];\n
\n \n
var validation_state_span = $(\'div.input > .ace_editor_validation_state\');\n var validation_state_span = $(\'div.input > .ace_editor_validation_state\');\n
if(validation_state_span.length) {\n if(validation_state_span.length) {\n
...@@ -290,30 +290,27 @@ ...@@ -290,30 +290,27 @@
success: getTranslatedValidationStateTitleHandler});\n success: getTranslatedValidationStateTitleHandler});\n
}\n }\n
\n \n
var error_element = $(\'div.input > .error\');\n updateErrorWarningMessageDivWithJump();\n
\n
// Animate fields to emphasize the change\n
if(error_element.length) {\n if(error_element.length) {\n
// Animate field to emphasize the change\n error_element.css(\'opacity\', 0.0);\n
function getErrorMessageListHandler(data) {\n error_element.animate({opacity: 1.0}, {duration: 3000, queue: false});\n
error_arr = $.parseJSON(data);\n }\n
error_element.css(\'opacity\', 0.0);\n
error_element.html(error_arr.join(\'<br />\'));\n
error_element.animate({opacity: 1.0},\n
{duration: 3000, queue: false});\n
}\n
\n \n
$.ajax({type: \'GET\',\n if(warning_element.length) {\n
async: false,\n warning_element.css(\'opacity\', 0.0);\n
url: \'getErrorMessageList\',\n warning_element.animate({opacity: 1.0}, {duration: 3000, queue: false});\n
data: \'as_json:int=1\',\n
success: getErrorMessageListHandler});\n
}\n }\n
\n \n
if($(\'.maximize\').length ||\n if($(\'.maximize\').length ||\n
(document.fullScreenElement && document.fullScreenElement !== null &&\n (document.fullScreenElement && document.fullScreenElement !== null &&\n
(document.mozFullScreen || document.webkitIsFullScreen))) {\n (document.mozFullScreen || document.webkitIsFullScreen))) {\n
var msg_elem_classes = \'ace_editor_maximize_fullscreen_message\';\n var msg_elem_classes = \'ace_editor_maximize_fullscreen_message\';\n
if(error_arr.length) {\n if(error_arr.length || warning_arr.length) {\n
maximize_fullscreen_message = error_arr.join(\'<br />\');\n maximize_fullscreen_message = (error_arr.join(\'<br />\') + \'<br />\' +\n
warning_arr.join(\'<br />\'));\n
\n
msg_elem_classes += \' ace_editor_maximize_fullscreen_error_message\';\n msg_elem_classes += \' ace_editor_maximize_fullscreen_error_message\';\n
}\n }\n
\n \n
...@@ -349,6 +346,45 @@ ...@@ -349,6 +346,45 @@
\n \n
return false;\n return false;\n
}\n }\n
\n
function fillMessageElementAndArray(list, elem, arr) {\n
$.each(list, function(i, dict) {\n
line = dict[\'line\'];\n
column = dict[\'column\'];\n
if(line != null && column != null)\n
arr.push(\'<a href=&quot;#&quot; onclick=&quot;c=ace_editor.getCursorPosition();c.row=\' + (line - 1) + \';c.column=\' + column + \';ace_editor.gotoLine(line);ace_editor.moveCursorToPosition(c);ace_editor.focus();event.stopPropagation();event.preventDefault();&quot;>\' + dict[\'message\'] + \'</a>\');\n
else\n
arr.push(dict[\'message\']);\n
});\n
\n
elem.html(arr.join(\'<br />\'));\n
}\n
\n
function getErrorWarningMessageDictHandler(data) {\n
error_warning_dict = $.parseJSON(data);\n
\n
if(error_element.length) {\n
error_arr.length = 0;\n
fillMessageElementAndArray(error_warning_dict[\'error_list\'],\n
error_element, error_arr);\n
}\n
\n
if(warning_element.length) {\n
warning_arr.length = 0;\n
fillMessageElementAndArray(error_warning_dict[\'warning_list\'],\n
warning_element, warning_arr);\n
}\n
}\n
\n
function updateErrorWarningMessageDivWithJump() {\n
if(!error_element.length && !warning_element.length)\n
return;\n
\n
$.ajax({type: \'GET\',\n
async: false,\n
url: \'Component_getErrorWarningMessageDictAsJson\',\n
success: getErrorWarningMessageDictHandler});\n
}\n
\n \n
window.onload = function() {\n window.onload = function() {\n
ace_editor_container_div = $(\'#${container_div_id}\');\n ace_editor_container_div = $(\'#${container_div_id}\');\n
...@@ -387,6 +423,13 @@ ...@@ -387,6 +423,13 @@
*/\n */\n
if($$(\'div.actions > button.save[name=Base_edit:method]\').length)\n if($$(\'div.actions > button.save[name=Base_edit:method]\').length)\n
$$(\'${save_button}\').appendTo($(\'#${div_id}\'));\n $$(\'${save_button}\').appendTo($(\'#${div_id}\'));\n
\n
error_element = $$(\'div.input > .error\');\n
error_arr = [];\n
warning_element = $$(\'div.input > .warning\');\n
warning_arr = [];\n
\n
updateErrorWarningMessageDivWithJump();\n
\n \n
if(typeof document.cancelFullScreen != \'undefined\' ||\n if(typeof document.cancelFullScreen != \'undefined\' ||\n
(typeof document.mozFullScreenEnabled != \'undefined\' && document.mozFullScreenEnabled) ||\n (typeof document.mozFullScreenEnabled != \'undefined\' && document.mozFullScreenEnabled) ||\n
......
2013-08-15 arnaud.fontaine
* ZODB Components: pylint is now used and reports warnings. Also, allow to jump to error line/column by clicking on the error/warning message.
2013-08-13 arnaud.fontaine 2013-08-13 arnaud.fontaine
* ZODB Components: Force resize of Ace Editor on maximize/fullscreen. Otherwise, without passing the new height/width to resize() and without force, text is not visible when switching to maximize/fullscreen mode. * ZODB Components: Force resize of Ace Editor on maximize/fullscreen. Otherwise, without passing the new height/width to resize() and without force, text is not visible when switching to maximize/fullscreen mode.
......
13 14
\ No newline at end of file \ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[
import re\n
message_re = re.compile(\'[CRWEF]:\\s*(?P<line>\\d+),\\s*(?P<column>\\d+):\\s*.*\')\n
error_warning_dict = {\'error_list\': [], \'warning_list\': []}\n
\n
def getParsedMessageList(message_list):\n
result_list = []\n
for message in message_list:\n
line = None\n
column = None\n
message_obj = message_re.match(message)\n
if message_obj:\n
line = int(message_obj.group(\'line\'))\n
column = int(message_obj.group(\'column\'))\n
\n
result_list.append({\'line\': line, \'column\': column, \'message\': message})\n
\n
return result_list\n
\n
import json\n
return json.dumps({\'error_list\': getParsedMessageList(context.getTextContentErrorMessageList()),\n
\'warning_list\': getParsedMessageList(context.getTextContentWarningMessageList())})\n
]]></string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Component_getErrorWarningMessageDictAsJson</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -98,7 +98,8 @@ ...@@ -98,7 +98,8 @@
<string>my_id</string> <string>my_id</string>
<string>my_reference</string> <string>my_reference</string>
<string>my_version</string> <string>my_version</string>
<string>my_error_message_list</string> <string>my_text_content_error_message_list</string>
<string>my_text_content_warning_message_list</string>
</list> </list>
</value> </value>
</item> </item>
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>my_error_message_list</string> </value> <value> <string>my_text_content_error_message_list</string> </value>
</item> </item>
<item> <item>
<key> <string>message_values</string> </key> <key> <string>message_values</string> </key>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>css_class</string>
<string>editable</string>
<string>title</string>
<string>view_separator</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_text_content_warning_message_list</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>css_class</string> </key>
<value> <string>warning</string> </value>
</item>
<item>
<key> <string>editable</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_lines_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string>Click to edit the target</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Warnings</string> </value>
</item>
<item>
<key> <string>view_separator</string> </key>
<value> <string encoding="cdata"><![CDATA[
<br />
]]></string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
<key> <string>before_commit_script_name</string> </key> <key> <string>before_commit_script_name</string> </key>
<value> <value>
<list> <list>
<string>Component_validateAfterModified</string> <string>Component_checkSourceCodeAndValidateAfterModified</string>
</list> </list>
</value> </value>
</item> </item>
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
<item> <item>
<key> <string>guard</string> </key> <key> <string>guard</string> </key>
<value> <value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent> <none/>
</value> </value>
</item> </item>
<item> <item>
...@@ -76,11 +76,17 @@ ...@@ -76,11 +76,17 @@
</list> </list>
</value> </value>
</item> </item>
<item>
<key> <string>portal_type_group_filter</string> </key>
<value>
<none/>
</value>
</item>
<item> <item>
<key> <string>script_name</string> </key> <key> <string>script_name</string> </key>
<value> <value>
<list> <list>
<string>Component_setModifiedState</string> <string>Component_setModifiedStateIfValidated</string>
</list> </list>
</value> </value>
</item> </item>
...@@ -99,32 +105,4 @@ ...@@ -99,32 +105,4 @@
</dictionary> </dictionary>
</pickle> </pickle>
</record> </record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Guard" module="Products.DCWorkflow.Guard"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>expr</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>python: here.getValidationState() in (\'validated\', \'modified\')</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData> </ZopeData>
...@@ -50,7 +50,14 @@ ...@@ -50,7 +50,14 @@
</item> </item>
<item> <item>
<key> <string>_body</string> </key> <key> <string>_body</string> </key>
<value> <string>state_change[\'object\'].checkConsistencyAndValidate()\n <value> <string>obj = state_change[\'object\']\n
\n
error_list, warning_list = obj.checkSourceCode()\n
obj.setTextContentWarningMessageList(warning_list)\n
obj.setTextContentErrorMessageList(error_list)\n
\n
if not error_list and obj.getValidationState() == \'modified\':\n
obj.checkConsistencyAndValidate()\n
</string> </value> </string> </value>
</item> </item>
<item> <item>
...@@ -59,7 +66,7 @@ ...@@ -59,7 +66,7 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>Component_validateAfterModified</string> </value> <value> <string>Component_checkSourceCodeAndValidateAfterModified</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
...@@ -50,7 +50,9 @@ ...@@ -50,7 +50,9 @@
</item> </item>
<item> <item>
<key> <string>_body</string> </key> <key> <string>_body</string> </key>
<value> <string>state_change[\'object\'].modify()\n <value> <string>obj = state_change[\'object\']\n
if obj.getValidationState() == \'validated\':\n
obj.modify()\n
</string> </value> </string> </value>
</item> </item>
<item> <item>
...@@ -59,7 +61,7 @@ ...@@ -59,7 +61,7 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>Component_setModifiedState</string> </value> <value> <string>Component_setModifiedStateIfValidated</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
2013-08-15 arnaud.fontaine
* ZODB Components: Add pylint support and use a property for error message rather than workflow variable (error messages are now reported everytime the source code is changed).
2013-07-19 arnaud.fontaine 2013-07-19 arnaud.fontaine
* Add save button to Ace Editor to save source code while staying on the same page. * Add save button to Ace Editor to save source code while staying on the same page.
......
41107 41108
\ No newline at end of file \ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Property Sheet" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_count</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>_mt_index</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>_tree</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string>ZODB Components properties common to Document Component, Extension Component and Test Component</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Component</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Property Sheet</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="OOBTree" module="BTrees.OOBTree"/>
</pickle>
<pickle>
<none/>
</pickle>
</record>
</ZopeData>
...@@ -2,45 +2,33 @@ ...@@ -2,45 +2,33 @@
<ZopeData> <ZopeData>
<record id="1" aka="AAAAAAAAAAE="> <record id="1" aka="AAAAAAAAAAE=">
<pickle> <pickle>
<global name="VariableDefinition" module="Products.DCWorkflow.Variables"/> <global name="Standard Property" module="erp5.portal_type"/>
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
<item> <item>
<key> <string>default_expr</string> </key> <key> <string>categories</string> </key>
<value> <value>
<none/> <tuple>
<string>elementary_type/lines</string>
</tuple>
</value> </value>
</item> </item>
<item>
<key> <string>default_value</string> </key>
<value> <string></string> </value>
</item>
<item> <item>
<key> <string>description</string> </key> <key> <string>description</string> </key>
<value> <string></string> </value> <value> <string>Source Code Error Messages</string> </value>
</item>
<item>
<key> <string>for_catalog</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>for_status</string> </key>
<value> <int>1</int> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>error_message</string> </value> <value> <string>text_content_error_message_property</string> </value>
</item> </item>
<item> <item>
<key> <string>info_guard</string> </key> <key> <string>portal_type</string> </key>
<value> <value> <string>Standard Property</string> </value>
<none/>
</value>
</item> </item>
<item> <item>
<key> <string>update_always</string> </key> <key> <string>property_default</string> </key>
<value> <int>1</int> </value> <value> <string>python: []</string> </value>
</item> </item>
</dictionary> </dictionary>
</pickle> </pickle>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Standard Property" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>elementary_type/lines</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string>Source Code Warning Messages</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>text_content_warning_message_property</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Standard Property</string> </value>
</item>
<item>
<key> <string>property_default</string> </key>
<value> <string>python: []</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
2013-08-15 arnaud.fontaine
* ZODB Components: Add error and warning property for Components.
2012-09-04 arnaud.fontaine 2012-09-04 arnaud.fontaine
* message_attribute_does_not_match was incorrectly renamed to message_attribute_match when migrating StringAttributeMatch constraint from filesystem to ZODB. * message_attribute_does_not_match was incorrectly renamed to message_attribute_match when migrating StringAttributeMatch constraint from filesystem to ZODB.
......
61 62
\ No newline at end of file \ No newline at end of file
...@@ -57,6 +57,7 @@ CategoryRelatedMembershipArityConstraint ...@@ -57,6 +57,7 @@ CategoryRelatedMembershipArityConstraint
Chain Chain
Codification Codification
Comment Comment
Component
Computer Computer
Configurable Configurable
ConfiguratorItem ConfiguratorItem
......
...@@ -31,6 +31,13 @@ from types import ModuleType ...@@ -31,6 +31,13 @@ from types import ModuleType
from . import aq_method_lock from . import aq_method_lock
import sys import sys
class PackageType(ModuleType):
"""
If a module has a __path__attribute, it will be treated as a package
(PEP 302), this is required for Introspection (for example pylint)
"""
__path__ = []
class DynamicModule(ModuleType): class DynamicModule(ModuleType):
"""This module may generate new objects at runtime.""" """This module may generate new objects at runtime."""
# it's useful to have such a generic utility # it's useful to have such a generic utility
...@@ -95,7 +102,7 @@ def initializeDynamicModules(): ...@@ -95,7 +102,7 @@ def initializeDynamicModules():
erp5.component.test: erp5.component.test:
holds Live Test modules previously found in bt5 in $INSTANCE_HOME/test holds Live Test modules previously found in bt5 in $INSTANCE_HOME/test
""" """
erp5 = ModuleType("erp5") erp5 = PackageType("erp5")
sys.modules["erp5"] = erp5 sys.modules["erp5"] = erp5
# Document classes without physical import path # Document classes without physical import path
...@@ -106,6 +113,7 @@ def initializeDynamicModules(): ...@@ -106,6 +113,7 @@ def initializeDynamicModules():
from accessor_holder import AccessorHolderType, AccessorHolderModuleType from accessor_holder import AccessorHolderType, AccessorHolderModuleType
erp5.accessor_holder = AccessorHolderModuleType("erp5.accessor_holder") erp5.accessor_holder = AccessorHolderModuleType("erp5.accessor_holder")
erp5.accessor_holder.__path__ = []
sys.modules["erp5.accessor_holder"] = erp5.accessor_holder sys.modules["erp5.accessor_holder"] = erp5.accessor_holder
erp5.accessor_holder.property_sheet = \ erp5.accessor_holder.property_sheet = \
...@@ -127,7 +135,7 @@ def initializeDynamicModules(): ...@@ -127,7 +135,7 @@ def initializeDynamicModules():
loadTempPortalTypeClass) loadTempPortalTypeClass)
# ZODB Components # ZODB Components
erp5.component = ModuleType("erp5.component") erp5.component = PackageType("erp5.component")
sys.modules["erp5.component"] = erp5.component sys.modules["erp5.component"] = erp5.component
from component_package import ComponentDynamicPackage from component_package import ComponentDynamicPackage
......
...@@ -43,6 +43,15 @@ from zLOG import LOG, INFO ...@@ -43,6 +43,15 @@ from zLOG import LOG, INFO
from ExtensionClass import ExtensionClass from ExtensionClass import ExtensionClass
from Products.ERP5Type.Utils import convertToUpperCase from Products.ERP5Type.Utils import convertToUpperCase
# Pylint imports
from pylint.lint import Run
from pylint.reporters.text import TextReporter
import cStringIO
import tempfile
import sys
import re
pylint_message_re = re.compile('^(?P<type>[CRWEF]):\s*\d+,\s*\d+:\s*.*$')
class RecordablePropertyMetaClass(ExtensionClass): class RecordablePropertyMetaClass(ExtensionClass):
""" """
Meta-class for extension classes with registered setters and getters wrapped Meta-class for extension classes with registered setters and getters wrapped
...@@ -132,8 +141,10 @@ class ComponentMixin(PropertyRecordableMixin, Base): ...@@ -132,8 +141,10 @@ class ComponentMixin(PropertyRecordableMixin, Base):
(ERP5Type.patches.{User,PropertiedUser}) and modifications in (ERP5Type.patches.{User,PropertiedUser}) and modifications in
ERP5Security.ERP5UserFactory. ERP5Security.ERP5UserFactory.
XXX-arnau: add tests to ERP5 itself to make sure all securities are defined Component source code is checked upon modification of text_content property
properly everywhere (see naming convention test) whatever its Workflow state (checkSourceCode). On validated and modified
state, checkConsistency() is called to check id, reference, version and
errors/warnings messages (set when the Component is modified).
""" """
__metaclass__ = RecordablePropertyMetaClass __metaclass__ = RecordablePropertyMetaClass
...@@ -152,7 +163,8 @@ class ComponentMixin(PropertyRecordableMixin, Base): ...@@ -152,7 +163,8 @@ class ComponentMixin(PropertyRecordableMixin, Base):
'DublinCore', 'DublinCore',
'Version', 'Version',
'Reference', 'Reference',
'TextDocument') 'TextDocument',
'Component')
_recorded_property_name_tuple = ( _recorded_property_name_tuple = (
'reference', 'reference',
...@@ -166,9 +178,7 @@ class ComponentMixin(PropertyRecordableMixin, Base): ...@@ -166,9 +178,7 @@ class ComponentMixin(PropertyRecordableMixin, Base):
_message_version_not_set = "Version must be set" _message_version_not_set = "Version must be set"
_message_invalid_version = "Version cannot start with '_'" _message_invalid_version = "Version cannot start with '_'"
_message_text_content_not_set = "No source code" _message_text_content_not_set = "No source code"
_message_invalid_text_content = "Source code: ${error_message}" _message_text_content_error = "Error in Source Code: ${error_message}"
_message_text_content_syntax_error = "Syntax error in source code: "\
"${error_message} (line: ${line_number}, column: ${column_number})"
security.declareProtected(Permissions.ModifyPortalContent, 'checkConsistency') security.declareProtected(Permissions.ModifyPortalContent, 'checkConsistency')
def checkConsistency(self, *args, **kw): def checkConsistency(self, *args, **kw):
...@@ -223,27 +233,11 @@ class ComponentMixin(PropertyRecordableMixin, Base): ...@@ -223,27 +233,11 @@ class ComponentMixin(PropertyRecordableMixin, Base):
message=self._message_text_content_not_set, message=self._message_text_content_not_set,
mapping={})) mapping={}))
else: else:
message = None for error_message in self.getTextContentErrorMessageList():
try: error_list.append(ConsistencyMessage(self,
# Check for any error in the source code by trying to load it object_relative_url=object_relative_url,
self.load({}, text_content=text_content) message=self._message_text_content_error,
except SyntaxError, e: mapping=dict(error_message=error_message)))
mapping = dict(error_message=str(e),
line_number=e.lineno,
column_number=e.offset)
message = self._message_text_content_syntax_error
except Exception, e:
mapping = dict(error_message=str(e))
message = self._message_invalid_text_content
if message:
error_list.append(
ConsistencyMessage(self,
object_relative_url=self.getRelativeUrl(),
message=message,
mapping=mapping))
return error_list return error_list
...@@ -257,67 +251,73 @@ class ComponentMixin(PropertyRecordableMixin, Base): ...@@ -257,67 +251,73 @@ class ComponentMixin(PropertyRecordableMixin, Base):
stays in modified state and previously validated values are used for stays in modified state and previously validated values are used for
reference, version and text_content reference, version and text_content
""" """
error_list = self.checkConsistency() if not self.checkConsistency():
if error_list: text_content = self.getTextContent()
workflow = self.workflow_history['component_validation_workflow'][-1] # Even if pylint should report all errors, make sure that there is no
# error when executing the source code pylint before validating
# When checking consistency with validate_action, messages are stored try:
# into error_message workflow attribute as Message instances exec text_content in {}
workflow['error_message'] = [error.getMessage() for error in error_list] except BaseException, e:
else: self.setErrorMessageList(self.getTextContentErrorMessageList() +
for property_name in self._recorded_property_name_tuple: [str(e)])
self.clearRecordedProperty(property_name) else:
for property_name in self._recorded_property_name_tuple:
self.validate() self.clearRecordedProperty(property_name)
security.declareProtected(Permissions.AccessContentsInformation, self.validate()
'getErrorMessageList')
def hasErrorMessageList(self): security.declareProtected(Permissions.ModifyPortalContent, 'checkSourceCode')
""" def checkSourceCode(self):
Check whether there are error messages, useful to display errors in the UI
without calling getErrorMessageList() as it translates error messages
"""
workflow = self.workflow_history['component_validation_workflow'][-1]
return bool(workflow['error_message'])
security.declareProtected(Permissions.AccessContentsInformation,
'getErrorMessageList')
def getErrorMessageList(self, as_json=False):
"""
Return the checkConsistency errors which may have occurred when
the Component has been modified after being validated once
"""
current_workflow = self.workflow_history['component_validation_workflow'][-1]
error_list = [error.translate()
for error in current_workflow.get('error_message', [])]
# Dirty hack until RenderJS is used to save the source code
# (erp5_ace_editor/ace_editor_support)
if as_json:
import json
return json.dumps(error_list)
return error_list
security.declareProtected(Permissions.ModifyPortalContent, 'load')
def load(self, namespace_dict, validated_only=False, text_content=None):
""" """
Load the source code into the given dict. Using exec() rather than Check source code with pylint
imp.load_source() as the latter would required creating an intermediary
file. Also, for traceback readability sake, the destination module
__dict__ is given rather than creating an empty dict and returning it.
Initially, namespace_dict default parameter value was an empty dict to TODO-arnau: Get rid of NamedTemporaryFile (require a patch on pylint to
allow checking the source code before validate, but this is completely allow passing a string)
wrong as the object reference is kept accross each call
TODO-arnau: Not used anymore in component_package, so this could be
removed as soon as pyflakes is used instead
""" """
if text_content is None: source_code = self.getTextContent()
text_content = self.getTextContent(validated_only=validated_only) # checkConsistency() ensures that it cannot happen once validated/modified
if not source_code:
exec text_content in namespace_dict return [], []
#import time
#started = time.time()
error_list = []
warning_list = []
output_file = cStringIO.StringIO()
# pylint prints directly on stderr/stdout (only reporter content matters)
stderr = sys.stderr
stdout = sys.stdout
try:
sys.stderr = cStringIO.StringIO()
sys.stdout = cStringIO.StringIO()
with tempfile.NamedTemporaryFile() as input_file:
input_file.write(source_code)
input_file.seek(0)
Run([input_file.name, '--reports=n', '--indent-string=" "', '--zope=y',
'--disable=C'], reporter=TextReporter(output_file), exit=False)
output_file.reset()
for line in output_file:
message_obj = pylint_message_re.match(line)
if message_obj:
line = line.strip()
if line[0] in ('E', 'F'):
error_list.append(line)
else:
warning_list.append(line)
finally:
output_file.close()
sys.stderr = stderr
sys.stdout = stdout
#LOG('component', INFO, 'Checking time (pylint): %.2f' % (time.time() -
# started))
return error_list, warning_list
security.declareProtected(Permissions.ModifyPortalContent, 'PUT') security.declareProtected(Permissions.ModifyPortalContent, 'PUT')
def PUT(self, REQUEST, RESPONSE): def PUT(self, REQUEST, RESPONSE):
......
...@@ -1437,7 +1437,7 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1437,7 +1437,7 @@ class _TestZodbComponent(SecurityTestCase):
ComponentTool.reset = assertResetCalled ComponentTool.reset = assertResetCalled
try: try:
component = self._newComponent(valid_reference, component = self._newComponent(valid_reference,
'def foobar(*args, **kwargs):\n return 42') 'def foobar():\n return 42')
component.validate() component.validate()
self.tic() self.tic()
...@@ -1448,7 +1448,9 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1448,7 +1448,9 @@ class _TestZodbComponent(SecurityTestCase):
ComponentTool._reset_performed = False ComponentTool._reset_performed = False
self.assertEquals(component.getValidationState(), 'validated') self.assertEquals(component.getValidationState(), 'validated')
self.assertEquals(component.getErrorMessageList(), []) self.assertEquals(component.checkConsistency(), [])
self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(), [])
self.assertEquals(component.getReference(), valid_reference) self.assertEquals(component.getReference(), valid_reference)
self.assertEquals(component.getReference(validated_only=True), valid_reference) self.assertEquals(component.getReference(validated_only=True), valid_reference)
self.assertModuleImportable(valid_reference) self.assertModuleImportable(valid_reference)
...@@ -1456,7 +1458,6 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1456,7 +1458,6 @@ class _TestZodbComponent(SecurityTestCase):
# Check that checkConsistency returns the proper error message for the # Check that checkConsistency returns the proper error message for the
# following reserved keywords # following reserved keywords
invalid_reference_dict = { invalid_reference_dict = {
None: ComponentMixin._message_reference_not_set,
# '_version' could clash with Version package name # '_version' could clash with Version package name
'ReferenceReservedKeywords_version': ComponentMixin._message_invalid_reference, 'ReferenceReservedKeywords_version': ComponentMixin._message_invalid_reference,
# Besides of clashing with protected attributes/methods, it does not # Besides of clashing with protected attributes/methods, it does not
...@@ -1479,10 +1480,11 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1479,10 +1480,11 @@ class _TestZodbComponent(SecurityTestCase):
# Should be in modified state as an error has been encountered # Should be in modified state as an error has been encountered
self.assertEquals(component.getValidationState(), 'modified') self.assertEquals(component.getValidationState(), 'modified')
error_list = component.getErrorMessageList() self.assertEquals([m.getMessage().translate()
self.assertNotEquals(error_list, []) for m in component.checkConsistency()],
self.assertEquals(len(error_list), 1) [error_message])
self.assertEquals(error_message, error_list[0]) self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(), [])
self.assertEquals(component.getReference(), invalid_reference) self.assertEquals(component.getReference(), invalid_reference)
self.assertEquals(component.getReference(validated_only=True), valid_reference) self.assertEquals(component.getReference(validated_only=True), valid_reference)
self._component_tool.reset(force=True, self._component_tool.reset(force=True,
...@@ -1502,7 +1504,9 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1502,7 +1504,9 @@ class _TestZodbComponent(SecurityTestCase):
ComponentTool._reset_performed = False ComponentTool._reset_performed = False
self.assertEquals(component.getValidationState(), 'validated') self.assertEquals(component.getValidationState(), 'validated')
self.assertEquals(component.getErrorMessageList(), []) self.assertEquals(component.checkConsistency(), [])
self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(), [])
self.assertEquals(component.getReference(), valid_reference) self.assertEquals(component.getReference(), valid_reference)
self.assertEquals(component.getReference(validated_only=True), valid_reference) self.assertEquals(component.getReference(validated_only=True), valid_reference)
self.assertModuleImportable(valid_reference) self.assertModuleImportable(valid_reference)
...@@ -1521,7 +1525,7 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1521,7 +1525,7 @@ class _TestZodbComponent(SecurityTestCase):
ComponentTool.reset = assertResetCalled ComponentTool.reset = assertResetCalled
try: try:
component = self._newComponent(reference, component = self._newComponent(reference,
'def foobar(*args, **kwargs):\n return 42', 'def foobar():\n return 42',
valid_version) valid_version)
component.validate() component.validate()
...@@ -1533,7 +1537,9 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1533,7 +1537,9 @@ class _TestZodbComponent(SecurityTestCase):
ComponentTool._reset_performed = False ComponentTool._reset_performed = False
self.assertEquals(component.getValidationState(), 'validated') self.assertEquals(component.getValidationState(), 'validated')
self.assertEquals(component.getErrorMessageList(), []) self.assertEquals(component.checkConsistency(), [])
self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(), [])
self.assertEquals(component.getVersion(), valid_version) self.assertEquals(component.getVersion(), valid_version)
self.assertEquals(component.getVersion(validated_only=True), valid_version) self.assertEquals(component.getVersion(validated_only=True), valid_version)
self.assertModuleImportable(reference) self.assertModuleImportable(reference)
...@@ -1557,10 +1563,11 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1557,10 +1563,11 @@ class _TestZodbComponent(SecurityTestCase):
# Should be in modified state as an error has been encountered # Should be in modified state as an error has been encountered
self.assertEquals(component.getValidationState(), 'modified') self.assertEquals(component.getValidationState(), 'modified')
error_list = component.getErrorMessageList() self.assertEquals([m.getMessage().translate()
self.assertNotEquals(error_list, []) for m in component.checkConsistency()],
self.assertEquals(len(error_list), 1) [error_message])
self.assertEquals(error_message, error_list[0]) self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(), [])
self.assertEquals(component.getVersion(), invalid_version) self.assertEquals(component.getVersion(), invalid_version)
self.assertEquals(component.getVersion(validated_only=True), valid_version) self.assertEquals(component.getVersion(validated_only=True), valid_version)
self._component_tool.reset(force=True, self._component_tool.reset(force=True,
...@@ -1580,7 +1587,9 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1580,7 +1587,9 @@ class _TestZodbComponent(SecurityTestCase):
ComponentTool._reset_performed = False ComponentTool._reset_performed = False
self.assertEquals(component.getValidationState(), 'validated') self.assertEquals(component.getValidationState(), 'validated')
self.assertEquals(component.getErrorMessageList(), []) self.assertEquals(component.checkConsistency(), [])
self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(), [])
self.assertEquals(component.getVersion(), valid_version) self.assertEquals(component.getVersion(), valid_version)
self.assertEquals(component.getVersion(validated_only=True), valid_version) self.assertEquals(component.getVersion(validated_only=True), valid_version)
self.assertModuleImportable(reference) self.assertModuleImportable(reference)
...@@ -1594,10 +1603,35 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1594,10 +1603,35 @@ class _TestZodbComponent(SecurityTestCase):
validated but not when an error was encountered (implemented in validated but not when an error was encountered (implemented in
dynamic_class_generation_interaction_workflow) dynamic_class_generation_interaction_workflow)
""" """
valid_code = 'def foobar(*args, **kwargs):\n return 42' # Error/Warning properties must be set everytime the source code is
# modified, even in Draft state
component = self._newComponent('TestComponentWithSyntaxError', 'print "ok"')
self.tic()
self.assertEquals(component.checkConsistency(), [])
self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(), [])
component.setTextContent('import sys')
self.tic()
self.assertEquals(component.checkConsistency(), [])
self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(),
["W: 1, 0: Unused import sys (unused-import)"])
component.setTextContent('import unexistent_module')
self.tic()
self.assertEquals(
[m.getMessage().translate() for m in component.checkConsistency()],
["Error in Source Code: F: 1, 0: Unable to import 'unexistent_module' (import-error)"])
self.assertEquals(component.getTextContentErrorMessageList(),
["F: 1, 0: Unable to import 'unexistent_module' (import-error)"])
self.assertEquals(component.getTextContentWarningMessageList(),
["W: 1, 0: Unused import unexistent_module (unused-import)"])
valid_code = 'def foobar():\n return 42'
ComponentTool.reset = assertResetCalled ComponentTool.reset = assertResetCalled
try: try:
component = self._newComponent('TestComponentWithSyntaxError', valid_code) component.setTextContent(valid_code)
component.validate() component.validate()
self.tic() self.tic()
...@@ -1607,7 +1641,9 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1607,7 +1641,9 @@ class _TestZodbComponent(SecurityTestCase):
ComponentTool._reset_performed = False ComponentTool._reset_performed = False
self.assertEquals(component.getValidationState(), 'validated') self.assertEquals(component.getValidationState(), 'validated')
self.assertEquals(component.getErrorMessageList(), []) self.assertEquals(component.checkConsistency(), [])
self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(), [])
self.assertEquals(component.getTextContent(), valid_code) self.assertEquals(component.getTextContent(), valid_code)
self.assertEquals(component.getTextContent(validated_only=True), valid_code) self.assertEquals(component.getTextContent(validated_only=True), valid_code)
self.assertModuleImportable('TestComponentWithSyntaxError') self.assertModuleImportable('TestComponentWithSyntaxError')
...@@ -1615,13 +1651,27 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1615,13 +1651,27 @@ class _TestZodbComponent(SecurityTestCase):
# Check that checkConsistency returns the proper error message for the # Check that checkConsistency returns the proper error message for the
# following Python errors # following Python errors
invalid_code_dict = ( invalid_code_dict = (
(None, ComponentMixin._message_text_content_not_set), (None,
('def foobar(*args, **kwargs)\n return 42', 'Syntax error in source code:'), # There could be no source code until validated, so checkConsistency()
# is used instead
[ComponentMixin._message_text_content_not_set],
[],
[]),
('def foobar(*args, **kwargs)\n return 42',
["Error in Source Code: E: 1, 0: invalid syntax (syntax-error)"],
["E: 1, 0: invalid syntax (syntax-error)"],
[]),
# Make sure that foobar NameError is at the end to make sure that after # Make sure that foobar NameError is at the end to make sure that after
# defining foobar function, it is not available at all # defining foobar function, it is not available at all
('foobar', 'Source code:')) ('foobar',
["Error in Source Code: E: 1, 0: Undefined variable 'foobar' (undefined-variable)"],
for invalid_code, error_message in invalid_code_dict: ["E: 1, 0: Undefined variable 'foobar' (undefined-variable)"],
["W: 1, 0: Statement seems to have no effect (pointless-statement)"]))
for (invalid_code,
check_consistency_list,
error_list,
warning_list) in invalid_code_dict:
# Reset should not be performed # Reset should not be performed
ComponentTool.reset = assertResetNotCalled ComponentTool.reset = assertResetNotCalled
try: try:
...@@ -1632,10 +1682,12 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1632,10 +1682,12 @@ class _TestZodbComponent(SecurityTestCase):
# Should be in modified state as an error has been encountered # Should be in modified state as an error has been encountered
self.assertEquals(component.getValidationState(), 'modified') self.assertEquals(component.getValidationState(), 'modified')
error_list = component.getErrorMessageList() self.assertEquals([m.getMessage().translate()
self.assertNotEqual(error_list, []) for m in component.checkConsistency()],
self.assertEquals(len(error_list), 1) check_consistency_list)
self.assertTrue(error_list[0].startswith(error_message)) self.assertEquals(component.getTextContentErrorMessageList(), error_list)
self.assertEquals(component.getTextContentWarningMessageList(), warning_list)
self.assertEquals(component.getTextContent(), invalid_code) self.assertEquals(component.getTextContent(), invalid_code)
self.assertEquals(component.getTextContent(validated_only=True), valid_code) self.assertEquals(component.getTextContent(validated_only=True), valid_code)
self._component_tool.reset(force=True, self._component_tool.reset(force=True,
...@@ -1655,7 +1707,9 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1655,7 +1707,9 @@ class _TestZodbComponent(SecurityTestCase):
ComponentTool._reset_performed = False ComponentTool._reset_performed = False
self.assertEquals(component.getValidationState(), 'validated') self.assertEquals(component.getValidationState(), 'validated')
self.assertEquals(component.getErrorMessageList(), []) self.assertEquals(component.checkConsistency(), [])
self.assertEquals(component.getTextContentErrorMessageList(), [])
self.assertEquals(component.getTextContentWarningMessageList(), [])
self.assertEquals(component.getTextContent(), valid_code) self.assertEquals(component.getTextContent(), valid_code)
self.assertEquals(component.getTextContent(validated_only=True), valid_code) self.assertEquals(component.getTextContent(validated_only=True), valid_code)
self.assertModuleImportable('TestComponentWithSyntaxError') self.assertModuleImportable('TestComponentWithSyntaxError')
......
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