diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-08-07 17:01:16 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-08-07 17:01:16 +0200 |
| commit | 65a80cf3fd2fd0c47c774ac9cc183d9d3bc4af55 (patch) | |
| tree | df78f525ff8140df45de948a02203d63c1108c54 | |
| parent | 43eca3079e488683983cd6ac2161c828b676f697 (diff) | |
| parent | 0c5cbbe0bb4209dd6924494cd0c4d07dd3892b9e (diff) | |
[MERGE] forward port branch saas-14 up to 0c5cbbe0bb4
| -rw-r--r-- | addons/auth_signup/models/res_partner.py | 12 | ||||
| -rw-r--r-- | addons/auth_signup/models/res_users.py | 12 | ||||
| -rw-r--r-- | addons/crm/tests/test_lead2opportunity.py | 2 | ||||
| -rw-r--r-- | addons/event_sale/models/sale_order.py | 11 | ||||
| -rw-r--r-- | addons/mail/models/mail_template.py | 12 | ||||
| -rw-r--r-- | addons/mail/views/mail_template_views.xml | 7 | ||||
| -rw-r--r-- | addons/point_of_sale/static/src/js/screens.js | 3 | ||||
| -rw-r--r-- | addons/report/models/report.py | 2 | ||||
| -rw-r--r-- | addons/sale/models/sale.py | 2 | ||||
| -rw-r--r-- | addons/web/controllers/main.py | 5 | ||||
| -rw-r--r-- | addons/website/models/ir_attachment.py | 6 | ||||
| -rw-r--r-- | addons/website_mail/controllers/main.py | 5 | ||||
| -rw-r--r-- | addons/website_mail/models/mail_message.py | 6 | ||||
| -rw-r--r-- | odoo/addons/base/ir/ir_actions.py | 12 | ||||
| -rw-r--r-- | odoo/addons/base/ir/ir_attachment.py | 21 | ||||
| -rw-r--r-- | odoo/addons/base/ir/ir_qweb/assetsbundle.py | 7 | ||||
| -rw-r--r-- | odoo/addons/base/module/module.py | 1 | ||||
| -rw-r--r-- | odoo/addons/base/res/res_users.py | 27 | ||||
| -rw-r--r-- | odoo/http.py | 1 | ||||
| -rw-r--r-- | odoo/models.py | 15 | ||||
| -rw-r--r-- | odoo/tools/misc.py | 21 |
21 files changed, 141 insertions, 49 deletions
diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index a1d9f7d3ec5..1f79b89e992 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -26,9 +26,9 @@ def now(**kwargs): class ResPartner(models.Model): _inherit = 'res.partner' - signup_token = fields.Char(copy=False) - signup_type = fields.Char(string='Signup Token Type', copy=False) - signup_expiration = fields.Datetime(copy=False) + signup_token = fields.Char(copy=False, groups="base.group_erp_manager") + signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager") + signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager") signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid') signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL') @@ -36,15 +36,17 @@ class ResPartner(models.Model): @api.depends('signup_token', 'signup_expiration') def _compute_signup_valid(self): dt = now() - for partner in self: + for partner in self.sudo(): partner.signup_valid = bool(partner.signup_token) and \ (not partner.signup_expiration or dt <= partner.signup_expiration) @api.multi def _compute_signup_url(self): """ proxy for function field towards actual implementation """ - result = self._get_signup_url_for_action() + result = self.sudo()._get_signup_url_for_action() for partner in self: + if any(u.has_group('base.group_user') for u in partner.user_ids if u != self.env.user): + self.env['res.users'].check_access_rights('write') partner.signup_url = result.get(partner.id, False) @api.multi diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py index f5c0a6f2d88..c170b7ab8a4 100644 --- a/addons/auth_signup/models/res_users.py +++ b/addons/auth_signup/models/res_users.py @@ -132,10 +132,20 @@ class ResUsers(models.Model): template = self.env.ref('auth_signup.reset_password_email') assert template._name == 'mail.template' + template_values = { + 'email_to': '${object.email|safe}', + 'email_cc': False, + 'auto_delete': True, + 'partner_to': False, + 'scheduled_date': False, + } + template.write(template_values) + for user in self: if not user.email: raise UserError(_("Cannot send email: user %s has no email address.") % user.name) - template.with_context(lang=user.lang).send_mail(user.id, force_send=True, raise_exception=True) + with self.env.cr.savepoint(): + template.with_context(lang=user.lang).send_mail(user.id, force_send=True, raise_exception=True) _logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email) @api.model diff --git a/addons/crm/tests/test_lead2opportunity.py b/addons/crm/tests/test_lead2opportunity.py index 861444b3f7c..e4847b5bf45 100644 --- a/addons/crm/tests/test_lead2opportunity.py +++ b/addons/crm/tests/test_lead2opportunity.py @@ -38,7 +38,7 @@ class TestLead2opportunity2win(TestCrmCases): crm_case_3.message_post(subject='Test note', body='Détails envoyés par le client sur le FAX pour la qualité') # I convert mass lead into opportunity customer. - mass = CrmLead2OpportunityPartnerMass.with_context({'active_model': 'crm.lead', 'active_ids': [crm_case_13.id, crm_case_2.id], 'active_id': crm_case_13.id}).create({ + mass = CrmLead2OpportunityPartnerMass.sudo(self.crm_salemanager.id).with_context({'active_model': 'crm.lead', 'active_ids': [crm_case_13.id, crm_case_2.id], 'active_id': crm_case_13.id}).create({ 'user_ids': [(6, 0, self.env.ref('base.user_root').ids)], 'team_id': self.env.ref("sales_team.team_sales_department").id }) diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 6902d8e4743..2ac4deaed5f 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -8,12 +8,13 @@ class SaleOrder(models.Model): @api.multi def action_confirm(self): - self.ensure_one() res = super(SaleOrder, self).action_confirm() - # confirm registration if it was free (otherwise it will be confirmed once invoice fully paid) - self.order_line._update_registrations(confirm=self.amount_total == 0, cancel_to_draft=False) - if any(self.order_line.filtered(lambda line: line.event_id)): - return self.env['ir.actions.act_window'].with_context(default_sale_order_id=self.id).for_xml_id('event_sale', 'action_sale_order_event_registration') + for order in self: + # confirm registration if it was free (otherwise it will be confirmed once invoice fully paid) + order.order_line._update_registrations(confirm=order.amount_total == 0, cancel_to_draft=False) + if any(order.order_line.filtered(lambda line: line.event_id)): + return self.env['ir.actions.act_window'].with_context(default_sale_order_id=order.id).for_xml_id( + 'event_sale', 'action_sale_order_event_registration') return res diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 5e9a96b3959..bf7b89c5c0a 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -255,22 +255,22 @@ class MailTemplate(models.Model): def unlink_action(self): for template in self: if template.ref_ir_act_window: - template.ref_ir_act_window.sudo().unlink() + template.ref_ir_act_window.unlink() if template.ref_ir_value: - template.ref_ir_value.sudo().unlink() + template.ref_ir_value.unlink() return True @api.multi def create_action(self): - ActWindowSudo = self.env['ir.actions.act_window'].sudo() - IrValuesSudo = self.env['ir.values'].sudo() + ActWindow = self.env['ir.actions.act_window'] + IrValues = self.env['ir.values'] view = self.env.ref('mail.email_compose_message_wizard_form') for template in self: src_obj = template.model_id.model button_name = _('Send Mail (%s)') % template.name - action = ActWindowSudo.create({ + action = ActWindow.create({ 'name': button_name, 'type': 'ir.actions.act_window', 'res_model': 'mail.compose.message', @@ -281,7 +281,7 @@ class MailTemplate(models.Model): 'view_id': view.id, 'target': 'new', }) - ir_value = IrValuesSudo.create({ + ir_value = IrValues.create({ 'name': button_name, 'model': src_obj, 'key2': 'client_action_multi', diff --git a/addons/mail/views/mail_template_views.xml b/addons/mail/views/mail_template_views.xml index 483c80ae394..4306f1fe8d1 100644 --- a/addons/mail/views/mail_template_views.xml +++ b/addons/mail/views/mail_template_views.xml @@ -9,7 +9,9 @@ <sheet> <div class="oe_button_box" name="button_box"> <field name="ref_ir_act_window" invisible="1"/> - <button class="oe_stat_button" name="create_action" type="object" + <button class="oe_stat_button" + groups="base.group_system" + name="create_action" type="object" attrs="{'invisible':[('ref_ir_act_window','!=',False)]}" icon="fa-plus" help="Display an option on related documents to open a composition wizard with this template"> <div class="o_form_field o_stat_info"> @@ -17,7 +19,8 @@ <span class="o_stat_text">Context Action</span> </div> </button> - <button name="unlink_action" type="object" + <button name="unlink_action" type="object" + groups="base.group_system" class="oe_stat_button" icon="fa-minus" attrs="{'invisible':[('ref_ir_act_window','=',False)]}" help="Remove the contextual action to use this template on related documents" widget="statinfo"> diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index e64e12ffd38..b37dc13ff95 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1317,6 +1317,9 @@ var ClientListScreenWidget = ScreenWidget.extend({ reload_partners: function(){ var self = this; return this.pos.load_new_partners().then(function(){ + // partners may have changed in the backend + self.partner_cache = new DomCache(); + self.render_list(self.pos.db.get_partners_sorted(1000)); // update the currently assigned client if it has been changed in db. diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 45ee4174f0f..4600c4eaf48 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -531,7 +531,7 @@ class Report(models.Model): :specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments :returns: list of string representing the wkhtmltopdf arguments """ - command_args = [] + command_args = ['--disable-local-file-access'] if paperformat.format and paperformat.format != 'custom': command_args.extend(['--page-size', paperformat.format]) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 3c5a2c5ea09..97ebacb14f6 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1018,13 +1018,13 @@ class SaleOrderLine(models.Model): @api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id') def _onchange_discount(self): - self.discount = 0.0 if not (self.product_id and self.product_uom and self.order_id.partner_id and self.order_id.pricelist_id and self.order_id.pricelist_id.discount_policy == 'without_discount' and self.env.user.has_group('sale.group_discount_per_so_line')): return + self.discount = 0.0 product = self.product_id.with_context( lang=self.order_id.partner_id.lang, partner=self.order_id.partner_id.id, diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index bbded29de1d..5d2d0c0bd2d 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -714,6 +714,8 @@ class Database(http.Controller): @http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False) def restore(self, master_pwd, backup_file, name, copy=False): try: + data_file = None + db.check_super(master_pwd) with tempfile.NamedTemporaryFile(delete=False) as data_file: backup_file.save(data_file) db.restore_db(name, data_file.name, str2bool(copy)) @@ -722,7 +724,8 @@ class Database(http.Controller): error = "Database restore error: %s" % (str(e) or repr(e)) return self._render_template(error=error) finally: - os.unlink(data_file.name) + if data_file: + os.unlink(data_file.name) @http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False) def change_password(self, master_pwd, master_pwd_new): diff --git a/addons/website/models/ir_attachment.py b/addons/website/models/ir_attachment.py index 215b182bfb8..eb81cd6702c 100644 --- a/addons/website/models/ir_attachment.py +++ b/addons/website/models/ir_attachment.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class Attachment(models.Model): @@ -10,3 +10,7 @@ class Attachment(models.Model): # related for backward compatibility with saas-6 website_url = fields.Char(string="Attachment URL", related='local_url', deprecated=True) + + @api.model + def get_serving_groups(self): + return super(Attachment, self).get_serving_groups() + ['website.group_website_designer'] diff --git a/addons/website_mail/controllers/main.py b/addons/website_mail/controllers/main.py index 546def6890a..802867df51e 100644 --- a/addons/website_mail/controllers/main.py +++ b/addons/website_mail/controllers/main.py @@ -20,14 +20,14 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', token_ optional keywords arguments: :param string token: access token if the object's model uses some kind of public access using tokens (usually a uuid4) to bypass access rules - :param string token_field: name of the field that contains the token on the object (defaults to 'token') + :param string token_field: name of the field that contains the token on the object (deprecated, use _mail_post_token_field) :param bool nosubscribe: set False if you want the partner to be set as follower of the object when posting (default to True) The rest of the kwargs are passed on to message_post() """ record = request.env[res_model].browse(res_id) author_id = request.env.user.partner_id.id if request.env.user.partner_id else False - if token and record and token == getattr(record.sudo(), token_field, None): + if token and record and token == getattr(record.sudo(), record._mail_post_token_field, None): record = record.sudo() if request.env.user == request.env.ref('base.public_user'): author_id = record.partner_id.id if hasattr(record, 'partner_id') else author_id @@ -35,6 +35,7 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', token_ if not author_id: raise NotFound() kw.pop('csrf_token', None) + kw.pop('attachment_ids', None) return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(body=message, message_type=kw.pop('message_type', "comment"), subtype=kw.pop('subtype', "mt_comment"), diff --git a/addons/website_mail/models/mail_message.py b/addons/website_mail/models/mail_message.py index d5e12e41b0b..e60392f62bd 100644 --- a/addons/website_mail/models/mail_message.py +++ b/addons/website_mail/models/mail_message.py @@ -56,3 +56,9 @@ class MailMessage(models.Model): if self.env.cr.fetchall(): raise AccessError(_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)) return super(MailMessage, self).check_access_rule(operation=operation) + + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + _mail_post_token_field = 'access_token' # token field for external posts, to be overridden diff --git a/odoo/addons/base/ir/ir_actions.py b/odoo/addons/base/ir/ir_actions.py index 3a6b4076ebd..5690577b597 100644 --- a/odoo/addons/base/ir/ir_actions.py +++ b/odoo/addons/base/ir/ir_actions.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime -import dateutil import logging import os import time @@ -13,9 +12,20 @@ from odoo import api, fields, models, tools, _ from odoo.exceptions import MissingError, UserError, ValidationError from odoo.report.report_sxw import report_sxw, report_rml from odoo.tools.safe_eval import safe_eval, test_python_expr +from odoo.tools.misc import wrap_module _logger = logging.getLogger(__name__) +# build dateutil helper, starting with the relevant *lazy* imports +import dateutil +import dateutil.parser +import dateutil.relativedelta +import dateutil.rrule +import dateutil.tz +mods = {'parser', 'relativedelta', 'rrule', 'tz'} +attribs = {atr for m in mods for atr in getattr(dateutil, m).__all__} +dateutil = wrap_module(dateutil, mods | attribs) + class IrActions(models.Model): _name = 'ir.actions.actions' diff --git a/odoo/addons/base/ir/ir_attachment.py b/odoo/addons/base/ir/ir_attachment.py index b6bd9a08ee7..f6302fc5794 100644 --- a/odoo/addons/base/ir/ir_attachment.py +++ b/odoo/addons/base/ir/ir_attachment.py @@ -10,7 +10,7 @@ import re from collections import defaultdict from odoo import api, fields, models, tools, SUPERUSER_ID, _ -from odoo.exceptions import AccessError +from odoo.exceptions import AccessError, ValidationError from odoo.tools import config, human_size, ustr, html_escape from odoo.tools.mimetypes import guess_mimetype @@ -262,6 +262,15 @@ class IrAttachment(models.Model): index_content = ustr("\n".join(words)) return index_content + @api.model + def get_serving_groups(self): + """ An ir.attachment record may be used as a fallback in the + http dispatch if its type field is set to "binary" and its url + field is set as the request's url. Only the groups returned by + this method are allowed to create and write on such records. + """ + return ['base.group_system'] + name = fields.Char('Attachment Name', required=True) datas_fname = fields.Char('File Name') description = fields.Text('Description') @@ -295,6 +304,16 @@ class IrAttachment(models.Model): self._table, ['res_model', 'res_id']) return res + @api.one + @api.constrains('type', 'url') + def _check_serving_attachments(self): + # restrict writing on attachments that could be served by the + # ir.http's dispatch exception handling + if self.type == 'binary' and self.url: + has_group = self.env.user.has_group + if not any([has_group(g) for g in self.get_serving_groups()]): + raise ValidationError("Sorry, you are not allowed to write on this document") + @api.model def check(self, mode, values=None): """Restricts the access to an ir.attachment, according to referred model diff --git a/odoo/addons/base/ir/ir_qweb/assetsbundle.py b/odoo/addons/base/ir/ir_qweb/assetsbundle.py index 3c11e9d0bc0..288e433fc69 100644 --- a/odoo/addons/base/ir/ir_qweb/assetsbundle.py +++ b/odoo/addons/base/ir/ir_qweb/assetsbundle.py @@ -8,7 +8,7 @@ import textwrap import uuid from datetime import datetime from subprocess import Popen, PIPE -from odoo import fields, tools +from odoo import fields, tools, SUPERUSER_ID from odoo.http import request from odoo.modules.module import get_resource_path import psycopg2 @@ -190,10 +190,11 @@ class AssetsBundle(object): self.env.cr.execute(""" SELECT max(id) FROM ir_attachment - WHERE url like %s + WHERE create_uid = %s + AND url like %s GROUP BY datas_fname ORDER BY datas_fname - """, [url_pattern]) + """, [SUPERUSER_ID, url_pattern]) attachment_ids = [r[0] for r in self.env.cr.fetchall()] return self.env['ir.attachment'].sudo().browse(attachment_ids) diff --git a/odoo/addons/base/module/module.py b/odoo/addons/base/module/module.py index e499eb6550b..82893800496 100644 --- a/odoo/addons/base/module/module.py +++ b/odoo/addons/base/module/module.py @@ -169,6 +169,7 @@ class Module(models.Model): 'doctitle_xform': False, 'output_encoding': 'unicode', 'xml_declaration': False, + 'file_insertion_enabled': False, } output = publish_string(source=module.description or '', settings_overrides=overrides, writer=MyWriter()) module.description_html = tools.html_sanitize(output) diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index 03058c4065c..8b3a9fd5549 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -282,6 +282,19 @@ class Users(models.Model): if any(user.company_ids and user.company_id not in user.company_ids for user in self): raise ValidationError(_('The chosen company is not in the allowed companies for this user')) + def _read_from_database(self, field_names, inherited_field_names=[]): + super(Users, self)._read_from_database(field_names, inherited_field_names) + canwrite = self.check_access_rights('write', raise_exception=False) + if not canwrite and set(USER_PRIVATE_FIELDS).intersection(field_names): + for record in self: + for f in USER_PRIVATE_FIELDS: + try: + record._cache[f] + record._cache[f] = '********' + except Exception: + # skip SpecialValue (e.g. for missing record or access right) + pass + @api.multi @api.constrains('action_id') def _check_action_id(self): @@ -299,19 +312,7 @@ class Users(models.Model): # safe fields only, so we read as super-user to bypass access rights self = self.sudo() - result = super(Users, self).read(fields=fields, load=load) - - canwrite = self.env['ir.model.access'].check('res.users', 'write', False) - if not canwrite: - def override_password(vals): - if (vals['id'] != self._uid): - for key in USER_PRIVATE_FIELDS: - if key in vals: - vals[key] = '********' - return vals - result = map(override_password, result) - - return result + return super(Users, self).read(fields=fields, load=load) @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): diff --git a/odoo/http.py b/odoo/http.py index 0075b1bed34..5a8f6448956 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -1523,6 +1523,7 @@ def db_filter(dbs, httprequest=None): if d == "www" and r: d = r.partition('.')[0] if odoo.tools.config['dbfilter']: + d, h = re.escape(d), re.escape(h) r = odoo.tools.config['dbfilter'].replace('%h', h).replace('%d', d) dbs = [i for i in dbs if re.match(r, i)] elif odoo.tools.config['db_name']: diff --git a/odoo/models.py b/odoo/models.py index cf80394380f..959aea918c8 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -60,7 +60,7 @@ _unlink = logging.getLogger(__name__ + '.unlink') regex_order = re.compile('^(\s*([a-z0-9:_]+|"[a-z0-9:_]+")(\s+(desc|asc))?\s*(,|$))+(?<!,)$', re.I) regex_object_name = re.compile(r'^[a-z0-9_.]+$') regex_pg_name = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I) -onchange_v7 = re.compile(r"^(\w+)\((.*)\)$") +onchange_v7 = re.compile(r"^([a-zA-Z]\w+)\((.*)\)$") AUTOINIT_RECALCULATE_STORED_FIELDS = 1000 @@ -3541,6 +3541,15 @@ class BaseModel(object): if table not in query.tables: query.tables.append(table) + if self._transient: + # One single implicit access rule for transient models: owner only! + # This is ok because we assert that TransientModels always have + # log_access enabled, so that 'create_uid' is always there. + domain = [('create_uid', '=', self._uid)] + tquery = self._where_calc(domain, active_test=False) + apply_rule(tquery.where_clause, tquery.where_clause_params, tquery.tables) + return + # apply main rules on the object Rule = self.env['ir.rule'] where_clause, where_params, tables = Rule.domain_get(self._name, mode) @@ -3688,10 +3697,6 @@ class BaseModel(object): """ self.sudo(access_rights_uid or self._uid).check_access_rights('read') - # For transient models, restrict access to the current user, except for the super-user - if self.is_transient() and self._log_access and self._uid != SUPERUSER_ID: - args = expression.AND(([('create_uid', '=', self._uid)], args or [])) - query = self._where_calc(args) self._apply_ir_rules(query, 'read') order_by = self._generate_order_by(order, query) diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 05c772e71e4..2e0bc23ed30 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -21,6 +21,7 @@ import socket import sys import threading import time +import types import werkzeug.utils import zipfile from cStringIO import StringIO @@ -1253,3 +1254,23 @@ class Pickle(object): dump = cPickle.dump pickle = Pickle + +def wrap_module(module, attr_list): + """Helper for wrapping a package/module to expose selected attributes + + :param Module module: the actual package/module to wrap, as returned by ``import <module>`` + :param iterable attr_list: a global list of attributes to expose, usually the top-level + attributes and their own main attributes. No support for hiding attributes in case + of name collision at different levels. + """ + attr_list = set(attr_list) + class WrappedModule(object): + def __getattr__(self, attrib): + if attrib in attr_list: + target = getattr(module, attrib) + if isinstance(target, types.ModuleType): + return wrap_module(target, attr_list) + return target + raise AttributeError(attrib) + # module and attr_list are in the closure + return WrappedModule() |
