Начальное наполнение

This commit is contained in:
parent 5daa5f9b2a
commit 6b93137929
136 changed files with 163240 additions and 1 deletions

View File

@ -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
View 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
View 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
View 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
View 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

View 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', '&lt;=', 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', '&lt;=', 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
View 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>

View 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
View 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>

View 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
View 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 volcanos 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 youre going abroad, youve chosen your destination and now you have to choose a hotel.</p>
<p>Ten years ago, youd 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 theres the problem of the reviewers 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. Youll not be surprised to learn that hotels sometimes post their own glowing reviews, or that competitors 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 hotels 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>Childrens 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 hotels 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 dont 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 relatives house out in the country that we find ourselves outside when the spender of the night sky suddenly decides to put on its 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 isnt 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, lets 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 dont 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 Earths 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

File diff suppressed because it is too large Load Diff

2676
i18n/ar.po Normal file

File diff suppressed because it is too large Load Diff

2024
i18n/az.po Normal file

File diff suppressed because it is too large Load Diff

2397
i18n/bg.po Normal file

File diff suppressed because it is too large Load Diff

2024
i18n/bs.po Normal file

File diff suppressed because it is too large Load Diff

2764
i18n/ca.po Normal file

File diff suppressed because it is too large Load Diff

2489
i18n/cs.po Normal file

File diff suppressed because it is too large Load Diff

2732
i18n/da.po Normal file

File diff suppressed because it is too large Load Diff

2781
i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

2024
i18n/el.po Normal file

File diff suppressed because it is too large Load Diff

2026
i18n/en_GB.po Normal file

File diff suppressed because it is too large Load Diff

2759
i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

2753
i18n/es_419.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/es_BO.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/es_CL.po Normal file

File diff suppressed because it is too large Load Diff

2024
i18n/es_CO.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/es_CR.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/es_DO.po Normal file

File diff suppressed because it is too large Load Diff

2023
i18n/es_EC.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/es_PA.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/es_PE.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/es_PY.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/es_VE.po Normal file

File diff suppressed because it is too large Load Diff

2748
i18n/et.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/eu.po Normal file

File diff suppressed because it is too large Load Diff

2389
i18n/fa.po Normal file

File diff suppressed because it is too large Load Diff

2745
i18n/fi.po Normal file

File diff suppressed because it is too large Load Diff

2767
i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/fr_BE.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/fr_CA.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/gl.po Normal file

File diff suppressed because it is too large Load Diff

2023
i18n/gu.po Normal file

File diff suppressed because it is too large Load Diff

2404
i18n/he.po Normal file

File diff suppressed because it is too large Load Diff

2035
i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

2390
i18n/hu.po Normal file

File diff suppressed because it is too large Load Diff

2754
i18n/id.po Normal file

File diff suppressed because it is too large Load Diff

2019
i18n/is.po Normal file

File diff suppressed because it is too large Load Diff

2742
i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

2470
i18n/ja.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/ka.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/kab.po Normal file

File diff suppressed because it is too large Load Diff

2025
i18n/km.po Normal file

File diff suppressed because it is too large Load Diff

2566
i18n/ko.po Normal file

File diff suppressed because it is too large Load Diff

2019
i18n/lb.po Normal file

File diff suppressed because it is too large Load Diff

2405
i18n/lt.po Normal file

File diff suppressed because it is too large Load Diff

2385
i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

2026
i18n/mk.po Normal file

File diff suppressed because it is too large Load Diff

2033
i18n/mn.po Normal file

File diff suppressed because it is too large Load Diff

2025
i18n/nb.po Normal file

File diff suppressed because it is too large Load Diff

2752
i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

2740
i18n/pl.po Normal file

File diff suppressed because it is too large Load Diff

2388
i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

2734
i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

2031
i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

2758
i18n/ru.po Normal file

File diff suppressed because it is too large Load Diff

2401
i18n/sk.po Normal file

File diff suppressed because it is too large Load Diff

2392
i18n/sl.po Normal file

File diff suppressed because it is too large Load Diff

2021
i18n/sq.po Normal file

File diff suppressed because it is too large Load Diff

2730
i18n/sr.po Normal file

File diff suppressed because it is too large Load Diff

2025
i18n/sr@latin.po Normal file

File diff suppressed because it is too large Load Diff

2404
i18n/sv.po Normal file

File diff suppressed because it is too large Load Diff

2724
i18n/th.po Normal file

File diff suppressed because it is too large Load Diff

2734
i18n/tr.po Normal file

File diff suppressed because it is too large Load Diff

2723
i18n/uk.po Normal file

File diff suppressed because it is too large Load Diff

2693
i18n/vi.po Normal file

File diff suppressed because it is too large Load Diff

2374
i18n/website_blog.pot Normal file

File diff suppressed because it is too large Load Diff

2454
i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

2444
i18n/zh_TW.po Normal file

File diff suppressed because it is too large Load Diff

7
models/__init__.py Normal file
View 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
View 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
View 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
View 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',
}

View 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

View 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 blog_blog_public blog.blog model_blog_blog base.group_public 1 0 0 0
3 blog_blog_portal blog.blog model_blog_blog base.group_portal 1 0 0 0
4 blog_blog_employee blog.blog model_blog_blog base.group_user 1 0 0 0
5 blog_blog blog.blog model_blog_blog website.group_website_designer 1 1 1 1
6 blog_post_public blog.post model_blog_post base.group_public 1 0 0 0
7 blog_post_portal blog.post model_blog_post base.group_portal 1 0 0 0
8 blog_post_employee blog.post model_blog_post base.group_user 1 0 0 0
9 blog_post blog.post model_blog_post website.group_website_designer 1 1 1 1
10 blog_tag_public blog.tag model_blog_tag base.group_public 1 0 0 0
11 blog_tag_portal blog.tag model_blog_tag base.group_portal 1 0 0 0
12 blog_tag_employee blog.tag model_blog_tag base.group_user 1 0 0 0
13 blog_tag_edition blog.tag model_blog_tag website.group_website_designer 1 1 1 1
14 blog_tag_category_public blog.tag.category model_blog_tag_category base.group_public 1 0 0 0
15 blog_tag_category_portal blog.tag.category model_blog_tag_category base.group_portal 1 0 0 0
16 blog_tag_category_employee blog.tag.category model_blog_tag_category base.group_user 1 0 0 0
17 blog_tag_category blog.tag.category model_blog_tag_category website.group_website_designer 1 1 1 1

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/src/img/blog_1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

BIN
static/src/img/blog_2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Some files were not shown because too many files have changed in this diff Show More