Most Powerful Open Source ERP

ERP5 Guideline Coding Best Practice

Table of Contents

Introduction

These guidelines have been written to ensure all code written is clean, maintainable and accessible code ensureing that code is resuable and can be understood in the future by anyone having to work on it.

While this often means additional effort in the short term, the long term benefits of a large maintainable codebase by far outweigh the time put in when writing new code.

The following rules crimes and guidelines apply to all programming languages used (the guidelines combine JavaScript rules and Python best practice and coding crimes).

Recommendation

#

Call Parent Method For Zope Hook Method Override In Python

The real life example was on manage_beforeDelete for example in CopySupport.py. This method is used to uncatalog a document when it's deleted. In that case, never forget to call (Parent class).manage_beforeDelete(self, item, container).

#

Code Should Work If All Else Fails

It is a good idea to write code in such way that it has a consistent failover behaviour in case some assumptions are not met. For example, if a piece of code requires a regular expression which is obtained as a user preference, it is better to consider the case where the user forgot to enter the regular expression and provide a failover behaviour. If no consistent failover behaviour can be defined, then raising exceptions with clear message is the best thing to do.

A good practice is to raise an exception with a message stating "No regular expression was defined. The wrong way would be to throw an error.

#

Comments Should Be Used Wisely

Should be used with reason but include enough information so that a reader can get a first grasp of what a part of code is supposed to do.

#

Prevent Acquisition From Acquiring Restricted Properties In Python

The design of ERP5 is based on implicit acquisition just as most of Zope 2. However, ERP5 has its own way of considering acquisition. In ERP5, acquisition of methods is considered as a good thing while acquisition of document properties is considered as risky . This is why all properties defined in property sheets are set to 'None' at the level of the class or at the level of the PropertyHolder instance created by _aq_dynamic in Base class.

This way, acquisition is blocked on all properties defined by property sheets on portal types. If one wants to acquire a given property from a parent object or through a relation, this should be explicitly defined in property sheets. For an example, see Amount.py.

A good example would be acquiring methods through context or through containment, which is perfectly acceptable. It is used for example to access the current WebSection with the method getWebSectionValue in WebSection.py. It may be used also to display on a child object some properties which are defined on its parent. This is considered as normal in ERP5. The required use of accessors in ERP5 for getting and setting properties removes most risks related to implicit acquisition in Zope.

The use of acquisition for properties should therefore be considered only for implementation purpose and with great care. This following bad example (broken link) bug fix gives a very good example of what can happen if one does not care enough about property acquisition. Before this bug fix, 'getToolByName' was called on 'business_template' and therefore included 'business_template' in its acquisition chain.

The new business template which was created shortly after by 'newContent' would then also acquire 'business_template' properties. As a result, calling 'install' and 'uninstall' would install not only those items which were defined on the new business template but also all items for which nothing was defined on the new business template. This is because the way business template documents look up for business template items consists in accessing private properties such as '_message_translation_item' through 'getattr' rather than with an accessor.

This is perfectly acceptable in the context of ERP5 since such properties are considered as private or implementation related and we do not want to force implementors to use accessors at this level. However, it has risks. The risk in this case was to massively destroy installed scripts and forms in an ERP5 site in case of revert operation (and retrieve them later thanks to the portal_trash). The bug fix consisted in removing the acquisition context of 'business_template' by invoking 'getPortalObject()'.

To prevent further risk, we have set all private properties to None on the BusinessTemplate.py class with the following. This is a kind of application of the another principle, to write code that works even if all else fails.

#

Python Class Method Should Have Security Declaration

When writing a class from product that will be stored in ZODB, don't forget to declare appropriate security for methods. Usually you will use "Access contents informations" (for example here) to allow the user getting information from the object, or "Modify portal content" to allow the user modifying the object. For more information you can refer to Security chapter in the zope developer's guide, in the zope book or ERP5 5A Security Model.

The way to access content information is shown for example in the PasswordTool.py.

#

Use Of JavaScript Globals Is Discouraged

Variable declarations should always be made using var to not declare them as global variables. This avoids conflicts from using a variable name across different functions as well as conflicts with global variables declared by 3rd party plugins.

Good Example:

function sum(x, y) {
  var result = x + y;
  return result;
}

Bad Example:

function sum(x, y) {
  // missing var declaration, implied global
  result = x + y;
  return result;
}

Rule

#

A Non Anonymous JavaScript Method Must Be Declared Before Use

Non anonymous functions should be declared before use.

Good Example:

// [...]
function namedFunction() {
  return;
}
return {
  "namedFunction": namedFunction
};

Bad Example:

// [...]
return {
  "namedFunction": function namedFunction() {
    return;
  }
};
#

Abbreviations Are Not Allowed

Abbreviations should not be used to avoid confusion.

Good Example:

last_array_element_value = 1;

Bad Example:

l_val = 1;
#

Code Must Be Validated With JsLint

JSLint (http://jslint.com/) is a quality tools that inspects code and warns about potential problems. It is available online and can also be integrated into several development environments, so errors will be highlighted when writing code.

Before validating your code in JSLint, you should use a code beautifier to fix basic syntax errors (like indentation) automatically. There are a number of beautifiers available online. The following seem to be the best working:

Here, javascript sources have to begin with this header: /*jslint indent: 2, maxlen: 80, nomen: true */, which means it uses two spaces indentation, 80 maximum characters by line and allow the use of '_' as first variable name character. Other JSLint options can be added in sub functions if necessary; Allowed options are:

  • ass: true if assignment should be allowed outside of statement position.
  • bitwise: true if bitwise operators should be allowed.
  • continue: true if the continue statement should be allowed.
  • newcap: true if Initial Caps with constructor function is optional.
  • regexp: true if . and [^...] should be allowed in RegExp literals. They match more material than might be expected, allowing attackers to confuse applications. These forms should not be used when validating in secure applications.
  • unparam: true if warnings should not be given for unused parameters.

Good Example:

/*jslint indent: 2, maxlen: 80, nomen: true */
/*global jIO */
(function (jIO) {
  "use strict";
  var my_code = "is",
      perfect = "isn't it?";

  function doSomething(parameter_one) {
    return jIO.util.ajax({url: parameter_one});
  }

  // camel case tolerated for libraries
  window.myLib = {
    other_entries: "text",
    doSomething: doSomething
  };
}(jIO));

Bad Example:

var myCode = "is", perfect = "isn't it?";

var myLib = {
  otherEntries: "text",
  doSomething: function doSomething(parameterOne) {
    return jIO.util.ajax({url: parameterOne});
  }
};
#

Class And Id Attributes Are Lowercase With Underscore Separator

JavaScript can access elements by their ID attribute and class names. When assigning IDs and class names with multiple words, these should also be separated by an underscore (same as variables).

Good Example:

element.setAttribute("class", "my_class");
or
<div class="my_class" />

Bad Example:

element.setAttribute("class", "myClass");
or
<div class="myClass" />
#

JavaScript Closing Bracket Is At Indent Of Function Call

The closing brace should be on the same indent as the original function call.

Good Example:

function func() {
  return {
    "name": "Batman"
  };
}

Bad Example:

function func() {
  return {
           "name": "Batman"
         };
}
#

JavaScript Constructor Starts With Captial Letter

A constructor function starting with new should always start with a capital letter.

Good Example:

var test = new Application();

Bad Example:

var test = new application();
#

JavaScript Opening Brace Is On Line Of Current Statement

Always put the opening brace on the same line as the previous statement.

Good Example:

function func () {
  return {
    "name": "Batman"
  };
}

Bad Example:

function func() {
  return
  {
    "name": "Batman"
  };
}
#

Method Name Is Camelcase Starting With Lowercase Letter

A method/function should always start with a small letter.

Good Example:

function myFunction() {...}

Bad Example:

function MyFunction() {...}
#

Never Hardcode If Not Necessary

Hardcoded names or properties are hard to maintain and make code difficult to extend. Workflow state names for example should always use an accessor like shown below:

Good Examples:

if self.getSimulationState() in self.getPortalDraftOrderStateList():

Bad Examples:

if self.getSimulationState() == "draft":
#

Plural Is Not Allowed

Plural should not be used when assigning names or declaring parameters. Use _list or _dict instead.

Good Example:

var delivery_note_list = ["one", "two"];

Bad Example:

var delivery_notes = ["one", "two"];
#

Private Method Starts With Underscore

Private methods should use a leading underscore to separate them from public methods (although this does not technically make a method private).

Good Example:

var person = {
  "getName": function () {
    return this._getFirst() + " " + this._getLast();
  },
  "_getFirst": function () {
    // ...
  },
  "_getLast": function () {
    // ...
  }
};

Bad Example:

var person = {
  "getName": function () {
    return this.getFirst() + " " + this.getLast();
  },
  // private function
  "getFirst": function () {
    // ...
  }
};
#

Sessions Are Not Allowed

In our experience, sessions in Zope are buggy and incompatible with clustering. They generate a lot of ZODB conflicts too. Right approaches are based on the same concepts as the ones desribed in not accessing the temp folder across multiple requests.

#

Use Methods To Access Properties In ERP5

Always use accessors. Never access properties directly. ERP5 design is method oriented. All accessors can be automatically overloaded to support different data access schemes or extend a static property into a dynamic property (ex. price). All methods can also act in ERP5 as events through interaction workflows. This allows for implementing complex interactions and constraints. Not using accessors would lead to non generic code and potential inconsistencies with the general ERP5 system. Only the core of ERP5 may in some case access properties directly.

Good Example:

getter = self.getTitle()
setter = self.setTitle("foo")

Bad Example:

self.title
#

Use Of Id Attribute Is Not Allowed

"id" attributes should not be used in HTML element, because when working with gadgets (reusable compontents), duplicate Ids will break an application. For CSS selection, always prefer class attributes. For other HTML manipulation need, javascript should be used.

Good Example:

<style> .my_app > ul > li { ... } </style>
<div class="my_app">My App...</div>

Bad Example:

<style> #my_app > ul > li { ... } </style>
<div id="my_app">My App...</div>
#

Use Two Space Indentation

Tabs and 2-space indentation are being used equally. Since a lot of errors on JSLint often result from mixed use of space and tab using 2 spaces throughout prevents these errors up front.

Good Example:

function outer(a, b) {
  var c = 1,
    d = 2,
    inner;
  if (a > b) {
    inner = function () {
      return {
        "r": c - d
      };
    };
  } else {
    inner = function () {
      return {
        "r": c + d
      };
    };
  }
  return inner;
}

Bad Example:

function outer(a, b) {
  var c = 1,
  d = 2,
  inner;
  
  if (a > b) {
  inner = function () {
  return {
  "r": c - d
  }}}
};

Crime

#

Cookies Are Not Allowed

ERP5 has been designed to be as much cookie technology independent as possible. In addition, cookies have many hidden limitations. For example, big size cookies are sometimes "scrambled" by Apache or Squid front ends. This means that a system based on the exchange of big size cookies can not be reliable (rfc2109 specifies a limit of 4096 bytes for a cookie). The right solution is, sadly, to use standard forms to exchange data. This may involve using encrypted binhexed or uuencoded data in hidden form fields.

#

FailUnless Should Not Be Used In Python Unit Tests To Test Identity

In unit test, use assertEqual, so we see the difference in traceback

Good Example:

self.assertEqual(self.getSimulationState(), "draft")
self.assertNotEqual(self.getSimulationState(), "delivered")

Bad Example:

self.failUnless(self.getSimulationState() == "draft")
self.failUnless(self.getSimulationState() != "delivered")
#

Never Access state_change Object In ERP5 Workflow Scripts

Even though it's likely to work you should instead use state_change['attribute_name'] as default access method.

#

Never Hardcode Interactions

Hardcoded interactions may require extensibility so use interaction workflows each time you have to implement something such as whenever X is called on A, invoke Y on B, even it may be a bit harder to debug at first.

Imagine that you want to call a method (eg. 'expand') each time an order or an order line is reindexed so that the simulation takes into account changes in that order. A simple (but wrong) approach consists in invoking expand from the overloaded reindex method defined on the Order or Order Line classes. The problem with this approach are multiple. First of all, there is no single point where this interaction pattern is explicitly defined. If there are a few interactions in the system, this is fine. But in an ERP system with many interactions, it leads to spaghetti code and no way to understand what happens.

Second, if one wants to extend the Order model and allow the user, let us say, to add a Resource instance inside certain orders, and at the same time a Rule which changes the invoicing based on parameters defined on that Resource instance, it is necessary to call 'expand' each time the Resource instance is modified. If we follow the simple approach, we must overload also the 'reindex' method of the Resource class (by creating for example a local Resource class with portal_classes). In the end, implementing interactions in a complex system may lead to overload most classes.

With interaction workflows, extending an interaction becomes much more simple. Adding an interaction definition for the Resource class each time the resource is part of an Order can provide a straightforward solution without having to overload any method.

Do not however overuse interaction workflows or create too many interaction workflows. It is better to have a couple of well defined interactions than dozens of small interactions. Also, if an interaction really does not need to be extended, it is acceptable to use interaction classes, decorators and/or method wrapper classes to achieve at the level of the python source code the same result as interaction workflows.

#

Never Hide Exceptions

Never hide exceptions to the user because exceptions may result in data inconsistency or system unstability and corruption. It is better to display an error to the user rather than finish a transaction and save corrupted data. At least, someone knows there is an error. Otherwise, errors happen, data is corrupted, and nobody knows. For example, make sure that SQLCatalog ZSQL methods generate exceptions and errors in the user interface in case of syntax error or database adapter connectivity problem.

Remember that hasattr hides all exceptions, including ZODB Conflict Errors, it's much safer to use getattr(obj, name, _marker)is not _marker in those situations.

As long as you re-raise the same exception, you can catch all types of exceptions.

Good Example:

try:
  ...
except:
  LOG(...)
  raise

Bad Example:

try:
  ...
except:
  pass
#

Never Modify 100 Objects In A Transaction

If you need to modify large amounts of Zope objects (more than 100), please use activities. Modifying too many objects in a single transaction leads to conflicts which degrade the performance of the Zope application server and eventually make applications unusable.

#

Never Name A Base Category The Same As A Property In ERP5

Base category ids and property ids are meant to form a global vocabulary where every id is defined once and once only. This requires base categories to have a different id from properties. The same is true for base domains (which must be named with an id which is neither a category id or a property id).

#

Never Override Python Builtin Names

Pay attention to not override builtin python names (as returned by dir(__builtins__) in the Python interpreter) as it makes debugging harder and hides errors.

If you do so, the benefit is that you can use some python code checkers like PyFlakes, pylint or PyChecker.

#

Never Store Calculation Result In Python Property

Always use properties to store document 'original' content (ie. the one which was entered by the user). If you need to process document original content and store the result somewhere, either make this is a new document (and Simulation should be used if possible to track relations between calculated documents and original documents), or this is a workflow variable (with complete historisation), or this is an internal property to be handled within the class code.

#

Never Use aq_parent To Get Document Parent In Python

Use the getParentValue() method instead of aq_parent, which is defined on Base class. aq_parent is only useful if you wish to get the acquisition context of an object, which may be different from its physical container. For example, aq_parent is useful to calculate a breadcrumb whenever the acquisition path results from the combination of multiple physical path (ex. web_site_module/a/b/web_page_module/c). Refer to the Zope Developer Guide for more information on acquisition.

#

Never Use ContentValues or ObjectValues On More Than 50 Documents In ZODB

objectValues and contentValues are suitable for folders with a few documents (ex. less than 50). Many folders in ERP5 may include millions of documents. Use searchFolder if you need to list or sort documents in such folder.

#

Never Use DoActionFor In ERP5

Never use doActionFor to implement logic. ERP5 generates transition methods for all workflow transitions implemented as workflow methods. If you need to programmaticaly trigger a transition on a workflow, use transition methods. Transition methods are intended to represent the workflow logic, unlike workflow actions which should be used for user interface only.

Good Examples:

context.submit()

Bad Examples:

context.getPortalObject().portal_workflow.doActionFor(object, 'submit_action')
#

Never Use GetPath To Retrieve A Url In ERP5

Use absolute_url_path instead or absolute_url, because getPath only returns the path of an object in the OFS. This means the physical path. Whereas absolute_url_path takes into account all virtual hosting configuration. absolute_url includes the host part of the URL (http://hostname/url/to/object) while absolute_url_path only returns the user URL (/url/to/object).

#

Never Use Python If Obj if Obj Is Not A Simple Type

Truth value testing on Zope / ERP5 objects has surprising behavior. For example "if obj" returns false if the object is an empty folder, and "if obj" returns true if it is a folder with sub objects. This is because python uses __len__ if __nonzero__ method doesn't exist on object. So "if obj" MUST NOT be used to check if the object is None.

Good Example:

document = context.getFollowUpRelatedValue(portal_type='File')
if document is None:
  document = ...

Bad Example:

document = context.getFollowUpRelatedValue(portal_type='File')
if not document:
  # Here document may exist, but the conditional is False as document doesn't have sub-objects
#

Never Never Use Reindex In ERP5

Sometimes, it is possible that you feel that it will be easier if you can use immediateReindexObject or recursiveImmediateReindexObject. Theses two methods must be used only for debugging. So never use it in any script, never use it in the ERP5 Code.

Each time we use immediateReindexObject, it will work for development, but it will fails on production if you have more than 2 users.

If you need immediateReindex, this probably means that you are doing the wrong way.

#

Never Use Temp ZODB Folder Across Multiple Requests

Storing into temp folder is incompatible with ZEO and clustering, unless this is only for a single transaction. If you need to share data over multiple transactions, either user ERP5 preferences, an ERP5 selection object or any ad hoc storage. ERP5 selection object implementation is worth having a look at because it has been implemented to reduce ZODB conflicts by hashing selections per use. Make sure though that all changes to ZODB storage happen within the same mounting point of ZODB (ex. /erp5/portal_selections). This allows for selective compacting ZODB from time to time or for using different ZODB backends whenever complete history and traceability is not needed.

#

Never Write To ZODB If Not Required

Each write transaction into the ZODB creates risks of conflicts and increases the size of the ZODB. Unnecessary writes can this way easily increase a 100 MB database into a 10 GB database within a short time.