[IMP] website_sale : llow bulk uploading of images from frontend
This commit adds more functionalities to the web editor when editing a product's page in order to make it more user friendly to add change or resequence the many images a product may have. TaskId-2692189 closes odoo/odoo#85686 Signed-off-by: William Braeckman (wbr) <wbr@odoo.com>
This commit is contained in:
committed by
William Braeckman
parent
8821fb0641
commit
3958d15086
@@ -454,6 +454,145 @@ class WebsiteSale(http.Controller):
|
||||
# Compatibility pre-v14
|
||||
return request.redirect(_build_url_w_params("/shop/%s" % slug(product), request.params), code=301)
|
||||
|
||||
@http.route(['/shop/product/extra-images'], type='json', auth='user', website=True)
|
||||
def add_product_images(self, images, product_product_id, product_template_id, combination_ids=None):
|
||||
"""
|
||||
Turns a list of image ids refering to ir.attachments to product.images,
|
||||
links all of them to product.
|
||||
:raises NotFound : If the user is not allowed to access Attachment model
|
||||
"""
|
||||
|
||||
if not (request.env.user.has_group('website.group_website_designer') or\
|
||||
request.env.user.has_group('website.group_website_publisher')):
|
||||
raise NotFound()
|
||||
|
||||
image_ids = request.env["ir.attachment"].browse(i['id'] for i in images)
|
||||
image_create_data = [Command.create({
|
||||
'name': image.name, # Images uploaded from url do not have any datas. This recovers them manually
|
||||
'image_1920': image.datas if image.datas else request.env['ir.qweb.field.image'].load_remote_url(image.url),
|
||||
}) for image in image_ids]
|
||||
|
||||
product_product = request.env['product.product'].browse(int(product_product_id)) if product_product_id else False
|
||||
product_template = request.env['product.template'].browse(int(product_template_id)) if product_template_id else False
|
||||
|
||||
if product_product and not product_template:
|
||||
product_template = product_product.product_tmpl_id
|
||||
|
||||
if not product_product and product_template and product_template.has_dynamic_attributes():
|
||||
combination = request.env['product.template.attribute.value'].browse(combination_ids)
|
||||
product_product = product_template._get_variant_for_combination(combination)
|
||||
if not product_product:
|
||||
product_product = request.env['product.product'].browse(
|
||||
product_template.create_product_variant(combination_ids))
|
||||
if product_template.has_configurable_attributes and product_product:
|
||||
product_product.write({
|
||||
'product_variant_image_ids': image_create_data
|
||||
})
|
||||
else:
|
||||
product_template.write({
|
||||
'product_template_image_ids': image_create_data
|
||||
})
|
||||
|
||||
@http.route(['/shop/product/clear-images'], type='json', auth='user', website=True)
|
||||
def clear_product_images(self, product_product_id, product_template_id):
|
||||
"""
|
||||
Unlinks all images from the product.
|
||||
"""
|
||||
if not (request.env.user.has_group('website.group_website_designer') or\
|
||||
request.env.user.has_group('website.group_website_publisher')):
|
||||
raise NotFound()
|
||||
|
||||
product_product = request.env['product.product'].browse(int(product_product_id)) if product_product_id else False
|
||||
product_template = request.env['product.template'].browse(int(product_template_id)) if product_template_id else False
|
||||
|
||||
if product_product and not product_template:
|
||||
product_template = product_product.product_tmpl_id
|
||||
|
||||
if product_product and product_product.product_variant_image_ids:
|
||||
product_product.product_variant_image_ids.unlink()
|
||||
else:
|
||||
product_template.product_template_image_ids.unlink()
|
||||
|
||||
@http.route(['/shop/product/remove-image'], type='json', auth='user', website=True)
|
||||
def remove_product_image(self, image_res_model, image_res_id):
|
||||
"""
|
||||
Delete or clear the product's image.
|
||||
"""
|
||||
if not (request.env.user.has_group('website.group_website_designer') or\
|
||||
request.env.user.has_group('website.group_website_publisher')) or\
|
||||
image_res_model not in ['product.product', 'product.template', 'product.image']:
|
||||
raise NotFound()
|
||||
|
||||
image_res_id = int(image_res_id)
|
||||
if image_res_model == 'product.product':
|
||||
request.env['product.product'].browse(image_res_id).image_1920 = False
|
||||
elif image_res_model == 'product.template':
|
||||
request.env['product.template'].browse(image_res_id).image_1920 = False
|
||||
else:
|
||||
request.env['product.image'].browse(image_res_id).unlink()
|
||||
|
||||
@http.route(['/shop/product/resequence-image'], type='json', auth='user', website=True)
|
||||
def resequence_product_image(self, image_res_model, image_res_id, move):
|
||||
if not (request.env.user.has_group('website.group_website_designer') or\
|
||||
request.env.user.has_group('website.group_website_publisher')) or\
|
||||
image_res_model not in ['product.product', 'product.template', 'product.image'] or\
|
||||
move not in ['first', 'left', 'right', 'last']:
|
||||
raise NotFound()
|
||||
|
||||
image_res_id = int(image_res_id)
|
||||
image_to_resequence = request.env[image_res_model].browse(image_res_id)
|
||||
product = request.env['product.product']
|
||||
product_template = request.env['product.template']
|
||||
if image_res_model == 'product.product':
|
||||
product = image_to_resequence
|
||||
product_template = product.product_tmpl_id
|
||||
elif image_res_model == 'product.template':
|
||||
product_template = image_to_resequence
|
||||
product = product_template.product_variant_id
|
||||
else:
|
||||
product = image_to_resequence.product_variant_id
|
||||
product_template = product.product_tmpl_id or image_to_resequence.product_tmpl_id
|
||||
|
||||
if not product and not product_template:
|
||||
raise ValidationError(_("Product not found"))
|
||||
|
||||
product_images = (product or product_template)._get_images()
|
||||
if image_to_resequence not in product_images:
|
||||
raise ValidationError(_("Invalid image"))
|
||||
|
||||
image_idx = product_images.index(image_to_resequence)
|
||||
new_image_idx = 0
|
||||
if move == 'left':
|
||||
new_image_idx = max(0, image_idx - 1)
|
||||
elif move == 'right':
|
||||
new_image_idx = min(len(product_images) - 1, image_idx + 1)
|
||||
elif move == 'last':
|
||||
new_image_idx = len(product_images) - 1
|
||||
|
||||
# no-op resequences
|
||||
if new_image_idx == image_idx:
|
||||
return
|
||||
# We can not move an embedded image to the first position (main product image)
|
||||
if image_res_model == 'product.image' and image_to_resequence.video_url and product_images[new_image_idx]._name != 'product.image':
|
||||
raise ValidationError(_("Can not resequence embedded image/video with a non compatible image."))
|
||||
|
||||
# Swap images
|
||||
other_image = product_images[new_image_idx]
|
||||
source_field = hasattr(image_to_resequence, 'video_url') and image_to_resequence.video_url and 'video_url' or 'image_1920'
|
||||
target_field = hasattr(other_image, 'video_url') and other_image.video_url and 'video_url' or 'image_1920'
|
||||
previous_data = other_image[target_field]
|
||||
other_image[source_field] = image_to_resequence[source_field]
|
||||
image_to_resequence[target_field] = previous_data
|
||||
if source_field == 'video_url' and target_field != 'video_url':
|
||||
image_to_resequence.video_url = False
|
||||
if target_field == 'video_url' and source_field != 'video_url':
|
||||
other_image.video_url = False
|
||||
|
||||
if hasattr(other_image, 'video_url'):
|
||||
other_image._onchange_video_url()
|
||||
if hasattr(image_to_resequence, 'video_url'):
|
||||
image_to_resequence._onchange_video_url()
|
||||
|
||||
def _prepare_product_values(self, product, category, search, **kwargs):
|
||||
ProductCategory = request.env['product.public.category']
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
@@ -34,7 +36,8 @@ class ProductImage(models.Model):
|
||||
@api.onchange('video_url')
|
||||
def _onchange_video_url(self):
|
||||
if not self.image_1920:
|
||||
self.image_1920 = get_video_thumbnail(self.video_url) or False
|
||||
thumbnail = get_video_thumbnail(self.video_url)
|
||||
self.image_1920 = thumbnail and base64.b64encode(thumbnail) or False
|
||||
|
||||
@api.depends('video_url')
|
||||
def _compute_embed_code(self):
|
||||
|
||||
@@ -3,10 +3,15 @@ odoo.define('website_sale.editor', function (require) {
|
||||
|
||||
var options = require('web_editor.snippets.options');
|
||||
const Wysiwyg = require('website.wysiwyg');
|
||||
const { ComponentWrapper } = require('web.OwlCompatibility');
|
||||
const { MediaDialog, MediaDialogWrapper } = require('@web_editor/components/media_dialog/media_dialog');
|
||||
const { useWowlService } = require('@web/legacy/utils');
|
||||
const {qweb, _t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
const Dialog = require('web.Dialog');
|
||||
|
||||
const { onRendered } = owl;
|
||||
|
||||
Wysiwyg.include({
|
||||
custom_events: Object.assign(Wysiwyg.prototype.custom_events, {
|
||||
get_ribbons: '_onGetRibbons',
|
||||
@@ -643,8 +648,48 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
|
||||
}
|
||||
});
|
||||
|
||||
// Small override of the MediaDialogWrapper to retrieve the attachment ids instead of img elements
|
||||
class AttachmentMediaDialog extends MediaDialog {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async save() {
|
||||
const selectedMedia = this.selectedMedia[this.state.activeTab];
|
||||
if (selectedMedia.length) {
|
||||
this.props.save(selectedMedia);
|
||||
}
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
|
||||
class AttachmentMediaDialogWrapper extends MediaDialogWrapper {
|
||||
setup() {
|
||||
this.dialogs = useWowlService('dialog');
|
||||
|
||||
onRendered(() => {
|
||||
this.dialogs.add(AttachmentMediaDialog, this.props);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
options.registry.WebsiteSaleProductPage = options.Class.extend({
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async willStart() {
|
||||
let productProduct = this.$target[0].querySelector('[data-oe-model="product.product"]');
|
||||
let productTemplate = this.$target[0].querySelector('[data-oe-model="product.template"]');
|
||||
this.productProductID = productProduct ? productProduct.dataset.oeId : null;
|
||||
this.productTemplateID = productTemplate ? productTemplate.dataset.oeId : null;
|
||||
this.mode = "product.template";
|
||||
if (this.productProductID) {
|
||||
this.mode = "product.product"
|
||||
}
|
||||
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
_updateWebsiteConfig(params) {
|
||||
this._rpc({
|
||||
route: '/shop/config/website',
|
||||
@@ -670,6 +715,80 @@ options.registry.WebsiteSaleProductPage = options.Class.extend({
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Emulate click on the main image of the carousel.
|
||||
*/
|
||||
replaceMainImage: function () {
|
||||
const image = this.$target[0].querySelector(`[data-oe-model="${this.mode}"][data-oe-field=image_1920] img`);
|
||||
image.dispatchEvent(new Event('dblclick', {bubbles: true}));
|
||||
},
|
||||
|
||||
_getSelectedVariantValues($container) {
|
||||
const combination = $container.find('input.js_product_change:checked').data('combination');
|
||||
|
||||
if (combination) {
|
||||
return combination;
|
||||
}
|
||||
const values = [];
|
||||
|
||||
const variantsValuesSelectors = [
|
||||
'input.js_variant_change:checked',
|
||||
'select.js_variant_change'
|
||||
];
|
||||
_.each($container.find(variantsValuesSelectors.join(', ')), function (el) {
|
||||
values.push(+$(el).val());
|
||||
});
|
||||
|
||||
return values;
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompts the user for images, then saves the new images.
|
||||
*/
|
||||
addImages: function () {
|
||||
if(this.mode === 'product.template'){
|
||||
this.displayNotification({
|
||||
type: 'info',
|
||||
message: 'Pictures will be added to the main image. Use "Instant" attributes to set pictures on each variants'
|
||||
});
|
||||
}
|
||||
const dialog = new ComponentWrapper(this, AttachmentMediaDialogWrapper, {
|
||||
multiImages: true,
|
||||
onlyImages: true,
|
||||
save: attachments => {
|
||||
this._rpc({
|
||||
route: `/shop/product/extra-images`,
|
||||
params: {
|
||||
images: attachments,
|
||||
product_product_id: this.productProductID,
|
||||
product_template_id: this.productTemplateID,
|
||||
combination_ids: this._getSelectedVariantValues(this.$target.find('.js_add_cart_variants')),
|
||||
}
|
||||
}).then(() => {
|
||||
this.trigger_up('request_save', {reload: true, optionSelector: this.data.selector});
|
||||
});
|
||||
}
|
||||
});
|
||||
dialog.mount(document.body);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes all extra-images from the product.
|
||||
*/
|
||||
clearImages: function () {
|
||||
this._rpc({
|
||||
route: `/shop/product/clear-images`,
|
||||
params: {
|
||||
model: this.mode,
|
||||
product_product_id: this.productProductID,
|
||||
product_template_id: this.productTemplateID,
|
||||
combination_ids: this._getSelectedVariantValues(this.$target.find('.js_add_cart_variants')),
|
||||
}
|
||||
}).then(() => {
|
||||
this.trigger_up('request_save', {reload: true, optionSelector: this.data.selector});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -681,7 +800,7 @@ options.registry.WebsiteSaleProductPage = options.Class.extend({
|
||||
return this.$target.data('image_layout');
|
||||
}
|
||||
return this._super(...arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.WebsiteSaleProductPageGrid = options.Class.extend({
|
||||
@@ -792,5 +911,57 @@ options.registry.SnippetSave.include({
|
||||
return await this._super(...arguments)
|
||||
&& !this.$target.hasClass('o_wsale_alternative_products');
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
options.registry.ReplaceMedia.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async willStart() {
|
||||
const parent = this.$target.parent();
|
||||
this.isProductPageImage = this.$target.closest('#product_detail').length > 0;
|
||||
// Product Page images may be the product's image or a record of `product.image`
|
||||
this.recordModel = parent.data('oe-model');
|
||||
this.recordId = parent.data('oe-id');
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* Removes the image in the back-end
|
||||
*/
|
||||
async removeMedia() {
|
||||
this._rpc({
|
||||
route: '/shop/product/remove-image',
|
||||
params: {
|
||||
image_res_model: this.recordModel,
|
||||
image_res_id: this.recordId,
|
||||
},
|
||||
}).then(() => this.trigger_up('request_save', {reload: true, optionSelector: '#product_detail_main'}));
|
||||
},
|
||||
/**
|
||||
* Change sequence of product page images
|
||||
*
|
||||
*/
|
||||
async setPosition(previewMode, widgetValue, params) {
|
||||
this._rpc({
|
||||
route: '/shop/product/resequence-image',
|
||||
params: {
|
||||
image_res_model: this.recordModel,
|
||||
image_res_id: this.recordId,
|
||||
move: widgetValue,
|
||||
},
|
||||
}).then(() => this.trigger_up('request_save', {reload: true, optionSelector: '#product_detail_main'}));
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _computeWidgetVisibility(widgetName, params) {
|
||||
if (['media_wsale_resequence', 'media_wsale_remove'].includes(widgetName)) {
|
||||
// Only include these if we are inside of the product's page images
|
||||
return this.isProductPageImage;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -16,3 +16,4 @@ from . import test_website_sale_show_compare_list_price
|
||||
from . import test_website_sale_visitor
|
||||
from . import test_website_sale_product
|
||||
from . import test_website_sale_shop_variant_exclusion
|
||||
from . import test_website_editor
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
ATTACHMENT_DATA = [
|
||||
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGKqf3geEAAA//8EGgIyYKYzzgAAAABJRU5ErkJggg==",
|
||||
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGKqvvQEEAAA//8EBQI0GMlQsAAAAABJRU5ErkJggg==",
|
||||
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGKKLakBBAAA//8ChwFQsvFlAwAAAABJRU5ErkJggg==",
|
||||
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGJqkdoACAAA//8CfAFRzSyOUAAAAABJRU5ErkJggg==",
|
||||
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGLSfxgICAAA//8CrAFkoLBhpQAAAABJRU5ErkJggg==",
|
||||
]
|
||||
ATTACHMENT_COUNT = 5
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProductPictureController(HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.website = cls.env['website'].browse(1)
|
||||
cls.WebsiteSaleController = WebsiteSale()
|
||||
cls.product = cls.env['product.product'].create({
|
||||
'name': 'Storage Test Box',
|
||||
'standard_price': 70.0,
|
||||
'list_price': 79.0,
|
||||
'website_published': True,
|
||||
})
|
||||
|
||||
cls.attachments = cls.env['ir.attachment'].create([
|
||||
{
|
||||
'datas': ATTACHMENT_DATA[i],
|
||||
'name': f'image0{i}.gif',
|
||||
'public': True
|
||||
}
|
||||
for i in range(ATTACHMENT_COUNT)])
|
||||
|
||||
def _create_product_images(self):
|
||||
with MockRequest(self.product.env, website=self.website):
|
||||
self.WebsiteSaleController.add_product_images(
|
||||
[{'id': attachment.id} for attachment in self.attachments],
|
||||
self.product.id,
|
||||
self.product.product_tmpl_id.id,
|
||||
)
|
||||
|
||||
def test_bulk_image_upload(self):
|
||||
# Turns attachments to product_images
|
||||
self._create_product_images()
|
||||
|
||||
# Check if the media now exists on the product :
|
||||
for i, image in enumerate(self.product.product_template_image_ids):
|
||||
# Check if all names are now in the product
|
||||
self.assertIn(image.name, self.attachments.mapped('name'))
|
||||
# Check if image datas are the same
|
||||
self.assertEqual(image.image_1920, ATTACHMENT_DATA[i])
|
||||
# Check if exactly ATTACHMENT_COUNT images were saved (no dupes/misses?)
|
||||
self.assertEqual(ATTACHMENT_COUNT, len(self.product.product_template_image_ids))
|
||||
|
||||
def test_image_clear(self):
|
||||
# First create some images
|
||||
self._create_product_images()
|
||||
self.assertEqual(ATTACHMENT_COUNT, len(self.product.product_template_image_ids))
|
||||
|
||||
# Remove all images
|
||||
# (Exception raised if error)
|
||||
with MockRequest(self.product.env, website=self.website):
|
||||
self.WebsiteSaleController.clear_product_images(
|
||||
self.product.id,
|
||||
self.product.product_tmpl_id.id,
|
||||
)
|
||||
# According to the product, there are no variants images.
|
||||
self.assertEqual(0, len(self.product.product_template_image_ids))
|
||||
|
||||
def test_extra_images_with_new_variant(self):
|
||||
# Test that adding images for a variant that is not yet created works
|
||||
product_attribute = self.env['product.attribute'].create({
|
||||
"name": "Test attribute",
|
||||
"create_variant": "dynamic",
|
||||
})
|
||||
product_attribute_values = self.env['product.attribute.value'].create([
|
||||
{
|
||||
"name" : "Test Dynamic 1",
|
||||
"attribute_id": product_attribute.id,
|
||||
"sequence": 1,
|
||||
},
|
||||
{
|
||||
"name" : "Test Dynamic 2",
|
||||
"attribute_id": product_attribute.id,
|
||||
"sequence": 2,
|
||||
}
|
||||
])
|
||||
product_template = self.env['product.template'].create({
|
||||
"name": "test product",
|
||||
"website_published": True,
|
||||
})
|
||||
product_template_attribute_line = self.env['product.template.attribute.line'].create({
|
||||
"attribute_id": product_attribute.id,
|
||||
"product_tmpl_id": product_template.id,
|
||||
"value_ids": product_attribute_values,
|
||||
})
|
||||
self.assertEqual(0, len(product_template.product_variant_ids))
|
||||
with MockRequest(product_template.env, website=self.website):
|
||||
self.WebsiteSaleController.add_product_images(
|
||||
[{'id': self.attachments[0].id}],
|
||||
False,
|
||||
product_template.id,
|
||||
[product_template_attribute_line.product_template_value_ids[0].id],
|
||||
)
|
||||
self.assertEqual(1, len(product_template.product_variant_ids))
|
||||
|
||||
def test_resequence_images(self):
|
||||
self._create_product_images()
|
||||
with MockRequest(self.product.env, website=self.website):
|
||||
# Test moving to first position
|
||||
images = self.product._get_images()
|
||||
data_source = images[2].image_1920
|
||||
data_target = images[0].image_1920
|
||||
self.WebsiteSaleController.resequence_product_image(
|
||||
images[2]._name,
|
||||
images[2].id,
|
||||
'first',
|
||||
)
|
||||
images = self.product._get_images()
|
||||
self.assertEqual(images[2].image_1920, data_target)
|
||||
self.assertEqual(images[0].image_1920, data_source)
|
||||
# Test moving one to the left
|
||||
data_source = images[2].image_1920
|
||||
data_target = images[1].image_1920
|
||||
self.WebsiteSaleController.resequence_product_image(
|
||||
images[2]._name,
|
||||
images[2].id,
|
||||
'left',
|
||||
)
|
||||
images = self.product._get_images()
|
||||
self.assertEqual(images[2].image_1920, data_target)
|
||||
self.assertEqual(images[1].image_1920, data_source)
|
||||
# Test moving one to the right
|
||||
data_source = images[2].image_1920
|
||||
data_target = images[3].image_1920
|
||||
self.WebsiteSaleController.resequence_product_image(
|
||||
images[2]._name,
|
||||
images[2].id,
|
||||
'right',
|
||||
)
|
||||
images = self.product._get_images()
|
||||
self.assertEqual(images[2].image_1920, data_target)
|
||||
self.assertEqual(images[3].image_1920, data_source)
|
||||
# Test moving one to the last position
|
||||
data_source = images[2].image_1920
|
||||
data_target = images[-1].image_1920
|
||||
self.WebsiteSaleController.resequence_product_image(
|
||||
images[2]._name,
|
||||
images[2].id,
|
||||
'last',
|
||||
)
|
||||
images = self.product._get_images()
|
||||
self.assertEqual(images[2].image_1920, data_target)
|
||||
self.assertEqual(images[-1].image_1920, data_source)
|
||||
# Test moving an image with a video_url instead of image_1920
|
||||
data_target = images[1].image_1920
|
||||
images[2].video_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
self.WebsiteSaleController.resequence_product_image(
|
||||
images[2]._name,
|
||||
images[2].id,
|
||||
'left',
|
||||
)
|
||||
images = self.product._get_images()
|
||||
self.assertEqual(images[1].video_url, "https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||
self.assertEqual(images[2].video_url, False)
|
||||
self.assertEqual(images[2].image_1920, data_target)
|
||||
# Test that it is not possible to move an "embedded" video to the first position
|
||||
with self.assertRaises(ValidationError):
|
||||
self.WebsiteSaleController.resequence_product_image(
|
||||
images[1]._name,
|
||||
images[1].id,
|
||||
'left',
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
self.WebsiteSaleController.resequence_product_image(
|
||||
images[1]._name,
|
||||
images[1].id,
|
||||
'first',
|
||||
)
|
||||
@@ -196,6 +196,13 @@
|
||||
<we-button data-set-image-layout="carousel">Carousel</we-button>
|
||||
<we-button data-set-image-layout="grid">Grid</we-button>
|
||||
</we-select>
|
||||
<we-row string="Main image">
|
||||
<we-button class="o_we_bg_brand_primary" data-replace-main-image="true" data-no-preview="true">Replace</we-button>
|
||||
</we-row>
|
||||
<we-row string="Extra Images">
|
||||
<we-button class="o_we_bg_success" data-add-images="true" data-no-preview="true">Add</we-button>
|
||||
<we-button class="o_we_bg_danger" data-clear-images="true" data-no-preview="true">Remove all</we-button>
|
||||
</we-row>
|
||||
</div>
|
||||
<div data-selector="#product_detail #o-carousel-product" data-no-check="true">
|
||||
<we-select string="Image Zoom" data-no-preview="true" data-reload="/">
|
||||
@@ -278,6 +285,20 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="snippets_options_web_editor" inherit_id="web_editor.snippet_options" name="e-commerce base snippet options">
|
||||
<xpath expr="//div[@data-js='ReplaceMedia']" position="inside">
|
||||
<we-row string="Re-order">
|
||||
<we-button class="fa fa-fw fa-angle-double-left" data-no-preview="true" title="Move to first" data-set-position="first" data-name="media_wsale_resequence"/>
|
||||
<we-button class="fa fa-fw fa-angle-left" data-no-preview="true" title="Move to previous" data-set-position="left" data-name="media_wsale_resequence"/>
|
||||
<we-button class="fa fa-fw fa-angle-right" data-no-preview="true" title="Move to next" data-set-position="right" data-name="media_wsale_resequence"/>
|
||||
<we-button class="fa fa-fw fa-angle-double-right" data-no-preview="true" title="Move to last" data-set-position="last" data-name="media_wsale_resequence"/>
|
||||
</we-row>
|
||||
</xpath>
|
||||
<xpath expr="//div[@data-js='ReplaceMedia']/we-row" position="inside">
|
||||
<we-button class="o_we_bg_danger" data-remove-media="true" data-no-preview="true" data-name="media_wsale_remove">Remove</we-button>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="product_searchbar_input_snippet_options" inherit_id="website.searchbar_input_snippet_options" name="product search bar snippet options">
|
||||
<xpath expr="//div[@data-js='SearchBar']/we-select[@data-name='scope_opt']" position="inside">
|
||||
<we-button data-set-search-type="products" data-select-data-attribute="products" data-name="search_products_opt" data-form-action="/shop">Products</we-button>
|
||||
|
||||
Reference in New Issue
Block a user