Начальное наполнение
91
README.md
@ -1,2 +1,91 @@
|
||||
# website_blog
|
||||
Odoo Blog
|
||||
----------
|
||||
|
||||
Write, Design, Promote and Engage with <a href="https://www.odoo.com/app/blog">Odoo Blog</a>.
|
||||
|
||||
Express yourself with the Odoo enterprise grade blogging platform. Write
|
||||
beautiful blog posts, engage with visitors, translate content and moderate
|
||||
social streams.
|
||||
|
||||
Get your blog posts efficiently referenced in Google and translated in mutiple
|
||||
languages in just a few clicks.
|
||||
|
||||
Write Beautiful Blog Posts
|
||||
--------------------------
|
||||
|
||||
Drag & Drop well designed *'Building Blocks'* to create beautifull blog posts
|
||||
that perfectly integrates images, videos, call-to-actions, quotes, banners,
|
||||
etc.
|
||||
|
||||
With our unique *'edit inline'* approach, you don't need to be a designer to
|
||||
create awsome, good-looking, content. Each blog post will look like it's
|
||||
designed by a professional designer.
|
||||
|
||||
Automated Translation by Professionals
|
||||
--------------------------------------
|
||||
|
||||
Get your blog posts translated in multiple languages with no effort. Our
|
||||
translation "on demand" feature allows you to benefit from professional
|
||||
translators to translate all your changes automatically. (\$0.05 per word)
|
||||
Translated versions are updated automatically once translated by professionals
|
||||
(around 32 hours).
|
||||
|
||||
Engage With Your Visitors
|
||||
-------------------------
|
||||
|
||||
The integrated website live chat feature allows you to start chatting in real time with
|
||||
your visitors to get feedback on your recent posts or get ideas to write new
|
||||
posts.
|
||||
|
||||
Engaging with your visitors is also a great way to convert visitors into
|
||||
customers.
|
||||
|
||||
Build Visitor Loyalty
|
||||
---------------------
|
||||
|
||||
The one click *follow* button will allow visitors to receive your blog posts by
|
||||
email with no effort, without having to register. Social media icons allow
|
||||
visitors to share your best blog posts easily.
|
||||
|
||||
Google Analytics Integration
|
||||
----------------------------
|
||||
|
||||
Get a clear visibility of your sales funnel. Odoo's Google Analytics trackers
|
||||
are configured by default to track all kinds of events related to shopping
|
||||
carts, call-to-actions, etc.
|
||||
|
||||
As Odoo marketing tools (mass mailing, campaigns, etc) are also linked with
|
||||
Google Analytics, you get a 360° view of your business.
|
||||
|
||||
SEO Optimized Blog Posts
|
||||
------------------------
|
||||
|
||||
SEO tools are ready to use, with no configuration required. Odoo suggests
|
||||
keywords for your titles according to Google's most searched terms, Google
|
||||
Analytics tracks interests of your visitors, sitemaps are created automatically
|
||||
for quick Google indexing, etc.
|
||||
|
||||
The system even creates structured content automatically to promote your
|
||||
products and events effectively in Google.
|
||||
|
||||
Designer-Friendly Themes
|
||||
------------------------
|
||||
|
||||
Themes are awesome and easy to design. You don't need to develop to create new
|
||||
pages, themes or building blocks. We use a clean HTML structure, a
|
||||
[bootstrap](http://getbootstrap.com/) CSS and our modularity allows you to
|
||||
distribute your themes easily.
|
||||
|
||||
The building block approach allows the website to remain clean after end-users
|
||||
start creating new contents.
|
||||
|
||||
Easy Access Rights
|
||||
------------------
|
||||
|
||||
Not everyone requires the same access to your website. Designers manage the
|
||||
layout of the site, editors approve content and authors write that content.
|
||||
This lets you organize your publishing process according to your needs.
|
||||
|
||||
Other access rights are related to business objects (products, people, events,
|
||||
etc) and directly following Odoo's standard access rights management, so you do
|
||||
not have to configure things twice.
|
||||
|
5
__init__.py
Normal file
@ -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
|
55
__manifest__.py
Normal file
@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Blog',
|
||||
'category': 'Website/Website',
|
||||
'sequence': 200,
|
||||
'website': 'https://www.odoo.com/app/blog',
|
||||
'summary': 'Publish blog posts, announces, news',
|
||||
'version': '1.1',
|
||||
'depends': ['website_mail', 'website_partner'],
|
||||
'data': [
|
||||
'data/mail_message_subtype_data.xml',
|
||||
'data/mail_templates.xml',
|
||||
'data/website_blog_data.xml',
|
||||
'data/blog_snippet_template_data.xml',
|
||||
'views/website_blog_views.xml',
|
||||
'views/website_blog_components.xml',
|
||||
'views/website_blog_posts_loop.xml',
|
||||
'views/website_blog_templates.xml',
|
||||
'views/snippets/snippets.xml',
|
||||
'views/snippets/s_blog_posts.xml',
|
||||
'views/website_pages_views.xml',
|
||||
'views/blog_post_add.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/website_blog_security.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/website_blog_demo.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'assets': {
|
||||
'website.assets_wysiwyg': [
|
||||
'website_blog/static/src/js/options.js',
|
||||
'website_blog/static/src/snippets/s_blog_posts/options.js',
|
||||
],
|
||||
'website.assets_editor': [
|
||||
'website_blog/static/src/js/tours/website_blog.js',
|
||||
'website_blog/static/src/js/components/*.js',
|
||||
'website_blog/static/src/js/systray_items/*.js',
|
||||
],
|
||||
'website.backend_assets_all_wysiwyg': [
|
||||
'website_blog/static/src/js/wysiwyg_adapter.js',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'website_blog/static/tests/**/*',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'website_blog/static/src/scss/website_blog.scss',
|
||||
'website_blog/static/src/js/contentshare.js',
|
||||
'website_blog/static/src/js/website_blog.js',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
4
controllers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
308
controllers/main.py
Normal file
@ -0,0 +1,308 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import werkzeug
|
||||
import itertools
|
||||
import pytz
|
||||
import babel.dates
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import http, fields, tools, models
|
||||
from odoo.addons.http_routing.models.ir_http import slug, unslug
|
||||
from odoo.addons.website.controllers.main import QueryURL
|
||||
from odoo.addons.portal.controllers.portal import _build_url_w_params
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import html2plaintext
|
||||
from odoo.tools.misc import get_lang
|
||||
from odoo.tools import sql
|
||||
|
||||
|
||||
class WebsiteBlog(http.Controller):
|
||||
_blog_post_per_page = 12 # multiple of 2,3,4
|
||||
_post_comment_per_page = 10
|
||||
|
||||
def tags_list(self, tag_ids, current_tag):
|
||||
tag_ids = list(tag_ids) # required to avoid using the same list
|
||||
if current_tag in tag_ids:
|
||||
tag_ids.remove(current_tag)
|
||||
else:
|
||||
tag_ids.append(current_tag)
|
||||
tag_ids = request.env['blog.tag'].browse(tag_ids)
|
||||
return ','.join(slug(tag) for tag in tag_ids)
|
||||
|
||||
def nav_list(self, blog=None):
|
||||
dom = blog and [('blog_id', '=', blog.id)] or []
|
||||
if not request.env.user.has_group('website.group_website_designer'):
|
||||
dom += [('post_date', '<=', fields.Datetime.now())]
|
||||
groups = request.env['blog.post']._read_group(
|
||||
dom, groupby=['post_date:month'])
|
||||
|
||||
locale = get_lang(request.env).code
|
||||
tzinfo = pytz.timezone(request.context.get('tz', 'utc') or 'utc')
|
||||
fmt = tools.DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
res = defaultdict(list)
|
||||
for [start] in groups:
|
||||
year = babel.dates.format_datetime(start, format='yyyy', tzinfo=tzinfo, locale=locale)
|
||||
res[year].append({
|
||||
'date_begin': start.strftime(fmt),
|
||||
'date_end': (start + models.READ_GROUP_TIME_GRANULARITY['month']).strftime(fmt),
|
||||
'month': babel.dates.format_datetime(start, format='MMMM', tzinfo=tzinfo, locale=locale),
|
||||
'year': year,
|
||||
})
|
||||
return res
|
||||
|
||||
def _get_blog_post_search_options(self, blog=None, active_tags=None, date_begin=None, date_end=None, state=None, **post):
|
||||
return {
|
||||
'displayDescription': True,
|
||||
'displayDetail': False,
|
||||
'displayExtraDetail': False,
|
||||
'displayExtraLink': False,
|
||||
'displayImage': False,
|
||||
'allowFuzzy': not post.get('noFuzzy'),
|
||||
'blog': str(blog.id) if blog else None,
|
||||
'tag': ','.join([str(id) for id in active_tags.ids]),
|
||||
'date_begin': date_begin,
|
||||
'date_end': date_end,
|
||||
'state': state,
|
||||
}
|
||||
|
||||
def _prepare_blog_values(self, blogs, blog=False, date_begin=False, date_end=False, tags=False, state=False, page=False, search=None, **post):
|
||||
""" Prepare all values to display the blogs index page or one specific blog"""
|
||||
BlogPost = request.env['blog.post']
|
||||
BlogTag = request.env['blog.tag']
|
||||
|
||||
# prepare domain
|
||||
domain = request.website.website_domain()
|
||||
|
||||
if blog:
|
||||
domain += [('blog_id', '=', blog.id)]
|
||||
|
||||
if date_begin and date_end:
|
||||
domain += [("post_date", ">=", date_begin), ("post_date", "<=", date_end)]
|
||||
active_tag_ids = tags and [unslug(tag)[1] for tag in tags.split(',')] or []
|
||||
active_tags = BlogTag
|
||||
if active_tag_ids:
|
||||
active_tags = BlogTag.browse(active_tag_ids).exists()
|
||||
fixed_tag_slug = ",".join(slug(t) for t in active_tags)
|
||||
if fixed_tag_slug != tags:
|
||||
path = request.httprequest.full_path
|
||||
new_url = path.replace("/tag/%s" % tags, fixed_tag_slug and "/tag/%s" % fixed_tag_slug or "", 1)
|
||||
if new_url != path: # check that really replaced and avoid loop
|
||||
return request.redirect(new_url, 301)
|
||||
domain += [('tag_ids', 'in', active_tags.ids)]
|
||||
|
||||
if request.env.user.has_group('website.group_website_designer'):
|
||||
count_domain = domain + [("website_published", "=", True), ("post_date", "<=", fields.Datetime.now())]
|
||||
published_count = BlogPost.search_count(count_domain)
|
||||
unpublished_count = BlogPost.search_count(domain) - published_count
|
||||
|
||||
if state == "published":
|
||||
domain += [("website_published", "=", True), ("post_date", "<=", fields.Datetime.now())]
|
||||
elif state == "unpublished":
|
||||
domain += ['|', ("website_published", "=", False), ("post_date", ">", fields.Datetime.now())]
|
||||
else:
|
||||
domain += [("post_date", "<=", fields.Datetime.now())]
|
||||
|
||||
use_cover = request.website.is_view_active('website_blog.opt_blog_cover_post')
|
||||
fullwidth_cover = request.website.is_view_active('website_blog.opt_blog_cover_post_fullwidth_design')
|
||||
|
||||
# if blog, we show blog title, if use_cover and not fullwidth_cover we need pager + latest always
|
||||
offset = (page - 1) * self._blog_post_per_page
|
||||
if not blog and use_cover and not fullwidth_cover and not tags and not date_begin and not date_end and not search:
|
||||
offset += 1
|
||||
|
||||
options = self._get_blog_post_search_options(
|
||||
blog=blog,
|
||||
active_tags=active_tags,
|
||||
date_begin=date_begin,
|
||||
date_end=date_end,
|
||||
state=state,
|
||||
**post
|
||||
)
|
||||
total, details, fuzzy_search_term = request.website._search_with_fuzzy("blog_posts_only", search,
|
||||
limit=page * self._blog_post_per_page, order="is_published desc, post_date desc, id asc", options=options)
|
||||
posts = details[0].get('results', BlogPost)
|
||||
first_post = BlogPost
|
||||
if posts and not blog and posts[0].website_published:
|
||||
first_post = posts[0]
|
||||
posts = posts[offset:offset + self._blog_post_per_page]
|
||||
|
||||
url_args = dict()
|
||||
if search:
|
||||
url_args["search"] = search
|
||||
|
||||
if date_begin and date_end:
|
||||
url_args["date_begin"] = date_begin
|
||||
url_args["date_end"] = date_end
|
||||
|
||||
pager = tools.lazy(lambda: request.website.pager(
|
||||
url=request.httprequest.path.partition('/page/')[0],
|
||||
total=total,
|
||||
page=page,
|
||||
step=self._blog_post_per_page,
|
||||
url_args=url_args,
|
||||
))
|
||||
|
||||
if not blogs:
|
||||
all_tags = request.env['blog.tag']
|
||||
else:
|
||||
all_tags = tools.lazy(lambda: blogs.all_tags(join=True) if not blog else blogs.all_tags().get(blog.id, request.env['blog.tag']))
|
||||
tag_category = tools.lazy(lambda: sorted(all_tags.mapped('category_id'), key=lambda category: category.name.upper()))
|
||||
other_tags = tools.lazy(lambda: sorted(all_tags.filtered(lambda x: not x.category_id), key=lambda tag: tag.name.upper()))
|
||||
nav_list = tools.lazy(self.nav_list)
|
||||
# for performance prefetch the first post with the others
|
||||
post_ids = (first_post | posts).ids
|
||||
# and avoid accessing related blogs one by one
|
||||
posts.blog_id
|
||||
|
||||
return {
|
||||
'date_begin': date_begin,
|
||||
'date_end': date_end,
|
||||
'first_post': first_post.with_prefetch(post_ids),
|
||||
'other_tags': other_tags,
|
||||
'tag_category': tag_category,
|
||||
'nav_list': nav_list,
|
||||
'tags_list': self.tags_list,
|
||||
'pager': pager,
|
||||
'posts': posts.with_prefetch(post_ids),
|
||||
'tag': tags,
|
||||
'active_tag_ids': active_tags.ids,
|
||||
'domain': domain,
|
||||
'state_info': state and {"state": state, "published": published_count, "unpublished": unpublished_count},
|
||||
'blogs': blogs,
|
||||
'blog': blog,
|
||||
'search': fuzzy_search_term or search,
|
||||
'search_count': total,
|
||||
'original_search': fuzzy_search_term and search,
|
||||
}
|
||||
|
||||
@http.route([
|
||||
'/blog',
|
||||
'/blog/page/<int:page>',
|
||||
'/blog/tag/<string:tag>',
|
||||
'/blog/tag/<string:tag>/page/<int:page>',
|
||||
'''/blog/<model("blog.blog"):blog>''',
|
||||
'''/blog/<model("blog.blog"):blog>/page/<int:page>''',
|
||||
'''/blog/<model("blog.blog"):blog>/tag/<string:tag>''',
|
||||
'''/blog/<model("blog.blog"):blog>/tag/<string:tag>/page/<int:page>''',
|
||||
], type='http', auth="public", website=True, sitemap=True)
|
||||
def blog(self, blog=None, tag=None, page=1, search=None, **opt):
|
||||
Blog = request.env['blog.blog']
|
||||
blogs = tools.lazy(lambda: Blog.search(request.website.website_domain(), order="create_date asc, id asc"))
|
||||
|
||||
if not blog and len(blogs) == 1:
|
||||
url = QueryURL('/blog/%s' % slug(blogs[0]), search=search, **opt)()
|
||||
return request.redirect(url, code=302)
|
||||
|
||||
date_begin, date_end = opt.get('date_begin'), opt.get('date_end')
|
||||
|
||||
if tag and request.httprequest.method == 'GET':
|
||||
# redirect get tag-1,tag-2 -> get tag-1
|
||||
tags = tag.split(',')
|
||||
if len(tags) > 1:
|
||||
url = QueryURL('' if blog else '/blog', ['blog', 'tag'], blog=blog, tag=tags[0], date_begin=date_begin, date_end=date_end, search=search)()
|
||||
return request.redirect(url, code=302)
|
||||
|
||||
values = self._prepare_blog_values(blogs=blogs, blog=blog, tags=tag, page=page, search=search, **opt)
|
||||
|
||||
# in case of a redirection need by `_prepare_blog_values` we follow it
|
||||
if isinstance(values, werkzeug.wrappers.Response):
|
||||
return values
|
||||
|
||||
if blog:
|
||||
values['main_object'] = blog
|
||||
values['blog_url'] = QueryURL('/blog', ['blog', 'tag'], blog=blog, tag=tag, date_begin=date_begin, date_end=date_end, search=search)
|
||||
|
||||
return request.render("website_blog.blog_post_short", values)
|
||||
|
||||
@http.route(['''/blog/<model("blog.blog"):blog>/feed'''], type='http', auth="public", website=True, sitemap=True)
|
||||
def blog_feed(self, blog, limit='15', **kwargs):
|
||||
v = {}
|
||||
v['blog'] = blog
|
||||
v['base_url'] = blog.get_base_url()
|
||||
v['posts'] = request.env['blog.post'].search([('blog_id', '=', blog.id)], limit=min(int(limit), 50), order="post_date DESC")
|
||||
v['html2plaintext'] = html2plaintext
|
||||
r = request.render("website_blog.blog_feed", v, headers=[('Content-Type', 'application/atom+xml')])
|
||||
return r
|
||||
|
||||
@http.route([
|
||||
'''/blog/<model("blog.blog"):blog>/post/<model("blog.post", "[('blog_id','=',blog.id)]"):blog_post>''',
|
||||
], type='http', auth="public", website=True, sitemap=False)
|
||||
def old_blog_post(self, blog, blog_post, tag_id=None, page=1, enable_editor=None, **post):
|
||||
# Compatibility pre-v14
|
||||
return request.redirect(_build_url_w_params("/blog/%s/%s" % (slug(blog), slug(blog_post)), request.params), code=301)
|
||||
|
||||
@http.route([
|
||||
'''/blog/<model("blog.blog"):blog>/<model("blog.post", "[('blog_id','=',blog.id)]"):blog_post>''',
|
||||
], type='http', auth="public", website=True, sitemap=True)
|
||||
def blog_post(self, blog, blog_post, tag_id=None, page=1, enable_editor=None, **post):
|
||||
""" Prepare all values to display the blog.
|
||||
|
||||
:return dict values: values for the templates, containing
|
||||
|
||||
- 'blog_post': browse of the current post
|
||||
- 'blog': browse of the current blog
|
||||
- 'blogs': list of browse records of blogs
|
||||
- 'tag': current tag, if tag_id in parameters
|
||||
- 'tags': all tags, for tag-based navigation
|
||||
- 'pager': a pager on the comments
|
||||
- 'nav_list': a dict [year][month] for archives navigation
|
||||
- 'next_post': next blog post, to direct the user towards the next interesting post
|
||||
"""
|
||||
BlogPost = request.env['blog.post']
|
||||
date_begin, date_end = post.get('date_begin'), post.get('date_end')
|
||||
|
||||
domain = request.website.website_domain()
|
||||
blogs = blog.search(domain, order="create_date, id asc")
|
||||
|
||||
tag = None
|
||||
if tag_id:
|
||||
tag = request.env['blog.tag'].browse(int(tag_id))
|
||||
blog_url = QueryURL('', ['blog', 'tag'], blog=blog_post.blog_id, tag=tag, date_begin=date_begin, date_end=date_end)
|
||||
|
||||
if not blog_post.blog_id.id == blog.id:
|
||||
return request.redirect("/blog/%s/%s" % (slug(blog_post.blog_id), slug(blog_post)), code=301)
|
||||
|
||||
tags = request.env['blog.tag'].search([])
|
||||
|
||||
# Find next Post
|
||||
blog_post_domain = [('blog_id', '=', blog.id)]
|
||||
if not request.env.user.has_group('website.group_website_designer'):
|
||||
blog_post_domain += [('post_date', '<=', fields.Datetime.now())]
|
||||
|
||||
all_post = BlogPost.search(blog_post_domain)
|
||||
|
||||
if blog_post not in all_post:
|
||||
return request.redirect("/blog/%s" % (slug(blog_post.blog_id)))
|
||||
|
||||
# should always return at least the current post
|
||||
all_post_ids = all_post.ids
|
||||
current_blog_post_index = all_post_ids.index(blog_post.id)
|
||||
nb_posts = len(all_post_ids)
|
||||
next_post_id = all_post_ids[(current_blog_post_index + 1) % nb_posts] if nb_posts > 1 else None
|
||||
next_post = next_post_id and BlogPost.browse(next_post_id) or False
|
||||
|
||||
values = {
|
||||
'tags': tags,
|
||||
'tag': tag,
|
||||
'blog': blog,
|
||||
'blog_post': blog_post,
|
||||
'blogs': blogs,
|
||||
'main_object': blog_post,
|
||||
'nav_list': self.nav_list(blog),
|
||||
'enable_editor': enable_editor,
|
||||
'next_post': next_post,
|
||||
'date': date_begin,
|
||||
'blog_url': blog_url,
|
||||
}
|
||||
response = request.render("website_blog.blog_post_complete", values)
|
||||
|
||||
if blog_post.id not in request.session.get('posts_viewed', []):
|
||||
if sql.increment_fields_skiplock(blog_post, 'visits'):
|
||||
if not request.session.get('posts_viewed'):
|
||||
request.session['posts_viewed'] = []
|
||||
request.session['posts_viewed'].append(blog_post.id)
|
||||
request.session.touch()
|
||||
return response
|
35
data/blog_snippet_template_data.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Filters for Dynamic Filter -->
|
||||
<record id="dynamic_snippet_latest_blog_post_filter" model="ir.filters">
|
||||
<field name="name">Latest Blog Posts</field>
|
||||
<field name="model_id">blog.post</field>
|
||||
<field name="user_id" eval="False" />
|
||||
<field name="domain">[('post_date', '<=', context_today())]</field>
|
||||
<field name="sort">['post_date desc']</field>
|
||||
<field name="action_id" ref="website.action_website"/>
|
||||
</record>
|
||||
<record id="dynamic_snippet_most_viewed_blog_post_filter" model="ir.filters">
|
||||
<field name="name">Most Viewed Blog Posts</field>
|
||||
<field name="model_id">blog.post</field>
|
||||
<field name="user_id" eval="False" />
|
||||
<field name="domain">[('post_date', '<=', context_today()), ('visits', '!=', False)]</field>
|
||||
<field name="sort">['visits desc']</field>
|
||||
<field name="action_id" ref="website.action_website"/>
|
||||
</record>
|
||||
<!-- Dynamic Filter -->
|
||||
<record id="dynamic_filter_latest_blog_posts" model="website.snippet.filter">
|
||||
<field name="name">Latest Blog Posts</field>
|
||||
<field name="filter_id" ref="website_blog.dynamic_snippet_latest_blog_post_filter"/>
|
||||
<field name="field_names">name,teaser,subtitle</field>
|
||||
<field name="limit" eval="16"/>
|
||||
</record>
|
||||
<record id="dynamic_filter_most_viewed_blog_posts" model="website.snippet.filter">
|
||||
<field name="name">Most Viewed Blog Posts</field>
|
||||
<field name="filter_id" ref="website_blog.dynamic_snippet_most_viewed_blog_post_filter"/>
|
||||
<field name="field_names">name,teaser,subtitle</field>
|
||||
<field name="limit" eval="16"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
13
data/ir_asset.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="website_blog.s_latest_posts_000_scss" model="ir.asset">
|
||||
<field name="name">Latest posts 000 SCSS</field>
|
||||
<field name="bundle">web.assets_frontend</field>
|
||||
<field name="path">website_blog/static/src/snippets/s_latest_posts/000.scss</field>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
12
data/mail_message_subtype_data.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
|
||||
<!-- Blog-related subtypes for messaging / Chatter -->
|
||||
<record id="mt_blog_blog_published" model="mail.message.subtype">
|
||||
<field name="name">Published Post</field>
|
||||
<field name="res_model">blog.blog</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="description">Published Post</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
12
data/mail_templates.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<template id="blog_post_template_new_post">
|
||||
<p>A new post <t t-esc="post.name" /> has been published on the <t t-esc="object.name" /> blog. Click here to access the blog :</p>
|
||||
<p style="margin-left: 30px; margin-top: 10 px; margin-bottom: 10px;">
|
||||
<a t-attf-href="/blog/#{slug(object)}/#{slug(post)}"
|
||||
style="padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer;background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px">
|
||||
Access post
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
</data></odoo>
|
41
data/website_blog_data.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="blog_blog_1" model="blog.blog">
|
||||
<field name="name">Our blog</field>
|
||||
<field name="subtitle">We are a team of passionate people whose goal is to improve everyone's life.</field>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_5.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
|
||||
</record>
|
||||
|
||||
<record id="menu_blog" model="website.menu">
|
||||
<field name="name">Blog</field>
|
||||
<field name="url">/blog</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">40</field>
|
||||
</record>
|
||||
|
||||
<!-- Blog-related subtypes for messaging / Chatter -->
|
||||
<record id="mt_blog_blog_published" model="mail.message.subtype">
|
||||
<field name="name">Published Post</field>
|
||||
<field name="res_model">blog.blog</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="description">Published Post</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
<data>
|
||||
|
||||
<!-- jump to blog at install -->
|
||||
<record id="action_open_website" model="ir.actions.act_url">
|
||||
<field name="name">Website Blogs</field>
|
||||
<field name="target">self</field>
|
||||
<field name="url" eval="'/blog/'"/>
|
||||
</record>
|
||||
<record id="base.open_menu" model="ir.actions.todo">
|
||||
<field name="action_id" ref="action_open_website"/>
|
||||
<field name="state">open</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
406
data/website_blog_demo.xml
Normal file
@ -0,0 +1,406 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="blog_blog_1" model="blog.blog">
|
||||
<field name="name">Travel</field>
|
||||
<field name="subtitle">Holiday tips</field>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/blog_1.jpeg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
|
||||
</record>
|
||||
<record id="blog_blog_2" model="blog.blog">
|
||||
<field name="name">Astronomy</field>
|
||||
<field name="subtitle">Astronomy is “stargazing"</field>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/blog_2.jpeg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
|
||||
</record>
|
||||
|
||||
<!-- TAGS -->
|
||||
<record id="blog_tag_1" model="blog.tag">
|
||||
<field name="name">hotels</field>
|
||||
</record>
|
||||
<record id="blog_tag_2" model="blog.tag">
|
||||
<field name="name">adventure</field>
|
||||
</record>
|
||||
<record id="blog_tag_3" model="blog.tag">
|
||||
<field name="name">guides</field>
|
||||
</record>
|
||||
<record id="blog_tag_4" model="blog.tag">
|
||||
<field name="name">telescopes</field>
|
||||
</record>
|
||||
<record id="blog_tag_5" model="blog.tag">
|
||||
<field name="name">discovery</field>
|
||||
</record>
|
||||
|
||||
<!-- POSTS -->
|
||||
<record id="blog_post_1" model="blog.post">
|
||||
<field name="name">Sierra Tarahumara</field>
|
||||
<field name="subtitle">An exciting mix of relaxation, culture, history, wildlife and hiking.</field>
|
||||
<field name="blog_id" ref="blog_blog_1"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="time.strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_1'), ref('blog_tag_2')])]"/>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_1.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0"}</field>
|
||||
<field name="content"><![CDATA[
|
||||
<p class="lead">Sierra Tarahumara, popularly known as Copper Canyon is situated in Mexico. The area is a favorite destination among those seeking an adventurous vacation.</p>
|
||||
<p>Copper Canyon is one of the six gorges in the area. Although the name suggests that the gorge might have some relevance to copper mining, this is not the case. The name is derived from the copper and green lichen covering the canyon. Copper Canyon has two climatic zones. The region features an alpine climate at the top and a subtropical climate at the lower levels. Winters are cold with frequent snowstorms at the higher altitudes. Summers are dry and hot. The capital city, Chihuahua, is a high altitude desert where weather ranges from cold winters to hot summers. The region is unique because of the various ecosystems that exist within it.</p>
|
||||
|
||||
<p>Another unique feature of Copper Canyon is the presence of the Tarahumara Indian culture. These semi-nomadic people live in cave dwellings. Their livelihood chiefly depends on farming and cattle ranching.</p>
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_1_1.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by PoloX Hernandez, @elpolox</figcaption>
|
||||
</figure>
|
||||
|
||||
<blockquote class="blockquote my-5">
|
||||
<em class="h4 my-0">Apart from the native population, the local wildlife is also a major crowd puller.</em>
|
||||
<footer class="blockquote-footer text-muted">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||
</blockquote>
|
||||
|
||||
<p>Several migratory and native birds, mammals and reptiles call Copper Canyon their home. The exquisite fauna in this near-pristine land is also worth checking out.</p>
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_1_2.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by Boris Smokrovic, @borisworkshop</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>A traveler may choose to explore the area by hiking around the canyon or venturing into it. Detailed planning is required for those who wish to venture into the depths of the canyon. There are a number of travel companies that specialize in organizing tours to the region. Visitors can fly to Copper Canyon using a tourist visa, which is valid for 180 days. Travelers can also drive from anywhere in the United States and acquire a visa at the Mexican customs station at the border.</p>
|
||||
<p>A holiday to the Copper Canyon promises to be an exciting mix of relaxation, culture, history, wildlife and hiking.</p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="blog_post_2" model="blog.post">
|
||||
<field name="name">Maui helicopter tours</field>
|
||||
<field name="subtitle">A great way to discover hidden places</field>
|
||||
<field name="blog_id" ref="blog_blog_1"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="(datetime.now()-relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_2')])]"/>
|
||||
<field name="visits">246</field>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_2.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
|
||||
<field name="content"><![CDATA[
|
||||
<section class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p class="lead">Maui helicopter tours are a great way to see the island from a different perspective and have a fun adventure. If you have never been on a helicopter before, this is a great place to do it.</p>
|
||||
<p>You will see all the beauty that Maui has to offer and can have a great time for the entire family. Tours are not too expensive and last from forty five minutes to over an hour. You can see places that are typically inaccessible with Maui helicopter tours. Places that are not available by foot or vehicle can be seen by air. Breathtaking sights await those who are up for some fun Maui helicopter tours. If you will be staying on the island for a considerable amount of time, you may want to think about doing multiple Maui helicopter tours.</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-auto">
|
||||
<figure>
|
||||
<img src="/website_blog/static/src/img/content_2_1.jpg" class="img-fluid" style="max-height:450px"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by Jon Ly, @jonatron</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h2 class="mt-5">East Maui</h2>
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_2_2.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by Anton Repponen, @repponen</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>East Maui helicopter tours will give you a view of the ten thousand foot volcano, Haleakala or House of the sun. This volcano is dormant and last erupted in 1790. You will be able to see the crater of the volcano and the dry, arid earth surrounding the south side of the volcano’s slop with Maui helicopter tours.</p>
|
||||
<p>The view of this is truly breathtaking and is a sight not to be missed. It is also highly educational with a chance to see a dormant volcano up close, something that can not be seen every day. On the northern and southern sides of the volcano, you will see an incredible different view however. These sides are lush and green and you will be able to see some beautiful waterfalls and gorgeous brush. Tropical rainforests abound on this side of the island and it is something that is not easily accessible by any other means than by air.</p>
|
||||
<p>Maui helicopter tours will allow you to see all of these sights. Make sure to take a camera or video with you when going on Maui helicopter tours to capture the beauty of the scenery and to show friends and family at home all the wonderful things you saw while on vacation.</p>
|
||||
|
||||
<h2 class="mt-5">Molokai Maui </h2>
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_2_3.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by Denys Nevozhai, @dnevozhai</figcaption>
|
||||
</figure>
|
||||
<p>Molokai Maui helicopter tours will take you to a different island but one that is only nine miles away and easily accessible by air. This island has a very small population with a different culture and scenery. The entire coast of the northeast is lined with cliffs and remote beaches. They are completely inaccessible by any other means of transportation than air.
|
||||
People who live on the island have never even seen this remarkable scenery unless they have taken Maui helicopter tours to view it. When the weather has been rainy and there is a lot of rainfall for he season you will see many astounding waterfalls.</p>
|
||||
<p>The cliffs in this region are among the highest in the world and to see water cascading from the high peaks is simply breathtaking. The short jaunt from Maui with Maui helicopter tours is well worth seeing the beauty of this natural environment.</p>
|
||||
<p>Maui helicopter tours are a great way to tour those places that can not be reached on foot or by car. The tours last approximately one hour and range from approximately one hundred eight five dollars to two hundred forty dollars person. For many, this is a once in a lifetime opportunity to see natural scenery that will not be available again. Taking cameras and videos to capture the moments will also allow you to relive the tour again and again as you reminisce throughout the years.</p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="blog_post_3" model="blog.post">
|
||||
<field name="name">How to choose the right hotel</field>
|
||||
<field name="subtitle">Facts you should bear in mind.</field>
|
||||
<field name="blog_id" ref="blog_blog_1"/>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_1')])]"/>
|
||||
<field name="visits">467</field>
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="(datetime.now()-relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_3.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
|
||||
<field name="content"><![CDATA[
|
||||
<p class="lead">So you’re going abroad, you’ve chosen your destination and now you have to choose a hotel.</p>
|
||||
<p>Ten years ago, you’d have probably visited your local travel agent and trusted the face-to-face advice you were given by the so called ‘experts’. The 21st Century way to select and book your hotel is of course on the Internet, by using travel websites.</p>
|
||||
|
||||
<p>But how do you sift through the amazing choices on offer? And more importantly, do you really trust the photographs and descriptions of the hotels that they have awarded themselves with the motivation of getting bookings? Traveler reviews can be helpful, but you need to exercise caution. They are often biased, sometimes out of date, and may not serve your interests at all. How do you know that the features that are important to the reviewer are important to you?</p>
|
||||
|
||||
<blockquote>
|
||||
<em class="h4 my-0">The more reviews you read, the more you notice how they tend to cluster at the extremes of opinion.</em>
|
||||
<footer class="blockquote-footer text-muted">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||
</blockquote>
|
||||
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_3_1.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by Jason Briscoe, @jbriscoe</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>Then there’s the problem of the reviewer’s motivation. The more reviews you read, the more you notice how they tend to cluster at the extremes of opinion. On one end, you have angry reviewers with axes to grind; at the other, you have delighted guests who lavish praise beyond belief. You’ll not be surprised to learn that hotels sometimes post their own glowing reviews, or that competitor’s line up for the chance to lambaste the competition with bad reviews. It makes sense to consider what is really important to you when selecting a hotel. You should then choose an online hotel directory that gives up-to-date, independent, impartial information that really matters.
|
||||
</p>
|
||||
|
||||
<h4 class="mt-4">Here are some of the key facts you should bear in mind:</h4>
|
||||
<ol>
|
||||
<li class="mb-3"><h5>Location</h5>
|
||||
If it matters that your hotel is, for example, on the beach, close to the theme park, or convenient for the airport, then location is paramount. Any decent directory should offer a location map of the hotel and its surroundings. There should be distance charts to the airport offered as well as some form of interactive map.</li>
|
||||
<li class="mb-3"><h5>Style</h5>
|
||||
It is important to choose a hotel that makes you feel comfortable – contemporary or traditional furnishings, local decor or international, formal or relaxed. The ideal hotel directory should let you know of the options available.</li>
|
||||
<li class="mb-3"><h5>Restaurants, Cafes and Bars</h5>
|
||||
Local color is great but the hotel’s own restaurants and bars can play an important part in your stay. You should be aware of choice, style and whether or not they are smart or informal. A good hotel report should tell you this, and particularly about breakfast facilities.</li>
|
||||
<li class="mb-3"><h5>Bedroom Facilities</h5>
|
||||
You should always carefully consider the type of facilities you need from your bedroom and find the hotel that has those you consider important. The hotel directory website should elaborate on matters such as: bed size, Internet Access (its cost, whether there is WIFI or wired broadband connection), Complimentary amenities, views from the room and luxury offerings like a Pillow menu or Bath menu, choice of smoking or non smoking rooms etc.</li>
|
||||
</ol>
|
||||
<p>These things really do matter and any decent hotel directory should give you this sort of advice on bedrooms – not just the number of rooms which is the usual option!</p>
|
||||
<ol>
|
||||
<li class="mb-3"><h5>Children’s’ Facilities</h5>
|
||||
More important to the family traveler than the business traveler, you should find out just how child friendly the hotel is from the directory and make your decision from there. One thing worth looking for is whether the hotel offers a baby sitters service. For the business traveler wishing to escape children this is of course very relevant too – perhaps a hotel that is not child friendly would be something more appropriate!</li>
|
||||
<li class="mb-3"> <h5>Leisure Facilities</h5>
|
||||
The site should offer a detailed analysis of leisure services within the hotel – spa, pool, gym, sauna – as well as details of any other facilities nearby such as golf courses. 7. Special Needs: the hotel directory site should advise the visitor of each hotel’s special needs services and accessibility policy. Whilst again this does not apply to every visitor, it is absolutely vital to some.</li>
|
||||
</ol>
|
||||
|
||||
<p>Finally and most importantly, the quality hotel directory inspection team should have visited the hotel in question on a regular basis, met the staff, slept in a bedroom and tried the food. They should experience the hotel as only a hotel guest can and it is only then that they are really in a strong position to write about the hotel.</p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="blog_post_4" model="blog.post">
|
||||
<field name="name">How To Look Up</field>
|
||||
<field name="subtitle">Be aware of this thing called “astronomy”</field>
|
||||
<field name="blog_id" ref="blog_blog_2"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_5')])]"/>
|
||||
<field name="visits">453</field>
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="(datetime.now()-relativedelta(days=6)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_4.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "text_align_class": "text-center", "opacity": "0.2"}</field>
|
||||
<field name="content"><![CDATA[
|
||||
<p class="lead">It is safe to say that at some point on our lives, each and every one of us has that moment when we are suddenly stunned when we come face to face with the enormity of the universe that we see in the night sky.</p>
|
||||
|
||||
<p>For many of us who are city dwellers, we don’t really notice that sky up there on a routine basis. The lights of the city do a good job of disguising the amazing display that is above all of our heads all of the time.</p>
|
||||
|
||||
<blockquote class="blockquote my-5">
|
||||
<em class="h4 my-0">That “Wow” moment is what astrology is all about.</em>
|
||||
<footer class="blockquote-footer text-muted">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||
</blockquote>
|
||||
|
||||
<p>So it might be that once a year vacation to a camping spot or a trip to a relative’s house out in the country that we find ourselves outside when the spender of the night sky suddenly decides to put on it’s spectacular show. If you have had that kind of moment when you were literally struck breathless by the spender the night sky can show to us, you can probably remember that exact moment when you could say little else but “wow” at what you saw.</p>
|
||||
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_4_1.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by Arto Marttinen, @wandervisions</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>That “Wow” moment is what astrology is all about. For some, that wow moment becomes a passion that leads to a career studying the stars. For a lucky few, that wow moment because an all consuming obsession that leads to them traveling to the stars in the space shuttle or on one of our early space missions. But for most of us astrology may become a pastime or a regular hobby. But we carry that wow moment with us for the rest of our lives and begin looking for ways to look deeper and learn more about the spectacular universe we see in the millions of stars above us each night.</p>
|
||||
|
||||
<h4>Get started</h4>
|
||||
<p>To get started in learning how to observe the stars much better, there are some basic things we might need to look deeper, beyond just what we can see with the naked eye and begin to study the stars as well as enjoy them. The first thing you need isn’t equipment at all but literature. A good star map will show you the major constellations, the location of the key stars we use to navigate the sky and the planets that will appear larger than stars. And if you add to that map some well done introductory materials into the hobby of astronomy, you are well on your way.</p>
|
||||
|
||||
<h4>Get a telescope</h4>
|
||||
<p>The next thing we naturally want to get is a good telescope. You may have seen a hobbyist who is well along in their study setting up those really cool looking telescopes on a hill somewhere. That excites the amateur astronomer in you because that must be the logical next step in the growth of your hobby. But how to buy a good telescope can be downright confusing and intimidating.</p>
|
||||
|
||||
<p>Before you go to that big expense, it might be a better next step from the naked eye to invest in a good set of binoculars. There are even binoculars that are suited for star gazing that will do just as good a job at giving you that extra vision you want to see just a little better the wonders of the universe. A well designed set of binoculars also gives you much more mobility and ability to keep your “enhanced vision” at your fingertips when that amazing view just presents itself to you.</p>
|
||||
|
||||
<p>None of this precludes you from moving forward with your plans to put together an awesome telescope system. Just be sure you get quality advice and training on how to configure your telescope to meet your needs. Using these guidelines, you will enjoy hours of enjoyment stargazing at the phenomenal sights in the night sky that are beyond the naked eye.</p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="blog_post_5" model="blog.post">
|
||||
<field name="name">What If They Let You Run The Hubble</field>
|
||||
<field name="subtitle">The beauty of astronomy is that anybody can do it.</field>
|
||||
<field name="blog_id" ref="blog_blog_2"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_4'), ref('blog_tag_5')])]"/>
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="(datetime.now()-relativedelta(days=4)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_5.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "text_align_class": "text-center", "opacity": "0.2"}</field>
|
||||
<field name="content"><![CDATA[
|
||||
<p class="lead">From the tiniest baby to the most advanced astrophysicist, there is something for anyone who wants to enjoy astronomy. In fact, it is a science that is so accessible that virtually anybody can do it virtually anywhere they are. All they have to know how to do is to look up.</p>
|
||||
<p>It really is amazing when you think about it that just by looking up on any given night, you could see virtually hundreds of thousands of stars, star systems, planets, moons, asteroids, comets and maybe a even an occasional space shuttle might wander by. It is even more breathtaking when you realize that the sky you are looking up at is for all intents and purposes the exact same sky that our ancestors hundreds and thousands of years ago enjoyed when they just looked up.</p>
|
||||
|
||||
<blockquote class="blockquote my-5">
|
||||
<em class="h4 my-0">There is something timeless about the cosmos.</em>
|
||||
<footer class="blockquote-footer text-muted">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||
</blockquote>
|
||||
|
||||
<p>There is something timeless about the cosmos. The fact that the planets and the moon and the stars beyond them have been there for ages does something to our sense of our place in the universe. In fact, many of the stars we “see” with our naked eye are actually light that came from that star hundreds of thousands of years ago. That light is just now reaching the earth. So in a very real way, looking up is like time travel.</p>
|
||||
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_5_1.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by SpaceX, @spacex</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>While anyone can look up and fall in love with the stars at any time, the fun of astronomy is learning how to become more and more skilled and equipped in star gazing that you see and understand more and more each time you look up. Here are some steps you can take to make the moments you can devote to your hobby of astronomy much more enjoyable.</p>
|
||||
|
||||
<h4>Get some history</h4>
|
||||
<p>Learning the background to the great discoveries in astronomy will make your moments star gazing more meaningful. It is one of the oldest sciences on earth so find out the greats of history who have looked at these stars before you.</p>
|
||||
|
||||
<h4>Know what you are looking at</h4>
|
||||
<p>It is great fun to start learning the constellations, how to navigate the night sky and find the planets and the famous stars. There are web sites and books galore to guide you.</p>
|
||||
|
||||
<h4>Get a geek</h4>
|
||||
<p>Astronomy clubs are lively places full of knowledgeable amateurs who love to share their knowledge with you. For the price of a coke and snacks, they will go star gazing with you and overwhelm you with trivia and great knowledge.</p>
|
||||
|
||||
<h4>Know when to look</h4>
|
||||
<p> Not only knowing the weather will make sure your star gazing is rewarding but if you learn when the big meteor showers and other big astronomy events will happen will make the excitement of astronomy come alive for you.</p>
|
||||
|
||||
<p>And when all is said and done,<b> get equipped</b>. Your quest for newer and better telescopes will be a lifelong one. Let yourself get addicted to astronomy and the experience will enrich every aspect of life. It will be an addiction you never want to break.</p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="blog_post_6" model="blog.post">
|
||||
<field name="name">Buying A Telescope</field>
|
||||
<field name="subtitle">Before you make your first purchase…</field>
|
||||
<field name="blog_id" ref="blog_blog_2"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_3'), ref('blog_tag_4')])]"/>
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="(datetime.now()-relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_6.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "text_align_class": "text-center", "opacity": "0.2"}</field>
|
||||
<field name="content"><![CDATA[
|
||||
<p class="lead">Buying the right telescope to take your love of astronomy to the next level is a big next step in the development of your passion for the stars.</p>
|
||||
<p>In many ways, it is a big step from someone who is just fooling around with astronomy to a serious student of the science. But you and I both know that there is still another big step after buying a telescope before you really know how to use it.</p>
|
||||
<p>So it is critically important that you get just the right telescope for where you are and what your star gazing preferences are. To start with, let’s discuss the three major kinds of telescopes and then lay down some “Telescope 101″ concepts to increase your chances that you will buy the right thing.</p>
|
||||
|
||||
<blockquote class="blockquote my-5">
|
||||
<em class="h4 my-0">It is critically important that you get just the right telescope.</em>
|
||||
<footer class="blockquote-footer text-muted">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||
</blockquote>
|
||||
|
||||
<p>So to select just the right kind of telescope, your objectives in using the telescope are important. To really understand the strengths and weaknesses not only of the lenses and telescope design but also in how the telescope performs in various star gazing situations, it is best to do some homework up front and get exposure to the different kinds. So before you make your first purchase…</p>
|
||||
<ul>
|
||||
<li>Above all, <b>establish a relationship with a reputable telescope shop</b> that employs people who know their stuff. If you buy your telescope at a Wal-Mart or department store, the odds you will get the right thing are remote.</li>
|
||||
<li><b>Pick the brains of the experts</b>. If you are not already active in an astronomy society or club, the sales people at the telescope store will be able to guide you to the active societies in your area. Once you have connections with people who have bought telescopes, you can get advice about what works and what to avoid that is more valid than anything you will get from a web article or a salesperson at Wal-Mart.</li>
|
||||
<li><b>Try before you buy.</b> This is another advantage of going on some field trips with the astronomy club. You can set aside some quality hours with people who know telescopes and have their rigs set up to examine their equipment, learn the key technical aspects, and try them out before you sink money in your own set up.</li>
|
||||
<li><b>Binoculars are lightweight and portable.</b> Unless you have the luxury to set up and operate an observatory from your deck, you are probably going to travel to perform your viewings. Binoculars go with you much easier and they are more lightweight to carry to the country and use while you are there than a cumbersome telescope set up kit.</li>
|
||||
</ul>
|
||||
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_6_1.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by Teddy Kelley, @teddykelley</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>There are other considerations to factor into your final purchase decision.</p>
|
||||
|
||||
<h4>How mobile must your telescope be?</h4>
|
||||
<p>The tripod or other accessory decisions will change significantly with a telescope that will live on your deck versus one that you plan to take to many remote locations.</p>
|
||||
<h4>Along those lines, how difficult is the set up and break down?</h4>
|
||||
<p>How complex is the telescope and will you have trouble with maintenance? Network to get the answers to these and other questions. If you do your homework like this, you will find just the right telescope for this next big step in the evolution of your passion for astronomy.</p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="blog_post_7" model="blog.post">
|
||||
<field name="name">Beyond The Eye</field>
|
||||
<field name="subtitle">Becoming part of the society of devoted amateur astronomers.</field>
|
||||
<field name="blog_id" ref="blog_blog_2"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="tag_ids" eval="[(6, 0, [ref('blog_tag_5')])]"/>
|
||||
<field name="is_published" eval="True"/>
|
||||
<field name="published_date" eval="(datetime.now()-relativedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="cover_properties">{"background-image": "url('/website_blog/static/src/img/cover_7.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
|
||||
<field name="content"><![CDATA[
|
||||
<p class="lead">For many of us, our very first experience of learning about the celestial bodies begins when we saw our first full moon in the sky. It is truly a magnificent view even to the naked eye.</p>
|
||||
<p>If the night is clear, you can see amazing detail of the lunar surface just star gazing on in your back yard.
|
||||
Naturally, as you grow in your love of astronomy, you will find many celestial bodies fascinating. But the moon may always be our first love because is the one far away space object that has the unique distinction of flying close to the earth and upon which man has walked.</p>
|
||||
|
||||
<blockquote class="blockquote my-5">
|
||||
<em class="h4 my-0">Your study of the moon, like anything else, can go from the simple to the very complex.</em>
|
||||
<footer class="blockquote-footer text-muted">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||
</blockquote>
|
||||
|
||||
<p>To gaze at the moon with the naked eye, making yourself familiar with the lunar map will help you pick out the seas, craters and other geographic phenomenon that others have already mapped to make your study more enjoyable. Moon maps can be had from any astronomy shop or online and they are well worth the investment.</p>
|
||||
|
||||
<h2>The best time to view the moon.</h2>
|
||||
<p>The best time to view the moon, obviously, is at night when there are few clouds and the weather is accommodating for a long and lasting study. The first quarter yields the greatest detail of study. And don’t be fooled but the blotting out of part of the moon when it is not in full moon stage. The phenomenon known as “earthshine” gives you the ability to see the darkened part of the moon with some detail as well, even if the moon is only at quarter or half display.</p>
|
||||
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_7_1.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by Patrick Brinksma, @patrickbrinksma</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>To kick it up a notch, a good pair of binoculars can do wonders for the detail you will see on the lunar surface. For best results, get a good wide field in the binocular settings so you can take in the lunar landscape in all its beauty. And because it is almost impossible to hold the binoculars still for the length of time you will want to gaze at this magnificent body in space, you may want to add to your equipment arsenal a good tripod that you can affix the binoculars to so you can study the moon in comfort and with a stable viewing platform.</p>
|
||||
<p>Of course, to take your moon worship to the ultimate, stepping your equipment up to a good starter telescope will give you the most stunning detail of the lunar surface. With each of these upgrades your knowledge and the depth and scope of what you will be able to see will improve geometrically. For many amateur astronomers, we sometimes cannot get enough of what we can see on this our closest space object. </p>
|
||||
|
||||
<figure class="mt-2 mb-4">
|
||||
<img src="/website_blog/static/src/img/content_7_2.jpg" class="img-fluid w-100"/>
|
||||
<figcaption class="figure-caption text-muted">Photo by Greg Rakozy, @grakozy</figcaption>
|
||||
</figure>
|
||||
|
||||
<p>To take it to a natural next level, you may want to take advantage of partnerships with other astronomers or by visiting one of the truly great telescopes that have been set up by professionals who have invested in better techniques for eliminating atmospheric interference to see the moon even better. The internet can give you access to the Hubble and many of the huge telescopes that are pointed at the moon all the time. Further, many astronomy clubs are working on ways to combine multiple telescopes, carefully synchronized with computers for the best view of the lunar landscape.</p>
|
||||
|
||||
<p>Becoming part of the society of devoted amateur astronomers will give you access to these organized efforts to reach new levels in our ability to study the Earth’s moon. And it will give you peers and friends who share your passion for astronomy and who can share their experience and areas of expertise as you seek to find where you might look next in the huge night sky, at the moon and beyond it in your quest for knowledge about the seemingly endless universe above us. </p>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="blog_comment_1" model="mail.message">
|
||||
<field name="body">Beautiful! I plan to go there next holidays.</field>
|
||||
<field name="model">blog.post</field>
|
||||
<field name="res_id" ref="blog_post_1"/>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo_portal"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
|
||||
<record id="blog_comment_2" model="mail.message">
|
||||
<field name="body">Hi! How long did you stay there?</field>
|
||||
<field name="model">blog.post</field>
|
||||
<field name="res_id" ref="blog_post_1"/>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
|
||||
<record id="blog_comment_3" model="mail.message">
|
||||
<field name="body">I'll follow your instructions next time! It will save myself from a weird place like last time :D</field>
|
||||
<field name="model">blog.post</field>
|
||||
<field name="res_id" ref="blog_post_3"/>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
|
||||
<record id="blog_comment_4" model="mail.message">
|
||||
<field name="body">Can't wait to buy a telescope!</field>
|
||||
<field name="model">blog.post</field>
|
||||
<field name="res_id" ref="blog_post_5"/>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
|
||||
<record id="blog_comment_5" model="mail.message">
|
||||
<field name="body">Light pollution can be really annoying when you want to observe the sky. The best places are far from cities. That's why I like to go camping.</field>
|
||||
<field name="model">blog.post</field>
|
||||
<field name="res_id" ref="blog_post_5"/>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo_portal"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
|
||||
<record id="blog_comment_6" model="mail.message">
|
||||
<field name="body">Great article! Do you have any good addresses to buy a telescope? Thanks!</field>
|
||||
<field name="model">blog.post</field>
|
||||
<field name="res_id" ref="blog_post_6"/>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
|
||||
<record id="blog_comment_7" model="mail.message">
|
||||
<field name="body">Great article. I learned so much about astronomy and the moon. How can I contact you to discuss about my experience?</field>
|
||||
<field name="model">blog.post</field>
|
||||
<field name="res_id" ref="blog_post_7"/>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
2023
i18n/af.po
Normal file
2676
i18n/ar.po
Normal file
2024
i18n/az.po
Normal file
2397
i18n/bg.po
Normal file
2024
i18n/bs.po
Normal file
2764
i18n/ca.po
Normal file
2489
i18n/cs.po
Normal file
2732
i18n/da.po
Normal file
2781
i18n/de.po
Normal file
2024
i18n/el.po
Normal file
2026
i18n/en_GB.po
Normal file
2759
i18n/es.po
Normal file
2753
i18n/es_419.po
Normal file
2021
i18n/es_BO.po
Normal file
2021
i18n/es_CL.po
Normal file
2024
i18n/es_CO.po
Normal file
2021
i18n/es_CR.po
Normal file
2021
i18n/es_DO.po
Normal file
2023
i18n/es_EC.po
Normal file
2021
i18n/es_PA.po
Normal file
2021
i18n/es_PE.po
Normal file
2021
i18n/es_PY.po
Normal file
2021
i18n/es_VE.po
Normal file
2748
i18n/et.po
Normal file
2021
i18n/eu.po
Normal file
2389
i18n/fa.po
Normal file
2745
i18n/fi.po
Normal file
2767
i18n/fr.po
Normal file
2021
i18n/fr_BE.po
Normal file
2021
i18n/fr_CA.po
Normal file
2021
i18n/gl.po
Normal file
2023
i18n/gu.po
Normal file
2404
i18n/he.po
Normal file
2035
i18n/hr.po
Normal file
2390
i18n/hu.po
Normal file
2754
i18n/id.po
Normal file
2019
i18n/is.po
Normal file
2742
i18n/it.po
Normal file
2470
i18n/ja.po
Normal file
2021
i18n/ka.po
Normal file
2021
i18n/kab.po
Normal file
2025
i18n/km.po
Normal file
2566
i18n/ko.po
Normal file
2019
i18n/lb.po
Normal file
2405
i18n/lt.po
Normal file
2385
i18n/lv.po
Normal file
2026
i18n/mk.po
Normal file
2033
i18n/mn.po
Normal file
2025
i18n/nb.po
Normal file
2752
i18n/nl.po
Normal file
2740
i18n/pl.po
Normal file
2388
i18n/pt.po
Normal file
2734
i18n/pt_BR.po
Normal file
2031
i18n/ro.po
Normal file
2758
i18n/ru.po
Normal file
2401
i18n/sk.po
Normal file
2392
i18n/sl.po
Normal file
2021
i18n/sq.po
Normal file
2730
i18n/sr.po
Normal file
2025
i18n/sr@latin.po
Normal file
2404
i18n/sv.po
Normal file
2724
i18n/th.po
Normal file
2734
i18n/tr.po
Normal file
2723
i18n/uk.po
Normal file
2693
i18n/vi.po
Normal file
2374
i18n/website_blog.pot
Normal file
2454
i18n/zh_CN.po
Normal file
2444
i18n/zh_TW.po
Normal file
7
models/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_qweb_fields
|
||||
from . import website
|
||||
from . import website_blog
|
||||
from . import website_snippet_filter
|
17
models/ir_qweb_fields.py
Normal file
@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, _
|
||||
|
||||
|
||||
class Field(models.AbstractModel):
|
||||
_inherit = 'ir.qweb.field'
|
||||
|
||||
@api.model
|
||||
def attributes(self, record, field_name, options, values):
|
||||
attrs = super().attributes(record, field_name, options, values)
|
||||
|
||||
if field_name == 'teaser' and self.env.context.get('edit_translations'):
|
||||
attrs['data-translate-error-tooltip'] = _("On your default language, empty the blog post description and save to get an automated (translated) summary.")
|
||||
|
||||
return attrs
|
43
models/website.py
Normal file
@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
from odoo.addons.http_routing.models.ir_http import url_for
|
||||
|
||||
|
||||
class Website(models.Model):
|
||||
_inherit = "website"
|
||||
|
||||
def get_suggested_controllers(self):
|
||||
suggested_controllers = super(Website, self).get_suggested_controllers()
|
||||
suggested_controllers.append((_('Blog'), url_for('/blog'), 'website_blog'))
|
||||
return suggested_controllers
|
||||
|
||||
def configurator_set_menu_links(self, menu_company, module_data):
|
||||
blogs = module_data.get('#blog', [])
|
||||
for idx, blog in enumerate(blogs):
|
||||
new_blog = self.env['blog.blog'].create({
|
||||
'name': blog['name'],
|
||||
'website_id': self.id,
|
||||
})
|
||||
blog_menu_values = {
|
||||
'name': blog['name'],
|
||||
'url': '/blog/%s' % new_blog.id,
|
||||
'sequence': blog['sequence'],
|
||||
'parent_id': menu_company.id if menu_company else self.menu_id.id,
|
||||
'website_id': self.id,
|
||||
}
|
||||
if idx == 0:
|
||||
blog_menu = self.env['website.menu'].search([('url', '=', '/blog'), ('website_id', '=', self.id)])
|
||||
blog_menu.write(blog_menu_values)
|
||||
else:
|
||||
self.env['website.menu'].create(blog_menu_values)
|
||||
super().configurator_set_menu_links(menu_company, module_data)
|
||||
|
||||
def _search_get_details(self, search_type, order, options):
|
||||
result = super()._search_get_details(search_type, order, options)
|
||||
if search_type in ['blogs', 'blogs_only', 'all']:
|
||||
result.append(self.env['blog.blog']._search_get_detail(self, order, options))
|
||||
if search_type in ['blogs', 'blog_posts_only', 'all']:
|
||||
result.append(self.env['blog.post']._search_get_detail(self, order, options))
|
||||
return result
|
364
models/website_blog.py
Normal file
@ -0,0 +1,364 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.addons.http_routing.models.ir_http import slug, unslug
|
||||
from odoo.addons.website.tools import text_from_html
|
||||
from odoo.tools.json import scriptsafe as json_scriptsafe
|
||||
from odoo.tools.translate import html_translate
|
||||
|
||||
|
||||
class Blog(models.Model):
|
||||
_name = 'blog.blog'
|
||||
_description = 'Blog'
|
||||
_inherit = [
|
||||
'mail.thread',
|
||||
'website.seo.metadata',
|
||||
'website.multi.mixin',
|
||||
'website.cover_properties.mixin',
|
||||
'website.searchable.mixin',
|
||||
]
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char('Blog Name', required=True, translate=True)
|
||||
subtitle = fields.Char('Blog Subtitle', translate=True)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
content = fields.Html('Content', translate=html_translate, sanitize=False)
|
||||
blog_post_ids = fields.One2many('blog.post', 'blog_id', 'Blog Posts')
|
||||
blog_post_count = fields.Integer("Posts", compute='_compute_blog_post_count')
|
||||
|
||||
@api.depends('blog_post_ids')
|
||||
def _compute_blog_post_count(self):
|
||||
for record in self:
|
||||
record.blog_post_count = len(record.blog_post_ids)
|
||||
|
||||
def write(self, vals):
|
||||
res = super(Blog, self).write(vals)
|
||||
if 'active' in vals:
|
||||
# archiving/unarchiving a blog does it on its posts, too
|
||||
post_ids = self.env['blog.post'].with_context(active_test=False).search([
|
||||
('blog_id', 'in', self.ids)
|
||||
])
|
||||
for blog_post in post_ids:
|
||||
blog_post.active = vals['active']
|
||||
return res
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, *, parent_id=False, subtype_id=False, **kwargs):
|
||||
""" Temporary workaround to avoid spam. If someone replies on a channel
|
||||
through the 'Presentation Published' email, it should be considered as a
|
||||
note as we don't want all channel followers to be notified of this answer. """
|
||||
self.ensure_one()
|
||||
if parent_id:
|
||||
parent_message = self.env['mail.message'].sudo().browse(parent_id)
|
||||
if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_blog.mt_blog_blog_published'):
|
||||
subtype_id = self.env.ref('mail.mt_note').id
|
||||
return super(Blog, self).message_post(parent_id=parent_id, subtype_id=subtype_id, **kwargs)
|
||||
|
||||
def all_tags(self, join=False, min_limit=1):
|
||||
BlogTag = self.env['blog.tag']
|
||||
req = """
|
||||
SELECT
|
||||
p.blog_id, count(*), r.blog_tag_id
|
||||
FROM
|
||||
blog_post_blog_tag_rel r
|
||||
join blog_post p on r.blog_post_id=p.id
|
||||
WHERE
|
||||
p.blog_id in %s
|
||||
GROUP BY
|
||||
p.blog_id,
|
||||
r.blog_tag_id
|
||||
ORDER BY
|
||||
count(*) DESC
|
||||
"""
|
||||
self._cr.execute(req, [tuple(self.ids)])
|
||||
tag_by_blog = {i.id: [] for i in self}
|
||||
all_tags = set()
|
||||
for blog_id, freq, tag_id in self._cr.fetchall():
|
||||
if freq >= min_limit:
|
||||
if join:
|
||||
all_tags.add(tag_id)
|
||||
else:
|
||||
tag_by_blog[blog_id].append(tag_id)
|
||||
|
||||
if join:
|
||||
return BlogTag.browse(all_tags)
|
||||
|
||||
for blog_id in tag_by_blog:
|
||||
tag_by_blog[blog_id] = BlogTag.browse(tag_by_blog[blog_id])
|
||||
|
||||
return tag_by_blog
|
||||
|
||||
@api.model
|
||||
def _search_get_detail(self, website, order, options):
|
||||
with_description = options['displayDescription']
|
||||
search_fields = ['name']
|
||||
fetch_fields = ['id', 'name']
|
||||
mapping = {
|
||||
'name': {'name': 'name', 'type': 'text', 'match': True},
|
||||
'website_url': {'name': 'url', 'type': 'text', 'truncate': False},
|
||||
}
|
||||
if with_description:
|
||||
search_fields.append('subtitle')
|
||||
fetch_fields.append('subtitle')
|
||||
mapping['description'] = {'name': 'subtitle', 'type': 'text', 'match': True}
|
||||
return {
|
||||
'model': 'blog.blog',
|
||||
'base_domain': [website.website_domain()],
|
||||
'search_fields': search_fields,
|
||||
'fetch_fields': fetch_fields,
|
||||
'mapping': mapping,
|
||||
'icon': 'fa-rss-square',
|
||||
'order': 'name desc, id desc' if 'name desc' in order else 'name asc, id desc',
|
||||
}
|
||||
|
||||
def _search_render_results(self, fetch_fields, mapping, icon, limit):
|
||||
results_data = super()._search_render_results(fetch_fields, mapping, icon, limit)
|
||||
for data in results_data:
|
||||
data['url'] = '/blog/%s' % data['id']
|
||||
return results_data
|
||||
|
||||
class BlogTagCategory(models.Model):
|
||||
_name = 'blog.tag.category'
|
||||
_description = 'Blog Tag Category'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
tag_ids = fields.One2many('blog.tag', 'category_id', string='Tags')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag category already exists!"),
|
||||
]
|
||||
|
||||
|
||||
class BlogTag(models.Model):
|
||||
_name = 'blog.tag'
|
||||
_description = 'Blog Tag'
|
||||
_inherit = ['website.seo.metadata']
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
category_id = fields.Many2one('blog.tag.category', 'Category', index=True)
|
||||
post_ids = fields.Many2many('blog.post', string='Posts')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists!"),
|
||||
]
|
||||
|
||||
|
||||
class BlogPost(models.Model):
|
||||
_name = "blog.post"
|
||||
_description = "Blog Post"
|
||||
_inherit = ['mail.thread', 'website.seo.metadata', 'website.published.multi.mixin',
|
||||
'website.cover_properties.mixin', 'website.searchable.mixin']
|
||||
_order = 'id DESC'
|
||||
_mail_post_access = 'read'
|
||||
|
||||
def _compute_website_url(self):
|
||||
super(BlogPost, self)._compute_website_url()
|
||||
for blog_post in self:
|
||||
blog_post.website_url = "/blog/%s/%s" % (slug(blog_post.blog_id), slug(blog_post))
|
||||
|
||||
def _default_content(self):
|
||||
return '''
|
||||
<p class="o_default_snippet_text">''' + _("Start writing here...") + '''</p>
|
||||
'''
|
||||
name = fields.Char('Title', required=True, translate=True, default='')
|
||||
subtitle = fields.Char('Sub Title', translate=True)
|
||||
author_id = fields.Many2one('res.partner', 'Author', default=lambda self: self.env.user.partner_id)
|
||||
author_avatar = fields.Binary(related='author_id.image_128', string="Avatar", readonly=False)
|
||||
author_name = fields.Char(related='author_id.display_name', string="Author Name", readonly=False, store=True)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
blog_id = fields.Many2one('blog.blog', 'Blog', required=True, ondelete='cascade', default=lambda self: self.env['blog.blog'].search([], limit=1))
|
||||
tag_ids = fields.Many2many('blog.tag', string='Tags')
|
||||
content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize=False)
|
||||
teaser = fields.Text('Teaser', compute='_compute_teaser', inverse='_set_teaser')
|
||||
teaser_manual = fields.Text(string='Teaser Content')
|
||||
|
||||
website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment')])
|
||||
|
||||
# creation / update stuff
|
||||
create_date = fields.Datetime('Created on', readonly=True)
|
||||
published_date = fields.Datetime('Published Date')
|
||||
post_date = fields.Datetime('Publishing date', compute='_compute_post_date', inverse='_set_post_date', store=True,
|
||||
help="The blog post will be visible for your visitors as of this date on the website if it is set as published.")
|
||||
create_uid = fields.Many2one('res.users', 'Created by', readonly=True)
|
||||
write_date = fields.Datetime('Last Updated on', readonly=True)
|
||||
write_uid = fields.Many2one('res.users', 'Last Contributor', readonly=True)
|
||||
visits = fields.Integer('No of Views', copy=False, default=0, readonly=True)
|
||||
website_id = fields.Many2one(related='blog_id.website_id', readonly=True, store=True)
|
||||
|
||||
@api.depends('content', 'teaser_manual')
|
||||
def _compute_teaser(self):
|
||||
for blog_post in self:
|
||||
if blog_post.teaser_manual:
|
||||
blog_post.teaser = blog_post.teaser_manual
|
||||
else:
|
||||
content = text_from_html(blog_post.content, True)
|
||||
blog_post.teaser = content[:200] + '...'
|
||||
|
||||
def _set_teaser(self):
|
||||
for blog_post in self:
|
||||
blog_post.teaser_manual = blog_post.teaser
|
||||
|
||||
@api.depends('create_date', 'published_date')
|
||||
def _compute_post_date(self):
|
||||
for blog_post in self:
|
||||
if blog_post.published_date:
|
||||
blog_post.post_date = blog_post.published_date
|
||||
else:
|
||||
blog_post.post_date = blog_post.create_date
|
||||
|
||||
def _set_post_date(self):
|
||||
for blog_post in self:
|
||||
blog_post.published_date = blog_post.post_date
|
||||
if not blog_post.published_date:
|
||||
blog_post.post_date = blog_post.create_date
|
||||
|
||||
def _check_for_publication(self, vals):
|
||||
if vals.get('is_published'):
|
||||
for post in self.filtered(lambda p: p.active):
|
||||
post.blog_id.message_post_with_source(
|
||||
'website_blog.blog_post_template_new_post',
|
||||
subject=post.name,
|
||||
render_values={'post': post},
|
||||
subtype_xmlid='website_blog.mt_blog_blog_published',
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
posts = super(BlogPost, self.with_context(mail_create_nolog=True)).create(vals_list)
|
||||
for post, vals in zip(posts, vals_list):
|
||||
post._check_for_publication(vals)
|
||||
return posts
|
||||
|
||||
def write(self, vals):
|
||||
result = True
|
||||
# archiving a blog post, unpublished the blog post
|
||||
if 'active' in vals and not vals['active']:
|
||||
vals['is_published'] = False
|
||||
for post in self:
|
||||
copy_vals = dict(vals)
|
||||
published_in_vals = set(vals.keys()) & {'is_published', 'website_published'}
|
||||
if (published_in_vals and 'published_date' not in vals and
|
||||
(not post.published_date or post.published_date <= fields.Datetime.now())):
|
||||
copy_vals['published_date'] = vals[list(published_in_vals)[0]] and fields.Datetime.now() or False
|
||||
result &= super(BlogPost, post).write(copy_vals)
|
||||
self._check_for_publication(vals)
|
||||
return result
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy_data(self, default=None):
|
||||
self.ensure_one()
|
||||
name = _("%s (copy)", self.name)
|
||||
default = dict(default or {}, name=name)
|
||||
return super(BlogPost, self).copy_data(default)
|
||||
|
||||
def _get_access_action(self, access_uid=None, force_website=False):
|
||||
""" Instead of the classic form view, redirect to the post on website
|
||||
directly if user is an employee or if the post is published. """
|
||||
self.ensure_one()
|
||||
user = self.env['res.users'].sudo().browse(access_uid) if access_uid else self.env.user
|
||||
if not force_website and user.share and not self.sudo().website_published:
|
||||
return super(BlogPost, self)._get_access_action(access_uid=access_uid, force_website=force_website)
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': self.website_url,
|
||||
'target': 'self',
|
||||
'target_type': 'public',
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
def _notify_get_recipients_groups(self, message, model_description, msg_vals=None):
|
||||
""" Add access button to everyone if the document is published. """
|
||||
groups = super()._notify_get_recipients_groups(
|
||||
message, model_description, msg_vals=msg_vals
|
||||
)
|
||||
if not self:
|
||||
return groups
|
||||
|
||||
self.ensure_one()
|
||||
if self.website_published:
|
||||
for _group_name, _group_method, group_data in groups:
|
||||
group_data['has_button_access'] = True
|
||||
|
||||
return groups
|
||||
|
||||
def _notify_thread_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||
""" Override to avoid keeping all notified recipients of a comment.
|
||||
We avoid tracking needaction on post comments. Only emails should be
|
||||
sufficient. """
|
||||
if msg_vals is None:
|
||||
msg_vals = {}
|
||||
if msg_vals.get('message_type', message.message_type) == 'comment':
|
||||
return
|
||||
return super(BlogPost, self)._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
|
||||
def _default_website_meta(self):
|
||||
res = super(BlogPost, self)._default_website_meta()
|
||||
res['default_opengraph']['og:description'] = res['default_twitter']['twitter:description'] = self.subtitle
|
||||
res['default_opengraph']['og:type'] = 'article'
|
||||
res['default_opengraph']['article:published_time'] = self.post_date
|
||||
res['default_opengraph']['article:modified_time'] = self.write_date
|
||||
res['default_opengraph']['article:tag'] = self.tag_ids.mapped('name')
|
||||
# background-image might contain single quotes eg `url('/my/url')`
|
||||
res['default_opengraph']['og:image'] = res['default_twitter']['twitter:image'] = json_scriptsafe.loads(self.cover_properties).get('background-image', 'none')[4:-1].strip("'")
|
||||
res['default_opengraph']['og:title'] = res['default_twitter']['twitter:title'] = self.name
|
||||
res['default_meta_description'] = self.subtitle
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _search_get_detail(self, website, order, options):
|
||||
with_description = options['displayDescription']
|
||||
with_date = options['displayDetail']
|
||||
blog = options.get('blog')
|
||||
tags = options.get('tag')
|
||||
date_begin = options.get('date_begin')
|
||||
date_end = options.get('date_end')
|
||||
state = options.get('state')
|
||||
domain = [website.website_domain()]
|
||||
if blog:
|
||||
domain.append([('blog_id', '=', unslug(blog)[1])])
|
||||
if tags:
|
||||
active_tag_ids = [unslug(tag)[1] for tag in tags.split(',')] or []
|
||||
if active_tag_ids:
|
||||
domain.append([('tag_ids', 'in', active_tag_ids)])
|
||||
if date_begin and date_end:
|
||||
domain.append([("post_date", ">=", date_begin), ("post_date", "<=", date_end)])
|
||||
if self.env.user.has_group('website.group_website_designer'):
|
||||
if state == "published":
|
||||
domain.append([("website_published", "=", True), ("post_date", "<=", fields.Datetime.now())])
|
||||
elif state == "unpublished":
|
||||
domain.append(['|', ("website_published", "=", False), ("post_date", ">", fields.Datetime.now())])
|
||||
else:
|
||||
domain.append([("post_date", "<=", fields.Datetime.now())])
|
||||
search_fields = ['name', 'author_name']
|
||||
def search_in_tags(env, search_term):
|
||||
tags_like_search = env['blog.tag'].search([('name', 'ilike', search_term)])
|
||||
return [('tag_ids', 'in', tags_like_search.ids)]
|
||||
fetch_fields = ['name', 'website_url']
|
||||
mapping = {
|
||||
'name': {'name': 'name', 'type': 'text', 'match': True},
|
||||
'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
|
||||
}
|
||||
if with_description:
|
||||
search_fields.append('content')
|
||||
fetch_fields.append('content')
|
||||
mapping['description'] = {'name': 'content', 'type': 'text', 'html': True, 'match': True}
|
||||
if with_date:
|
||||
fetch_fields.append('published_date')
|
||||
mapping['detail'] = {'name': 'published_date', 'type': 'date'}
|
||||
return {
|
||||
'model': 'blog.post',
|
||||
'base_domain': domain,
|
||||
'search_fields': search_fields,
|
||||
'search_extra': search_in_tags,
|
||||
'fetch_fields': fetch_fields,
|
||||
'mapping': mapping,
|
||||
'icon': 'fa-rss',
|
||||
}
|
57
models/website_snippet_filter.py
Normal file
@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import models, fields, _
|
||||
|
||||
|
||||
class WebsiteSnippetFilter(models.Model):
|
||||
_inherit = 'website.snippet.filter'
|
||||
|
||||
def _get_hardcoded_sample(self, model):
|
||||
samples = super()._get_hardcoded_sample(model)
|
||||
if model._name == 'blog.post':
|
||||
data = [{
|
||||
'cover_properties': '{"background-image": "url(\'/website_blog/static/src/img/cover_2.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0"}',
|
||||
'name': _('Islands'),
|
||||
'subtitle': _('Alone in the ocean'),
|
||||
'post_date': fields.Date.today() - timedelta(days=1),
|
||||
'website_url': "",
|
||||
}, {
|
||||
'cover_properties': '{"background-image": "url(\'/website_blog/static/src/img/cover_3.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0"}',
|
||||
'name': _('With a View'),
|
||||
'subtitle': _('Awesome hotel rooms'),
|
||||
'post_date': fields.Date.today() - timedelta(days=2),
|
||||
'website_url': "",
|
||||
}, {
|
||||
'cover_properties': '{"background-image": "url(\'/website_blog/static/src/img/cover_4.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0"}',
|
||||
'name': _('Skies'),
|
||||
'subtitle': _('Taking pictures in the dark'),
|
||||
'post_date': fields.Date.today() - timedelta(days=3),
|
||||
'website_url': "",
|
||||
}, {
|
||||
'cover_properties': '{"background-image": "url(\'/website_blog/static/src/img/cover_5.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0"}',
|
||||
'name': _('Satellites'),
|
||||
'subtitle': _('Seeing the world from above'),
|
||||
'post_date': fields.Date.today() - timedelta(days=4),
|
||||
'website_url': "",
|
||||
}, {
|
||||
'cover_properties': '{"background-image": "url(\'/website_blog/static/src/img/cover_6.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0"}',
|
||||
'name': _('Viewpoints'),
|
||||
'subtitle': _('Seaside vs mountain side'),
|
||||
'post_date': fields.Date.today() - timedelta(days=5),
|
||||
'website_url': "",
|
||||
}, {
|
||||
'cover_properties': '{"background-image": "url(\'/website_blog/static/src/img/cover_7.jpg\')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0"}',
|
||||
'name': _('Jungle'),
|
||||
'subtitle': _('Spotting the fauna'),
|
||||
'post_date': fields.Date.today() - timedelta(days=6),
|
||||
'website_url': "",
|
||||
}]
|
||||
merged = []
|
||||
for index in range(0, max(len(samples), len(data))):
|
||||
merged.append({**samples[index % len(samples)], **data[index % len(data)]})
|
||||
# merge definitions
|
||||
samples = merged
|
||||
return samples
|
17
security/ir.model.access.csv
Normal file
@ -0,0 +1,17 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
blog_blog_public,blog.blog,model_blog_blog,base.group_public,1,0,0,0
|
||||
blog_blog_portal,blog.blog,model_blog_blog,base.group_portal,1,0,0,0
|
||||
blog_blog_employee,blog.blog,model_blog_blog,base.group_user,1,0,0,0
|
||||
blog_blog,blog.blog,model_blog_blog,website.group_website_designer,1,1,1,1
|
||||
blog_post_public,blog.post,model_blog_post,base.group_public,1,0,0,0
|
||||
blog_post_portal,blog.post,model_blog_post,base.group_portal,1,0,0,0
|
||||
blog_post_employee,blog.post,model_blog_post,base.group_user,1,0,0,0
|
||||
blog_post,blog.post,model_blog_post,website.group_website_designer,1,1,1,1
|
||||
blog_tag_public,blog.tag,model_blog_tag,base.group_public,1,0,0,0
|
||||
blog_tag_portal,blog.tag,model_blog_tag,base.group_portal,1,0,0,0
|
||||
blog_tag_employee,blog.tag,model_blog_tag,base.group_user,1,0,0,0
|
||||
blog_tag_edition,blog.tag,model_blog_tag,website.group_website_designer,1,1,1,1
|
||||
blog_tag_category_public,blog.tag.category,model_blog_tag_category,base.group_public,1,0,0,0
|
||||
blog_tag_category_portal,blog.tag.category,model_blog_tag_category,base.group_portal,1,0,0,0
|
||||
blog_tag_category_employee,blog.tag.category,model_blog_tag_category,base.group_user,1,0,0,0
|
||||
blog_tag_category,blog.tag.category,model_blog_tag_category,website.group_website_designer,1,1,1,1
|
|
18
security/website_blog_security.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record model="ir.rule" id="website_blog_post_public">
|
||||
<field name="name">Blog Post: public: published only</field>
|
||||
<field name="model_id" ref="model_blog_post"/>
|
||||
<field name="domain_force">[('website_published', '=', True)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="website_blog_public">
|
||||
<field name="name">Blog: active only</field>
|
||||
<field name="model_id" ref="model_blog_blog"/>
|
||||
<field name="domain_force">[('active', '=', True)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
BIN
static/description/icon.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
1
static/description/icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M47 28A25 25 0 0 0 22 3v25h25Z" fill="#985184"/><path d="M39 32a21.001 21.001 0 0 0-21-21v21h21Z" fill="#088BF5"/><path d="M38.615 28A20.997 20.997 0 0 0 22 11.385V28h16.615Z" fill="#144496"/><path d="M6.942 29.419c.271-1.53 1.413-2.824 2.93-3.319l12.56-4.1s2.868.33 4.05 1.488C27.664 24.645 28 27.454 28 27.454l-4.222 12.382a4.375 4.375 0 0 1-3.261 2.845L4 46l2.942-16.581Z" fill="#2EBCFA"/></svg>
|
After Width: | Height: | Size: 491 B |
BIN
static/src/img/anonymous.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
static/src/img/blog_1.jpeg
Normal file
After Width: | Height: | Size: 203 KiB |
BIN
static/src/img/blog_2.jpeg
Normal file
After Width: | Height: | Size: 292 KiB |
BIN
static/src/img/content_1_1.jpg
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
static/src/img/content_1_2.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
static/src/img/content_2_1.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
static/src/img/content_2_2.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
static/src/img/content_2_3.jpg
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
static/src/img/content_3_1.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
static/src/img/content_4_1.jpg
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
static/src/img/content_5_1.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
static/src/img/content_6_1.jpg
Normal file
After Width: | Height: | Size: 41 KiB |