[IMP] website_slides: add an overview (kanban view) on course model

PURPOSE

eLearning should have its own application in manager. Indeed this is becoming
a big application and having it embedded inside Website application is not
enough anymore. It should also hold updated and easy-to-use menus,
navigation, actions and views in backend.

SPECIFICATIONS

Add course (slide.channel) kanban view. Each tile should contain

  * Title of the course (make it big, o_primary)
  * Tags below the title
  * Primary button "New Lesson"
  * Statistics :
    * Attendees
    * Running (progress != 100%)
    * (if one content is certificate) Certified or Finished
  * See
    * v1: https://drive.google.com/a/odoo.com/file/d/13K8FmkbwmBG4JQ5gH3Q5T8OlWPs3blcu/view?usp=drivesdk

With FP remarks https://drive.google.com/a/odoo.com/file/d/1mb7LUT0VUNs5fMcKB9c0Bfaqk2-XFFuu/view?usp=drivesdk
  * remove globe, type and visibility of course, lessen padding between rating
    views and watch time;
  * for rating, instead of 8.00 => 5 reviews (4.5/5);
  * replace "View Lesson" / "Invite" by "View Course" => Send to the front-end
  * In the menu of the card, add:
    * Invite (same behavior than before, when it was a primary btn)
  * click on x Contents:  land on the contents, group by category with sequence
  * re-order of the 3 infos/Stats at the bottom of the card: 1. Content,
    2. Attendee, 3. Certified
    * + number: font-weight: 500 + font-size 1.2em
    * + labels: remove text-uppercase, + text-color: #666666
    * + the "whole box" is clickable

LINKS

Task 1978729
PR #35061

Co-Authored-By: Thibault Delavallée <tde@odoo.com>
Co-Authored-By: Jérémy Hennecart <jeh@odoo.com>
This commit is contained in:
Thibault Delavallée
2019-08-05 08:40:33 +00:00
co-authored by Jérémy Hennecart
parent 665d0dfa67
commit 98bdb241de
9 changed files with 182 additions and 17 deletions
+1
View File
@@ -29,6 +29,7 @@ Featuring
'views/res_config_settings_views.xml',
'views/res_partner_views.xml',
'views/rating_rating_views.xml',
'views/res_partner_views.xml',
'views/slide_question_views.xml',
'views/slide_slide_views.xml',
'views/slide_channel_partner_views.xml',
@@ -8,6 +8,6 @@ class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
website_slide_google_app_key = fields.Char(related='website_id.website_slide_google_app_key', readonly=False)
module_website_sale_slides = fields.Boolean(string="Sell courses")
module_website_slides_forum = fields.Boolean(string="Forum on Courses")
module_website_sale_slides = fields.Boolean(string="Sell on eCommerce")
module_website_slides_forum = fields.Boolean(string="Forum")
module_website_slides_survey = fields.Boolean(string="Certifications")
+54 -10
View File
@@ -73,6 +73,7 @@ class ChannelUsersRelation(models.Model):
def _post_completion_hook(self):
pass
class Channel(models.Model):
""" A channel is a container of slides. """
_name = 'slide.channel'
@@ -96,6 +97,7 @@ class Channel(models.Model):
string="Course type", default="documentation", required=True)
sequence = fields.Integer(default=10, help='Display order')
user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.uid)
color = fields.Integer('Color Index', default=0, help='Used to decorate kanban view')
tag_ids = fields.Many2many(
'slide.channel.tag', 'slide_channel_tag_rel', 'channel_id', 'tag_id',
string='Tags', help='Used to categorize and filter displayed channels/courses')
@@ -151,6 +153,7 @@ class Channel(models.Model):
'res.partner', 'slide_channel_partner', 'channel_id', 'partner_id',
string='Members', help="All members of the channel.", context={'active_test': False})
members_count = fields.Integer('Attendees count', compute='_compute_members_count')
members_done_count = fields.Integer('Attendees Done Count', compute='_compute_members_done_count')
is_member = fields.Boolean(string='Is Member', compute='_compute_is_member')
channel_partner_ids = fields.One2many('slide.channel.partner', 'channel_id', string='Members Information', groups='website.group_website_publisher')
upload_group_ids = fields.Many2many(
@@ -184,6 +187,13 @@ class Channel(models.Model):
for channel in self:
channel.members_count = data.get(channel.id, 0)
@api.depends('channel_partner_ids.channel_id', 'channel_partner_ids.completed')
def _compute_members_done_count(self):
read_group_res = self.env['slide.channel.partner'].sudo().read_group(['&', ('channel_id', 'in', self.ids), ('completed', '=', True)], ['channel_id'], 'channel_id')
data = dict((res['channel_id'][0], res['channel_id_count']) for res in read_group_res)
for channel in self:
channel.members_done_count = data.get(channel.id, 0)
@api.depends('channel_partner_ids.partner_id')
@api.model
def _compute_is_member(self):
@@ -366,21 +376,34 @@ class Channel(models.Model):
# Business / Actions
# ---------------------------------------------------------
def action_redirect_to_members(self):
action = self.env.ref('website_slides.slide_channel_partner_action').read()[0]
action['view_mode'] = 'tree'
action['domain'] = [('channel_id', 'in', self.ids)]
def action_redirect_to_members(self, state=None):
action = self.env.ref('website_slides.res_partner_action_slide_channel').read()[0]
action['context'] = {'active_test': False}
if state == 'running':
action['domain'] = [('id', 'in', self.env['slide.channel.partner'].sudo().search([
('channel_id', 'in', self.ids),
('completed', '=', False)
]).mapped('partner_id').ids)]
elif state == 'completed':
action['domain'] = [('id', 'in', self.env['slide.channel.partner'].sudo().search([
('channel_id', 'in', self.ids),
('completed', '=', True)
]).mapped('partner_id').ids)]
else:
action['domain'] = [('id', 'in', self.mapped('partner_ids').ids)]
if len(self) == 1:
action['context'] = {'default_channel_id': self.id}
action['display_name'] = _('Attendees of %s') % self.name
action['context'] = {'active_test': False, 'default_channel_id': self.id}
return action
def action_redirect_to_running_members(self):
return self.action_redirect_to_members('running')
def action_redirect_to_done_members(self):
return self.action_redirect_to_members('completed')
def action_channel_invite(self):
self.ensure_one()
if self.enroll != 'invite':
raise UserError(_("You cannot send invitations for channels that are not set as 'invite'."))
template = self.env.ref('website_slides.mail_template_slide_channel_invite', raise_if_not_found=False)
local_context = dict(
@@ -474,6 +497,27 @@ class Channel(models.Model):
if removed_channel_partner_domain:
self.env['slide.channel.partner'].sudo().search(removed_channel_partner_domain).unlink()
def action_view(self):
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'slide.channel',
'res_id': self.id,
}
def action_view_slides(self):
action = self.env.ref('website_slides.slide_slide_action').read()[0]
action['context'] = {'default_channel_id': self.id}
action['domain'] = [('channel_id', "=", self.id)]
return action
def action_view_ratings(self):
action = self.env.ref('website_slides.rating_rating_action_slide_channel').read()[0]
action['name'] = _('Rating of %s') % (self.name)
action['domain'] = [('res_id', 'in', self.ids)]
return action
# ---------------------------------------------------------
# Rating Mixin API
# ---------------------------------------------------------
@@ -0,0 +1,7 @@
$o-kanban-large-record-width: 400px;
.o_kanban_view.o_slide_channel_kanban {
.o_kanban_record {
width: $o-kanban-large-record-width
}
}
+1
View File
@@ -4,6 +4,7 @@
<template id="assets_backend" name="Slides Backend Assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/rating_rating_views.scss"/>
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/slide_channel_views.scss"/>
</xpath>
</template>
@@ -38,7 +38,7 @@
<div class="o_setting_right_pane">
<label for="module_website_slides_forum"/>
<div class="text-muted">
Allow Forum on Courses
Create a community and let the members help each others
</div>
</div>
</div>
@@ -50,7 +50,7 @@
<div class="o_setting_right_pane">
<label for="module_website_slides_survey"/>
<div class="text-muted">
Allow to take certifications
Evaluate your students and certify them
</div>
</div>
</div>
@@ -62,7 +62,7 @@
<div class="o_setting_right_pane">
<label for="module_website_sale_slides"/>
<div class="text-muted">
Sell courses on your website
Generate revenues thanks to your courses
</div>
</div>
</div>
@@ -36,4 +36,9 @@
</field>
</record>
<record id="res_partner_action_slide_channel" model="ir.actions.act_window">
<field name="name">Attendees</field>
<field name="res_model">res.partner</field>
<field name="view_mode">kanban,tree,form</field>
</record>
</data></odoo>
@@ -218,11 +218,103 @@
</field>
</record>
<record id="slide_channel_view_kanban" model="ir.ui.view">
<field name="name">slide.channel.view.kanban</field>
<field name="model">slide.channel</field>
<field name="arch" type="xml">
<kanban string="eLearning Overview" class="o_emphasize_colors o_kanban_dashboard o_slide_channel_kanban breadcrumb_item active" edit="false">
<field name="color"/>
<field name="website_published"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v" aria-hidden="false"/>
</a>
<div class="dropdown-menu" role="menu">
<ul class="oe_kanban_colorpicker" data-field="color"/>
<t t-if="widget.deletable">
<a class="dropdown-item" role="menuitem" type="delete">Delete</a>
</t>
<a class="dropdown-item" role="menuitem" type="edit">
Edit
</a>
<a class="dropdown-item" name="action_view_slides" role="menuitem" type="object">
Lessons
</a>
<a class="dropdown-item" name="action_channel_invite" role="menuitem" type="object">
Invite
</a>
</div>
</div>
<div class="o_kanban_card_header">
<div class="o_kanban_card_header_title mb16">
<div class="o_primary">
<a name="action_view" type="object" class="mr-auto">
<span><field name="name" class="o_primary"/></span>
</a>
</div>
<div t-if="record.tag_ids">
<field name="tag_ids" widget="many2many_tags"/>
</div>
</div>
</div>
<div class="container o_kanban_card_content mt0">
<div class="row">
<div class="col-6 o_kanban_primary_left">
<button class="btn btn-primary" name="open_website_url" type="object">View course</button>
</div>
<div class="col-6 o_kanban_primary_right">
<div class="d-flex">
<span class="mr-auto"><label for="rating_avg" class="mb0">Rating</label></span>
<a name="action_view_ratings" type="object">
<field name="rating_count"/><span class="ml-2">reviews</span> (<field name="rating_avg_stars"/>/5)
</a>
</div>
<div class="d-flex">
<span class="mr-auto"><label for="total_views" class="mb0">Views</label></span>
<field name="total_views"/>
</div>
<div class="d-flex">
<span class="mr-auto"><label for="total_time" class="mb0">Watch Time</label></span>
<field name="total_time" widget="float_time"/>
</div>
</div>
</div>
<div class="row mt3">
<div class="col-4 border-right">
<a name="action_view_slides" type="object" class="d-flex flex-column align-items-center">
<span class="font-weight-bold"><field name="total_slides"/></span>
<span class="text-secondary">Contents</span>
</a>
</div>
<div class="col-4 border-right">
<a name="action_redirect_to_members" type="object" class="d-flex flex-column align-items-center">
<span class="font-weight-bold"><field name="members_count"/></span>
<span class="text-secondary">Attendees</span>
</a>
</div>
<div class="col-4">
<a name="action_redirect_to_done_members" type="object" class="d-flex flex-column align-items-center">
<span class="font-weight-bold"><field name="members_done_count"/></span>
<span class="text-secondary">Finished</span>
</a>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="slide_channel_action_overview" model="ir.actions.act_window">
<field name="name">eLearning Overview</field>
<field name="res_model">slide.channel</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="slide_channel_view_tree"/>
<field name="view_mode">kanban,tree,form</field>
<field name="view_id" ref="slide_channel_view_kanban"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a course
@@ -10,4 +10,19 @@
</xpath>
</field>
</record>
<record id="slide_channel_view_kanban" model="ir.ui.view">
<field name="name">slide.channel.view.kanban.inherit.survey</field>
<field name="model">slide.channel</field>
<field name="inherit_id" ref="website_slides.slide_channel_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='members_running_count']" position="after">
<field name="nbr_certification"/>
</xpath>
<xpath expr="//a[@name='action_redirect_to_done_members']/span" position="replace">
<t t-if="record.nbr_certification.raw_value"><span class="text-uppercase">Certified</span></t>
<t t-else=""><span class="text-uppercase">Finished</span></t>
</xpath>
</field>
</record>
</odoo>