[IMP] web_unsplash: Implement Beacon Alternative and credit Unsplash

Unsplash API requirements does not directly allow to store their images on
our server (We should use hotlinking unless the image is edited (cropped etc)).
Still, they have the `Image Beacon Alternative` to allow to store their images
on our server if we really need it with our use case.

With the Beacon Alternative, we should ping Unsplash server to notify an image
view.
To do so, we need to be able to recognize Unsplash imagesin the DOM, this is
why we are now using /unsplash/ in the URL.
The Unsplash image name should be the image ID as it will be the identifier
needed to ping their server.

This commit also adds credit to Unsplash in the media dialog next to the
unsplash input search.
It also add the `w-100` on the media dialog card as we lost that UI behavior
from BS3 to BS4.

opw-35060

closes odoo/odoo#27377
This commit is contained in:
Romain Derie
2018-10-26 10:13:07 +00:00
parent 8c9174ff80
commit 40c6bc3ad2
8 changed files with 140 additions and 89 deletions
+1 -1
View File
@@ -90,7 +90,7 @@
<input type="hidden" name="filters" t-att-value="widget.firstFilters.join('_')"/>
<input t-if="widget.options.res_id" type="hidden" name="res_id" t-att-value="widget.options.res_id"/>
<input t-if="widget.options.res_model" type="hidden" name="res_model" t-att-value="widget.options.res_model"/>
<div class="card">
<div class="o_media_image_card card w-100">
<div class="card-body p-0">
<div class="form-inline">
<input type="file" class="d-none" name="upload" t-att-accept="widget.accept" multiple="multiple"/>
+24 -16
View File
@@ -7,7 +7,7 @@ import requests
import werkzeug.utils
from PIL import Image
from odoo import http, tools
from odoo import http, tools, _
from odoo.http import request
logger = logging.getLogger(__name__)
@@ -39,9 +39,12 @@ class Web_Unsplash(http.Controller):
else:
res_id = None
for key in unsplashurls:
url = unsplashurls[key].get('url')
for key, value in unsplashurls.items():
url = value.get('url')
try:
if not url.startswith('https://images.unsplash.com/'):
logger.exception("ERROR: Unknown Unsplash URL!: " + url)
raise Exception(_("ERROR: Unknown Unsplash URL!"))
req = requests.get(url)
if req.status_code != requests.codes.ok:
continue
@@ -56,20 +59,19 @@ class Web_Unsplash(http.Controller):
logger.exception("Timeout: " + str(e))
continue
image = Image.open(io.BytesIO(datas))
image_format = image.format
name = key
# optimized image before save
if image_format and image_format in ('PNG', 'JPEG'):
datas = tools.image_save_for_web(image)
name = key
# append image extension in name
if image_format:
name += '.' + image_format.lower()
if mimetype in ('image/jpeg', 'image/png'):
image = Image.open(io.BytesIO(datas))
if image.format in ('PNG', 'JPEG'):
datas = tools.image_save_for_web(image)
# append image extension in name
name += '.' + image.format
attachment = Attachments.create({
'name': name,
'url': '/unsplash/' + name,
'datas_fname': name,
'mimetype': mimetype,
'datas': base64.b64encode(datas),
@@ -78,7 +80,7 @@ class Web_Unsplash(http.Controller):
'res_model': res_model,
})
attachment.generate_access_token()
uploads.extend(attachment.read(['name', 'mimetype', 'checksum', 'res_id', 'res_model', 'access_token']))
uploads.extend(attachment.read(['name', 'mimetype', 'checksum', 'res_id', 'res_model', 'access_token', 'url']))
return uploads
@@ -88,8 +90,14 @@ class Web_Unsplash(http.Controller):
return request.env['ir.config_parameter'].sudo().get_param('unsplash.access_key')
raise werkzeug.exceptions.NotFound()
@http.route("/web_unsplash/set_client_id", type='json', auth="user")
def set_unsplash_client_id(self, key, **post):
@http.route("/web_unsplash/get_app_id", type='json', auth="public")
def get_unsplash_app_id(self, **post):
return request.env['ir.config_parameter'].sudo().get_param('unsplash.app_id')
@http.route("/web_unsplash/save_unsplash", type='json', auth="user")
def save_unsplash(self, **post):
if request.env.user._has_unsplash_key_rights():
return request.env['ir.config_parameter'].sudo().set_param('unsplash.access_key', key)
request.env['ir.config_parameter'].sudo().set_param('unsplash.app_id', post.get('appId'))
request.env['ir.config_parameter'].sudo().set_param('unsplash.access_key', post.get('key'))
return True
raise werkzeug.exceptions.NotFound()
@@ -0,0 +1,26 @@
odoo.define('web_unsplash.beacon', function (require) {
'use strict';
var base = require('web_editor.base');
var ajax = require('web.ajax');
base.ready().then(function () {
var unsplash_images = [];
_.each($('img[src*="/unsplash/"]'), function (img, index) {
// get image id from URL (`/unsplash/xYdf5feoI.jpg` -> `xYdf5feoI`)
unsplash_images.push(img.src.split('/unsplash/')[1].split('.')[0]);
});
if (unsplash_images) {
ajax.jsonRpc('/web_unsplash/get_app_id', 'call').then(function (appID) {
if (appID) {
var unsplash_view_url = 'https://views.unsplash.com/v';
$.get(unsplash_view_url, {
photo_id: unsplash_images.join(','),
app_id: appID,
});
}
});
}
});
});
@@ -17,7 +17,7 @@ ImageWidget.include({
'input input.unsplash_search': '_onChangeUnsplashSearch',
'dblclick .unsplash_img_container [data-imgid]': '_onUnsplashImgDblClick',
'click .unsplash_img_container [data-imgid]': '_onUnsplashImgClick',
'click button.access_key': '_onSetAccessKey',
'click button.save_unsplash': '_onSaveUnsplash',
}),
/**
* @override
@@ -84,7 +84,7 @@ ImageWidget.include({
route: '/web_unsplash/attachment/add',
params: {
unsplashurls: self._unsplash.selectedImages,
res_model : self.options.res_model || "ir.ui.view",
res_model : self.options.res_model,
res_id: self.options.res_id,
}
}).then(function (images) {
@@ -93,7 +93,7 @@ ImageWidget.include({
}
_.each(images, function (image) {
image.src = _.str.sprintf('/web/image/%s', image.id);
image.src = image.url;
image.isDocument = !(/gif|jpe|jpg|png/.test(image.mimetype));
});
self.images = images;
@@ -130,7 +130,7 @@ ImageWidget.include({
if (!this._unsplash.query) {
return this._super.apply(this, arguments);
}
this.unsplashAPI.getImages(self._unsplash.query, this.IMAGES_PER_PAGE, this.page).then(function (res) {
this.unsplashAPI.getImages(this._unsplash.query, this.IMAGES_PER_PAGE, this.page).then(function (res) {
self._unsplash.isMaxed = res.isMaxed;
var rows = _(res.images).chain()
.groupBy(function (a, index) { return Math.floor(index / self.IMAGES_PER_ROW); })
@@ -161,19 +161,29 @@ ImageWidget.include({
/**
* @private
*/
_onSetAccessKey: function () {
_onSaveUnsplash: function () {
var self = this;
var key = this.$('#accessKeyInput').val().trim();
if (key) {
this._rpc({
route: '/web_unsplash/set_client_id',
params: {
'key': key,
},
}).then(function () {
self.unsplashAPI.clientId = key;
self._renderImages();
});
var appId = this.$('#appIdInput').val().trim();
this.$('#accessKeyInput').toggleClass('is-invalid', !key);
this.$('#appIdInput').toggleClass('is-invalid', !appId);
if (key && appId) {
var params = {
'key': key,
'appId': appId,
};
if (!this.$el.find('.is-invalid').length) {
this._rpc({
route: '/web_unsplash/save_unsplash',
params: params,
}).then(function () {
self.unsplashAPI.clientId = key;
self._renderImages();
});
}
}
},
/**
@@ -29,24 +29,17 @@
.not-found {
font-size: 25px;
padding: 50px 0;
padding: 30px 0;
}
.unsplash_error {
padding: 50px 0;
.title {
font-size: 25px;
text-transform: uppercase;
}
.details {
font-size: 20px;
.access_key_box {
padding: 9px;
background-color: #fcfcfc;
border: 1px solid #ededee;
#accessKeyInput {
width: 300px;
}
padding: 30px 0;
.access_key_box {
padding: 9px;
background-color: #fcfcfc;
border: 1px solid #ededee;
input {
min-width: 300px;
}
}
}
@@ -2,10 +2,10 @@
<templates id="template" xml:space="preserve">
<t t-extend="web_editor.dialog.image">
<t t-jquery=".card-body .form-inline" t-operation="append">
<div class="ml8">
<span class="text-muted">— or —</span>
<input type="text" class="form-control unsplash_search mr8" placeholder="Search from Unsplash" style="width: 250px;"/>
<t t-jquery=".o_media_image_card .form-inline" t-operation="append">
<div class="ml-auto border-left pl-4">
<span class="d-block font-weight-bold">Photos by <a href="https://unsplash.com" target="_blank">Unsplash</a></span>
<input type="text" class="form-control unsplash_search mr8" placeholder="Search free high-resolution photos" style="width: 250px;"/>
</div>
</t>
<t t-jquery=".existing-attachments" t-operation="after">
@@ -29,49 +29,56 @@
</div>
</t>
<t t-name="web_unsplash.dialog.error.credentials">
<h4 class="text-muted">
<t t-esc="title"/>
</h4>
<div class="details">
<t t-esc="subtitle"/>
<div class="form-group btn-group mt-4 access_key_box">
<input type="text" class="form-control w-100" id="accessKeyInput" placeholder="Paste your access key here"/>
</div>
<a href="https://www.odoo.com/documentation/user/unsplash_access_key.html" target="_blank"><i class="fa fa-arrow-right"/> Generate an access key</a>
<div class="form-group btn-group mt-4 access_key_box">
<input type="text" class="form-control w-100" id="appIdInput" placeholder="Paste your application ID here"/>
</div>
<a href="https://www.odoo.com/documentation/user/unsplash_application_id.html" target="_blank"><i class="fa fa-arrow-right"/> How to find my Unsplash Application ID?</a>
<button type="button" class="btn btn-primary btn-block mt-4 save_unsplash">Apply</button>
</div>
</t>
<t t-name="web_unsplash.dialog.error.content">
<div class="row mt16 text-muted unsplash_error">
<div class="m-auto text-center">
<t t-if="key_not_found">
<p class="title">
Access key is not set
</p>
<div class="details">
<a href="https://unsplash.com/oauth/applications" target="_blank"><i class="fa fa-arrow-right"/> Generate an access key</a>
<div>
<div class="form-group btn-group access_key_box">
<input type="text" class="form-control float-left" id="accessKeyInput" placeholder="Paste your access key here"/>
<button type="button" class="btn btn-primary access_key">Apply</button>
</div>
</div>
</div>
<t t-call="web_unsplash.dialog.error.credentials">
<t t-set="title">
Unsplash requires an access key and an application ID
</t>
</t>
</t>
<t t-elif="status == 403">
<p class="title">
Search is temporary unavailable
</p>
<h4 class="text-muted">
Search is temporarily unavailable
</h4>
<div class="details">
The max number of searches is exceeded. Please retry in an hour or extend to a better account.
</div>
</t>
<t t-elif="status == 401">
<p class="title">
Unauthorized Key
</p>
<div class="details">
Please check your unsplash api key.
<div>
<div class="form-group btn-group mt16 access_key_box">
<input type="text" class="form-control float-left" id="accessKeyInput" placeholder="Paste your access key here"/>
<button type="button" class="btn btn-primary access_key">Add</button>
</div>
</div>
</div>
<t t-call="web_unsplash.dialog.error.credentials">
<t t-set="title">
Unauthorized Key
</t>
<t t-set="subtitle">
Please check your Unsplash access key and application ID.
</t>
</t>
</t>
<t t-else="">
<p class="title">
<h4 class="text-muted">
Something went wrong
</p>
</h4>
<div class="details">
Please check your internet connection or contact administrator.
</div>
@@ -6,14 +6,16 @@
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="web_unsplash_settings" position="inside">
<div class="content-group" attrs="{'invisible': [('module_web_unsplash', '=', False)]}">
<label for="unsplash_access_key" class="o_light_label"/>
<field name="unsplash_access_key"/>
</div>
<div>
<a href="https://unsplash.com/oauth/applications" class="oe_link" target="_blank">
<i class="fa fa-arrow-right"/> Generate an Access Key
</a>
<div attrs="{'invisible': [('module_web_unsplash', '=', False)]}">
<div class="content-group mt16">
<label for="unsplash_access_key" class="o_light_label"/>
<field name="unsplash_access_key"/>
</div>
<div>
<a href="https://www.odoo.com/documentation/user/unsplash_access_key.html" class="oe_link" target="_blank">
<i class="fa fa-arrow-right"/> Generate an Access Key
</a>
</div>
</div>
</div>
</field>
@@ -1,10 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="web_unsplash_assets" inherit_id="web_editor.assets_editor" name="Unsplash Images">
<template id="web_unsplash_assets" inherit_id="web_editor.assets_editor" name="Unsplash Images Editor Assets">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/web_unsplash/static/src/scss/unsplash.scss"/>
<script type="text/javascript" src="/web_unsplash/static/src/js/unsplashapi.js"></script>
<script type="text/javascript" src="/web_unsplash/static/src/js/unsplash_image_widget.js"></script>
</xpath>
</template>
<template id="assets_common" inherit_id="web_editor.assets_common" name="Unsplash Images Common Assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/web_unsplash/static/src/js/unsplash_beacon.js"></script>
</xpath>
</template>
</odoo>