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