[IMP] website_slides: indicate new content on courses to users

Purpose of this commit is to help users knowing there is new content in a
course by adding a visual insight. It is done through a new content arrow
displayed in courses homepage.

Regarding the file "website_slides_templates_homepage.xml", the choice to
incorporate the t-call attribute into a 't' balise was necessary to display
the customize option (part front) associated with the model course_card.

Side dish usability improvements raised during development

  * display completed courses as last instead of first in "my courses";
  * do not show promote strategy field for training courses as it has no
    use, only for documentation courses;

TASK ID 2025186
Closes PR #36703

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
laa
2019-11-20 14:59:07 +00:00
committed by Thibault Delavallée
parent a8449773dc
commit 92cc770c90
11 changed files with 145 additions and 14 deletions
+2 -1
View File
@@ -240,7 +240,8 @@ class WebsiteSlides(WebsiteProfile):
domain = request.website.website_domain()
channels_all = request.env['slide.channel'].search(domain)
if not request.env.user._is_public():
channels_my = channels_all.filtered(lambda channel: channel.is_member).sorted('completion', reverse=True)[:3]
#If a course is completed, we don't want to see it in first position but in last
channels_my = channels_all.filtered(lambda channel: channel.is_member).sorted(lambda channel: 0 if channel.completed else channel.completion, reverse=True)[:3]
else:
channels_my = request.env['slide.channel']
channels_popular = channels_all.sorted('total_votes', reverse=True)[:3]
@@ -11,6 +11,7 @@
<field name="slide_type">presentation</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="True"/>
<field name="public_views">10</field>
<field name="completion_time">2.5</field>
@@ -24,6 +25,7 @@
<field name="slide_type">infographic</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">5</field>
<field name="completion_time">1</field>
@@ -37,6 +39,7 @@
<field name="slide_type">infographic</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">2</field>
<field name="completion_time">2</field>
@@ -51,6 +54,7 @@
<field name="slide_type">document</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">0</field>
<field name="completion_time">1</field>
@@ -64,6 +68,7 @@
<field name="slide_type">quiz</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">0</field>
<field name="completion_time">1</field>
@@ -129,6 +134,7 @@
<field name="slide_type">infographic</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="True"/>
<field name="public_views">5</field>
<field name="completion_time">0.5</field>
@@ -142,6 +148,7 @@
<field name="slide_type">infographic</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">5</field>
<field name="completion_time">1.5</field>
@@ -155,6 +162,7 @@
<field name="slide_type">infographic</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">10</field>
<field name="completion_time">1</field>
@@ -180,6 +188,7 @@
<field name="slide_type">video</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">0</field>
<field name="completion_time">1</field>
@@ -228,6 +237,7 @@
</div>
</section></field>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="True"/>
<field name="public_views">5</field>
<field name="completion_time">1</field>
@@ -285,6 +295,7 @@
<field name="slide_type">presentation</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">10</field>
<field name="completion_time">2.5</field>
@@ -300,6 +311,7 @@
<field name="slide_type">video</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">0</field>
<field name="completion_time">1</field>
@@ -334,6 +346,7 @@
<field name="slide_type">presentation</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="True"/>
<field name="public_views">0</field>
<field name="completion_time">1</field>
@@ -431,6 +444,7 @@
</div>
</section></field>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">0</field>
<field name="completion_time">2</field>
@@ -446,6 +460,7 @@
<field name="slide_type">video</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">0</field>
<field name="completion_time">1</field>
@@ -461,6 +476,7 @@
<field name="slide_type">presentation</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_2_gard2"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">0</field>
<field name="completion_time">2</field>
@@ -492,6 +508,7 @@
<field name="slide_type">infographic</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_3_furn0"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="True"/>
<field name="public_views">10</field>
<field name="completion_time">12</field>
@@ -507,6 +524,7 @@
<field name="slide_type">video</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_3_furn0"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">10</field>
<field name="completion_time">3</field>
@@ -558,6 +576,7 @@
<field name="slide_type">presentation</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="True"/>
<field name="public_views">10</field>
<field name="completion_time">2.5</field>
@@ -583,6 +602,7 @@
<field name="slide_type">video</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">0</field>
<field name="completion_time">3</field>
@@ -597,6 +617,7 @@
<field name="slide_type">video</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="True"/>
<field name="public_views">5</field>
<field name="completion_time">3</field>
@@ -609,6 +630,7 @@
<field name="slide_type">quiz</field>
<field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="public_views">0</field>
<field name="completion_time">0.5</field>
@@ -3,6 +3,8 @@
import uuid
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, tools, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.exceptions import UserError, AccessError
@@ -161,6 +163,7 @@ class Channel(models.Model):
completed = fields.Boolean('Done', compute='_compute_user_statistics', compute_sudo=False)
completion = fields.Integer('Completion', compute='_compute_user_statistics', compute_sudo=False)
can_upload = fields.Boolean('Can Upload', compute='_compute_can_upload', compute_sudo=False)
partner_has_new_content = fields.Boolean(compute='_compute_partner_has_new_content', compute_sudo=False)
# karma generation
karma_gen_slide_vote = fields.Integer(string='Lesson voted', default=1)
karma_gen_channel_rank = fields.Integer(string='Course ranked', default=5)
@@ -297,6 +300,25 @@ class Channel(models.Model):
def _get_can_publish_error_message(self):
return _("Publishing is restricted to the responsible of training courses or members of the publisher group for documentation courses")
@api.depends('slide_partner_ids')
@api.depends_context('uid')
def _compute_partner_has_new_content(self):
new_published_slides = self.env['slide.slide'].sudo().search([
('is_published', '=', True),
('date_published', '>', fields.Datetime.now() - relativedelta(days=7)),
('channel_id', 'in', self.ids),
('is_category', '=', False)
])
slide_partner_completed = self.env['slide.slide.partner'].sudo().search([
('channel_id', 'in', self.ids),
('partner_id', '=', self.env.user.partner_id.id),
('slide_id', 'in', new_published_slides.ids),
('completed', '=', True)
]).mapped('slide_id')
for channel in self:
new_slides = new_published_slides.filtered(lambda slide: slide.channel_id == channel)
channel.partner_has_new_content = any(slide not in slide_partner_completed for slide in new_slides)
@api.depends('name', 'website_id.domain')
def _compute_website_url(self):
super(Channel, self)._compute_website_url()
@@ -8,6 +8,7 @@ import re
import requests
import PyPDF2
from dateutil.relativedelta import relativedelta
from PIL import Image
from werkzeug import urls
@@ -123,6 +124,7 @@ class Slide(models.Model):
channel_id = fields.Many2one('slide.channel', string="Course", required=True)
tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags')
is_preview = fields.Boolean('Allow Preview', default=False, help="The course is accessible by anyone : the users don't need to join the channel to access the content of the course.")
is_new_slide = fields.Boolean('Is New Slide', compute='_compute_is_new_slide')
completion_time = fields.Float('Duration', digits=(10, 4), help="The estimated completion time for this slide")
# Categories
is_category = fields.Boolean('Is a category', default=False)
@@ -189,6 +191,11 @@ class Slide(models.Model):
('exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both.")
]
@api.depends('date_published', 'is_published')
def _compute_is_new_slide(self):
for slide in self:
slide.is_new_slide = slide.date_published > fields.Datetime.now() - relativedelta(days=7) if slide.is_published else False
@api.depends('channel_id.slide_ids.is_category', 'channel_id.slide_ids.sequence')
def _compute_category_id(self):
""" Will take all the slides of the channel for which the index is higher
@@ -19,9 +19,9 @@ odoo.define('website_slides.slide.preview', function (require) {
}).then(function (isPreview) {
if (isPreview) {
$slideTarget.removeClass('badge-light badge-hide border');
$slideTarget.addClass('badge-success');
$slideTarget.addClass('badge-success py-1');
} else {
$slideTarget.removeClass('badge-success');
$slideTarget.removeClass('badge-success py-1');
$slideTarget.addClass('badge-light badge-hide border');
}
});
@@ -29,6 +29,35 @@ $o-wslides-fs-side-width: 300px;
}
}
.o_wslides_arrow {
position: absolute;
height: 24px;
margin-left: -5px;
margin-top: 10px;
line-height: 1.8em;
padding-left: 8px;
padding-right: 5px;
background: #17A2B8;
color: white;
box-shadow: 0px 0px 3px gray;
z-index: 1;
text-decoration: none;
&:after {
// the triangle
content: "";
position: absolute;
height: 0px;
width: 0px;
right: 0;
margin-right: -15px;
border-left: 15px solid #17A2B8;
border-bottom: 12px solid transparent;
border-top: 12px solid transparent;
}
}
.o_wslides_body {
background-color: $o-wslides-color-bg;
+24 -2
View File
@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import math
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.addons.website_slides.tests import common
from odoo.exceptions import AccessError, UserError
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tools import mute_logger, float_compare
@@ -14,6 +16,26 @@ from odoo.tools import mute_logger, float_compare
@tagged('functional')
class TestChannelStatistics(common.SlidesCase):
@mute_logger('odoo.models')
def test_channel_new_content(self):
(self.slide | self.slide_2).write({'date_published': fields.Datetime.now() + relativedelta(days=-6)})
self.slide_3.write({'date_published': fields.Datetime.now() + relativedelta(days=-8)})
self.assertTrue(all(slide.is_new_slide for slide in (self.slide | self.slide_2)))
self.assertFalse(self.slide_3.is_new_slide)
channel_aspublisher = self.channel.with_user(self.user_publisher)
self.assertTrue(channel_aspublisher.partner_has_new_content)
(self.slide | self.slide_2).with_user(self.user_publisher).action_set_completed()
self.assertFalse(channel_aspublisher.partner_has_new_content)
channel_aspublisher._action_add_members(self.user_portal.partner_id)
channel_asportal = self.channel.with_user(self.user_portal)
self.assertTrue(channel_asportal.partner_has_new_content)
(self.slide | self.slide_2).write({'date_published': fields.Datetime.now() + relativedelta(days=-8)})
channel_asportal.invalidate_cache(['partner_has_new_content'])
self.assertFalse(channel_asportal.partner_has_new_content)
@mute_logger('odoo.models')
def test_channel_statistics(self):
channel_publisher = self.channel.with_user(self.user_publisher)
@@ -100,7 +100,8 @@
</group>
<group name="display" string="Display">
<field name="visibility" widget="radio"/>
<field name="promote_strategy" widget="radio"/>
<field name="promote_strategy" widget="radio"
attrs="{'invisible': [('channel_type', '=', 'training')]}"/>
</group>
</group>
<div attrs="{'invisible': [('enroll', '!=', 'invite')]}">
@@ -420,15 +420,18 @@
</div>
<div class="d-flex flex-row">
<a t-if="channel.can_upload" href="#">
<span t-att-data-slide-id="slide.id" t-attf-class="o_wslides_js_slide_toggle_is_preview badge #{'badge-success py-1' if slide.is_preview else 'badge-light badge-hide border'} font-weight-normal px-2 m-1">Free preview</span>
</a>
<span t-elif="slide.is_preview and not channel.is_member" class="badge badge-success font-weight-normal px-2 py-1 m-1">Free preview</span>
<span t-if="slide.is_new_slide and not channel_progress[slide.id].get('completed')" class="badge badge-info badge-arrow-right font-weight-normal px-2 py-1 m-1">
New
</span>
<span t-if="slide.question_ids" t-att-class="'badge font-weight-bold px-2 py-1 m-1 %s' % ('badge-success' if channel_progress[slide.id].get('completed') else 'badge-warning')">
<i t-attf-class="fa fa-fw #{'fa-check' if channel_progress[slide.id].get('completed') else 'fa-flag'}"/>
<t t-esc="channel_progress[slide.id].get('quiz_karma_won', 0) if channel_progress[slide.id].get('completed') else channel_progress[slide.id].get('quiz_karma_gain', 0)"/> xp
</span>
<span class="badge badge-danger font-weight-normal px-2 py-1 m-1" t-if="not slide.website_published">Unpublished</span>
<a t-if="channel.can_upload" href="#">
<span t-att-data-slide-id="slide.id" t-attf-class="o_wslides_js_slide_toggle_is_preview badge #{'badge-success' if slide.is_preview else 'badge-light badge-hide border'} font-weight-normal px-2 py-1 m-1">Free preview</span>
</a>
<span t-elif="slide.is_preview and not channel.is_member" class="badge badge-success font-weight-normal px-2 py-1 m-1">Free preview</span>
</div>
<div t-if="channel.is_member or channel.can_publish" class="pt-2 pb-2 border-left ml-2 mr-2 pl-2 d-flex flex-row align-items-center o_wslides_slides_list_slide_controls">
@@ -615,6 +618,7 @@
<template id='lesson_card' name="Lesson Card">
<div class="card w-100 o_wslides_lesson_card mb-4">
<t t-if="slide.is_new_slide and not channel_progress[slide.id].get('completed')" t-call="website_slides.course_card_information"/>
<t t-set="can_access" t-value="slide.is_preview or channel.is_member or channel.can_publish"/>
<t t-if="slide.image_1024">
@@ -149,7 +149,9 @@
</div>
<div class="row mx-n2 mt8">
<t t-foreach="channels_my[:3]" t-as="channel">
<div class="col-md-4 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1" t-call="website_slides.course_card"/>
<div class="col-md-4 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1">
<t t-call="website_slides.course_card"/>
</div>
</t>
</div>
</div>
@@ -165,7 +167,9 @@
</div>
<div class="row mx-n2 mt8">
<t t-foreach="channels_popular[:3]" t-as="channel">
<div class="col-md-4 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1" t-call="website_slides.course_card"/>
<div class="col-md-4 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1">
<t t-call="website_slides.course_card"/>
</div>
</t>
</div>
</div>
@@ -180,7 +184,9 @@
</div>
<div class="row mx-n2 mt8">
<t t-foreach="channels_newest[:3]" t-as="channel">
<div class="col-md-4 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1" t-call="website_slides.course_card"/>
<div class="col-md-4 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1">
<t t-call="website_slides.course_card"/>
</div>
</t>
</div>
</div>
@@ -296,7 +302,9 @@
</div>
<div t-else="" class="row mx-n2">
<t t-foreach="channels" t-as="channel">
<div class="col-12 col-sm-6 col-md-4 col-lg-3 px-2 d-flex flex-grow-1" t-call="website_slides.course_card"/>
<div class="col-12 col-sm-6 col-md-4 col-lg-3 px-2 d-flex flex-grow-1">
<t t-call="website_slides.course_card"/>
</div>
</t>
</div>
</div>
@@ -310,6 +318,7 @@
<div t-attf-class="card w-100 o_wslides_course_card mb-4 #{'o_wslides_course_unpublished' if not channel.is_published else ''}">
<t t-set="course_image" t-value="website.image_url(channel, 'image_1024')"/>
<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">
<i class="fa fa-graduation-cap fa-2x mr-3 mb-3 position-absolute text-white-75" style="right:0; bottom: 0"/>
@@ -344,6 +353,18 @@
</div>
</template>
<template id="course_card_information" name='Course Information'>
<t id="course_card_information_content">
</t>
</template>
<template id="course_card_information_arrow" inherit_id="website_slides.course_card_information"
active="True" customize_show="True" name='New Content Ribbon'>
<xpath expr="//t[@id='course_card_information_content']" position="inside">
<span class="o_wslides_arrow">New Content</span>
</xpath>
</template>
<template id='slides_home_achievements_small' name="Users">
<t class="o_wslides_home_aside">
</t>
@@ -12,6 +12,7 @@
<field name="category_id" ref="website_slides.slide_category_demo_5_0"/>
<field name="survey_id" ref="website_slides_survey.furniture_certification"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="description">Now that you have completed the course, it's time to test your knowledge!</field>
</record>
@@ -27,6 +28,7 @@
<field name="category_id" eval="False"/>
<field name="survey_id" ref="website_slides_survey.furniture_certification"/>
<field name="is_published" eval="True"/>
<field name="date_published" eval="datetime.now() - timedelta(days=8)"/>
<field name="is_preview" eval="False"/>
<field name="description">It's time to test your knowledge!</field>
</record>