[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:
qsm-odoo
2018-08-12 18:04:27 +02:00
parent 8517a3c335
commit af30c2ee61
9 changed files with 481 additions and 0 deletions
+5
View File
@@ -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;
}
}
}
+38
View File
@@ -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>