[IMP] website_slides: enable cover customization

PURPOSE

Before this commit, the headers for the eLearning module were fixed
(Odoo purple gradient or images), and did not always fit with the color theme
chosen by the users. This commit enables the customization of these
website_slides headers for homepages as well as courses and lessons
(on a per-course basis).

SPECIFICATIONS

This commit replaces the channel and lesson headers with `Cover` snippet, so
that they are customizable (by channel) with the website editor. The
customization includes background, additional filter, text alignment, and text
alignment.
The banners for categories of channels ("All courses", "Certifications", and
"My courses") are now also customizable, and other website blocks can be added
above and below them.

Before this commit, the static header background for courses was updated to a
different color palette for certification tracks (`website_slides_survey`).
With this commit, the aspect of the header will no longer be different when
a survey is added but keep the customization (or default value for courses).

The `o_wslides_gradient` class is now obsolete and is renamed to different
labels depending on its remaining local purpose.
The default aspect of courses and lesson without images is therefore switched
from the Odoo purple to the theme background color.

LINKS

Task-2516830

closes odoo/odoo#78257

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Florian Charlier
2022-02-25 10:25:28 +00:00
parent 8e6595857c
commit 6d33510beb
9 changed files with 155 additions and 106 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
{
'name': 'eLearning',
'version': '2.5',
'version': '2.6',
'sequence': 125,
'summary': 'Manage and publish an eLearning platform',
'website': 'https://www.odoo.com/app/elearning',
@@ -137,12 +137,30 @@ class Channel(models.Model):
'mail.thread', 'rating.mixin',
'mail.activity.mixin',
'image.mixin',
'website.cover_properties.mixin',
'website.seo.metadata',
'website.published.multi.mixin',
'website.searchable.mixin',
]
_order = 'sequence, id'
def _default_cover_properties(self):
""" Cover properties defaults are overridden to keep a consistent look for the slides
channels headers across Odoo versions (pre-customization, with purple gradient fitting the
homepage images, etc). Furthermore, as adding padding to the cover would not look great,
its height is set to fit to content (snippet option to change this also disabled on the view)."""
res = super()._default_cover_properties()
res.update({
"background_color_class": "o_cc3",
'background_color_style': (
'background-color: rgba(0, 0, 0, 0); '
'background-image: linear-gradient(120deg, #875A7B, #78516F);'
),
'opacity': '0',
'resize_class': 'cover_auto'
})
return res
def _default_access_token(self):
return str(uuid.uuid4())
@@ -33,9 +33,6 @@ $truncate-limits: 2, 3, 10;
// Common to new slides pages
// **************************************************
.o_wslides_gradient {
background-image: linear-gradient(120deg, #875A7B, darken(#875A7B, 10%));
}
.o_wslides_course_pict {
@include size(100%);
@@ -209,6 +206,10 @@ $truncate-limits: 2, 3, 10;
}
.o_wslides_card_default_background {
background-color: $o-wslides-color-bg; // same as body
}
// New home page
// **************************************************
.o_wslides_home_main {
@@ -321,22 +322,26 @@ $truncate-limits: 2, 3, 10;
.o_wslides_course_nav_search {
border-width: 0 1px;
button, button:hover, input, input::placeholder {
// Resolves into the color chosen for slide channel description through cover customization
color: inherit;
opacity: 0.8;
}
}
.breadcrumb-item, .breadcrumb-item.active{
.breadcrumb-item {
--max-lines: 2;
position: relative;
max-height: calc(1.4rem * var(--max-lines));
overflow: hidden;
text-overflow: ellipsis;
}
.breadcrumb-item.active a, .breadcrumb-item a:hover{
color: white;
}
.breadcrumb-item a, .breadcrumb-item + .breadcrumb-item::before, .o_wslides_course_nav_search input::placeholder {
color: rgba(white, 0.8);
a, a:hover, + .breadcrumb-item::before {
// Resolves into the color chosen for slide channel description through cover customization
color: inherit;
opacity: 0.8;
}
}
}
@@ -345,6 +350,9 @@ $truncate-limits: 2, 3, 10;
@include media-breakpoint-up(md) {
@include o-wslides-header-bar();
}
.o_wslides_header_text {
color:inherit;
}
}
.o_wslides_course_doc_header {
@@ -543,7 +551,7 @@ $truncate-limits: 2, 3, 10;
right: 5px;
}
.o_wslides_gradient {
.o_wslides_quiz_success_image {
width: 42%;
}
}
@@ -125,7 +125,7 @@
<t t-name="slide.slide.quiz.finish">
<div>
<button type="button" class="o_wslides_quiz_modal_close_btn close position-absolute" data-dismiss="modal" aria-label="Close">&#215;</button>
<div class="o_wslides_gradient d-none d-md-flex flex-shrink-0">
<div class="o_wslides_quiz_success_image d-none d-md-flex flex-shrink-0">
<img class="o_wslides_quiz_modal_hero" src="/website_slides/static/src/img/quiz_modal_success.svg" alt=""/>
</div>
<div class="d-flex flex-column flex-grow-1 justify-content-between pl-md-5 p-3 overflow-auto">
@@ -12,8 +12,8 @@
<li class="breadcrumb-item">
<a href="/slides">Courses</a>
</li>
<t t-set="breadcrumb_class" t-value="'breadcrumb-item %s' % ('active' if not slide else '')" />
<li t-att-class="'breadcrumb-item %s' % ('active' if not search_category and not search_tag and not search_slide_category and not slide else '')">
<t t-set="breadcrumb_class" t-value="'breadcrumb-item %s' % ('font-weight-bold' if not slide else '')" />
<li t-att-class="'breadcrumb-item %s' % ('font-weight-bold' if not search_category and not search_tag and not search_slide_category and not slide else '')">
<a t-att-href="'/slides/%s' % slug(channel)"><span t-esc="channel.name"/></a>
</li>
<li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_category" t-if="search_category">
@@ -28,7 +28,7 @@
<li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_slide_category" t-if="search_slide_category">
<a t-att-href="'/slides/%s?slide_category=%s' % (slug(channel), search_slide_category)"><span t-esc="slide_categories[search_slide_category]"/></a>
</li>
<li t-if="slide" class="breadcrumb-item active">
<li t-if="slide" class="breadcrumb-item font-weight-bold">
<a t-att-href="'/slides/slide/%s' % slug(slide)"><span t-esc="slide.name"/></a>
</li>
</ol>
@@ -38,8 +38,8 @@
<!-- search -->
<t t-call="website.website_search_box_input">
<t t-set="_classes" t-valuef="o_wslides_course_nav_search ml-1 position-relative"/>
<t t-set="_input_classes" t-valuef="border-0 rounded-0 bg-transparent text-white"/>
<t t-set="_submit_classes" t-valuef="btn-link text-white rounded-0 pr-1"/>
<t t-set="_input_classes" t-valuef="border-0 rounded-0 bg-transparent"/>
<t t-set="_submit_classes" t-valuef="btn-link rounded-0 pr-1"/>
<t t-set="search_type" t-valuef="slides"/>
<t t-set="action" t-valuef="/slides/all"/>
<t t-set="display_description" t-valuef="true"/>
@@ -110,53 +110,58 @@
<t t-set="body_classname" t-value="'o_wslides_body'"/>
<t t-call="website.layout">
<div id="wrap" t-attf-class="wrap mt-0">
<div t-attf-class="o_wslides_course_header o_wslides_gradient position-relative text-white pb-md-0 pt-2 pt-md-5 #{'pb-3' if channel.channel_type == 'training' else 'o_wslides_course_doc_header pb-5'}">
<t t-call="website_slides.course_nav"/>
<div class="container mt-5 mt-md-3 mt-xl-4">
<div class="row align-items-end align-items-md-stretch">
<!-- ==== Header Left ==== -->
<div class="col-12 col-md-4 col-lg-3">
<div class="d-flex align-items-end justify-content-around h-100">
<div t-if="channel.image_1920" t-field="channel.image_1920" t-options='{"widget": "image", "class": "o_wslides_course_pict d-inline-block mb-2 mt-3 my-md-0"}' class="h-100"/>
<div t-else="" class="h-100">
<img t-att-src="'/website_slides/static/src/img/channel-%s-default.jpg' % ('training' if channel.channel_type == 'training' else 'documentation')"
class="o_wslides_course_pict d-inline-block mb-2 mt-3 my-md-0"/>
<div class="oe_structure oe_empty" t-call="website.record_cover">
<t t-set="_record" t-value="channel"/>
<t t-set="use_filters" t-value="True"/>
<t t-set="use_size" t-value="True"/>
<t t-set="use_text_align" t-value="True"/>
<div t-attf-class="o_wslides_course_header position-relative pb-md-0 pt-2 pt-md-5 #{'pb-3' if channel.channel_type == 'training' else 'o_wslides_course_doc_header pb-5'}">
<t t-call="website_slides.course_nav"/>
<div class="container mt-5 mt-md-3 mt-xl-4">
<div class="row align-items-end align-items-md-stretch">
<!-- ==== Header Left ==== -->
<div class="col-12 col-md-4 col-lg-3">
<div class="d-flex align-items-end justify-content-around h-100">
<div t-if="channel.image_1920" t-field="channel.image_1920" t-options='{"widget": "image", "class": "o_wslides_course_pict d-inline-block mb-2 mt-3 my-md-0"}' class="h-100"/>
<div t-else="" class="h-100">
<img t-att-src="'/website_slides/static/src/img/channel-%s-default.jpg' % ('training' if channel.channel_type == 'training' else 'documentation')"
class="o_wslides_course_pict d-inline-block mb-2 mt-3 my-md-0"/>
</div>
</div>
</div>
</div>
<!-- ==== Header Right ==== -->
<div class="col-12 col-md-8 col-lg-9 d-flex flex-column">
<div class="d-flex flex-column">
<h1 t-field="channel.name"/>
<div class="mb-0 mb-xl-3" t-field="channel.description"/>
<!-- ==== Header Right ==== -->
<div class="col-12 col-md-8 col-lg-9 d-flex flex-column">
<div class="d-flex flex-column">
<h1 t-field="channel.name"/>
<div class="mb-0 mb-xl-3" t-field="channel.description"/>
<div t-if="channel.channel_type == 'documentation'" class="d-flex mb-md-5">
<button role="button" class="btn text-white pl-0" title="Share Channel"
aria-label="Share Channel"
data-toggle="modal" t-att-data-target="'#slideChannelShareModal_%s' % channel.id">
<i class="fa fa-share-square"></i> Share
</button>
<div t-if="channel.channel_type == 'documentation'" class="d-flex mb-md-5">
<button role="button" class="btn text-white pl-0" title="Share Channel"
aria-label="Share Channel"
data-toggle="modal" t-att-data-target="'#slideChannelShareModal_%s' % channel.id">
<i class="fa fa-share-square"></i> Share
</button>
</div>
</div>
<div class="d-flex flex-column justify-content-center h5 flex-grow-1 mb-md-5" t-if="channel.allow_comment">
<t t-call="portal_rating.rating_stars_static_popup_composer">
<t t-set="rating_avg" t-value="rating_avg"/>
<t t-set="rating_count" t-value="rating_count"/>
<t t-set="object" t-value="channel"/>
<t t-set="token" t-value="channel.access_token"/>
<t t-set="hash" t-value="message_post_hash"/>
<t t-set="pid" t-value="message_post_pid"/>
<t t-set="default_message" t-value="last_message"/>
<t t-set="default_message_id" t-value="last_message_id"/>
<t t-set="default_rating_value" t-value="last_rating_value"/>
<t t-set="default_attachment_ids" t-value="last_message_attachment_ids"/>
<t t-set="force_submit_url" t-value="'/slides/mail/update_comment' if last_message_id else False"/>
<t t-set="rate_with_void_content" t-value="True"/>
<t t-set="disable_composer" t-value="not channel.can_review"/>
<t t-set="_link_btn_classes" t-value="'btn-sm btn-link mx-3 o_wslides_header_text'"/>
</t>
</div>
</div>
<div class="d-flex flex-column justify-content-center h5 flex-grow-1 mb-md-5" t-if="channel.allow_comment">
<t t-call="portal_rating.rating_stars_static_popup_composer">
<t t-set="rating_avg" t-value="rating_avg"/>
<t t-set="rating_count" t-value="rating_count"/>
<t t-set="object" t-value="channel"/>
<t t-set="token" t-value="channel.access_token"/>
<t t-set="hash" t-value="message_post_hash"/>
<t t-set="pid" t-value="message_post_pid"/>
<t t-set="default_message" t-value="last_message"/>
<t t-set="default_message_id" t-value="last_message_id"/>
<t t-set="default_rating_value" t-value="last_rating_value"/>
<t t-set="default_attachment_ids" t-value="last_message_attachment_ids"/>
<t t-set="force_submit_url" t-value="'/slides/mail/update_comment' if last_message_id else False"/>
<t t-set="rate_with_void_content" t-value="True"/>
<t t-set="disable_composer" t-value="not channel.can_review"/>
<t t-set="_link_btn_classes" t-value="'btn-sm btn-link text-white mx-3'"/>
</t>
</div>
</div>
</div>
@@ -757,9 +762,9 @@
</t>
<t t-else="">
<a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}#{query_string}" t-title="slide.name">
<div class="card-img-top border-bottom o_wslides_gradient" t-attf-style="padding-top: 50%;"/>
<div class="o_wslides_card_default_background card-img-top border-bottom" t-attf-style="padding-top: 50%;"/>
</a>
<div t-else="" class="card-img-top border-bottom o_wslides_gradient" t-attf-style="padding-top: 50%;"/>
<div t-else="" class="o_wslides_card_default_background card-img-top border-bottom" t-attf-style="padding-top: 50%;"/>
</t>
<i t-if="channel_progress[slide.id].get('completed')" class="position-absolute py-1 px-2 h5 fa fa-check-circle text-primary" style="right:0; top:0;"/>
@@ -6,9 +6,10 @@
<t t-set="body_classname" t-value="'o_wslides_body'"/>
<t t-call="website.layout">
<div id="wrap" class="wrap o_wslides_wrap">
<section class="s_banner overflow-hidden bg-900" style="background-image: url(&quot;/website_slides/static/src/img/banner_default.svg&quot;); background-size: cover; background-position: 55% 65%" data-snippet="s_banner">
<div class="oe_structure oe_empty">
<section class="s_banner overflow-hidden" style="background-color:(0, 0, 0, 0); background-image: url(&quot;/website_slides/static/src/img/banner_default.svg&quot;); background-size: cover; background-position: 55% 65%" data-snippet="s_banner">
<div class="container align-items-center d-flex mb-5 mt-lg-5 pt-lg-4 pb-lg-1">
<div>
<div class="text-white">
<h1 class="display-3 mb-0">Reach new heights</h1>
<h2 class="mb-4">Start your online course today!</h2>
<div class="row mt-1 mb-3">
@@ -19,6 +20,7 @@
</div>
</div>
</section>
</div>
<div class="container mt16 o_wslides_home_nav position-relative">
<nav class="navbar navbar-expand-lg navbar-light shadow-sm" style="background: white!important">
<t t-call="website.website_search_box_input">
@@ -170,13 +172,25 @@
<t t-set="body_classname" t-value="'o_wslides_body'"/>
<t t-call="website.layout">
<div id="wrap" class="wrap o_wslides_wrap">
<section class="s_banner bg-900" style="background-image: url(&quot;/website_slides/static/src/img/banner_default_all.svg&quot;); background-size: cover; background-position: 80% 20%" data-snippet="s_banner">
<div class="container py-5">
<h1 t-if="search_my" class="display-3 mb-0">My Courses</h1>
<h1 t-elif="search_slide_category=='certification'" class="display-3 mb-0">Certifications</h1>
<h1 t-else="" class="display-3 mb-0">All Courses</h1>
</div>
</section>
<!-- Repeat structure for every section to allow customization through website editor. !-->
<div class="oe_structure oe_empty" t-if="search_my">
<section class="s_banner" data-snippet="s_banner"
style="background-color:(0, 0, 0, 0); background-image: url(&quot;/website_slides/static/src/img/banner_default_all.svg&quot;); background-size: cover; background-position: 80% 20%">
<div class="container py-5"><h1 class="display-3 mb-0 text-white">My Courses</h1></div>
</section>
</div>
<div class="oe_structure oe_empty" t-elif="search_slide_category == 'certification'">
<section class="s_banner" data-snippet="s_banner"
style="background-color:(0, 0, 0, 0); background-image: url(&quot;/website_slides/static/src/img/banner_default_all.svg&quot;); background-size: cover; background-position: 80% 20%">
<div class="container py-5"><h1 class="display-3 mb-0 text-white">Certifications</h1></div>
</section>
</div>
<div class="oe_structure oe_empty" t-else="">
<section class="s_banner" data-snippet="s_banner"
style="background-color:(0, 0, 0, 0); background-image: url(&quot;/website_slides/static/src/img/banner_default_all.svg&quot;); background-size: cover; background-position: 80% 20%">
<div class="container py-5"><h1 class="display-3 mb-0 text-white">All Courses</h1></div>
</section>
</div>
<div class="container mt16 o_wslides_home_nav position-relative">
<!-- Navbar dynamically composed using displayed channel tag groups. -->
<nav class="navbar navbar-expand-md navbar-light shadow-sm pl-0" style="background: white!important">
@@ -321,7 +335,7 @@
<a t-attf-href="/slides/#{slug(channel)}" t-title="channel.name">
<t t-if="channel.partner_has_new_content" t-call="website_slides.course_card_information"/>
<div t-if="channel.image_1024" class="card-img-top" t-attf-style="padding-top: 50%; background-image: url(#{course_image}); background-size: cover; background-position:center"/>
<div t-else="" class="o_wslides_gradient card-img-top position-relative" style="padding-top: 50%; opacity: 0.8">
<div t-else="" class="o_wslides_card_default_background card-img-top position-relative" style="padding-top: 50%; opacity: 0.8">
<i class="fa fa-graduation-cap fa-2x mr-3 mb-3 position-absolute text-white-75" style="right:0; bottom: 0"/>
</div>
</a>
@@ -6,39 +6,47 @@
<t t-set="body_classname" t-value="'o_wslides_body'"/>
<t t-call="website.layout">
<div id="wrap" class="wrap o_wslides_wrap">
<div class="o_wslides_lesson_header o_wslides_gradient position-relative text-white pb-0 pt-2 pt-md-5">
<t t-call="website_slides.course_nav">
<t t-set="channel" t-value="slide.channel_id"/>
</t>
<div class="container o_wslides_lesson_header_container mt-5 mt-md-3 mt-xl-4">
<div class="row align-items-end align-items-md-stretch">
<div t-attf-class="col-12 col-lg-9 d-flex flex-column #{'offset-lg-3' if slide.channel_id.channel_type == 'training' else ''}">
<h2 class="font-weight-medium w-100">
<a t-att-href="'/slides/%s' % (slug(slide.channel_id))" class="text-white text-decoration-none" t-field="slide.channel_id.name"/>
<t t-if="slide.channel_id.completed">
<small><span class="badge badge-pill badge-success pull-right my-1 py-1 px-2 font-weight-normal"><i class="fa fa-check"/> Completed</span></small>
</t>
</h2>
<t t-call="website.record_cover">
<t t-set="_record" t-value="slide.channel_id"/>
<t t-set="use_filters" t-value="True"/>
<t t-set="use_size" t-value="True"/>
<t t-set="use_text_align" t-value="True"/>
<div t-if="slide.channel_id.channel_type == 'documentation'" class="mb-3 small">
<span class="font-weight-normal">Last update:</span>
<t t-esc="slide.date_published" t-options="{'widget': 'date'}"/>
</div>
<div t-else="" t-if="not slide.channel_id.completed" class="d-flex align-items-center pb-3">
<div class="progress w-50 bg-black-25" style="height: 10px;">
<div class="progress-bar rounded-left" role="progressbar"
t-att-aria-valuenow="slide.channel_id.completion" aria-valuemin="0" aria-valuemax="100"
t-attf-style="width: #{slide.channel_id.completion}%;">
</div>
<div class="o_wslides_lesson_header position-relative pb-0 pt-2 pt-md-5">
<t t-call="website_slides.course_nav">
<t t-set="channel" t-value="slide.channel_id"/>
</t>
<div class="container o_wslides_lesson_header_container mt-5 mt-md-3 mt-xl-4">
<div class="row align-items-md-stretch">
<div t-attf-class="col-12 col-sm-9 d-flex flex-column #{'col-lg-6 offset-lg-3' if slide.channel_id.channel_type == 'training' else ''}">
<h2 class="font-weight-medium w-100">
<a t-att-href="'/slides/%s' % (slug(slide.channel_id))" class="text-white text-decoration-none" t-field="slide.channel_id.name"/>
</h2>
<div t-if="slide.channel_id.channel_type == 'documentation'" class="mb-3 small">
<span class="font-weight-normal">Last update:</span>
<t t-esc="slide.date_published" t-options="{'widget': 'date'}"/>
</div>
<i t-att-class="'fa fa-trophy m-0 ml-2 p-0 %s' % ('text-warning' if slide.channel_id.completed else 'text-black-50')"></i>
<small class="ml-2 text-white-50"><t t-esc="slide.channel_id.completion"/> %</small>
<div t-else="" t-if="not slide.channel_id.completed" class="d-flex align-items-center pb-3">
<div class="progress w-50 bg-black-25" style="height: 10px;">
<div class="progress-bar rounded-left" role="progressbar"
t-att-aria-valuenow="slide.channel_id.completion" aria-valuemin="0" aria-valuemax="100"
t-attf-style="width: #{slide.channel_id.completion}%;">
</div>
</div>
<i t-att-class="'fa fa-trophy m-0 ml-2 p-0 %s' % ('text-warning' if slide.channel_id.completed else 'text-black-50')"></i>
<small class="ml-2 text-white-50"><t t-esc="slide.channel_id.completion"/> %</small>
</div>
</div>
<div t-if="slide.channel_id.completed" class="col-12 col-sm-3">
<h2>
<small><span class="badge badge-pill badge-success font-weight-normal"><i class="fa fa-check"/> Completed</span></small>
</h2>
</div>
</div>
</div>
</div>
</div>
</t>
<div class="container o_wslides_lesson_main">
<div class="row">
<div t-attf-class="o_wslides_lesson_aside col-lg-3 #{'order-2' if slide.channel_id.channel_type == 'documentation' else ''}">
@@ -94,7 +102,7 @@
<t t-set="slide_image" t-value="website.image_url(aside_slide, 'image_1024')"/>
<div t-if="aside_slide.image_1024" class="flex-shrink-0 mr-1 border" t-attf-style="width: 20%; padding-top: 20%; background-image: url(#{slide_image}); background-size: cover; background-position:center"/>
<div t-else="" class="o_wslides_gradient flex-shrink-0 mr-1" t-attf-style="width: 20%; padding-top: 20%;"/>
<div t-else="" class="o_wslides_card_default_background flex-shrink-0 mr-1" t-attf-style="width: 20%; padding-top: 20%;"/>
<div class="overflow-hidden d-flex flex-column justify-content-start">
<h6 t-esc="aside_slide.name" class="o_wslides_desc_truncate_2 mb-1" style="line-height: 1.15"/>
<small class="text-600">
@@ -41,7 +41,7 @@
t-attf-onclick="location.href='/slides/#{slug(course.channel_id)}';">
<div t-if="course.channel_id.image_1024" class="pl-5 pr-4 rounded-left" t-attf-style="background-image: url(#{website.image_url(course.channel_id, 'image_1024')}); background-size: cover; background-position: center"/>
<div t-else="" class="o_wslides_gradient pl-5 pr-4 rounded-left position-relative" style="opacity: 0.8">
<div t-else="" class="o_wslides_card_default_background pl-5 pr-4 rounded-left position-relative" style="opacity: 0.8">
<i class="fa fa-graduation-cap fa-fw mr-2 mt-3 position-absolute text-white-75" style="right:0; top: 0"/>
</div>
@@ -10,10 +10,6 @@ $o_wss_color_2 : #485761;
// Course page
// **************************************************
#wrap.o_wss_certification_channel {
.o_wslides_course_header {
background-image: linear-gradient(120deg, $o_wss_color_1, $o_wss_color_2);
}
.o_wslides_course_pict {
@include media-breakpoint-up(md) {
border-color: $o_wss_color_2;