[ADD] web_unsplash: Find free high-resolution images from Unsplash
Move module from enterprise to community Work of @dep-odoo and @rdeodoo
This commit is contained in:
committed by
Jeremy Kersten
parent
0b608109ee
commit
732702d5e2
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Unsplash Image Library',
|
||||
'category': 'Web',
|
||||
'summary': 'Find free high-resolution images from Unsplash',
|
||||
'version': '1.0',
|
||||
'description': """Explore the free high-resolution image library of Unsplash.com and find images to use in Odoo. An Unsplash search bar is added to the image library modal.""",
|
||||
'depends': ['base_setup', 'web_editor'],
|
||||
'data': [
|
||||
'views/res_config_settings_view.xml',
|
||||
'views/web_unsplash_templates.xml',
|
||||
],
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import main
|
||||
@@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
import requests
|
||||
import werkzeug.utils
|
||||
|
||||
from PIL import Image
|
||||
from odoo import http, tools
|
||||
from odoo.http import request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Web_Unsplash(http.Controller):
|
||||
|
||||
# ------------------------------------------------------
|
||||
# add unsplash image url
|
||||
# ------------------------------------------------------
|
||||
@http.route('/web_unsplash/attachment/add', type='json', auth='user', methods=['POST'])
|
||||
def save_unsplash_url(self, unsplashurls=None, **kwargs):
|
||||
"""
|
||||
unsplashurls = {
|
||||
image_id1: image_url1,
|
||||
image_id2: image_url2,
|
||||
.....
|
||||
}
|
||||
"""
|
||||
if not unsplashurls:
|
||||
return []
|
||||
|
||||
uploads = []
|
||||
Attachments = request.env['ir.attachment']
|
||||
|
||||
res_model = kwargs.get('res_model', 'ir.ui.view')
|
||||
if res_model != 'ir.ui.view' and kwargs.get('res_id'):
|
||||
res_id = int(kwargs['res_id'])
|
||||
else:
|
||||
res_id = None
|
||||
|
||||
for key in unsplashurls:
|
||||
url = unsplashurls[key].get('url')
|
||||
try:
|
||||
req = requests.get(url)
|
||||
if req.status_code != requests.codes.ok:
|
||||
continue
|
||||
|
||||
# get mime-type of image url because unsplash url dosn't contains mime-types in url
|
||||
mimetype = req.headers.get('Content-Type')
|
||||
datas = req.content
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.exception("Connection Error: " + str(e))
|
||||
continue
|
||||
except requests.exceptions.Timeout as e:
|
||||
logger.exception("Timeout: " + str(e))
|
||||
continue
|
||||
|
||||
image = Image.open(io.BytesIO(datas))
|
||||
image_format = image.format
|
||||
|
||||
# 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()
|
||||
|
||||
attachment = Attachments.create({
|
||||
'name': name,
|
||||
'datas_fname': name,
|
||||
'mimetype': mimetype,
|
||||
'datas': base64.b64encode(datas),
|
||||
'public': res_model == 'ir.ui.view',
|
||||
'res_id': res_id,
|
||||
'res_model': res_model,
|
||||
})
|
||||
attachment.generate_access_token()
|
||||
uploads.extend(attachment.read(['name', 'mimetype', 'checksum', 'res_id', 'res_model', 'access_token']))
|
||||
|
||||
return uploads
|
||||
|
||||
@http.route("/web_unsplash/get_client_id", type='json', auth="user")
|
||||
def get_unsplash_client_id(self, **post):
|
||||
if request.env.user._has_unsplash_key_rights():
|
||||
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):
|
||||
if request.env.user._has_unsplash_key_rights():
|
||||
return request.env['ir.config_parameter'].sudo().set_param('unsplash.access_key', key)
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
unsplash_access_key = fields.Char("Access Key", config_parameter='unsplash.access_key')
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
@api.multi
|
||||
def _has_unsplash_key_rights(self):
|
||||
self.ensure_one()
|
||||
return self.has_group('base.group_erp_manager')
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,22 @@
|
||||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<h2 class="oe_slogan" style="color:#875A7B;">Free high-resolution image library</h2>
|
||||
<h3 class="oe_slogan">An Unsplash search bar is added to the image library modal...</h3>
|
||||
<div class="oe_span12">
|
||||
<div class="oe_demo oe_picture oe_screenshot">
|
||||
<img src="unsplash_interface_bar.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<h3 class="oe_slogan">...to explore <a href="https://unsplash.com/">Unsplash.com</a> and find images to use in Odoo.</h3>
|
||||
<div class="oe_span12">
|
||||
<div class="oe_demo oe_picture oe_screenshot">
|
||||
<img src="unsplash_interface.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 372 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,219 @@
|
||||
odoo.define('web_unsplash.image_widgets', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var UnsplashAPI = require('unsplash.api');
|
||||
|
||||
var ImageWidget = require('web_editor.widget').ImageWidget;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var unsplashAPI = null;
|
||||
|
||||
ImageWidget.include({
|
||||
xmlDependencies: ImageWidget.prototype.xmlDependencies.concat(
|
||||
['/web_unsplash/static/src/xml/unsplash_image_widget.xml']
|
||||
),
|
||||
events: _.extend({}, ImageWidget.prototype.events, {
|
||||
'input input.unsplash_search': '_onChangeUnsplashSearch',
|
||||
'dblclick .unsplash_img_container [data-imgid]': '_onUnsplashImgDblClick',
|
||||
'click .unsplash_img_container [data-imgid]': '_onUnsplashImgClick',
|
||||
'click button.access_key': '_onSetAccessKey',
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._unsplash = {
|
||||
selectedImages: {},
|
||||
isMaxed: false,
|
||||
query: false,
|
||||
};
|
||||
// TODO This is a `hack` to prevent the UnsplashAPI to be destroyed every time
|
||||
// the media dialog is closed.
|
||||
// Indeed, UnsplashAPI has a cache system to recude unsplash call, it is
|
||||
// then better to keep its state to benefic from it from one media dialog
|
||||
// call to another.
|
||||
// Unsplash API will either be (it's still being discussed):
|
||||
// * a service (ideally coming with an improvement to not auto load the service)
|
||||
// * initialized in the website_root (trigger_up)
|
||||
var def = this._super.apply(this, arguments);
|
||||
if (unsplashAPI === null) {
|
||||
this.unsplashAPI = new UnsplashAPI(this);
|
||||
unsplashAPI = this.unsplashAPI;
|
||||
} else {
|
||||
this.unsplashAPI = unsplashAPI;
|
||||
this.unsplashAPI.setParent(this);
|
||||
}
|
||||
return def;
|
||||
},
|
||||
destroy: function () {
|
||||
// TODO See `hack` explained in `init`. This prevent the media dialog destroy
|
||||
// to destroy unsplashAPI when destroying the children
|
||||
this.unsplashAPI.setParent(undefined);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
getControlPanelConfig: function () {
|
||||
var config = this._super.apply(this, arguments);
|
||||
if (this._unsplash.query) {
|
||||
_.extend(config, {
|
||||
pagerLeftEnabled: this.page > 1,
|
||||
pagerRightEnabled: !this._unsplash.isMaxed,
|
||||
});
|
||||
}
|
||||
return config;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
save: function () {
|
||||
if (!this._unsplash.query) {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
var self = this;
|
||||
var args = arguments;
|
||||
var _super = this._super;
|
||||
return this._rpc({
|
||||
route: '/web_unsplash/attachment/add',
|
||||
params: {
|
||||
unsplashurls: self._unsplash.selectedImages,
|
||||
res_model : self.options.res_model || "ir.ui.view",
|
||||
res_id: self.options.res_id,
|
||||
}
|
||||
}).then(function (images) {
|
||||
for (var img in self._unsplash.selectedImages) {
|
||||
self.unsplashAPI.notifyDownload(self._unsplash.selectedImages[img].download_url);
|
||||
}
|
||||
|
||||
_.each(images, function (image) {
|
||||
image.src = _.str.sprintf('/web/image/%s', image.id);
|
||||
image.isDocument = !(/gif|jpe|jpg|png/.test(image.mimetype));
|
||||
});
|
||||
self.images = images;
|
||||
return _super.apply(self, args);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Highlights selected image, when an image is clicked and when pager change
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_highlightSelectedImages: function () {
|
||||
var self = this;
|
||||
if (!this._unsplash.query) {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
this.$('.o_unsplash_img_cell.o_selected').removeClass("o_selected");
|
||||
var $select = this.$('.o_unsplash_img_cell [data-imgid]').filter(function () {
|
||||
return $(this).data('imgid') in self._unsplash.selectedImages;
|
||||
});
|
||||
$select.closest('.o_unsplash_img_cell').addClass("o_selected");
|
||||
return $select;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_renderImages: function () {
|
||||
var self = this;
|
||||
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) {
|
||||
self._unsplash.isMaxed = res.isMaxed;
|
||||
var rows = _(res.images).chain()
|
||||
.groupBy(function (a, index) { return Math.floor(index / self.IMAGES_PER_ROW); })
|
||||
.values()
|
||||
.value();
|
||||
|
||||
self.$('.unsplash_img_container').html(QWeb.render('web_unsplash.dialog.image.content', { rows: rows }));
|
||||
self._highlightSelectedImages();
|
||||
}).fail(function (err) {
|
||||
self.$('.unsplash_img_container').html(QWeb.render('web_unsplash.dialog.error.content', err));
|
||||
}).always(function () {
|
||||
self._toggleAttachmentContaines(false);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_toggleAttachmentContaines: function (hideUnsplash) {
|
||||
this.$('.existing-attachments').toggleClass('o_hidden', !hideUnsplash);
|
||||
this.$('.unsplash_img_container').toggleClass('o_hidden', hideUnsplash);
|
||||
this.trigger_up('update_control_panel');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onSetAccessKey: 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();
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onChangeUnsplashSearch: _.debounce(function () {
|
||||
// oldPage saves the original image widget pager.
|
||||
// Emptying the unsplash search will set the pager to its previous state
|
||||
this._unsplash.query = this.$('.unsplash_search').val().trim();
|
||||
if (this._unsplash.query) {
|
||||
this.oldPage = this.page;
|
||||
this.page = 1;
|
||||
this._renderImages();
|
||||
} else {
|
||||
this.page = this.oldPage || 0;
|
||||
this._toggleAttachmentContaines(true);
|
||||
}
|
||||
}, 1000),
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onUnsplashImgClick: function (ev) {
|
||||
var imgid = $(ev.currentTarget).data('imgid');
|
||||
var url = $(ev.currentTarget).data('url');
|
||||
var download_url = $(ev.currentTarget).data('download-url');
|
||||
if (!this.multiImages) {
|
||||
this._unsplash.selectedImages = {};
|
||||
}
|
||||
if (imgid in this._unsplash.selectedImages) {
|
||||
delete this._unsplash.selectedImages[imgid];
|
||||
} else {
|
||||
this._unsplash.selectedImages[imgid] = {url: url, download_url: download_url};
|
||||
}
|
||||
this._highlightSelectedImages();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onUnsplashImgDblClick: function (ev) {
|
||||
this.trigger_up('save_request');
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
odoo.define('unsplash.api', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Class = require('web.Class');
|
||||
var rpc = require('web.rpc');
|
||||
var Mixins = require('web.mixins');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
|
||||
var UnsplashCore = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent) {
|
||||
Mixins.EventDispatcherMixin.init.call(this, arguments);
|
||||
this.setParent(parent);
|
||||
|
||||
this._cache = {};
|
||||
this.clientId = false;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets unsplash images from query string.
|
||||
*
|
||||
* @param {String} query search terms
|
||||
* @param {Integer} pageSize number of image to display per page
|
||||
* @param {Integer} pageNumber page number to retrieve
|
||||
*/
|
||||
getImages: function (query, pageSize, pageNumber) {
|
||||
var self = this;
|
||||
var to = pageSize * pageNumber;
|
||||
var from = to - pageSize;
|
||||
var cachedData = this._cache[query];
|
||||
|
||||
if (cachedData && (cachedData.images.length >= to || (cachedData.totalImages !== 0 && cachedData.totalImages < to))) {
|
||||
return $.when({ images: cachedData.images.slice(from, to), isMaxed: to > cachedData.totalImages });
|
||||
}
|
||||
return this._getAPIKey().then(function (clientID) {
|
||||
if (!clientID) {
|
||||
return $.Deferred().reject({ key_not_found: true });
|
||||
}
|
||||
return self._fetchImages(query).then(function (cachedData) {
|
||||
return { images: cachedData.images.slice(from, to), isMaxed: to > cachedData.totalImages };
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Notifies Unsplash from an image download. (API requirement)
|
||||
*
|
||||
* @param {String} url url of the image to notify
|
||||
*/
|
||||
notifyDownload: function (url) {
|
||||
$.get(url, { client_id: this.clientId });
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks and retrieves the unsplash API key
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_getAPIKey: function () {
|
||||
var self = this;
|
||||
if (this.clientId) {
|
||||
return $.Deferred().resolve(self.clientId);
|
||||
}
|
||||
return this._rpc({
|
||||
route: '/web_unsplash/get_client_id',
|
||||
}).then(function (res) {
|
||||
self.clientId = res;
|
||||
return res;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Fetches images from unsplash and stores it in cache
|
||||
*
|
||||
* @param {String} query search terms
|
||||
* @private
|
||||
*/
|
||||
_fetchImages: function (query) {
|
||||
if (!this._cache[query]) {
|
||||
this._cache[query] = {
|
||||
images: [],
|
||||
maxPages: 0,
|
||||
totalImages: 0,
|
||||
pageCached: 0
|
||||
};
|
||||
}
|
||||
var cachedData = this._cache[query];
|
||||
var payload = {
|
||||
query: query,
|
||||
page: cachedData.pageCached + 1,
|
||||
client_id: this.clientId,
|
||||
per_page: 30, // max size from unsplash API
|
||||
};
|
||||
return $.get('https://api.unsplash.com/search/photos/', payload).then(function (result) {
|
||||
cachedData.pageCached++;
|
||||
cachedData.images.push.apply(cachedData.images, result.results);
|
||||
cachedData.maxPages = result.total_pages;
|
||||
cachedData.totalImages = result.total;
|
||||
return cachedData;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return UnsplashCore;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
.unsplash_img_container {
|
||||
$o-border-image: 2px;
|
||||
.o_unsplash_img_cell {
|
||||
.o_image {
|
||||
cursor: pointer;
|
||||
transition: opacity 400ms;
|
||||
|
||||
&.o_webimage {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background-image: none;
|
||||
|
||||
img {
|
||||
max-height: 150px;
|
||||
margin: 0 auto;
|
||||
border: $o-border-image solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
&.o_selected .o_image img {
|
||||
border-color: $o-brand-primary;
|
||||
box-shadow: 0px 0px 2px 2px $o-brand-secondary;
|
||||
}
|
||||
.author_name {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.not-found {
|
||||
font-size: 25px;
|
||||
padding: 50px 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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;"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-jquery=".existing-attachments" t-operation="after">
|
||||
<div class="unsplash_img_container"></div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web_unsplash.dialog.image.content">
|
||||
<div class="row mt16" t-as="row" t-foreach="rows">
|
||||
<div class="col-sm-2 o_unsplash_img_cell" t-as="img" t-foreach="row">
|
||||
<div class="o_image o_webimage" t-att-data-imgid="img.id" t-att-data-url="img.urls.regular" t-att-data-download-url="img.links.download_location">
|
||||
<img class="img-fluid" t-att-src="img.urls.thumb" />
|
||||
</div>
|
||||
<div class="author_name text-center"><a t-att-href="img.user.links.html" target="_blank"><t t-esc="img.user.name"/></a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt16" t-if="rows.length == 0">
|
||||
<div class="text-center text-muted not-found">
|
||||
<i class="fa fa-camera mr8"/> Photos not found
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web_unsplash.dialog.error.content">
|
||||
<div class="row mt16 text-muted unsplash_error">
|
||||
<t t-if="key_not_found">
|
||||
<div class="text-center">
|
||||
<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>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="status == 403">
|
||||
<div class="text-center">
|
||||
<p class="title">
|
||||
Search is temporary unavailable
|
||||
</p>
|
||||
<div class="details">
|
||||
The max number of searches is exceeded. Please retry in an hour or extend to a better account.
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="status == 401">
|
||||
<div class="text-center">
|
||||
<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>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="text-center">
|
||||
<p class="title">
|
||||
Something went wrong
|
||||
</p>
|
||||
<div class="details">
|
||||
Please check your internet connection or contact administrator.
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.web.unsplash</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<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>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="web_unsplash_assets" inherit_id="web_editor.assets_editor" name="Unsplash Images">
|
||||
<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>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user