Last chunk of portal type classes / zodb property sheets.
[erp5.git] / product / ERP5Type / dynamic / portal_type_class.py
1 ##############################################################################
2 #
3 # Copyright (c) 2010 Nexedi SARL and Contributors. All Rights Reserved.
4 # Nicolas Dumazet <nicolas.dumazet@nexedi.com>
5 # Arnaud Fontaine <arnaud.fontaine@nexedi.com>
6 #
7 # WARNING: This program as such is intended to be used by professional
8 # programmers who take the whole responsibility of assessing all potential
9 # consequences resulting from its eventual inadequacies and bugs
10 # End users who are looking for a ready-to-use solution with commercial
11 # guarantees and support are strongly adviced to contract a Free Software
12 # Service Company
13 #
14 # This program is Free Software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; either version 2
17 # of the License, or (at your option) any later version.
18 #
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27 #
28 ##############################################################################
29
30 import sys
31 import os
32 import inspect
33 from types import ModuleType
34
35 from dynamic_module import registerDynamicModule
36 from accessor_holder import _generateBaseAccessorHolder, _generatePreferenceToolAccessorHolder
37
38 from Products.ERP5Type.Base import _aq_reset, Base
39 from Products.ERP5Type.Globals import InitializeClass
40 from Products.ERP5Type.Utils import setDefaultClassProperties
41 from Products.ERP5Type import document_class_registry, mixin_class_registry
42
43 from zope.interface import classImplements
44 from zLOG import LOG, ERROR, INFO, WARNING
45
46 def _importClass(classpath):
47 try:
48 module_path, class_name = classpath.rsplit('.', 1)
49 module = __import__(module_path, {}, {}, (module_path,))
50 klass = getattr(module, class_name)
51
52 # XXX is this required? (here?)
53 setDefaultClassProperties(klass)
54 InitializeClass(klass)
55
56 return klass
57 except StandardError:
58 raise ImportError('Could not import document class %s' % classpath)
59
60 def _fillAccessorHolderList(accessor_holder_list,
61 create_accessor_holder_func,
62 property_sheet_name_set,
63 accessor_holder_module,
64 property_sheet_module):
65 """
66 Fill the accessor holder list with the given Property Sheets (which
67 could be coming either from the filesystem or ZODB)
68 """
69 for property_sheet_name in property_sheet_name_set:
70 # LOG("ERP5Type.dynamic", INFO,
71 # "Getting accessor holder for " + property_sheet_name)
72
73 try:
74 # Get the already generated accessor holder
75 accessor_holder_list.append(getattr(accessor_holder_module,
76 property_sheet_name))
77
78 except AttributeError:
79 # Generate the accessor holder as it has not been done yet
80 try:
81 accessor_holder_class = \
82 create_accessor_holder_func(getattr(property_sheet_module,
83 property_sheet_name))
84
85 except AttributeError:
86 LOG("ERP5Type.dynamic", ERROR,
87 "Ignoring missing Property Sheet " + property_sheet_name)
88
89 raise
90
91 accessor_holder_list.append(accessor_holder_class)
92
93 setattr(accessor_holder_module, property_sheet_name,
94 accessor_holder_class)
95
96 # LOG("ERP5Type.dynamic", INFO,
97 # "Created accessor holder for %s in %s" % (property_sheet_name,
98 # accessor_holder_module))
99
100 # LOG("ERP5Type.dynamic", INFO,
101 # "Got accessor holder for " + property_sheet_name)
102
103 # Loading Cache Factory portal type would generate the accessor holder
104 # for Cache Factory, itself defined with Standard Property thus
105 # loading the portal type Standard Property, itself defined with
106 # Standard Property and so on...
107 #
108 # NOTE: only the outer Property Sheets is stored in the accessor
109 # holder module
110 property_sheet_generating_portal_type_set = set()
111
112 # 'Types Tool' is required to access 'site.portal_types' and the
113 # former requires 'Base Type'. Thus, 'generating' is meaningful to
114 # avoid infinite recursion, whereas 'type_class' avoids accessing to
115 # portal_type
116 #
117 # For example, loading 'Types Tool' will try to load 'Types Tool' when
118 # accessing 'site.portal_types'. Therefore the inner one is just an
119 # import of 'Types Tool' class without any mixin, interface or
120 # Property Sheet to allow the outer (which will actually be stored in
121 # 'erp5.portal_type') to be fully generated.
122 #
123 # Solver Tool, as a TypeProvider, will also be required to access
124 # site.portal_types
125 core_portal_type_class_dict = {
126 'Base Type': {'type_class': 'ERP5TypeInformation',
127 'generating': False},
128 'Types Tool': {'type_class': 'TypesTool',
129 'generating': False},
130 'Solver Tool': {'type_class': 'SolverTool',
131 'generating': False}
132 }
133
134 def generatePortalTypeClass(site, portal_type_name):
135 """
136 Given a portal type, look up in Types Tool the corresponding
137 Base Type object holding the definition of this portal type,
138 and computes __bases__ and __dict__ for the class that will
139 be created to represent this portal type
140
141 Returns tuple with 4 items:
142 - base_tuple: a tuple of classes to be used as __bases__
143 - interface_list: list of zope interfaces the portal type implements
144 - base_category_list: base categories defined on the portal_type itself
145 (a.k.a. excluding categories from Property sheets and documents)
146 - attribute dictionary: any additional attributes to put on the class
147 """
148 # LOG("ERP5Type.dynamic", INFO, "Loading portal type " + portal_type_name)
149
150 global core_portal_type_class_dict
151
152 if portal_type_name in core_portal_type_class_dict:
153 if not core_portal_type_class_dict[portal_type_name]['generating']:
154 # Loading the (full) outer portal type class
155 core_portal_type_class_dict[portal_type_name]['generating'] = True
156 else:
157 # Loading the inner portal type class without any mixin,
158 # interface or Property Sheet
159 klass = _importClass(document_class_registry.get(
160 core_portal_type_class_dict[portal_type_name]['type_class']))
161
162 # LOG("ERP5Type.dynamic", INFO,
163 # "Loaded portal type %s (INNER)" % portal_type_name)
164
165 # Don't do anything else, just allow to load fully the outer
166 # portal type class
167 return ((klass,), [], [], {})
168
169 # Do not use __getitem__ (or _getOb) because portal_type may exist in a
170 # type provider other than Types Tool.
171 portal_type = getattr(site.portal_types, portal_type_name, None)
172
173 type_class = None
174 base_category_list = []
175
176 if portal_type is not None:
177 # type_class has a compatibility getter that should return
178 # something even if the field is not set (i.e. Base Type object
179 # was not migrated yet). It only works if factory_method_id is set.
180 type_class = portal_type.getTypeClass()
181
182 # The Tools used to have 'Folder' or None as type_class instead of
183 # 'NAME Tool', so make sure the type_class is correct
184 #
185 # NOTE: under discussion so might be removed later on
186 if portal_type_name.endswith('Tool') and type_class in ('Folder', None):
187 type_class = portal_type_name.replace(' ', '')
188
189 mixin_list = portal_type.getTypeMixinList()
190 interface_list = portal_type.getTypeInterfaceList()
191 base_category_list = portal_type.getTypeBaseCategoryList()
192
193 # But if neither factory_init_method_id nor type_class are set on
194 # the portal type, we have to try to guess, for compatibility.
195 # Moreover, some tools, such as 'Activity Tool', don't have any
196 # portal type
197 if type_class is None:
198 if portal_type_name in core_portal_type_class_dict:
199 # Only happen when portal_types is empty (e.g. when creating a
200 # new ERP5Site)
201 type_class = core_portal_type_class_dict[portal_type_name]['type_class']
202 else:
203 # Try to figure out a coresponding document class from the
204 # document side. This can happen when calling newTempAmount for
205 # instance:
206 # Amount has no corresponding Base Type and will never have one
207 # But the semantic of newTempXXX requires us to create an
208 # object using the Amount Document, so we promptly do it:
209 type_class = portal_type_name.replace(' ', '')
210
211 mixin_list = []
212 interface_list = []
213
214 if type_class is None:
215 raise AttributeError('Document class is not defined on Portal Type %s' \
216 % portal_type_name)
217
218 type_class_path = document_class_registry.get(type_class)
219 if type_class_path is None:
220 raise AttributeError('Document class %s has not been registered:' \
221 ' cannot import it as base of Portal Type %s' \
222 % (type_class, portal_type_name))
223
224 klass = _importClass(type_class_path)
225
226 global property_sheet_generating_portal_type_set
227
228 accessor_holder_list = []
229
230 if portal_type_name not in property_sheet_generating_portal_type_set:
231 # LOG("ERP5Type.dynamic", INFO,
232 # "Filling accessor holder list for portal_type " + portal_type_name)
233
234 property_sheet_generating_portal_type_set.add(portal_type_name)
235
236 property_sheet_tool = getattr(site, 'portal_property_sheets', None)
237
238 property_sheet_set = set()
239
240 # The Property Sheet Tool may be None if the code is updated but
241 # the BT has not been upgraded yet with portal_property_sheets
242 if property_sheet_tool is None:
243 if not getattr(site, '_v_bootstrapping', False):
244 LOG("ERP5Type.dynamic", WARNING,
245 "Property Sheet Tool was not found. Please update erp5_core "
246 "Business Template")
247 else:
248 if portal_type is not None:
249 # Get the Property Sheets defined on the portal_type and use the
250 # ZODB Property Sheet rather than the filesystem only if it
251 # exists in ZODB
252 zodb_property_sheet_set = set(property_sheet_tool.objectIds())
253 for property_sheet in portal_type.getTypePropertySheetList():
254 if property_sheet in zodb_property_sheet_set:
255 property_sheet_set.add(property_sheet)
256
257 # XXX maybe this should be a generic hook, adding property sheets
258 # dynamically for a given portal type name? If done well, this
259 # system could perhaps help erp5_egov to get rid of aq_dynamic
260 if portal_type_name in ("Preference Tool",
261 "Preference",
262 "System Preference"):
263 for property_sheet in zodb_property_sheet_set:
264 if property_sheet.endswith('Preference'):
265 property_sheet_set.add(property_sheet)
266 else:
267 zodb_property_sheet_set = set()
268
269 # Get the Property Sheets defined on the document and its bases
270 # recursively. Fallback on the filesystem Property Sheet only and
271 # only if the ZODB Property Sheet does not exist
272 from Products.ERP5Type.Base import getClassPropertyList
273 for property_sheet in getClassPropertyList(klass):
274 # If the Property Sheet is a string, then this is a ZODB
275 # Property Sheet
276 #
277 # NOTE: The Property Sheets of a document should be given as a
278 # string from now on
279 if not isinstance(property_sheet, basestring):
280 property_sheet = property_sheet.__name__
281 if property_sheet in zodb_property_sheet_set:
282 property_sheet_set.add(property_sheet)
283
284 import erp5
285
286 if property_sheet_set:
287 # Initialize ZODB Property Sheets accessor holders
288 _fillAccessorHolderList(
289 accessor_holder_list,
290 property_sheet_tool.createZodbPropertySheetAccessorHolder,
291 property_sheet_set,
292 erp5.accessor_holder,
293 property_sheet_tool)
294
295 if "Base" in property_sheet_set:
296 accessor_holder = None
297 # useless if Base Category is not yet here
298 if hasattr(erp5.accessor_holder, "Base Category"):
299 accessor_holder = _generateBaseAccessorHolder(
300 site,
301 erp5.accessor_holder)
302 if accessor_holder is not None:
303 accessor_holder_list.append(accessor_holder)
304
305 # XXX a hook to add per-portal type accessor holders maybe?
306 if portal_type_name == "Preference Tool":
307 accessor_holder = _generatePreferenceToolAccessorHolder(
308 site,
309 accessor_holder_list,
310 erp5.accessor_holder)
311 accessor_holder_list.insert(0, accessor_holder)
312
313 base_category_set = set(base_category_list)
314 for accessor_holder in accessor_holder_list:
315 base_category_set.update(accessor_holder._categories)
316 base_category_list = list(base_category_set)
317
318 property_sheet_generating_portal_type_set.remove(portal_type_name)
319
320 # LOG("ERP5Type.dynamic", INFO,
321 # "Filled accessor holder list for portal_type %s (%s)" % \
322 # (portal_type_name, accessor_holder_list))
323
324 mixin_path_list = []
325 if mixin_list:
326 mixin_path_list = map(mixin_class_registry.__getitem__, mixin_list)
327 mixin_class_list = map(_importClass, mixin_path_list)
328
329 base_class_list = [klass] + accessor_holder_list + mixin_class_list
330
331 interface_class_list = []
332 if interface_list:
333 from Products.ERP5Type import interfaces
334 interface_class_list = [getattr(interfaces, name)
335 for name in interface_list]
336
337 if portal_type_name in core_portal_type_class_dict:
338 core_portal_type_class_dict[portal_type_name]['generating'] = False
339
340 #LOG("ERP5Type.dynamic", INFO,
341 # "Portal type %s loaded with bases %s" \
342 # % (portal_type_name, repr(baseclasses)))
343
344 return (tuple(base_class_list),
345 interface_class_list,
346 base_category_list,
347 dict(portal_type=portal_type_name))
348
349 from lazy_class import generateLazyPortalTypeClass
350 def initializeDynamicModules():
351 """
352 Create erp5 module and its submodules
353 erp5.portal_type
354 holds portal type classes
355 erp5.temp_portal_type
356 holds portal type classes for temp objects
357 erp5.document
358 holds document classes that have no physical import path,
359 for example classes created through ClassTool that are in
360 $INSTANCE_HOME/Document
361 erp5.accessor_holder
362 holds accessors of ZODB Property Sheets
363 """
364 erp5 = ModuleType("erp5")
365 sys.modules["erp5"] = erp5
366 erp5.document = ModuleType("erp5.document")
367 sys.modules["erp5.document"] = erp5.document
368 erp5.accessor_holder = ModuleType("erp5.accessor_holder")
369 sys.modules["erp5.accessor_holder"] = erp5.accessor_holder
370
371 portal_type_container = registerDynamicModule('erp5.portal_type',
372 generateLazyPortalTypeClass)
373
374 erp5.portal_type = portal_type_container
375
376 def loadTempPortalTypeClass(portal_type_name):
377 """
378 Returns a class suitable for a temporary portal type
379
380 This class will in fact be a subclass of erp5.portal_type.xxx, which
381 means that loading an attribute on this temporary portal type loads
382 the lazily-loaded parent class, and that any changes on the parent
383 class will be reflected on the temporary objects.
384 """
385 klass = getattr(portal_type_container, portal_type_name)
386
387 from Products.ERP5Type.Accessor.Constant import PropertyGetter as \
388 PropertyConstantGetter
389
390 class TempDocument(klass):
391 isTempDocument = PropertyConstantGetter('isTempDocument', value=True)
392 __roles__ = None
393 TempDocument.__name__ = "Temp " + portal_type_name
394
395 # Replace some attributes.
396 for name in ('isIndexable', 'reindexObject', 'recursiveReindexObject',
397 'activate', 'setUid', 'setTitle', 'getTitle', 'getUid'):
398 setattr(TempDocument, name, getattr(klass, '_temp_%s' % name))
399
400 # Make some methods public.
401 for method_id in ('reindexObject', 'recursiveReindexObject',
402 'activate', 'setUid', 'setTitle', 'getTitle',
403 'edit', 'setProperty', 'getUid', 'setCriterion',
404 'setCriterionPropertyList'):
405 setattr(TempDocument, '%s__roles__' % method_id, None)
406 return TempDocument
407
408 erp5.temp_portal_type = registerDynamicModule('erp5.temp_portal_type',
409 loadTempPortalTypeClass)
410
411 required_tool_list = [('portal_types', 'Base Type'),
412 ('portal_property_sheets', 'BaseType')]
413 last_sync = -1
414 def synchronizeDynamicModules(context, force=False):
415 """
416 Allow resetting all classes to ghost state, most likely done after
417 adding and removing mixins on the fly
418
419 Most of the time, this reset is only hypothetic:
420 * with force=False, the reset is only done if another node resetted
421 the classes since the last reset on this node.
422 * with force=True, forcefully reset the classes on the current node
423 and send out an invalidation to other nodes
424 """
425 portal = context.getPortalObject()
426
427 global last_sync
428 if force:
429 # hard invalidation to force sync between nodes
430 portal.newCacheCookie('dynamic_classes')
431 last_sync = portal.getCacheCookie('dynamic_classes')
432 else:
433 cookie = portal.getCacheCookie('dynamic_classes')
434 if cookie == last_sync:
435 # up to date, nothing to do
436 return
437 last_sync = cookie
438
439
440 import erp5
441
442 Base.aq_method_lock.acquire()
443 try:
444
445 migrated = False
446 for tool_id, line_id in required_tool_list:
447 # if the instance has no property sheet tool, or incomplete
448 # property sheets, we need to import some data to bootstrap
449 # (only likely to happen on the first run ever)
450 tool = getattr(portal, tool_id, None)
451 if tool is not None:
452 if getattr(tool, line_id, None) is None:
453 # tool exists, but is incomplete
454 portal._delObject(tool_id)
455 else:
456 # tool exists, Base Type is represented; probably OK
457 continue
458
459 if not migrated:
460 # XXX: if some portal types are missing, for instance
461 # if some Tools have no portal types, this is likely to fail with an
462 # error. On the other hand, we can't proceed without this change,
463 # and if we dont import the xml, the instance wont start.
464 portal.migrateToPortalTypeClass()
465 migrated = True
466
467 LOG('ERP5Site', INFO, 'importing transitional %s tool'
468 ' from Products.ERP5.bootstrap to be able to load'
469 ' core items...' % tool_id)
470
471 from Products.ERP5.ERP5Site import getBootstrapDirectory
472 bundle_path = os.path.join(getBootstrapDirectory(),
473 '%s.xml' % tool_id)
474 assert os.path.exists(bundle_path), 'Please update ERP5 product'
475
476 try:
477 tool = portal._importObjectFromFile(
478 bundle_path,
479 id=tool_id,
480 verify=False,
481 set_owner=False,
482 suppress_events=True)
483 from Products.ERP5.Document.BusinessTemplate import _recursiveRemoveUid
484 _recursiveRemoveUid(tool)
485 portal._setOb(tool_id, tool)
486 except:
487 import traceback; traceback.print_exc()
488 raise
489
490 if not getattr(portal, '_v_bootstrapping', False):
491 LOG('ERP5Site', INFO, 'Transition successful, please update your'
492 ' business templates')
493
494
495 LOG("ERP5Type.dynamic", 0, "Resetting dynamic classes")
496 for class_name, klass in inspect.getmembers(erp5.portal_type,
497 inspect.isclass):
498 klass.restoreGhostState()
499
500 # Clear accessor holders of ZODB Property Sheets
501 for property_sheet_id in erp5.accessor_holder.__dict__.keys():
502 if not property_sheet_id.startswith('__'):
503 delattr(erp5.accessor_holder, property_sheet_id)
504 finally:
505 Base.aq_method_lock.release()
506
507 # Necessary because accessors are wrapped in WorkflowMethod by
508 # _aq_dynamic (performed in createAccessorHolder)
509 _aq_reset()