[IMP] website_blog: integrate snippet latest posts
Moved from the themes repository. Now, users can use a snippet which showcases the latest blog posts as soon as they install the blog app. Note: in the future we may want to develop a snippet which can showcase any model records (products, projects, ...). task-1874216
This commit is contained in:
@@ -293,3 +293,8 @@ class WebsiteBlog(http.Controller):
|
||||
if not post_id:
|
||||
return False
|
||||
return request.env['blog.post'].browse(int(post_id)).write({'cover_properties': json.dumps(cover_properties)})
|
||||
|
||||
@http.route(['/blog/render_latest_posts'], type='json', auth='public', website=True)
|
||||
def render_latest_posts(self, template, domain, limit=None, order='published_date desc'):
|
||||
posts = request.env['blog.post'].search(domain, limit=limit, order=order)
|
||||
return request.env.ref(template).render({'posts': posts})
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,106 @@
|
||||
odoo.define('website_blog.s_latest_posts_editor', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var sOptions = require('web_editor.snippets.options');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
sOptions.registry.js_get_posts_limit = sOptions.Class.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
postsLimit: function (previewMode, value, $opt) {
|
||||
value = parseInt(value);
|
||||
this.$target.attr('data-posts-limit', value).data('postsLimit', value);
|
||||
this.trigger_up('animation_start_demand', {
|
||||
editableMode: true,
|
||||
$target: this.$target,
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_setActive: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.$('[data-posts-limit]').addBack('[data-posts-limit]')
|
||||
.removeClass('active')
|
||||
.filter(function () {
|
||||
return (self.$target.data('postsLimit') || 3) == $(this).data('postsLimit');
|
||||
})
|
||||
.addClass('active');
|
||||
},
|
||||
});
|
||||
|
||||
sOptions.registry.js_get_posts_selectBlog = sOptions.Class.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
var def = this._rpc({
|
||||
model: 'blog.blog',
|
||||
method: 'search_read',
|
||||
args: [[], ['name', 'id']],
|
||||
}).then(function (blogs) {
|
||||
var $menu = self.$el.find('[data-filter-by-blog-id="0"]').parent();
|
||||
_.each(blogs, function (blog) {
|
||||
$menu.append($('<a/>', {
|
||||
class: 'dropdown-item',
|
||||
'data-filter-by-blog-id': blog.id,
|
||||
'data-no-preview': 'true',
|
||||
text: blog.name,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
return $.when(this._super.apply(this, arguments), def);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
filterByBlogId: function (previewMode, value, $opt) {
|
||||
value = parseInt(value);
|
||||
this.$target.attr('data-filter-by-blog-id', value).data('filterByBlogId', value);
|
||||
this.trigger_up('animation_start_demand', {
|
||||
editableMode: true,
|
||||
$target: this.$target,
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_setActive: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.$('[data-filter-by-blog-id]').addBack('[data-filter-by-blog-id]')
|
||||
.removeClass('active')
|
||||
.filter(function () {
|
||||
return (self.$target.data('filterByBlogId') || 0) == $(this).data('filterByBlogId');
|
||||
})
|
||||
.addClass('active');
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
odoo.define('website_blog.s_latest_posts_frontend', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var sAnimation = require('website.content.snippets.animation');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
sAnimation.registry.js_get_posts = sAnimation.Class.extend({
|
||||
selector : '.js_get_posts',
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
var limit = self.$target.data('postsLimit') || 3;
|
||||
var blogID = self.$target.data('filterByBlogId');
|
||||
var template = self.$target.data('template') || 'website_blog.s_latest_posts_list_template';
|
||||
var loading = self.$target.data('loading');
|
||||
|
||||
this.$target.empty(); // Compatibility with db that saved content inside by mistake
|
||||
this.$target.attr('contenteditable', 'False'); // Prevent user edition
|
||||
|
||||
var domain = [['website_published', '=', true]];
|
||||
if (blogID) {
|
||||
domain.push(['blog_id', '=', parseInt(blogID)]);
|
||||
}
|
||||
|
||||
var def = $.Deferred();
|
||||
this._rpc({
|
||||
route: '/blog/render_latest_posts',
|
||||
params: {
|
||||
template: template,
|
||||
domain: domain,
|
||||
limit: limit,
|
||||
},
|
||||
}).then(function (posts) {
|
||||
var $posts = $(posts).filter('.s_latest_posts_post');
|
||||
if (!$posts.length) {
|
||||
self.$target.append($('<div/>', {class: 'col-md-6 offset-md-3'})
|
||||
.append($('<div/>', {
|
||||
class: 'alert alert-warning alert-dismissible text-center',
|
||||
text: _t("No blog post was found. Make sure your posts are published."),
|
||||
})));
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading && loading === true) {
|
||||
// Perform an intro animation
|
||||
self._showLoading($posts);
|
||||
} else {
|
||||
self.$target.html($posts);
|
||||
}
|
||||
}, function (e) {
|
||||
if (self.editableMode) {
|
||||
self.$target.append($('<p/>', {
|
||||
class: 'text-danger',
|
||||
text: _t("An error occured with this latest posts block. If the problem persists, please consider deleting it and adding a new one"),
|
||||
}));
|
||||
}
|
||||
}).always(def.resolve.bind(def));
|
||||
return $.when(this._super.apply(this, arguments), def);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
this.$target.empty();
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_showLoading: function ($posts) {
|
||||
var self = this;
|
||||
|
||||
_.each($posts, function (post) {
|
||||
var $post = $(post);
|
||||
var $loadingContainer = $post.find('.loading_container');
|
||||
var $thumb = $post.find('.thumb .o_blog_cover_image');
|
||||
var $progress = $('<div/>', {
|
||||
class: 'progress js-loading',
|
||||
}).append($('<div/>', {
|
||||
class: 'progress-bar',
|
||||
role: 'progressbar',
|
||||
'aria-valuenow': '0',
|
||||
'aria-valuemin': '0',
|
||||
'aria-valuemax': '100',
|
||||
css: {
|
||||
width: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
// If can't find loading container or thumb inside the post, then they are the post itself
|
||||
if (!$loadingContainer.length) {
|
||||
$loadingContainer = $post;
|
||||
}
|
||||
if (!$thumb.length) {
|
||||
$thumb = $post;
|
||||
}
|
||||
|
||||
$post.addClass('js-loading');
|
||||
$progress.appendTo($loadingContainer);
|
||||
$post.appendTo(self.$target);
|
||||
|
||||
var m = $thumb.css('background-image').match(/url\(["']?(.+)["']?\)/);
|
||||
var bg = m ? m[1] : 'none';
|
||||
var loaded = false;
|
||||
|
||||
var $bar = $progress.find('.progress-bar');
|
||||
$bar.css('width', '50%').attr('aria-valuenow', '50');
|
||||
|
||||
var $dummyImg = $('<img/>');
|
||||
|
||||
// Show the post after 5sec in any case
|
||||
var timer = setTimeout(function () {
|
||||
$dummyImg.remove();
|
||||
$post.removeClass('js-loading');
|
||||
$progress.hide();
|
||||
}, 5000);
|
||||
|
||||
$dummyImg.load(function () {
|
||||
$bar.css('width', '100%').attr('aria-valuenow', '100');
|
||||
setTimeout(function () {
|
||||
$post.removeClass('js-loading');
|
||||
$progress.fadeOut(500);
|
||||
}, 500);
|
||||
$dummyImg.remove();
|
||||
clearTimeout(timer);
|
||||
});
|
||||
|
||||
$dummyImg.attr('src', bg);
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
$s-latest-posts-big-picture-figure-bg: null;
|
||||
|
||||
@mixin s-latest-posts-js-get-posts-hook {};
|
||||
@mixin s-latest-posts-figure-hook {};
|
||||
@@ -232,3 +232,122 @@
|
||||
padding: 2px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Latest posts snippets
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
.s_latest_posts .media > a {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
> .o_blog_cover_container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
> .o_blog_cover_component {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
}
|
||||
> .o_blog_cover_image {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$s-latest-posts-big-picture-figure-bg: $white !default;
|
||||
|
||||
.s_latest_posts_big_picture {
|
||||
.s_latest_posts_post {
|
||||
position: relative;
|
||||
height: 250px;
|
||||
background: $s-latest-posts-big-picture-figure-bg;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 300ms;
|
||||
|
||||
&.js-loading {
|
||||
background: gray('200') !important;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
opacity: 0.8;
|
||||
|
||||
> .o_blog_cover_component {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
}
|
||||
> .o_blog_cover_image {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
> div, > div > a {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
padding: 2em;
|
||||
color: $white;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.25em;
|
||||
backface-visibility: hidden;
|
||||
&::after, &::before {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> a {
|
||||
z-index: 1000;
|
||||
text-indent: 200%;
|
||||
white-space: nowrap;
|
||||
font-size: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: 300;
|
||||
span {
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 1em 0 0;
|
||||
letter-spacing: 1px;
|
||||
font-size: 68.5%;
|
||||
}
|
||||
}
|
||||
@include s-latest-posts-figure-hook;
|
||||
}
|
||||
|
||||
@include s-latest-posts-js-get-posts-hook;
|
||||
}
|
||||
|
||||
.s_latest_posts, .s_latest_posts_big_picture {
|
||||
|
||||
.js_get_posts {
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.progress.js-loading {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
border-radius: 0;
|
||||
|
||||
.progress-bar {
|
||||
@include o-position-absolute(45%, 10%, auto, 10%);
|
||||
height: 10px;
|
||||
margin: 0;
|
||||
background: gray('600');
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="website_blog" inherit_id="website.snippets" name="Snippet Blog">
|
||||
<xpath expr="//div[@id='snippet_feature']/div[hasclass('o_panel_body')]/t" position="before">
|
||||
<t t-snippet="website_blog.s_latest_posts" t-thumbnail="/website_blog/static/src/img/s_latest_posts.jpg"/>
|
||||
<t t-snippet="website_blog.s_latest_posts_big_picture" t-thumbnail="/website_blog/static/src/img/s_latest_posts_big_picture.jpg"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[@id='snippet_options']" position="inside">
|
||||
<div data-js="blog_cover" data-selector=".website_blog .o_blog_cover_container" data-no-check="true">
|
||||
<a href="#" class="dropdown-item" data-change="true" data-no-preview="true">Change Cover</a>
|
||||
@@ -37,4 +42,37 @@
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Snippets' Options -->
|
||||
<template id="s_latest_posts_options" inherit_id="website.snippet_options">
|
||||
<xpath expr="//div[@data-js='background']" position="before">
|
||||
<div data-js="js_get_posts_selectBlog" data-selector=".js_get_posts">
|
||||
<div class="dropdown-submenu">
|
||||
<a tabindex="-1" href="#" class="dropdown-item">Choose a blog</a>
|
||||
<div class="dropdown-menu">
|
||||
<a href="#" class="dropdown-item" data-filter-by-blog-id="0" data-no-preview="true">All blogs</a>
|
||||
<!-- the blog list will be generated in js -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-js="js_get_posts_limit" data-selector=".js_get_posts">
|
||||
<div class="dropdown-submenu">
|
||||
<a tabindex="-1" href="#" class="dropdown-item">Number of posts</a>
|
||||
<div class="dropdown-menu" data-no-preview="true">
|
||||
<a href="#" class="dropdown-item" data-posts-limit="1">1 post</a>
|
||||
<a href="#" class="dropdown-item" data-posts-limit="2">2 posts</a>
|
||||
<a href="#" class="dropdown-item" data-posts-limit="3">3 posts</a>
|
||||
<a href="#" class="dropdown-item" data-posts-limit="4">4 posts</a>
|
||||
<a href="#" class="dropdown-item" data-posts-limit="5">5 posts</a>
|
||||
<a href="#" class="dropdown-item" data-posts-limit="6">6 posts</a>
|
||||
<a href="#" class="dropdown-item" data-posts-limit="7">7 posts</a>
|
||||
<a href="#" class="dropdown-item" data-posts-limit="8">8 posts</a>
|
||||
<a href="#" class="dropdown-item" data-posts-limit="9">9 posts</a>
|
||||
<a href="#" class="dropdown-item" data-posts-limit="10">10 posts</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="_assets_primary_variables" inherit_id="web._assets_primary_variables">
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/website_blog/static/src/scss/primary_variables.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_editor" inherit_id="website.assets_editor" name="Blog Editor">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/website_blog/static/src/js/website_blog.editor.js"></script>
|
||||
<script type="text/javascript" src="/website_blog/static/src/js/s_latest_posts_editor.js"/>
|
||||
<script type="text/javascript" src="/website_blog/static/src/js/website.tour.blog.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
@@ -14,6 +21,7 @@
|
||||
|
||||
<script type="text/javascript" src="/website_blog/static/src/js/contentshare.js"/>
|
||||
<script type="text/javascript" src="/website_blog/static/src/js/website_blog.js"/>
|
||||
<script type="text/javascript" src="/website_blog/static/src/js/s_latest_posts_frontend.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -585,4 +593,62 @@
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template id="s_latest_posts" name="Latest Posts - List">
|
||||
<section class="s_latest_posts">
|
||||
<div class="container">
|
||||
<h2 class="text-center">Latest Posts</h2>
|
||||
<div class="row js_get_posts"
|
||||
data-loading="true"
|
||||
data-template="website_blog.s_latest_posts_list_template"/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template id="s_latest_posts_big_picture" name="Latest Posts - Big Images">
|
||||
<section class="s_latest_posts_big_picture">
|
||||
<div class="container-fluid">
|
||||
<h2 class="text-center">Latest Posts</h2>
|
||||
<div class="row js_get_posts"
|
||||
data-loading="true"
|
||||
data-template="website_blog.s_latest_posts_big_picture_template"/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template id="s_latest_posts_list_template">
|
||||
<t t-foreach="posts" t-as="p">
|
||||
<t t-set="properties" t-value="json.loads(p.cover_properties)"/>
|
||||
<div class="col-12 media mt-3 s_latest_posts_post">
|
||||
<a t-attf-href="/blog/#{p.blog_id.id}/post/#{p.id}">
|
||||
<t t-call="website_blog.blog_cover">
|
||||
<t t-set="cover_properties" t-value="properties"/>
|
||||
<t t-set="additionnal_classes" t-value="'loading_container thumb'"/>
|
||||
</t>
|
||||
</a>
|
||||
<div class="media-body ml-3">
|
||||
<h4 t-field="p.name"/>
|
||||
<p t-field="p.subtitle"/>
|
||||
<a class="btn btn-sm btn-secondary" t-attf-href="/blog/#{p.blog_id.id}/post/#{p.id}">Read More</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="s_latest_posts_big_picture_template">
|
||||
<t t-foreach="posts" t-as="p">
|
||||
<t t-set="properties" t-value="json.loads(p.cover_properties)"/>
|
||||
<div class="col-12 col-md-6 my-3 loading_container s_latest_posts_post">
|
||||
<t t-call="website_blog.blog_cover">
|
||||
<t t-set="cover_properties" t-value="properties"/>
|
||||
<t t-set="additionnal_classes" t-value="'thumb'"/>
|
||||
</t>
|
||||
<div>
|
||||
<h2 t-field="p.name"/>
|
||||
<p t-field="p.subtitle"/>
|
||||
<a t-attf-href="/blog/#{p.blog_id.id}/post/#{p.id}">Read More</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user