[IMP] res.config.installer: fix addons install-status display
[odoo/odoo.git] / bin / addons / base / res / res_config.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import os
23 import base64
24 import random
25 from operator import attrgetter
26
27 from osv import osv, fields
28 import tools
29 from tools.translate import _
30 import netsvc
31 import pooler
32
33
34
35 class res_config_configurable(osv.osv_memory):
36     ''' Base classes for new-style configuration items
37
38     Configuration items should inherit from this class, implement
39     the execute method (and optionally the cancel one) and have
40     their view inherit from the related res_config_view_base view.
41     '''
42     _name = 'res.config'
43     logger = netsvc.Logger()
44
45     def get_current_progress(self, cr, uid, context=None):
46         '''Return a description the current progress of configuration:
47         a tuple of (non_open_todos:int, total_todos: int)
48         '''
49         return (self.pool.get('ir.actions.todo')\
50                 .search_count(cr, uid, [('state','<>','open')], context),
51                 self.pool.get('ir.actions.todo')\
52                 .search_count(cr, uid, [], context))
53
54     def _progress(self, cr, uid, context=None):
55         closed, total = self.get_current_progress(cr, uid, context=context)
56         if total:
57             return round(closed*100./total)
58         return 100.
59
60     def _get_image(self, cr, uid, context=None):
61         file_no = str(random.randint(1,3))
62         path = os.path.join('base','res','config_pixmaps/%s.png'%file_no)
63         file_data = tools.file_open(path,'rb').read()
64         return base64.encodestring(file_data)
65
66     _columns = dict(
67         progress = fields.float('Configuration Progress', readonly=True),
68         config_logo = fields.binary('Image', readonly=True),
69         )
70     _defaults = dict(
71         progress = _progress,
72         config_logo = _get_image
73         )
74
75     def _next_action(self, cr, uid, context=None):
76         todos = self.pool.get('ir.actions.todo')
77         self.logger.notifyChannel('actions', netsvc.LOG_INFO,
78                                   'getting next %s' % todos)
79         active_todos = todos.search(cr, uid, [('state','=','open')],
80                                     limit=1)
81         dont_skip_todo = True
82         if active_todos:
83             todo_obj = todos.browse(cr, uid, active_todos[0], context=None)
84             todo_groups = map(lambda x:x.id, todo_obj.groups_id)
85             if todo_groups:
86                 cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN %s",(uid, tuple(todo_groups),))
87                 dont_skip_todo = bool(cr.fetchone())
88             if dont_skip_todo:
89                 return todos.browse(cr, uid, active_todos[0], context=None)
90             else:
91                 todos.write(cr, uid, active_todos[0], {'state':'skip'}, context=None)
92                 return self._next_action(cr, uid)
93         return None
94
95     def _set_previous_todo(self, cr, uid, state, context=None):
96         """ lookup the previous (which is still the next at this point)
97         ir.actions.todo, set it to whatever state was provided.
98
99         Raises
100         `LookupError`: if we couldn't find *any* previous todo
101         `ValueError`: if no state is provided
102         anything ir_actions_todo.write can throw
103         """
104         if context is None:
105             context = {}
106         # this is ultra brittle, but apart from storing the todo id
107         # into the res.config view, I'm not sure how to get the
108         # "previous" todo
109         previous_todo = self._next_action(cr, uid, context=context)
110         if not previous_todo:
111             raise LookupError(_("Couldn't find previous ir.actions.todo"))
112         if not state:
113             raise ValueError(_("Can't set an ir.actions.todo's state to "
114                                "nothingness"))
115         previous_todo.write({'state':state})
116
117     def _next(self, cr, uid, context=None):
118         self.logger.notifyChannel('actions', netsvc.LOG_INFO,
119                                   'getting next operation')
120         next = self._next_action(cr, uid)
121         self.logger.notifyChannel('actions', netsvc.LOG_INFO,
122                                   'next action is %s' % next)
123         if next:
124             action = next.action_id
125             return {
126                 'view_mode': action.view_mode,
127                 'view_type': action.view_type,
128                 'view_id': action.view_id and [action.view_id.id] or False,
129                 'res_model': action.res_model,
130                 'type': action.type,
131                 'target': action.target,
132             }
133         self.logger.notifyChannel(
134             'actions', netsvc.LOG_INFO,
135             'all configuration actions have been executed')
136
137         current_user_menu = self.pool.get('res.users')\
138             .browse(cr, uid, uid).menu_id
139         # return the action associated with the menu
140         return self.pool.get(current_user_menu.type)\
141             .read(cr, uid, current_user_menu.id)
142
143     def start(self, cr, uid, ids, context=None):
144         ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
145         for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
146             if (todo.restart=='always') or (todo.restart=='onskip' and (todo.state in ('skip','cancel'))):
147                 todo.write({'state':'open'})
148         return self.next(cr, uid, ids, context)
149
150     def next(self, cr, uid, ids, context=None):
151         """ Returns the next todo action to execute (using the default
152         sort order)
153         """
154         return self._next(cr, uid, context=context)
155
156     def execute(self, cr, uid, ids, context=None):
157         """ Method called when the user clicks on the ``Next`` button.
158
159         Execute *must* be overloaded unless ``action_next`` is overloaded
160         (which is something you generally don't need to do).
161
162         If ``execute`` returns an action dictionary, that action is executed
163         rather than just going to the next configuration item.
164         """
165         raise NotImplementedError(
166             'Configuration items need to implement execute')
167     def cancel(self, cr, uid, ids, context=None):
168         """ Method called when the user click on the ``Skip`` button.
169
170         ``cancel`` should be overloaded instead of ``action_skip``. As with
171         ``execute``, if it returns an action dictionary that action is
172         executed in stead of the default (going to the next configuration item)
173
174         The default implementation is a NOOP.
175
176         ``cancel`` is also called by the default implementation of
177         ``action_cancel``.
178         """
179         pass
180
181     def action_next(self, cr, uid, ids, context=None):
182         """ Action handler for the ``next`` event.
183
184         Sets the status of the todo the event was sent from to
185         ``done``, calls ``execute`` and -- unless ``execute`` returned
186         an action dictionary -- executes the action provided by calling
187         ``next``.
188         """
189         try:
190             self._set_previous_todo(cr, uid, state='done', context=context)
191         except Exception, e:
192             raise osv.except_osv(_('Error'), e.message)
193         next = self.execute(cr, uid, ids, context=None)
194         if next: return next
195         return self.next(cr, uid, ids, context=context)
196
197     def action_skip(self, cr, uid, ids, context=None):
198         """ Action handler for the ``skip`` event.
199
200         Sets the status of the todo the event was sent from to
201         ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
202         an action dictionary -- executes the action provided by calling
203         ``next``.
204         """
205         try:
206             self._set_previous_todo(cr, uid, state='skip', context=context)
207         except Exception, e:
208             raise osv.except_osv(_('Error'), e.message)
209         next = self.cancel(cr, uid, ids, context=None)
210         if next: return next
211         return self.next(cr, uid, ids, context=context)
212
213     def action_cancel(self, cr, uid, ids, context=None):
214         """ Action handler for the ``cancel`` event. That event isn't
215         generated by the res.config.view.base inheritable view, the
216         inherited view has to overload one of the buttons (or add one
217         more).
218
219         Sets the status of the todo the event was sent from to
220         ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
221         an action dictionary -- executes the action provided by calling
222         ``next``.
223         """
224         try:
225             self._set_previous_todo(cr, uid, state='cancel', context=context)
226         except Exception, e:
227             raise osv.except_osv(_('Error'), e.message)
228         next = self.cancel(cr, uid, ids, context=None)
229         if next: return next
230         return self.next(cr, uid, ids, context=context)
231
232 res_config_configurable()
233
234 class res_config_installer(osv.osv_memory):
235     """ New-style configuration base specialized for addons selection
236     and installation.
237
238     Basic usage
239     -----------
240
241     Subclasses can simply define a number of _columns as
242     fields.boolean objects. The keys (column names) should be the
243     names of the addons to install (when selected). Upon action
244     execution, selected boolean fields (and those only) will be
245     interpreted as addons to install, and batch-installed.
246
247     Additional addons
248     -----------------
249
250     It is also possible to require the installation of an additional
251     addon set when a specific preset of addons has been marked for
252     installation (in the basic usage only, additionals can't depend on
253     one another).
254
255     These additionals are defined through the ``_install_if``
256     property. This property is a mapping of a collection of addons (by
257     name) to a collection of addons (by name) [#]_, and if all the *key*
258     addons are selected for installation, then the *value* ones will
259     be selected as well. For example::
260
261         _install_if = {
262             ('sale','crm'): ['sale_crm'],
263         }
264
265     This will install the ``sale_crm`` addon if and only if both the
266     ``sale`` and ``crm`` addons are selected for installation.
267
268     You can define as many additionals as you wish, and additionals
269     can overlap in key and value. For instance::
270
271         _install_if = {
272             ('sale','crm'): ['sale_crm'],
273             ('sale','project'): ['project_mrp'],
274         }
275
276     will install both ``sale_crm`` and ``project_mrp`` if all of
277     ``sale``, ``crm`` and ``project`` are selected for installation.
278
279     Hook methods
280     ------------
281
282     Subclasses might also need to express dependencies more complex
283     than that provided by additionals. In this case, it's possible to
284     define methods of the form ``_if_%(name)s`` where ``name`` is the
285     name of a boolean field. If the field is selected, then the
286     corresponding module will be marked for installation *and* the
287     hook method will be executed.
288
289     Hook methods take the usual set of parameters (cr, uid, ids,
290     context) and can return a collection of additional addons to
291     install (if they return anything, otherwise they should not return
292     anything, though returning any "falsy" value such as None or an
293     empty collection will have the same effect).
294
295     Complete control
296     ----------------
297
298     The last hook is to simply overload the ``modules_to_install``
299     method, which implements all the mechanisms above. This method
300     takes the usual set of parameters (cr, uid, ids, context) and
301     returns a ``set`` of addons to install (addons selected by the
302     above methods minus addons from the *basic* set which are already
303     installed) [#]_ so an overloader can simply manipulate the ``set``
304     returned by ``res_config_installer.modules_to_install`` to add or
305     remove addons.
306
307     Skipping the installer
308     ----------------------
309
310     Unless it is removed from the view, installers have a *skip*
311     button which invokes ``action_skip`` (and the ``cancel`` hook from
312     ``res.config``). Hooks and additionals *are not run* when skipping
313     installation, even for already installed addons.
314
315     Again, setup your hooks accordinly.
316
317     .. [#] note that since a mapping key needs to be hashable, it's
318            possible to use a tuple or a frozenset, but not a list or a
319            regular set
320
321     .. [#] because the already-installed modules are only pruned at
322            the very end of ``modules_to_install``, additionals and
323            hooks depending on them *are guaranteed to execute*. Setup
324            your hooks accordingly.
325     """
326     _name = 'res.config.installer'
327     _inherit = 'res.config'
328
329     _install_if = {}
330
331     def _already_installed(self, cr, uid, context=None):
332         """ For each module (boolean fields in a res.config.installer),
333         check if it's already installed (either 'to install', 'to upgrade' or 'installed')
334         and if it is, check it by default
335         """
336         modules = self.pool.get('ir.module.module')
337
338         selectable = [field for field in self._columns
339                       if type(self._columns[field]) is fields.boolean]
340         return modules.browse(
341             cr, uid,
342             modules.search(cr, uid,
343                            [('name','in',selectable),
344                             ('state','in',['to install', 'installed', 'to upgrade'])],
345                            context=context),
346             context=context)
347
348
349     def modules_to_install(self, cr, uid, ids, context=None):
350         """ selects all modules to install:
351
352         * checked boolean fields
353         * return values of hook methods. Hook methods are of the form
354           ``_if_%(addon_name)s``, and are called if the corresponding
355           addon is marked for installation. They take the arguments
356           cr, uid, ids and context, and return an iterable of addon
357           names
358         * additionals, additionals are setup through the ``_install_if``
359           class variable. ``_install_if`` is a dict of {iterable:iterable}
360           where key and value are iterables of addon names.
361
362           If all the addons in the key are selected for installation
363           (warning: addons added through hooks don't count), then the
364           addons in the value are added to the set of modules to install
365         * not already installed
366         """
367         base = set(module_name
368                    for installer in self.read(cr, uid, ids, context=context)
369                    for module_name, to_install in installer.iteritems()
370                    if module_name != 'id'
371                    if type(self._columns[module_name]) is fields.boolean
372                    if to_install)
373
374         hooks_results = set()
375         for module in base:
376             hook = getattr(self, '_if_%s'%(module), None)
377             if hook:
378                 hooks_results.update(hook(cr, uid, ids, context=None) or set())
379
380         additionals = set(
381             module for requirements, consequences \
382                        in self._install_if.iteritems()
383                    if base.issuperset(requirements)
384                    for module in consequences)
385
386         return (base | hooks_results | additionals) - set(
387             map(attrgetter('name'), self._already_installed(cr, uid, context)))
388
389     def default_get(self, cr, uid, fields_list, context=None):
390         ''' If an addon is already installed, check it by default
391         '''
392         defaults = super(res_config_installer, self).default_get(
393             cr, uid, fields_list, context=context)
394
395         return dict(defaults,
396                     **dict.fromkeys(
397                         map(attrgetter('name'),
398                             self._already_installed(cr, uid, context=context)),
399                         True))
400
401     def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
402         """ If an addon is already installed, set it to readonly as
403         res.config.installer doesn't handle uninstallations of already
404         installed addons
405         """
406         fields = super(res_config_installer, self).fields_get(
407             cr, uid, fields, context, write_access)
408
409         for module in self._already_installed(cr, uid, context=context):
410             if module.name not in fields:
411                 continue
412             fields[module.name].update(
413                 readonly=True,
414                 help=fields[module.name].get('help', '') +
415                      _('\n\nThis addon is already installed on your system'))
416
417         return fields
418
419     def execute(self, cr, uid, ids, context=None):
420         modules = self.pool.get('ir.module.module')
421         to_install = list(self.modules_to_install(
422             cr, uid, ids, context=context))
423         self.logger.notifyChannel(
424             'installer', netsvc.LOG_INFO,
425             'Selecting addons %s to install'%to_install)
426         modules.state_update(
427             cr, uid,
428             modules.search(cr, uid, [('name','in',to_install)]),
429             'to install', ['uninstalled'], context=context)
430         cr.commit() #TOFIX: after remove this statement, installation wizard is fail 
431         pooler.restart_pool(cr.dbname, update_module=True)
432 res_config_installer()
433
434 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
435     'wizards (ir.actions.configuration.wizard). Old-style configuration '\
436     'wizards have been deprecated.\n'\
437     'The addon should be migrated to res.config objects.'
438 class ir_actions_configuration_wizard(osv.osv_memory):
439     ''' Compatibility configuration wizard
440
441     The old configuration wizard has been replaced by res.config, but in order
442     not to break existing but not-yet-migrated addons, the old wizard was
443     reintegrated and gutted.
444     '''
445     _name='ir.actions.configuration.wizard'
446     _inherit = 'res.config'
447
448     def _next_action_note(self, cr, uid, ids, context=None):
449         next = self._next_action(cr, uid)
450         if next:
451             # if the next one is also an old-style extension, you never know...
452             if next.note:
453                 return next.note
454             return "Click 'Continue' to configure the next addon..."
455         return "Your database is now fully configured.\n\n"\
456             "Click 'Continue' and enjoy your OpenERP experience..."
457
458     _columns = {
459         'note': fields.text('Next Wizard', readonly=True),
460         }
461     _defaults = {
462         'note': _next_action_note,
463         }
464
465     def execute(self, cr, uid, ids, context=None):
466         self.logger.notifyChannel(
467             'configuration', netsvc.LOG_WARNING, DEPRECATION_MESSAGE)
468
469 ir_actions_configuration_wizard()
470
471 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: