diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/portal/controllers | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/portal/controllers')
| -rw-r--r-- | addons/portal/controllers/__init__.py | 6 | ||||
| -rw-r--r-- | addons/portal/controllers/mail.py | 247 | ||||
| -rw-r--r-- | addons/portal/controllers/portal.py | 437 | ||||
| -rw-r--r-- | addons/portal/controllers/web.py | 26 |
4 files changed, 716 insertions, 0 deletions
diff --git a/addons/portal/controllers/__init__.py b/addons/portal/controllers/__init__.py new file mode 100644 index 00000000..c1b076c5 --- /dev/null +++ b/addons/portal/controllers/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import web +from . import portal +from . import mail diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py new file mode 100644 index 00000000..612191b7 --- /dev/null +++ b/addons/portal/controllers/mail.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import werkzeug +from werkzeug import urls +from werkzeug.exceptions import NotFound, Forbidden + +from odoo import http, _ +from odoo.http import request +from odoo.osv import expression +from odoo.tools import consteq, plaintext2html +from odoo.addons.mail.controllers.main import MailController +from odoo.addons.portal.controllers.portal import CustomerPortal +from odoo.exceptions import AccessError, MissingError, UserError + + +def _check_special_access(res_model, res_id, token='', _hash='', pid=False): + record = request.env[res_model].browse(res_id).sudo() + if token: # Token Case: token is the global one of the document + token_field = request.env[res_model]._mail_post_token_field + return (token and record and consteq(record[token_field], token)) + elif _hash and pid: # Signed Token Case: hash implies token is signed by partner pid + return consteq(_hash, record._sign_token(pid)) + else: + raise Forbidden() + + +def _message_post_helper(res_model, res_id, message, token='', _hash=False, pid=False, nosubscribe=True, **kw): + """ Generic chatter function, allowing to write on *any* object that inherits mail.thread. We + distinguish 2 cases: + 1/ If a token is specified, all logged in users will be able to write a message regardless + of access rights; if the user is the public user, the message will be posted under the name + of the partner_id of the object (or the public user if there is no partner_id on the object). + + 2/ If a signed token is specified (`hash`) and also a partner_id (`pid`), all post message will + be done under the name of the partner_id (as it is signed). This should be used to avoid leaking + token to all users. + + Required parameters + :param string res_model: model name of the object + :param int res_id: id of the object + :param string message: content of the message + + 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 hash: signed token by a partner if model uses some token field to bypass access right + post messages. + :param string pid: identifier of the res.partner used to sign the hash + :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) + + # check if user can post with special token/signed token. The "else" will try to post message with the + # current user access rights (_mail_post_access use case). + if token or (_hash and pid): + pid = int(pid) if pid else False + if _check_special_access(res_model, res_id, token=token, _hash=_hash, pid=pid): + record = record.sudo() + else: + raise Forbidden() + + # deduce author of message + author_id = request.env.user.partner_id.id if request.env.user.partner_id else False + + # Token Case: author is document customer (if not logged) or itself even if user has not the access + if token: + if request.env.user._is_public(): + # TODO : After adding the pid and sign_token in access_url when send invoice by email, remove this line + # TODO : Author must be Public User (to rename to 'Anonymous') + author_id = record.partner_id.id if hasattr(record, 'partner_id') and record.partner_id.id else author_id + else: + if not author_id: + raise NotFound() + # Signed Token Case: author_id is forced + elif _hash and pid: + author_id = pid + + email_from = None + if author_id and 'email_from' not in kw: + partner = request.env['res.partner'].sudo().browse(author_id) + email_from = partner.email_formatted if partner.email else None + + message_post_args = dict( + body=message, + message_type=kw.pop('message_type', "comment"), + subtype_xmlid=kw.pop('subtype_xmlid', "mail.mt_comment"), + author_id=author_id, + **kw + ) + + # This is necessary as mail.message checks the presence + # of the key to compute its default email from + if email_from: + message_post_args['email_from'] = email_from + + return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(**message_post_args) + + +class PortalChatter(http.Controller): + + def _portal_post_filter_params(self): + return ['token', 'hash', 'pid'] + + def _portal_post_check_attachments(self, attachment_ids, attachment_tokens): + if len(attachment_tokens) != len(attachment_ids): + raise UserError(_("An access token must be provided for each attachment.")) + for (attachment_id, access_token) in zip(attachment_ids, attachment_tokens): + try: + CustomerPortal._document_check_access(self, 'ir.attachment', attachment_id, access_token) + except (AccessError, MissingError): + raise UserError(_("The attachment %s does not exist or you do not have the rights to access it.", attachment_id)) + + @http.route(['/mail/chatter_post'], type='http', methods=['POST'], auth='public', website=True) + def portal_chatter_post(self, res_model, res_id, message, redirect=None, attachment_ids='', attachment_tokens='', **kw): + """Create a new `mail.message` with the given `message` and/or + `attachment_ids` and redirect the user to the newly created message. + + The message will be associated to the record `res_id` of the model + `res_model`. The user must have access rights on this target document or + must provide valid identifiers through `kw`. See `_message_post_helper`. + """ + url = redirect or (request.httprequest.referrer and request.httprequest.referrer + "#discussion") or '/my' + + res_id = int(res_id) + + attachment_ids = [int(attachment_id) for attachment_id in attachment_ids.split(',') if attachment_id] + attachment_tokens = [attachment_token for attachment_token in attachment_tokens.split(',') if attachment_token] + self._portal_post_check_attachments(attachment_ids, attachment_tokens) + + if message or attachment_ids: + # message is received in plaintext and saved in html + if message: + message = plaintext2html(message) + post_values = { + 'res_model': res_model, + 'res_id': res_id, + 'message': message, + 'send_after_commit': False, + 'attachment_ids': False, # will be added afterward + } + post_values.update((fname, kw.get(fname)) for fname in self._portal_post_filter_params()) + message = _message_post_helper(**post_values) + + if attachment_ids: + # sudo write the attachment to bypass the read access + # verification in mail message + record = request.env[res_model].browse(res_id) + message_values = {'res_id': res_id, 'model': res_model} + attachments = record._message_post_process_attachments([], attachment_ids, message_values) + + if attachments.get('attachment_ids'): + message.sudo().write(attachments) + + return request.redirect(url) + + @http.route('/mail/chatter_init', type='json', auth='public', website=True) + def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs): + is_user_public = request.env.user.has_group('base.group_public') + message_data = self.portal_message_fetch(res_model, res_id, domain=domain, limit=limit, **kwargs) + display_composer = False + if kwargs.get('allow_composer'): + display_composer = kwargs.get('token') or not is_user_public + return { + 'messages': message_data['messages'], + 'options': { + 'message_count': message_data['message_count'], + 'is_user_public': is_user_public, + 'is_user_employee': request.env.user.has_group('base.group_user'), + 'is_user_publisher': request.env.user.has_group('website.group_website_publisher'), + 'display_composer': display_composer, + 'partner_id': request.env.user.partner_id.id + } + } + + @http.route('/mail/chatter_fetch', type='json', auth='public', website=True) + def portal_message_fetch(self, res_model, res_id, domain=False, limit=10, offset=0, **kw): + if not domain: + domain = [] + # Only search into website_message_ids, so apply the same domain to perform only one search + # extract domain from the 'website_message_ids' field + model = request.env[res_model] + field = model._fields['website_message_ids'] + field_domain = field.get_domain_list(model) + domain = expression.AND([domain, field_domain, [('res_id', '=', res_id)]]) + + # Check access + Message = request.env['mail.message'] + if kw.get('token'): + access_as_sudo = _check_special_access(res_model, res_id, token=kw.get('token')) + if not access_as_sudo: # if token is not correct, raise Forbidden + raise Forbidden() + # Non-employee see only messages with not internal subtype (aka, no internal logs) + if not request.env['res.users'].has_group('base.group_user'): + domain = expression.AND([Message._get_search_domain_share(), domain]) + Message = request.env['mail.message'].sudo() + return { + 'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(), + 'message_count': Message.search_count(domain) + } + + @http.route(['/mail/update_is_internal'], type='json', auth="user", website=True) + def portal_message_update_is_internal(self, message_id, is_internal): + message = request.env['mail.message'].browse(int(message_id)) + message.write({'is_internal': is_internal}) + return message.is_internal + + +class MailController(MailController): + + @classmethod + def _redirect_to_record(cls, model, res_id, access_token=None, **kwargs): + """ If the current user doesn't have access to the document, but provided + a valid access token, redirect him to the front-end view. + If the partner_id and hash parameters are given, add those parameters to the redirect url + to authentify the recipient in the chatter, if any. + + :param model: the model name of the record that will be visualized + :param res_id: the id of the record + :param access_token: token that gives access to the record + bypassing the rights and rules restriction of the user. + :param kwargs: Typically, it can receive a partner_id and a hash (sign_token). + If so, those two parameters are used to authentify the recipient in the chatter, if any. + :return: + """ + if issubclass(type(request.env[model]), request.env.registry['portal.mixin']): + uid = request.session.uid or request.env.ref('base.public_user').id + record_sudo = request.env[model].sudo().browse(res_id).exists() + try: + record_sudo.with_user(uid).check_access_rights('read') + record_sudo.with_user(uid).check_access_rule('read') + except AccessError: + if record_sudo.access_token and access_token and consteq(record_sudo.access_token, access_token): + record_action = record_sudo.with_context(force_website=True).get_access_action() + if record_action['type'] == 'ir.actions.act_url': + pid = kwargs.get('pid') + hash = kwargs.get('hash') + url = record_action['url'] + if pid and hash: + url = urls.url_parse(url) + url_params = url.decode_query() + url_params.update([("pid", pid), ("hash", hash)]) + url = url.replace(query=urls.url_encode(url_params)).to_url() + return werkzeug.utils.redirect(url) + return super(MailController, cls)._redirect_to_record(model, res_id, access_token=access_token) diff --git a/addons/portal/controllers/portal.py b/addons/portal/controllers/portal.py new file mode 100644 index 00000000..f0f4920a --- /dev/null +++ b/addons/portal/controllers/portal.py @@ -0,0 +1,437 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import functools +import json +import logging +import math +import re + +from werkzeug import urls + +from odoo import fields as odoo_fields, http, tools, _, SUPERUSER_ID +from odoo.exceptions import ValidationError, AccessError, MissingError, UserError, AccessDenied +from odoo.http import content_disposition, Controller, request, route +from odoo.tools import consteq + +# -------------------------------------------------- +# Misc tools +# -------------------------------------------------- + +_logger = logging.getLogger(__name__) +def pager(url, total, page=1, step=30, scope=5, url_args=None): + """ Generate a dict with required value to render `website.pager` template. This method compute + url, page range to display, ... in the pager. + :param url : base url of the page link + :param total : number total of item to be splitted into pages + :param page : current page + :param step : item per page + :param scope : number of page to display on pager + :param url_args : additionnal parameters to add as query params to page url + :type url_args : dict + :returns dict + """ + # Compute Pager + page_count = int(math.ceil(float(total) / step)) + + page = max(1, min(int(page if str(page).isdigit() else 1), page_count)) + scope -= 1 + + pmin = max(page - int(math.floor(scope/2)), 1) + pmax = min(pmin + scope, page_count) + + if pmax - pmin < scope: + pmin = pmax - scope if pmax - scope > 0 else 1 + + def get_url(page): + _url = "%s/page/%s" % (url, page) if page > 1 else url + if url_args: + _url = "%s?%s" % (_url, urls.url_encode(url_args)) + return _url + + return { + "page_count": page_count, + "offset": (page - 1) * step, + "page": { + 'url': get_url(page), + 'num': page + }, + "page_first": { + 'url': get_url(1), + 'num': 1 + }, + "page_start": { + 'url': get_url(pmin), + 'num': pmin + }, + "page_previous": { + 'url': get_url(max(pmin, page - 1)), + 'num': max(pmin, page - 1) + }, + "page_next": { + 'url': get_url(min(pmax, page + 1)), + 'num': min(pmax, page + 1) + }, + "page_end": { + 'url': get_url(pmax), + 'num': pmax + }, + "page_last": { + 'url': get_url(page_count), + 'num': page_count + }, + "pages": [ + {'url': get_url(page_num), 'num': page_num} for page_num in range(pmin, pmax+1) + ] + } + + +def get_records_pager(ids, current): + if current.id in ids and (hasattr(current, 'website_url') or hasattr(current, 'access_url')): + attr_name = 'access_url' if hasattr(current, 'access_url') else 'website_url' + idx = ids.index(current.id) + return { + 'prev_record': idx != 0 and getattr(current.browse(ids[idx - 1]), attr_name), + 'next_record': idx < len(ids) - 1 and getattr(current.browse(ids[idx + 1]), attr_name), + } + return {} + + +def _build_url_w_params(url_string, query_params, remove_duplicates=True): + """ Rebuild a string url based on url_string and correctly compute query parameters + using those present in the url and those given by query_params. Having duplicates in + the final url is optional. For example: + + * url_string = '/my?foo=bar&error=pay' + * query_params = {'foo': 'bar2', 'alice': 'bob'} + * if remove duplicates: result = '/my?foo=bar2&error=pay&alice=bob' + * else: result = '/my?foo=bar&foo=bar2&error=pay&alice=bob' + """ + url = urls.url_parse(url_string) + url_params = url.decode_query() + if remove_duplicates: # convert to standard dict instead of werkzeug multidict to remove duplicates automatically + url_params = url_params.to_dict() + url_params.update(query_params) + return url.replace(query=urls.url_encode(url_params)).to_url() + + +class CustomerPortal(Controller): + + MANDATORY_BILLING_FIELDS = ["name", "phone", "email", "street", "city", "country_id"] + OPTIONAL_BILLING_FIELDS = ["zipcode", "state_id", "vat", "company_name"] + + _items_per_page = 20 + + def _prepare_portal_layout_values(self): + """Values for /my/* templates rendering. + + Does not include the record counts. + """ + # get customer sales rep + sales_user = False + partner = request.env.user.partner_id + if partner.user_id and not partner.user_id._is_public(): + sales_user = partner.user_id + + return { + 'sales_user': sales_user, + 'page_name': 'home', + } + + def _prepare_home_portal_values(self, counters): + """Values for /my & /my/home routes template rendering. + + Includes the record count for the displayed badges. + where 'coutners' is the list of the displayed badges + and so the list to compute. + """ + return {} + + @route(['/my/counters'], type='json', auth="user", website=True) + def counters(self, counters, **kw): + return self._prepare_home_portal_values(counters) + + @route(['/my', '/my/home'], type='http', auth="user", website=True) + def home(self, **kw): + values = self._prepare_portal_layout_values() + return request.render("portal.portal_my_home", values) + + @route(['/my/account'], type='http', auth='user', website=True) + def account(self, redirect=None, **post): + values = self._prepare_portal_layout_values() + partner = request.env.user.partner_id + values.update({ + 'error': {}, + 'error_message': [], + }) + + if post and request.httprequest.method == 'POST': + error, error_message = self.details_form_validate(post) + values.update({'error': error, 'error_message': error_message}) + values.update(post) + if not error: + values = {key: post[key] for key in self.MANDATORY_BILLING_FIELDS} + values.update({key: post[key] for key in self.OPTIONAL_BILLING_FIELDS if key in post}) + for field in set(['country_id', 'state_id']) & set(values.keys()): + try: + values[field] = int(values[field]) + except: + values[field] = False + values.update({'zip': values.pop('zipcode', '')}) + partner.sudo().write(values) + if redirect: + return request.redirect(redirect) + return request.redirect('/my/home') + + countries = request.env['res.country'].sudo().search([]) + states = request.env['res.country.state'].sudo().search([]) + + values.update({ + 'partner': partner, + 'countries': countries, + 'states': states, + 'has_check_vat': hasattr(request.env['res.partner'], 'check_vat'), + 'redirect': redirect, + 'page_name': 'my_details', + }) + + response = request.render("portal.portal_my_details", values) + response.headers['X-Frame-Options'] = 'DENY' + return response + + @route('/my/security', type='http', auth='user', website=True, methods=['GET', 'POST']) + def security(self, **post): + values = self._prepare_portal_layout_values() + values['get_error'] = get_error + + if request.httprequest.method == 'POST': + values.update(self._update_password( + post['old'].strip(), + post['new1'].strip(), + post['new2'].strip() + )) + + return request.render('portal.portal_my_security', values, headers={ + 'X-Frame-Options': 'DENY' + }) + + def _update_password(self, old, new1, new2): + for k, v in [('old', old), ('new1', new1), ('new2', new2)]: + if not v: + return {'errors': {'password': {k: _("You cannot leave any password empty.")}}} + + if new1 != new2: + return {'errors': {'password': {'new2': _("The new password and its confirmation must be identical.")}}} + + try: + request.env['res.users'].change_password(old, new1) + except UserError as e: + return {'errors': {'password': e.name}} + except AccessDenied as e: + msg = e.args[0] + if msg == AccessDenied().args[0]: + msg = _('The old password you provided is incorrect, your password was not changed.') + return {'errors': {'password': {'old': msg}}} + + # update session token so the user does not get logged out (cache cleared by passwd change) + new_token = request.env.user._compute_session_token(request.session.sid) + request.session.session_token = new_token + + return {'success': {'password': True}} + + @http.route('/portal/attachment/add', type='http', auth='public', methods=['POST'], website=True) + def attachment_add(self, name, file, res_model, res_id, access_token=None, **kwargs): + """Process a file uploaded from the portal chatter and create the + corresponding `ir.attachment`. + + The attachment will be created "pending" until the associated message + is actually created, and it will be garbage collected otherwise. + + :param name: name of the file to save. + :type name: string + + :param file: the file to save + :type file: werkzeug.FileStorage + + :param res_model: name of the model of the original document. + To check access rights only, it will not be saved here. + :type res_model: string + + :param res_id: id of the original document. + To check access rights only, it will not be saved here. + :type res_id: int + + :param access_token: access_token of the original document. + To check access rights only, it will not be saved here. + :type access_token: string + + :return: attachment data {id, name, mimetype, file_size, access_token} + :rtype: dict + """ + try: + self._document_check_access(res_model, int(res_id), access_token=access_token) + except (AccessError, MissingError) as e: + raise UserError(_("The document does not exist or you do not have the rights to access it.")) + + IrAttachment = request.env['ir.attachment'] + access_token = False + + # Avoid using sudo or creating access_token when not necessary: internal + # users can create attachments, as opposed to public and portal users. + if not request.env.user.has_group('base.group_user'): + IrAttachment = IrAttachment.sudo().with_context(binary_field_real_user=IrAttachment.env.user) + access_token = IrAttachment._generate_access_token() + + # At this point the related message does not exist yet, so we assign + # those specific res_model and res_is. They will be correctly set + # when the message is created: see `portal_chatter_post`, + # or garbage collected otherwise: see `_garbage_collect_attachments`. + attachment = IrAttachment.create({ + 'name': name, + 'datas': base64.b64encode(file.read()), + 'res_model': 'mail.compose.message', + 'res_id': 0, + 'access_token': access_token, + }) + return request.make_response( + data=json.dumps(attachment.read(['id', 'name', 'mimetype', 'file_size', 'access_token'])[0]), + headers=[('Content-Type', 'application/json')] + ) + + @http.route('/portal/attachment/remove', type='json', auth='public') + def attachment_remove(self, attachment_id, access_token=None): + """Remove the given `attachment_id`, only if it is in a "pending" state. + + The user must have access right on the attachment or provide a valid + `access_token`. + """ + try: + attachment_sudo = self._document_check_access('ir.attachment', int(attachment_id), access_token=access_token) + except (AccessError, MissingError) as e: + raise UserError(_("The attachment does not exist or you do not have the rights to access it.")) + + if attachment_sudo.res_model != 'mail.compose.message' or attachment_sudo.res_id != 0: + raise UserError(_("The attachment %s cannot be removed because it is not in a pending state.", attachment_sudo.name)) + + if attachment_sudo.env['mail.message'].search([('attachment_ids', 'in', attachment_sudo.ids)]): + raise UserError(_("The attachment %s cannot be removed because it is linked to a message.", attachment_sudo.name)) + + return attachment_sudo.unlink() + + def details_form_validate(self, data): + error = dict() + error_message = [] + + # Validation + for field_name in self.MANDATORY_BILLING_FIELDS: + if not data.get(field_name): + error[field_name] = 'missing' + + # email validation + if data.get('email') and not tools.single_email_re.match(data.get('email')): + error["email"] = 'error' + error_message.append(_('Invalid Email! Please enter a valid email address.')) + + # vat validation + partner = request.env.user.partner_id + if data.get("vat") and partner and partner.vat != data.get("vat"): + if partner.can_edit_vat(): + if hasattr(partner, "check_vat"): + if data.get("country_id"): + data["vat"] = request.env["res.partner"].fix_eu_vat_number(int(data.get("country_id")), data.get("vat")) + partner_dummy = partner.new({ + 'vat': data['vat'], + 'country_id': (int(data['country_id']) + if data.get('country_id') else False), + }) + try: + partner_dummy.check_vat() + except ValidationError: + error["vat"] = 'error' + else: + error_message.append(_('Changing VAT number is not allowed once document(s) have been issued for your account. Please contact us directly for this operation.')) + + # error message for empty required fields + if [err for err in error.values() if err == 'missing']: + error_message.append(_('Some required fields are empty.')) + + unknown = [k for k in data if k not in self.MANDATORY_BILLING_FIELDS + self.OPTIONAL_BILLING_FIELDS] + if unknown: + error['common'] = 'Unknown field' + error_message.append("Unknown field '%s'" % ','.join(unknown)) + + return error, error_message + + def _document_check_access(self, model_name, document_id, access_token=None): + document = request.env[model_name].browse([document_id]) + document_sudo = document.with_user(SUPERUSER_ID).exists() + if not document_sudo: + raise MissingError(_("This document does not exist.")) + try: + document.check_access_rights('read') + document.check_access_rule('read') + except AccessError: + if not access_token or not document_sudo.access_token or not consteq(document_sudo.access_token, access_token): + raise + return document_sudo + + def _get_page_view_values(self, document, access_token, values, session_history, no_breadcrumbs, **kwargs): + if access_token: + # if no_breadcrumbs = False -> force breadcrumbs even if access_token to `invite` users to register if they click on it + values['no_breadcrumbs'] = no_breadcrumbs + values['access_token'] = access_token + values['token'] = access_token # for portal chatter + + # Those are used notably whenever the payment form is implied in the portal. + if kwargs.get('error'): + values['error'] = kwargs['error'] + if kwargs.get('warning'): + values['warning'] = kwargs['warning'] + if kwargs.get('success'): + values['success'] = kwargs['success'] + # Email token for posting messages in portal view with identified author + if kwargs.get('pid'): + values['pid'] = kwargs['pid'] + if kwargs.get('hash'): + values['hash'] = kwargs['hash'] + + history = request.session.get(session_history, []) + values.update(get_records_pager(history, document)) + + return values + + def _show_report(self, model, report_type, report_ref, download=False): + if report_type not in ('html', 'pdf', 'text'): + raise UserError(_("Invalid report type: %s", report_type)) + + report_sudo = request.env.ref(report_ref).with_user(SUPERUSER_ID) + + if not isinstance(report_sudo, type(request.env['ir.actions.report'])): + raise UserError(_("%s is not the reference of a report", report_ref)) + + if hasattr(model, 'company_id'): + report_sudo = report_sudo.with_company(model.company_id) + + method_name = '_render_qweb_%s' % (report_type) + report = getattr(report_sudo, method_name)([model.id], data={'report_type': report_type})[0] + reporthttpheaders = [ + ('Content-Type', 'application/pdf' if report_type == 'pdf' else 'text/html'), + ('Content-Length', len(report)), + ] + if report_type == 'pdf' and download: + filename = "%s.pdf" % (re.sub('\W+', '-', model._get_report_base_filename())) + reporthttpheaders.append(('Content-Disposition', content_disposition(filename))) + return request.make_response(report, headers=reporthttpheaders) + +def get_error(e, path=''): + """ Recursively dereferences `path` (a period-separated sequence of dict + keys) in `e` (an error dict or value), returns the final resolution IIF it's + an str, otherwise returns None + """ + for k in (path.split('.') if path else []): + if not isinstance(e, dict): + return None + e = e.get(k) + + return e if isinstance(e, str) else None diff --git a/addons/portal/controllers/web.py b/addons/portal/controllers/web.py new file mode 100644 index 00000000..9e6e6175 --- /dev/null +++ b/addons/portal/controllers/web.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.addons.web.controllers.main import Home +from odoo.http import request + + +class Home(Home): + + @http.route() + def index(self, *args, **kw): + if request.session.uid and not request.env['res.users'].sudo().browse(request.session.uid).has_group('base.group_user'): + return http.local_redirect('/my', query=request.params, keep_hash=True) + return super(Home, self).index(*args, **kw) + + def _login_redirect(self, uid, redirect=None): + if not redirect and not request.env['res.users'].sudo().browse(uid).has_group('base.group_user'): + redirect = '/my' + return super(Home, self)._login_redirect(uid, redirect=redirect) + + @http.route('/web', type='http', auth="none") + def web_client(self, s_action=None, **kw): + if request.session.uid and not request.env['res.users'].sudo().browse(request.session.uid).has_group('base.group_user'): + return http.local_redirect('/my', query=request.params, keep_hash=True) + return super(Home, self).web_client(s_action, **kw) |
