[IMP] portal: allow attachment upload from portal chatter
Before this commit, it was only possible to add attachments on documents from the backend or by sending them by email. It is now possible to add them also from the portal chatter, including for portal/public users who have a valid access_token. Part of task-37264 closes odoo/odoo#34526 Signed-off-by: Olivier Dony (odo) <odo@openerp.com> Co-authored-by: Pratima Gupta <pgu@odoo.com> Co-authored-by: Sébastien Theys <seb@odoo.com>
This commit is contained in:
committed by
Sébastien Theys
co-authored by
Sébastien Theys
parent
997bd55016
commit
61de1c263a
@@ -10,7 +10,8 @@ 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.exceptions import AccessError
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
|
||||
|
||||
def _check_special_access(res_model, res_id, token='', _hash='', pid=False):
|
||||
@@ -78,7 +79,9 @@ def _message_post_helper(res_model, res_id, message, token='', nosubscribe=True,
|
||||
author_id = kw.get('pid')
|
||||
|
||||
kw.pop('csrf_token', None)
|
||||
kw.pop('redirect', None)
|
||||
kw.pop('attachment_ids', None)
|
||||
kw.pop('attachment_tokens', None)
|
||||
kw.pop('hash', None)
|
||||
kw.pop('pid', None)
|
||||
return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(
|
||||
@@ -92,14 +95,66 @@ def _message_post_helper(res_model, res_id, message, token='', nosubscribe=True,
|
||||
|
||||
class PortalChatter(http.Controller):
|
||||
|
||||
def _post_process_portal_attachments(self, message, res_model, res_id, attachment_ids, attachment_tokens):
|
||||
"""Associate "pending" attachments to a message and its main record.
|
||||
|
||||
The user must have the rights to all attachments or he must provide a
|
||||
valid access_token for each of them. The attachments have to be on a
|
||||
"pending" state as well: res_id=0 and res_model='mail.compose.message'.
|
||||
|
||||
:param message: the related message on which to link the attachments
|
||||
:type message: recordset of one `mail.message`
|
||||
|
||||
:param res_model: the related model that will be saved on the attachment
|
||||
:type res_model: string
|
||||
|
||||
:param res_id: the id of the record that will be saved on the attachment
|
||||
:type res_id: int
|
||||
|
||||
:param attachment_ids: id of the attachments to associate
|
||||
:type attachment_ids: iterable of int
|
||||
|
||||
:param attachment_tokens: access_token of the attachments to associate.
|
||||
Must always be the same length as `attachment_ids`, but only has to
|
||||
contain a valid access_token if the user does not have the rights to
|
||||
access the attachment without token.
|
||||
:type attachment_tokens: iterable of string
|
||||
"""
|
||||
message.ensure_one()
|
||||
attachments = request.env['ir.attachment'].sudo()
|
||||
for (attachment_id, access_token) in zip(attachment_ids, attachment_tokens):
|
||||
try:
|
||||
attachment = CustomerPortal._document_check_access(self, 'ir.attachment', attachment_id, access_token)
|
||||
if attachment.res_model == 'mail.compose.message' and attachment.res_id == 0:
|
||||
attachments += attachment
|
||||
except (AccessError, MissingError):
|
||||
pass
|
||||
attachments.write({'res_model': res_model, 'res_id': res_id})
|
||||
message.attachment_ids |= attachments
|
||||
|
||||
@http.route(['/mail/chatter_post'], type='http', methods=['POST'], auth='public', website=True)
|
||||
def portal_chatter_post(self, res_model, res_id, message, **kw):
|
||||
url = request.httprequest.referrer
|
||||
if message:
|
||||
def portal_chatter_post(self, res_model, res_id, message, redirect=None, attachment_ids=None, attachment_tokens=None, **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'
|
||||
if message or (attachment_ids and attachment_tokens):
|
||||
res_id = int(res_id)
|
||||
# message is received in plaintext and saved in html
|
||||
message = plaintext2html(message)
|
||||
_message_post_helper(res_model, int(res_id), message, **kw)
|
||||
url = url + "#discussion"
|
||||
if message:
|
||||
message = plaintext2html(message)
|
||||
message = _message_post_helper(res_model=res_model, res_id=res_id, message=message, **kw)
|
||||
|
||||
if attachment_ids and attachment_tokens and message:
|
||||
attachment_ids = [int(res_id) for res_id in attachment_ids.split(',')]
|
||||
attachment_tokens = attachment_tokens.split(',')
|
||||
self._post_process_portal_attachments(message=message, res_model=res_model, res_id=res_id,
|
||||
attachment_ids=attachment_ids, attachment_tokens=attachment_tokens)
|
||||
|
||||
return request.redirect(url)
|
||||
|
||||
@http.route('/mail/chatter_init', type='json', auth='public', website=True)
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import fields as odoo_fields, tools, _, SUPERUSER_ID
|
||||
from odoo import fields as odoo_fields, http, tools, _, SUPERUSER_ID
|
||||
from odoo.exceptions import ValidationError, AccessError, MissingError, UserError
|
||||
from odoo.http import content_disposition, Controller, request, route
|
||||
from odoo.tools import consteq
|
||||
@@ -196,6 +196,85 @@ class CustomerPortal(Controller):
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
return response
|
||||
|
||||
@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 `_post_process_portal_attachments`,
|
||||
# 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 = []
|
||||
@@ -249,7 +328,7 @@ class CustomerPortal(Controller):
|
||||
document.check_access_rights('read')
|
||||
document.check_access_rule('read')
|
||||
except AccessError:
|
||||
if not access_token or not consteq(document_sudo.access_token, access_token):
|
||||
if not access_token or not document_sudo.access_token or not consteq(document_sudo.access_token, access_token):
|
||||
raise
|
||||
return document_sudo
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
odoo.define('portal.chatter', function (require) {
|
||||
'use strict';
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var time = require('web.time');
|
||||
@@ -18,6 +19,9 @@ var PortalComposer = publicWidget.Widget.extend({
|
||||
template: 'portal.Composer',
|
||||
xmlDependencies: ['/portal/static/src/xml/portal_chatter.xml'],
|
||||
events: {
|
||||
'change .o_portal_chatter_file_input': '_onFileInputChange',
|
||||
'click .o_portal_chatter_attachment_btn': '_onAttachmentButtonClick',
|
||||
'click .o_portal_chatter_attachment_delete': 'async _onAttachmentDeleteClick',
|
||||
'click .o_portal_chatter_composer_btn': 'async _onSubmitButtonClick',
|
||||
},
|
||||
|
||||
@@ -34,6 +38,20 @@ var PortalComposer = publicWidget.Widget.extend({
|
||||
'res_model': false,
|
||||
'res_id': false,
|
||||
});
|
||||
this.attachments = [];
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$attachmentButton = this.$('.o_portal_chatter_attachment_btn');
|
||||
this.$fileInput = this.$('.o_portal_chatter_file_input');
|
||||
this.$sendButton = this.$('.o_portal_chatter_composer_btn');
|
||||
this.$attachments = this.$('.o_portal_chatter_composer_form .o_portal_chatter_attachments');
|
||||
this.$attachmentIds = this.$('.o_portal_chatter_attachment_ids');
|
||||
this.$attachmentTokens = this.$('.o_portal_chatter_attachment_tokens');
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -43,8 +61,98 @@ var PortalComposer = publicWidget.Widget.extend({
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onAttachmentButtonClick: function () {
|
||||
this.$fileInput.click();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onAttachmentDeleteClick: function (ev) {
|
||||
var self = this;
|
||||
var attachmentId = $(ev.currentTarget).closest('.o_portal_chatter_attachment').data('id');
|
||||
var accessToken = _.find(this.attachments, {'id': attachmentId}).access_token;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.$sendButton.prop('disabled', true);
|
||||
|
||||
return this._rpc({
|
||||
route: '/portal/attachment/remove',
|
||||
params: {
|
||||
'attachment_id': attachmentId,
|
||||
'access_token': accessToken,
|
||||
},
|
||||
}).then(function () {
|
||||
self.attachments = _.reject(self.attachments, {'id': attachmentId});
|
||||
self._updateAttachments();
|
||||
self.$sendButton.prop('disabled', false);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onFileInputChange: function () {
|
||||
var self = this;
|
||||
|
||||
this.$sendButton.prop('disabled', true);
|
||||
|
||||
return Promise.all(_.map(this.$fileInput[0].files, function (file) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var data = {
|
||||
'name': file.name,
|
||||
'file': file,
|
||||
'res_id': self.options.res_id,
|
||||
'res_model': self.options.res_model,
|
||||
'access_token': self.options.token,
|
||||
};
|
||||
ajax.post('/portal/attachment/add', data).then(function (attachment) {
|
||||
self.attachments.push(attachment);
|
||||
self._updateAttachments();
|
||||
resolve();
|
||||
}).guardedCatch(function (error) {
|
||||
self.displayNotification({
|
||||
title: _t("Oops! Something went wrong."),
|
||||
message: _.str.sprintf(_t("The file <strong>%s</strong> could not be saved."),
|
||||
_.escape(file.name)),
|
||||
type: 'warning',
|
||||
sticky: true,
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
})).then(function () {
|
||||
self.$sendButton.prop('disabled', false);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Returns a Promise that is never resolved to prevent sending the form
|
||||
* twice when clicking twice on the button, in combination with the `async`
|
||||
* in the event definition.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onSubmitButtonClick: function () {
|
||||
return new Promise(function() {});
|
||||
return new Promise(function (resolve, reject) {});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_updateAttachments: function () {
|
||||
this.$attachmentIds.val(_.pluck(this.attachments, 'id'));
|
||||
this.$attachmentTokens.val(_.pluck(this.attachments, 'access_token'));
|
||||
this.$attachments.html(qweb.render('portal.Chatter.Attachments', {
|
||||
attachments: this.attachments,
|
||||
showDelete: true,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -450,6 +450,20 @@ img.o_we_custom_image {
|
||||
.o_portal_chatter_pager {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_portal_chatter_attachment {
|
||||
.o_portal_chatter_attachment_name {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.o_portal_chatter_attachment_delete {
|
||||
@include o-position-absolute($top: 0, $right: 0);
|
||||
opacity: 0;
|
||||
}
|
||||
&:hover .o_portal_chatter_attachment_delete {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form label {
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
-->
|
||||
<t t-name="portal.Composer">
|
||||
<div class="o_portal_chatter_composer" t-if="widget.options['allow_composer']">
|
||||
<t t-set="discussion_url" t-value="window.encodeURI(window.location.href.split('#')[0] + '#discussion')"/>
|
||||
<t t-if="!widget.options['display_composer']">
|
||||
<h4>Leave a comment</h4>
|
||||
<p>You must be <a t-attf-href="/web/login?redirect=#{window.encodeURI(window.location.href + '#discussion')}">logged in</a> to post a comment.</p>
|
||||
<p>You must be <a t-attf-href="/web/login?redirect=#{discussion_url}">logged in</a> to post a comment.</p>
|
||||
</t>
|
||||
<t t-if="widget.options['display_composer']">
|
||||
<div class="media">
|
||||
@@ -41,18 +42,46 @@
|
||||
<input type='hidden' name="hash" t-att-value="widget.options['hash']" t-if="widget.options['hash']"/>
|
||||
<input type="hidden" name="sha_in" t-att-value="widget.options['sha_in']" t-if="widget.options['sha_in']"/>
|
||||
<input type="hidden" name="sha_time" t-att-value="widget.options['sha_time']" t-if="widget.options['sha_time']"/>
|
||||
<input type="hidden" name="redirect" t-att-value="discussion_url"/>
|
||||
<input type="hidden" name="attachment_ids" class="o_portal_chatter_attachment_ids"/>
|
||||
<input type="hidden" name="attachment_tokens" class="o_portal_chatter_attachment_tokens"/>
|
||||
<div class="alert alert-danger mt8 mb0 o_portal_chatter_composer_error" style="display:none;" role="alert">
|
||||
Oops! Something went wrong. Try to reload the page and log in.
|
||||
</div>
|
||||
<button t-attf-class="o_portal_chatter_composer_btn btn btn-primary mt8 o_website_message_post_helper" type="submit">Send</button>
|
||||
<div class="o_portal_chatter_attachments mt-3"/>
|
||||
<div class="mt8">
|
||||
<button t-attf-class="o_portal_chatter_composer_btn btn btn-primary" type="submit">Send</button>
|
||||
<button class="o_portal_chatter_attachment_btn btn btn-secondary" type="button" title="Add attachment">
|
||||
<i class="fa fa-paperclip"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="d-none">
|
||||
<input type="file" class="o_portal_chatter_file_input" multiple="multiple"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="portal.Chatter.Attachments">
|
||||
<div t-if="attachments.length" class="row">
|
||||
<div t-foreach="attachments" t-as="attachment" class="col-lg-2 col-md-3 col-sm-6">
|
||||
<div class="o_portal_chatter_attachment mb-2 position-relative text-center" t-att-data-id="attachment.id">
|
||||
<button t-if="showDelete" class="o_portal_chatter_attachment_delete btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
|
||||
<div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype"/>
|
||||
<div class='o_portal_chatter_attachment_name'><t t-esc='attachment.name'/></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Widget PortalChatter, and subtemplates
|
||||
-->
|
||||
@@ -71,12 +100,9 @@
|
||||
<t t-raw="message.body"/>
|
||||
|
||||
<div class="o_portal_chatter_attachments">
|
||||
<div class="col-lg-2 col-md-3 text-center" t-foreach='message.attachment_ids' t-as='attachment'>
|
||||
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
|
||||
<div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80"/>
|
||||
<div class='oe_attachment_name'><t t-raw='attachment.name'/></div>
|
||||
</a>
|
||||
</div>
|
||||
<t t-call="portal.Chatter.Attachments">
|
||||
<t t-set="attachments" t-value="message.attachment_ids"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_load_process
|
||||
from . import test_attachment
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
from odoo import http, tests
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tests.tagged('post_install', '-at_install')
|
||||
class TestUi(tests.HttpCase):
|
||||
|
||||
@mute_logger('odoo.addons.website.models.ir_http', 'odoo.http')
|
||||
def test_01_portal_attachment(self):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
# For this test we need a parent document with an access_token field:
|
||||
# attachment itself has an access_token so we can use it as parent too.
|
||||
record = self.env['ir.attachment'].create({
|
||||
'name': 'a record with an access_token field',
|
||||
})
|
||||
|
||||
# Test public user can't create attachment without token of document
|
||||
create_data = {
|
||||
'name': "new attachment",
|
||||
'res_model': record._name,
|
||||
'res_id': record.id,
|
||||
'csrf_token': http.WebRequest.csrf_token(self),
|
||||
}
|
||||
create_url = base_url + '/portal/attachment/add'
|
||||
files = [('file', ('test.txt', b'test', 'plain/text'))]
|
||||
res = self.url_open(url=create_url, data=create_data, files=files)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertTrue("you do not have the rights" in res.text)
|
||||
|
||||
# Test public user can create attachment with token
|
||||
create_data['access_token'] = record.generate_access_token()[0]
|
||||
res = self.url_open(url=create_url, data=create_data, files=files)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
create_res = json.loads(res.content.decode('utf-8'))
|
||||
self.assertEqual(create_res['name'], "new attachment")
|
||||
|
||||
# Test created attachment is private
|
||||
res_binary = self.url_open('/web/content/%d' % create_res['id'])
|
||||
self.assertEqual(res_binary.status_code, 404)
|
||||
|
||||
# Test created access_token is working
|
||||
res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
|
||||
self.assertEqual(res_binary.status_code, 200)
|
||||
|
||||
# Test mimetype is neutered as non-admin
|
||||
files = [('file', ('test.svg', b'<svg></svg>', 'image/svg+xml'))]
|
||||
res = self.url_open(url=create_url, data=create_data, files=files)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
create_res = json.loads(res.content.decode('utf-8'))
|
||||
self.assertEqual(create_res['mimetype'], 'text/plain')
|
||||
|
||||
res_binary = self.url_open('/web/content/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
|
||||
self.assertEqual(res_binary.headers['Content-Type'], 'text/plain')
|
||||
self.assertEqual(res_binary.content, b'<svg></svg>')
|
||||
|
||||
res_image = self.url_open('/web/image/%d?access_token=%s' % (create_res['id'], create_res['access_token']))
|
||||
self.assertEqual(res_image.headers['Content-Type'], 'text/plain')
|
||||
self.assertEqual(res_image.content, b'<svg></svg>')
|
||||
|
||||
# Test attachment can't be removed without valid token
|
||||
remove_data = {
|
||||
'attachment_id': create_res['id'],
|
||||
'access_token': 'wrong',
|
||||
}
|
||||
remove_url = base_url + '/portal/attachment/remove'
|
||||
res = self.opener.post(url=remove_url, json={'params': remove_data})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(self.env['ir.attachment'].search([('id', '=', create_res['id'])]))
|
||||
self.assertTrue("you do not have the rights" in res.text)
|
||||
|
||||
# Test attachment can be removed with token if "pending" state
|
||||
remove_data['access_token'] = create_res['access_token']
|
||||
res = self.opener.post(url=remove_url, json={'params': remove_data})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
remove_res = json.loads(res.content.decode('utf-8'))['result']
|
||||
self.assertFalse(self.env['ir.attachment'].search([('id', '=', create_res['id'])]))
|
||||
self.assertTrue(remove_res is True)
|
||||
|
||||
# Test attachment can't be removed if not "pending" state
|
||||
remove_data = {
|
||||
'attachment_id': record.id,
|
||||
'access_token': record.access_token,
|
||||
}
|
||||
res = self.opener.post(url=remove_url, json={'params': remove_data})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(self.env['ir.attachment'].search([('id', '=', record.id)]))
|
||||
self.assertTrue("not in a pending state" in res.text)
|
||||
|
||||
# Test attachment can't be removed if attached to a message
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': 'a record with an access_token field',
|
||||
'res_model': 'mail.compose.message',
|
||||
'res_id': 0,
|
||||
'access_token': self.env['ir.attachment']._generate_access_token(),
|
||||
})
|
||||
message = self.env['mail.message'].create({
|
||||
'attachment_ids': [(6, 0, attachment.ids)],
|
||||
})
|
||||
remove_data = {
|
||||
'attachment_id': attachment.id,
|
||||
'access_token': attachment.access_token,
|
||||
}
|
||||
res = self.opener.post(url=remove_url, json={'params': remove_data})
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTrue(attachment.exists())
|
||||
self.assertTrue("it is linked to a message" in res.text)
|
||||
message.unlink()
|
||||
|
||||
# Need a `mail.thread` record for the following test
|
||||
thread_record = self.env['mail.channel'].create({
|
||||
'name': 'channel',
|
||||
'public': 'public',
|
||||
})
|
||||
# Authenticate because public user can't create `mail.message` without
|
||||
# token, and `mail.channel` does not have an access_token.
|
||||
self.authenticate('portal', 'portal')
|
||||
|
||||
# Test attachment can't be associated if no token.
|
||||
post_url = base_url + '/mail/chatter_post'
|
||||
post_data = {
|
||||
'res_model': thread_record._name,
|
||||
'res_id': thread_record.id,
|
||||
'message': "test message",
|
||||
'attachment_ids': attachment.id,
|
||||
'attachment_tokens': 'false',
|
||||
'csrf_token': http.WebRequest.csrf_token(self),
|
||||
}
|
||||
self.assertFalse(thread_record.message_ids)
|
||||
res = self.url_open(url=post_url, data=post_data)
|
||||
thread_record.invalidate_cache(fnames=['message_ids'], ids=thread_record.ids)
|
||||
message = thread_record.message_ids[0]
|
||||
self.assertFalse(message.attachment_ids)
|
||||
|
||||
# Test attachment can't be associated if not "pending" state
|
||||
post_data['attachment_tokens'] = attachment.access_token
|
||||
attachment.write({'res_model': 'model'})
|
||||
res = self.url_open(url=post_url, data=post_data)
|
||||
thread_record.invalidate_cache(fnames=['message_ids'], ids=thread_record.ids)
|
||||
message = thread_record.message_ids[0]
|
||||
self.assertFalse(message.attachment_ids)
|
||||
|
||||
# Test attachment can be associated if all good
|
||||
attachment.write({'res_model': 'mail.compose.message'})
|
||||
res = self.url_open(url=post_url, data=post_data)
|
||||
thread_record.invalidate_cache(fnames=['message_ids'], ids=thread_record.ids)
|
||||
message = thread_record.message_ids[0]
|
||||
self.assertEqual(len(message.attachment_ids), 1)
|
||||
@@ -883,11 +883,11 @@ class HttpCase(TransactionCase):
|
||||
self.opener = requests.Session()
|
||||
self.opener.cookies['session_id'] = self.session_id
|
||||
|
||||
def url_open(self, url, data=None, timeout=10, headers=None):
|
||||
def url_open(self, url, data=None, files=None, timeout=10, headers=None):
|
||||
if url.startswith('/'):
|
||||
url = "http://%s:%s%s" % (HOST, PORT, url)
|
||||
if data:
|
||||
return self.opener.post(url, data=data, timeout=timeout, headers=headers)
|
||||
if data or files:
|
||||
return self.opener.post(url, data=data, files=files, timeout=timeout, headers=headers)
|
||||
return self.opener.get(url, timeout=timeout, headers=headers)
|
||||
|
||||
def _wait_remaining_requests(self):
|
||||
|
||||
Reference in New Issue
Block a user