[IMP] use fields_get() instead of _all_columns.get(): this method has the advantage...
[odoo/odoo.git] / openerp / 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 import logging
22 from operator import attrgetter
23 import re
24
25 from openerp import pooler, SUPERUSER_ID
26 from openerp.osv import osv, fields
27 from openerp.tools import ustr
28 from openerp.tools.translate import _
29 from openerp import exceptions
30
31 _logger = logging.getLogger(__name__)
32
33 class res_config_configurable(osv.osv_memory):
34     ''' Base classes for new-style configuration items
35
36     Configuration items should inherit from this class, implement
37     the execute method (and optionally the cancel one) and have
38     their view inherit from the related res_config_view_base view.
39     '''
40     _name = 'res.config'
41
42     def _next_action(self, cr, uid, context=None):
43         Todos = self.pool['ir.actions.todo']
44         _logger.info('getting next %s', Todos)
45
46         active_todos = Todos.browse(cr, uid,
47             Todos.search(cr, uid, ['&', ('type', '=', 'automatic'), ('state','=','open')]),
48                                     context=context)
49
50         user_groups = set(map(
51             lambda g: g.id,
52             self.pool['res.users'].browse(cr, uid, [uid], context=context)[0].groups_id))
53
54         valid_todos_for_user = [
55             todo for todo in active_todos
56             if not todo.groups_id or bool(user_groups.intersection((
57                 group.id for group in todo.groups_id)))
58         ]
59
60         if valid_todos_for_user:
61             return valid_todos_for_user[0]
62
63         return None
64
65     def _next(self, cr, uid, context=None):
66         _logger.info('getting next operation')
67         next = self._next_action(cr, uid, context=context)
68         _logger.info('next action is %s', next)
69         if next:
70             res = next.action_launch(context=context)
71             res['nodestroy'] = False
72             return res
73         # reload the client; open the first available root menu
74         menu_obj = self.pool.get('ir.ui.menu')
75         menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False)], context=context)
76         return {
77             'type': 'ir.actions.client',
78             'tag': 'reload',
79             'params': {'menu_id': menu_ids and menu_ids[0] or False},
80         }
81
82     def start(self, cr, uid, ids, context=None):
83         return self.next(cr, uid, ids, context)
84
85     def next(self, cr, uid, ids, context=None):
86         """ Returns the next todo action to execute (using the default
87         sort order)
88         """
89         return self._next(cr, uid, context=context)
90
91     def execute(self, cr, uid, ids, context=None):
92         """ Method called when the user clicks on the ``Next`` button.
93
94         Execute *must* be overloaded unless ``action_next`` is overloaded
95         (which is something you generally don't need to do).
96
97         If ``execute`` returns an action dictionary, that action is executed
98         rather than just going to the next configuration item.
99         """
100         raise NotImplementedError(
101             'Configuration items need to implement execute')
102     def cancel(self, cr, uid, ids, context=None):
103         """ Method called when the user click on the ``Skip`` button.
104
105         ``cancel`` should be overloaded instead of ``action_skip``. As with
106         ``execute``, if it returns an action dictionary that action is
107         executed in stead of the default (going to the next configuration item)
108
109         The default implementation is a NOOP.
110
111         ``cancel`` is also called by the default implementation of
112         ``action_cancel``.
113         """
114         pass
115
116     def action_next(self, cr, uid, ids, context=None):
117         """ Action handler for the ``next`` event.
118
119         Sets the status of the todo the event was sent from to
120         ``done``, calls ``execute`` and -- unless ``execute`` returned
121         an action dictionary -- executes the action provided by calling
122         ``next``.
123         """
124         next = self.execute(cr, uid, ids, context=context)
125         if next: return next
126         return self.next(cr, uid, ids, context=context)
127
128     def action_skip(self, cr, uid, ids, context=None):
129         """ Action handler for the ``skip`` event.
130
131         Sets the status of the todo the event was sent from to
132         ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
133         an action dictionary -- executes the action provided by calling
134         ``next``.
135         """
136         next = self.cancel(cr, uid, ids, context=context)
137         if next: return next
138         return self.next(cr, uid, ids, context=context)
139
140     def action_cancel(self, cr, uid, ids, context=None):
141         """ Action handler for the ``cancel`` event. That event isn't
142         generated by the res.config.view.base inheritable view, the
143         inherited view has to overload one of the buttons (or add one
144         more).
145
146         Sets the status of the todo the event was sent from to
147         ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
148         an action dictionary -- executes the action provided by calling
149         ``next``.
150         """
151         next = self.cancel(cr, uid, ids, context=context)
152         if next: return next
153         return self.next(cr, uid, ids, context=context)
154
155 res_config_configurable()
156
157 class res_config_installer(osv.osv_memory):
158     """ New-style configuration base specialized for addons selection
159     and installation.
160
161     Basic usage
162     -----------
163
164     Subclasses can simply define a number of _columns as
165     fields.boolean objects. The keys (column names) should be the
166     names of the addons to install (when selected). Upon action
167     execution, selected boolean fields (and those only) will be
168     interpreted as addons to install, and batch-installed.
169
170     Additional addons
171     -----------------
172
173     It is also possible to require the installation of an additional
174     addon set when a specific preset of addons has been marked for
175     installation (in the basic usage only, additionals can't depend on
176     one another).
177
178     These additionals are defined through the ``_install_if``
179     property. This property is a mapping of a collection of addons (by
180     name) to a collection of addons (by name) [#]_, and if all the *key*
181     addons are selected for installation, then the *value* ones will
182     be selected as well. For example::
183
184         _install_if = {
185             ('sale','crm'): ['sale_crm'],
186         }
187
188     This will install the ``sale_crm`` addon if and only if both the
189     ``sale`` and ``crm`` addons are selected for installation.
190
191     You can define as many additionals as you wish, and additionals
192     can overlap in key and value. For instance::
193
194         _install_if = {
195             ('sale','crm'): ['sale_crm'],
196             ('sale','project'): ['project_mrp'],
197         }
198
199     will install both ``sale_crm`` and ``project_mrp`` if all of
200     ``sale``, ``crm`` and ``project`` are selected for installation.
201
202     Hook methods
203     ------------
204
205     Subclasses might also need to express dependencies more complex
206     than that provided by additionals. In this case, it's possible to
207     define methods of the form ``_if_%(name)s`` where ``name`` is the
208     name of a boolean field. If the field is selected, then the
209     corresponding module will be marked for installation *and* the
210     hook method will be executed.
211
212     Hook methods take the usual set of parameters (cr, uid, ids,
213     context) and can return a collection of additional addons to
214     install (if they return anything, otherwise they should not return
215     anything, though returning any "falsy" value such as None or an
216     empty collection will have the same effect).
217
218     Complete control
219     ----------------
220
221     The last hook is to simply overload the ``modules_to_install``
222     method, which implements all the mechanisms above. This method
223     takes the usual set of parameters (cr, uid, ids, context) and
224     returns a ``set`` of addons to install (addons selected by the
225     above methods minus addons from the *basic* set which are already
226     installed) [#]_ so an overloader can simply manipulate the ``set``
227     returned by ``res_config_installer.modules_to_install`` to add or
228     remove addons.
229
230     Skipping the installer
231     ----------------------
232
233     Unless it is removed from the view, installers have a *skip*
234     button which invokes ``action_skip`` (and the ``cancel`` hook from
235     ``res.config``). Hooks and additionals *are not run* when skipping
236     installation, even for already installed addons.
237
238     Again, setup your hooks accordingly.
239
240     .. [#] note that since a mapping key needs to be hashable, it's
241            possible to use a tuple or a frozenset, but not a list or a
242            regular set
243
244     .. [#] because the already-installed modules are only pruned at
245            the very end of ``modules_to_install``, additionals and
246            hooks depending on them *are guaranteed to execute*. Setup
247            your hooks accordingly.
248     """
249     _name = 'res.config.installer'
250     _inherit = 'res.config'
251
252     _install_if = {}
253
254     def already_installed(self, cr, uid, context=None):
255         """ For each module, check if it's already installed and if it
256         is return its name
257
258         :returns: a list of the already installed modules in this
259                   installer
260         :rtype: [str]
261         """
262         return map(attrgetter('name'),
263                    self._already_installed(cr, uid, context=context))
264
265     def _already_installed(self, cr, uid, context=None):
266         """ For each module (boolean fields in a res.config.installer),
267         check if it's already installed (either 'to install', 'to upgrade'
268         or 'installed') and if it is return the module's browse_record
269
270         :returns: a list of all installed modules in this installer
271         :rtype: [browse_record]
272         """
273         modules = self.pool.get('ir.module.module')
274
275         selectable = [field for field in self._columns
276                       if type(self._columns[field]) is fields.boolean]
277         return modules.browse(
278             cr, uid,
279             modules.search(cr, uid,
280                            [('name','in',selectable),
281                             ('state','in',['to install', 'installed', 'to upgrade'])],
282                            context=context),
283             context=context)
284
285
286     def modules_to_install(self, cr, uid, ids, context=None):
287         """ selects all modules to install:
288
289         * checked boolean fields
290         * return values of hook methods. Hook methods are of the form
291           ``_if_%(addon_name)s``, and are called if the corresponding
292           addon is marked for installation. They take the arguments
293           cr, uid, ids and context, and return an iterable of addon
294           names
295         * additionals, additionals are setup through the ``_install_if``
296           class variable. ``_install_if`` is a dict of {iterable:iterable}
297           where key and value are iterables of addon names.
298
299           If all the addons in the key are selected for installation
300           (warning: addons added through hooks don't count), then the
301           addons in the value are added to the set of modules to install
302         * not already installed
303         """
304         base = set(module_name
305                    for installer in self.read(cr, uid, ids, context=context)
306                    for module_name, to_install in installer.iteritems()
307                    if module_name != 'id'
308                    if type(self._columns[module_name]) is fields.boolean
309                    if to_install)
310
311         hooks_results = set()
312         for module in base:
313             hook = getattr(self, '_if_%s'% module, None)
314             if hook:
315                 hooks_results.update(hook(cr, uid, ids, context=None) or set())
316
317         additionals = set(
318             module for requirements, consequences \
319                        in self._install_if.iteritems()
320                    if base.issuperset(requirements)
321                    for module in consequences)
322
323         return (base | hooks_results | additionals).difference(
324                     self.already_installed(cr, uid, context))
325
326     def default_get(self, cr, uid, fields_list, context=None):
327         ''' If an addon is already installed, check it by default
328         '''
329         defaults = super(res_config_installer, self).default_get(
330             cr, uid, fields_list, context=context)
331
332         return dict(defaults,
333                     **dict.fromkeys(
334                         self.already_installed(cr, uid, context=context),
335                         True))
336
337     def fields_get(self, cr, uid, fields=None, context=None, write_access=True):
338         """ If an addon is already installed, set it to readonly as
339         res.config.installer doesn't handle uninstallations of already
340         installed addons
341         """
342         fields = super(res_config_installer, self).fields_get(
343             cr, uid, fields, context, write_access)
344
345         for name in self.already_installed(cr, uid, context=context):
346             if name not in fields:
347                 continue
348             fields[name].update(
349                 readonly=True,
350                 help= ustr(fields[name].get('help', '')) +
351                      _('\n\nThis addon is already installed on your system'))
352         return fields
353
354     def execute(self, cr, uid, ids, context=None):
355         modules = self.pool.get('ir.module.module')
356         to_install = list(self.modules_to_install(
357             cr, uid, ids, context=context))
358         _logger.info('Selecting addons %s to install', to_install)
359         modules.state_update(
360             cr, uid,
361             modules.search(cr, uid, [('name','in',to_install)]),
362             'to install', ['uninstalled'], context=context)
363         cr.commit() #TOFIX: after remove this statement, installation wizard is fail
364         new_db, self.pool = pooler.restart_pool(cr.dbname, update_module=True)
365
366 res_config_installer()
367
368 DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\
369     'wizards (ir.actions.configuration.wizard). Old-style configuration '\
370     'wizards have been deprecated.\n'\
371     'The addon should be migrated to res.config objects.'
372 class ir_actions_configuration_wizard(osv.osv_memory):
373     ''' Compatibility configuration wizard
374
375     The old configuration wizard has been replaced by res.config, but in order
376     not to break existing but not-yet-migrated addons, the old wizard was
377     reintegrated and gutted.
378     '''
379     _name='ir.actions.configuration.wizard'
380     _inherit = 'res.config'
381
382     def _next_action_note(self, cr, uid, ids, context=None):
383         next = self._next_action(cr, uid)
384         if next:
385             # if the next one is also an old-style extension, you never know...
386             if next.note:
387                 return next.note
388             return _("Click 'Continue' to configure the next addon...")
389         return _("Your database is now fully configured.\n\n"\
390             "Click 'Continue' and enjoy your OpenERP experience...")
391
392     _columns = {
393         'note': fields.text('Next Wizard', readonly=True),
394         }
395     _defaults = {
396         'note': _next_action_note,
397         }
398
399     def execute(self, cr, uid, ids, context=None):
400         _logger.warning(DEPRECATION_MESSAGE)
401
402 ir_actions_configuration_wizard()
403
404
405
406 class res_config_settings(osv.osv_memory):
407     """ Base configuration wizard for application settings.  It provides support for setting
408         default values, assigning groups to employee users, and installing modules.
409         To make such a 'settings' wizard, define a model like::
410
411             class my_config_wizard(osv.osv_memory):
412                 _name = 'my.settings'
413                 _inherit = 'res.config.settings'
414                 _columns = {
415                     'default_foo': fields.type(..., default_model='my.model'),
416                     'group_bar': fields.boolean(..., group='base.group_user', implied_group='my.group'),
417                     'module_baz': fields.boolean(...),
418                     'other_field': fields.type(...),
419                 }
420
421         The method ``execute`` provides some support based on a naming convention:
422
423         *   For a field like 'default_XXX', ``execute`` sets the (global) default value of
424             the field 'XXX' in the model named by ``default_model`` to the field's value.
425
426         *   For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
427             to/from the implied groups of 'group', depending on the field's value.
428             By default 'group' is the group Employee.  Groups are given by their xml id.
429
430         *   For a boolean field like 'module_XXX', ``execute`` triggers the immediate
431             installation of the module named 'XXX' if the field has value ``True``.
432
433         *   For the other fields, the method ``execute`` invokes all methods with a name
434             that starts with 'set_'; such methods can be defined to implement the effect
435             of those fields.
436
437         The method ``default_get`` retrieves values that reflect the current status of the
438         fields like 'default_XXX', 'group_XXX' and 'module_XXX'.  It also invokes all methods
439         with a name that starts with 'get_default_'; such methods can be defined to provide
440         current values for other fields.
441     """
442     _name = 'res.config.settings'
443
444     def copy(self, cr, uid, id, values, context=None):
445         raise osv.except_osv(_("Cannot duplicate configuration!"), "")
446
447     def _get_classified_fields(self, cr, uid, context=None):
448         """ return a dictionary with the fields classified by category::
449
450                 {   'default': [('default_foo', 'model', 'foo'), ...],
451                     'group':   [('group_bar', browse_group, browse_implied_group), ...],
452                     'module':  [('module_baz', browse_module), ...],
453                     'other':   ['other_field', ...],
454                 }
455         """
456         ir_model_data = self.pool.get('ir.model.data')
457         ir_module = self.pool.get('ir.module.module')
458         def ref(xml_id):
459             mod, xml = xml_id.split('.', 1)
460             return ir_model_data.get_object(cr, uid, mod, xml, context)
461
462         defaults, groups, modules, others = [], [], [], []
463         for name, field in self._columns.items():
464             if name.startswith('default_') and hasattr(field, 'default_model'):
465                 defaults.append((name, field.default_model, name[8:]))
466             elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
467                 field_group = getattr(field, 'group', 'base.group_user')
468                 groups.append((name, ref(field_group), ref(field.implied_group)))
469             elif name.startswith('module_') and isinstance(field, fields.boolean):
470                 mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
471                 record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
472                 modules.append((name, record))
473             else:
474                 others.append(name)
475
476         return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
477
478     def default_get(self, cr, uid, fields, context=None):
479         ir_values = self.pool.get('ir.values')
480         classified = self._get_classified_fields(cr, uid, context)
481
482         res = super(res_config_settings, self).default_get(cr, uid, fields, context)
483
484         # defaults: take the corresponding default value they set
485         for name, model, field in classified['default']:
486             value = ir_values.get_default(cr, uid, model, field)
487             if value is not None:
488                 res[name] = value
489
490         # groups: which groups are implied by the group Employee
491         for name, group, implied_group in classified['group']:
492             res[name] = implied_group in group.implied_ids
493
494         # modules: which modules are installed/to install
495         for name, module in classified['module']:
496             res[name] = module and module.state in ('installed', 'to install', 'to upgrade')
497
498         # other fields: call all methods that start with 'get_default_'
499         for method in dir(self):
500             if method.startswith('get_default_'):
501                 res.update(getattr(self, method)(cr, uid, fields, context))
502
503         return res
504
505     def execute(self, cr, uid, ids, context=None):
506         ir_values = self.pool.get('ir.values')
507         ir_module = self.pool.get('ir.module.module')
508         classified = self._get_classified_fields(cr, uid, context)
509
510         config = self.browse(cr, uid, ids[0], context)
511
512         # default values fields
513         for name, model, field in classified['default']:
514             ir_values.set_default(cr, uid, model, field, config[name])
515
516         # group fields: modify group / implied groups
517         for name, group, implied_group in classified['group']:
518             if config[name]:
519                 group.write({'implied_ids': [(4, implied_group.id)]})
520             else:
521                 group.write({'implied_ids': [(3, implied_group.id)]})
522                 implied_group.write({'users': [(3, u.id) for u in group.users]})
523
524         # other fields: execute all methods that start with 'set_'
525         for method in dir(self):
526             if method.startswith('set_'):
527                 getattr(self, method)(cr, uid, ids, context)
528
529         # module fields: install/uninstall the selected modules
530         to_install_missing_names = []
531         to_uninstall_ids = []
532         to_install_ids = []
533         lm = len('module_')
534         for name, module in classified['module']:
535             if config[name]:
536                 if not module:
537                     # missing module, will be provided by apps.openerp.com
538                     to_install_missing_names.append(name[lm:])
539                 elif module.state == 'uninstalled':
540                     # local module, to be installed
541                     to_install_ids.append(module.id)
542             else:
543                 if module and module.state in ('installed', 'to upgrade'):
544                     to_uninstall_ids.append(module.id)
545
546         if to_uninstall_ids:
547             ir_module.button_immediate_uninstall(cr, uid, to_uninstall_ids, context=context)
548         if to_install_ids:
549             ir_module.button_immediate_install(cr, uid, to_install_ids, context=context)
550
551         if to_install_missing_names:
552             return {
553                 'type': 'ir.actions.client',
554                 'tag': 'apps',
555                 'params': {'modules': to_install_missing_names},
556             }
557
558         config = self.pool.get('res.config').next(cr, uid, [], context=context) or {}
559         if config.get('type') not in ('ir.actions.act_window_close',):
560             return config
561
562         # force client-side reload (update user menu and current view)
563         return {
564             'type': 'ir.actions.client',
565             'tag': 'reload',
566         }
567
568     def cancel(self, cr, uid, ids, context=None):
569         # ignore the current record, and send the action to reopen the view
570         act_window = self.pool.get('ir.actions.act_window')
571         action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)])
572         if action_ids:
573             return act_window.read(cr, uid, action_ids[0], [], context=context)
574         return {}
575
576     def name_get(self, cr, uid, ids, context=None):
577         """ Override name_get method to return an appropriate configuration wizard
578         name, and not the generated name."""
579
580         if not ids:
581             return []
582         # name_get may receive int id instead of an id list
583         if isinstance(ids, (int, long)):
584             ids = [ids]
585
586         act_window = self.pool.get('ir.actions.act_window')
587         action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context)
588         name = self._name
589         if action_ids:
590             name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name']
591         return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)]
592
593     def get_option_path(self, cr, uid, menu_xml_id=None, context=None):
594         """
595         Fetch the path to a specified configuration view.
596
597         :param string menu_xml_id: the xml id of the menuitem where the view
598         is located, structured as follows: module_name.menuitem_xml_id
599         (e.g.: "base.menu_sale_config")
600         :return string: full path to the menuitem
601         (e.g.: "Settings/Configuration/Sales")
602         """
603         module_name, menu_xml_id = menu_xml_id.split('.')
604         dummy, menu_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, module_name, menu_xml_id)
605         ir_ui_menu = self.pool.get('ir.ui.menu').browse(cr, uid, menu_id, context=context)
606
607         return ir_ui_menu.complete_name
608
609     def get_option_name(self, cr, uid, full_field_name=None, context=None):
610         """
611         Fetch the human readable name of a specified configuration option.
612
613         :param string full_field_name: the full name of the field, structured
614         as follows: model_name.field_name
615         (e.g.: "sale.config.settings.fetchmail_lead")
616         :return string: human readable name of the field
617         (e.g.: "Create leads from incoming mails")
618         """
619         model_name, field_name = full_field_name.rsplit('.', 1)
620
621         return self.pool.get(model_name).fields_get(cr, uid, allfields=[field_name], context=context)[field_name]['string']
622
623 def get_warning_config(cr, msg, context=None):
624     """
625     Helper: return a WarningConfig exception with the given message where the
626     %(field:)s and/or %(menu:)s are replaced by the human readable field's name
627     and/or menuitem's full path.
628
629     Usage:
630     ------
631     Just include in your error message %(field:model_name.field_name)s to
632     obtain the human readable field's name, and/or
633     %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's full path.
634
635     Example of use:
636     ---------------
637     from openerp.addons.base.res.res_config import get_warning_config
638     raise get_warning_config(cr, _("Error: this action is prohibited. You should check the field %(field:sale.config.settings.fetchmail_lead)s in %(menu:base.menu_sale_config)s."), context=context)
639     will return an exception containing the following message:
640     Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales.
641     """
642
643     res_config_obj = pooler.get_pool(cr.dbname).get('res.config.settings')
644     regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s'
645
646     # Process the message
647     # 1/ find the path and/or field references, put them in a list
648     references = re.findall(regex_path, msg, flags=re.I)
649
650     # 2/ fetch the path and/or field replacement values
651     #    (full path and human readable field's name)
652     values = {}
653     for item in references:
654         ref_type, ref = item.split(':')
655         if ref_type == 'menu':
656             values[item] = res_config_obj.get_option_path(cr, SUPERUSER_ID, ref, context)
657         elif ref_type == 'field':
658             values[item] = res_config_obj.get_option_name(cr, SUPERUSER_ID, ref, context)
659
660     # 4/ substitute and return the result
661     return exceptions.WarningConfig(msg % values)
662
663 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: