[REF] website_slides: revamp channel form view

PURPOSE

Like already done for sale order, invoice of survey, purpose of this commit
is to remove category model and replace by a flagged line (slide). It allows
to easily reorder slides in an embedded list view.

SPECIFICATIONS

This commit completely reworks the slide.channel form view

  * improve the content management from the form itself by adding a tree view
    with the possibility of drag and dropping slides from category to category.
    This tree view has buttons to quickly add content or certification. It
    also allow to add a category (slide with flag is_category) without opening
    a modal;
  * reorder tabs and fields, improve some labels;
  * add a custom float field widget to display ratings as stars;
  * add stat button for partners who completed/certified ;the course
  * add stat button that redirects to the forum posts;
  * add stat button that redirects to the channel reviews;

LINKS

Task 1978731
PR #33255
This commit is contained in:
qmo-odoo
2019-08-08 08:29:34 +00:00
committed by Thibault Delavallée
parent b180c66a80
commit 340613d068
15 changed files with 368 additions and 56 deletions
@@ -5,7 +5,7 @@
<field name="model">slide.channel</field>
<field name="inherit_id" ref="website_slides.view_slide_channel_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_redirect_to_members']" position="before">
<xpath expr="//button[@name='action_redirect_to_done_members']" position="before">
<button class="oe_stat_button" type="object"
name="action_mass_mailing_attendees"
icon="fa-envelope-o"
@@ -14,6 +14,7 @@
type="object"
icon="fa-signal"
class="oe_stat_button"
attrs="{'invisible': [('enroll', '!=', 'payment')]}"
groups="sales_team.group_sale_salesman">
<field name="product_sale_revenues" string="Sales" widget="statinfo"/>
</button>
+13 -13
View File
@@ -113,16 +113,16 @@ class Channel(models.Model):
('most_viewed', 'Most Viewed')],
string="Featuring Policy", default='latest', required=True)
access_token = fields.Char("Security Token", copy=False, default=_default_access_token)
nbr_presentation = fields.Integer('Number of Presentations', compute='_compute_slides_statistics', store=True)
nbr_document = fields.Integer('Number of Documents', compute='_compute_slides_statistics', store=True)
nbr_video = fields.Integer('Number of Videos', compute='_compute_slides_statistics', store=True)
nbr_infographic = fields.Integer('Number of Infographics', compute='_compute_slides_statistics', store=True)
nbr_webpage = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True)
nbr_presentation = fields.Integer('Presentations', compute='_compute_slides_statistics', store=True)
nbr_document = fields.Integer('Documents', compute='_compute_slides_statistics', store=True)
nbr_video = fields.Integer('Videos', compute='_compute_slides_statistics', store=True)
nbr_infographic = fields.Integer('Infographics', compute='_compute_slides_statistics', store=True)
nbr_webpage = fields.Integer("Webpages", compute='_compute_slides_statistics', store=True)
nbr_quiz = fields.Integer("Number of Quizs", compute='_compute_slides_statistics', store=True)
total_slides = fields.Integer('# Slides', compute='_compute_slides_statistics', store=True)
total_views = fields.Integer('# Views', compute='_compute_slides_statistics', store=True)
total_votes = fields.Integer('# Votes', compute='_compute_slides_statistics', store=True)
total_time = fields.Float('# Hours', compute='_compute_slides_statistics', digits=(10, 4), store=True)
total_slides = fields.Integer('Content', compute='_compute_slides_statistics', store=True)
total_views = fields.Integer('Visits', compute='_compute_slides_statistics', store=True)
total_votes = fields.Integer('Votes', compute='_compute_slides_statistics', store=True)
total_time = fields.Float('Duration', compute='_compute_slides_statistics', digits=(10, 2), store=True)
rating_avg_stars = fields.Float("Rating Average (Stars)", compute='_compute_rating_stats', digits=(16, 1))
# configuration
allow_comment = fields.Boolean(
@@ -131,7 +131,7 @@ class Channel(models.Model):
" * like content and post comments on documentation course;\n"
" * post comment and review on training course;")
publish_template_id = fields.Many2one(
'mail.template', string='Published Template',
'mail.template', string='New Content Email',
help="Email template to send slide publication through email",
default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_published'))
share_template_id = fields.Many2one(
@@ -169,9 +169,9 @@ class Channel(models.Model):
karma_gen_channel_rank = fields.Integer(string='Course ranked', default=5)
karma_gen_channel_finish = fields.Integer(string='Course finished', default=10)
# Karma based actions
karma_review = fields.Integer('Add a review', default=10, help="Karma needed to add a review on the course")
karma_slide_comment = fields.Integer('Add a comment', default=3, help="Karma needed to add a comment on a slide of this course")
karma_slide_vote = fields.Integer('Vote on slide', default=3, help="Karma needed to like/dislike a slide of this course.")
karma_review = fields.Integer('Add Review', default=10, help="Karma needed to add a review on the course")
karma_slide_comment = fields.Integer('Add Comment', default=3, help="Karma needed to add a comment on a slide of this course")
karma_slide_vote = fields.Integer('Vote', default=3, help="Karma needed to like/dislike a slide of this course.")
can_review = fields.Boolean('Can Review', compute='_compute_action_rights')
can_comment = fields.Boolean('Can Comment', compute='_compute_action_rights')
can_vote = fields.Boolean('Can Vote', compute='_compute_action_rights')
+2 -2
View File
@@ -105,7 +105,7 @@ class Slide(models.Model):
channel_id = fields.Many2one('slide.channel', string="Channel", required=True)
tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags')
is_preview = fields.Boolean('Is 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.")
completion_time = fields.Float('# Hours', default=1, digits=(10, 4))
completion_time = fields.Float('Duration', default=1, digits=(10, 2))
# Categories
is_category = fields.Boolean('Is a category', default=False)
category_id = fields.Many2one('slide.slide', string="Category", compute="_compute_category_id", store=True)
@@ -152,7 +152,7 @@ class Slide(models.Model):
embedcount_ids = fields.One2many('slide.embed', 'slide_id', string="Embed Count")
slide_views = fields.Integer('# of Website Views', store=True, compute="_compute_slide_views")
public_views = fields.Integer('# of Public Views')
total_views = fields.Integer("Total # Views", default="0", compute='_compute_total', store=True)
total_views = fields.Integer("Views", default="0", compute='_compute_total', store=True)
# comments
comments_count = fields.Integer('Number of comments', compute="_compute_comments_count")
# channel
@@ -0,0 +1,43 @@
odoo.define('website_slides.ratingField', function (require) {
"use strict";
var basicFields = require('web.basic_fields');
var fieldRegistry = require('web.field_registry');
var core = require('web.core');
var QWeb = core.qweb;
var FieldFloatRating = basicFields.FieldFloat.extend({
xmlDependencies: !basicFields.FieldFloat.prototype.xmlDependencies ?
['/website_rating/static/src/xml/website_mail.xml'] : basicFields.FieldFloat.prototype.xmlDependencies.concat(
['/website_rating/static/src/xml/website_mail.xml']
),
description: "",
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_render: function () {
var self = this;
return Promise.resolve(this._super()).then(function () {
self.$el.html(QWeb.render('website_rating.rating_stars_static', {
'val': self.value / 2,
'inline_mode': true
}));
});
},
});
fieldRegistry.add('field_float_rating', FieldFloatRating);
return {
FieldFloatRating: FieldFloatRating,
};
});
@@ -0,0 +1,180 @@
odoo.define('survey.slide_category_one2many', function (require){
"use strict";
var Context = require('web.Context');
var FieldOne2Many = require('web.relational_fields').FieldOne2Many;
var FieldRegistry = require('web.field_registry');
var ListRenderer = require('web.ListRenderer');
var SectionListRenderer = ListRenderer.extend({
init: function (parent, state, params) {
this.sectionFieldName = "is_category";
this._super.apply(this, arguments);
},
_checkIfRecordIsSection: function (id){
var record = this._findRecordById(id);
return record && record.data[this.sectionFieldName];
},
_findRecordById: function (id){
return _.find(this.state.data, function (record){
return record.id === id;
});
},
/**
* Allows to hide specific field in case the record is a section
* and, in this case, makes the 'title' field take the space of all the other
* fields
* @private
* @override
* @param {*} record
* @param {*} node
* @param {*} index
* @param {*} options
*/
_renderBodyCell: function (record, node, index, options){
var $cell = this._super.apply(this, arguments);
var isSection = record.data[this.sectionFieldName];
if (isSection){
if (node.attrs.widget === "handle"){
return $cell;
} else if (node.attrs.name === "name"){
var nbrColumns = this._getNumberOfCols();
if (this.handleField){
nbrColumns--;
}
if (this.addTrashIcon){
nbrColumns--;
}
$cell.attr('colspan', nbrColumns);
} else {
$cell.removeClass('o_invisible_modifier');
return $cell.addClass('o_hidden');
}
}
return $cell;
},
/**
* Adds specific classes to rows that are sections
* to apply custom css on them
* @private
* @override
* @param {*} record
* @param {*} index
*/
_renderRow: function (record, index){
var $row = this._super.apply(this, arguments);
if (record.data[this.sectionFieldName]) {
$row.addClass("o_is_section");
}
return $row;
},
/**
* Adding this class after the view is rendered allows
* us to limit the custom css scope to this particular case
* and no other
* @private
* @override
*/
_renderView: function (){
var def = this._super.apply(this, arguments);
var self = this;
return def.then(function () {
self.$('table.o_list_table').addClass('o_section_list_view');
});
},
// Handlers
/**
* Overriden to allow different behaviours depending on
* the row the user clicked on.
* If the row is a section: edit inline
* else use a normal modal
* @private
* @override
* @param {*} ev
*/
_onRowClicked: function (ev){
var parent = this.getParent();
var recordId = $(ev.currentTarget).data('id');
var is_section = this._checkIfRecordIsSection(recordId);
if (is_section && parent.mode === "edit"){
this.editable = "bottom";
} else {
this.editable = null;
}
this._super.apply(this, arguments);
},
/**
* Overriden to allow different behaviours depending on
* the cell the user clicked on.
* If the cell is part of a section: edit inline
* else use a normal edit modal
* @private
* @override
* @param {*} ev
*/
_onCellClick: function (ev){
var parent = this.getParent();
var recordId = $(ev.currentTarget.parentElement).data('id');
var is_section = this._checkIfRecordIsSection(recordId);
if (is_section && parent.mode === "edit"){
this.editable = "bottom";
} else {
this.editable = null;
this.unselectRow();
}
this._super.apply(this, arguments);
},
/**
* In this case, navigating in the list caused issues.
* For example, editing a section then pressing enter would trigger
* the inline edition of the next element in the list. Which is not desired
* if the next element ends up being a question and not a section
* @override
* @param {*} ev
*/
_onNavigationMove: function (ev){
this.unselectRow();
},
});
var SectionFieldOne2Many = FieldOne2Many.extend({
init: function (parent, name, record, options){
this._super.apply(this, arguments);
this.sectionFieldName = "is_category";
this.rendered = false;
},
/**
* Overriden to use our custom renderer
* @private
* @override
*/
_getRenderer: function (){
if (this.view.arch.tag === 'tree'){
return SectionListRenderer;
}
return this._super.apply(this, arguments);
},
/**
* Overriden to allow different behaviours depending on
* the object we want to add. Adding a section would be done inline
* while adding a question would render a modal.
* @private
* @override
* @param {*} ev
*/
_onAddRecord: function (ev) {
var context_str = ev.data.context && ev.data.context[0];
var context = new Context(context_str).eval();
if (context['default_' + this.sectionFieldName]){
this.editable = "bottom";
} else {
this.editable = null;
}
this._super.apply(this, arguments);
},
});
FieldRegistry.add('slide_category_one2many', SectionFieldOne2Many);
});
@@ -0,0 +1,6 @@
table.o_section_list_view tr.o_data_row.o_is_section {
font-weight: bold;
background-color: #DDD;
border-top: 1px solid #BBB;
border-bottom: 1px solid #BBB;
}
+3
View File
@@ -5,6 +5,9 @@
<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"/>
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/category_backend.scss" t-ignore="true"/>
<script type="text/javascript" src="/website_slides/static/src/js/slide_category_one2many.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/rating_field_backend.js"/>
</xpath>
</template>
@@ -14,12 +14,30 @@
<sheet>
<widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_button_box" name="button_box">
<button name="action_redirect_to_done_members"
type="object"
icon="fa-trophy"
class="oe_stat_button"
groups="website.group_website_publisher">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="members_done_count" nolabel="1"/></span>
<span name="stat_label" class="o_stat_text">Finished</span>
</div>
</button>
<button name="action_redirect_to_members"
type="object"
icon="fa-users"
class="oe_stat_button"
groups="website.group_website_publisher">
<field name="members_count" string="Attendees" widget="statinfo"/>
</button>
<button name="action_view_ratings"
type="object"
icon="fa-star"
class="oe_stat_button"
groups="website.group_website_publisher"
attrs="{'invisible': [('allow_comment', '=', False)]}">
<field name="rating_count" string="Reviews" widget="statinfo"/>
</button>
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe">
@@ -28,74 +46,94 @@
</div>
<field name="image_1920" widget="image" class="oe_avatar" options="{'preview_image': 'image_128'}"/>
<div class="oe_title">
<label for="name" class="oe_edit_only" string="Name"/>
<h1><field name="name" default_focus="1" placeholder="Name"/></h1>
<label for="name" class="oe_edit_only" string="Course Title"/>
<h1><field name="name" default_focus="1" placeholder="Course Title"/></h1>
</div>
<group>
<group>
<field name="active" invisible="1"/>
<field name="enroll" widget="radio" options="{'horizontal': true}"/>
<field name="tag_ids" widget="many2many_tags" placeholder="Tags"/>
<field name="user_id"/>
</group>
</group>
<notebook colspan="4">
<page string="Content">
<field name="slide_ids" colspan="4" nolabel="1" widget="slide_category_one2many" context="{'default_channel_id': active_id}">
<tree decoration-bf="is_category" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="slide_type" attrs="{'invisible': [('slide_type', '=', 'category')]}"/>
<field name="completion_time" attrs="{'invisible': [('slide_type', '=', 'category')]}"/>
<field name="total_views" attrs="{'invisible': [('slide_type', '=', 'category')]}"/>
<field name="is_category" invisible="1"/>
<control>
<create name="add_slide_section" string="Add Section" context="{'default_is_category': True}"/>
<create name="add_slide_lesson" string="Add Content"/>
</control>
</tree>
</field>
</page>
<page string="Description">
<field name="description" colspan="4" nolabel="1"/>
</page>
<page string="Options">
<group>
<group>
<field name="channel_type" widget="radio"/>
<group string="Course">
<field string="Type" name="channel_type" widget="radio"/>
<field name="user_id"/>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="promote_strategy" widget="radio"/>
</group>
<group>
<field name="upload_group_ids" widget="many2many_tags"/>
<group string="Access Rights">
<field name="enroll" widget="radio" options="{'horizontal': true}"/>
<field name="upload_group_ids" string="New Content" widget="many2many_tags"/>
<field name="enroll_group_ids" widget="many2many_tags" groups="base.group_no_one"/>
<field name="visibility" widget="radio" groups="base.group_no_one"/>
</group>
</group>
<group>
<group string="Communication">
<field string="Allow Rating" name="allow_comment"/>
<field name="publish_template_id" domain="[('model','=','slide.slide')]"/>
<field name="share_template_id" domain="[('model','=','slide.slide')]"/>
</group>
<group string="Display">
<field name="visibility" widget="radio"/>
<field name="promote_strategy" widget="radio"/>
</group>
</group>
<group colspan="4">
<div attrs="{'invisible': [('enroll', '!=', 'invite')]}">
<label for="enroll_msg"/>
<field name="enroll_msg" colspan="4" nolabel="1"/>
</group>
</page>
<page string="Communication">
<group>
<field name="allow_comment"/>
</group>
</div>
</page>
<page string="Karma Rules" name="karma_rules">
<group>
<group string="Karma generation">
<field name="karma_gen_slide_vote"/>
<field name="karma_gen_channel_rank"/>
<field name="karma_gen_channel_finish"/>
</group>
<group string="Karma based actions">
<field name="karma_review"/>
<field name="karma_slide_comment"/>
<field name="karma_slide_vote"/>
</group>
<field name="karma_gen_channel_rank" string="Rank Course"/>
<field name="karma_gen_channel_finish" string="Finish Course"/>
<field name="karma_review" attrs="{'invisible': [('allow_comment', '!=', True)]}"/>
<field name="karma_slide_comment" attrs="{'invisible': [('allow_comment', '!=', True)]}"/>
<field name="karma_slide_vote" attrs="{'invisible': [('allow_comment', '!=', True)]}"/>
</group>
</page>
<page string="Statistics" groups="base.group_no_one">
<page string="Statistics">
<group>
<group>
<group class="col-4">
<field name="nbr_presentation"/>
<field name="nbr_document"/>
<field name="nbr_video"/>
<field name="nbr_infographic"/>
<field name="nbr_quiz" string="Quizzes"/>
<field name="nbr_webpage"/>
<separator/>
<field name="total_slides"/>
</group>
<group>
<field name="total_views"/>
<field name="total_votes"/>
<field name="total_time"/>
<field name="total_views" string="Visits"/>
<field string="Rating" name="rating_avg" widget="field_float_rating"/>
<label for="total_time" string="Watch Time"/>
<div>
<field name="total_time"/>
<span class="ml-2">Hours</span>
</div>
</group>
</group>
</page>
@@ -51,8 +51,8 @@
attrs="{'readonly': [('slide_type', 'in', ('document', 'presentation', 'video'))], 'invisible': [('is_category', '=', True)]}"
/>
<div class="oe_title">
<div class="oe_edit_only">
<label for="name" string="Content Title"/>
<div>
<label for="name" string="Name"/>
</div>
<h1>
<field name="name" default_focus="1" placeholder="Title"/>
@@ -90,7 +90,7 @@
<page string="External Links" name="external_links" >
<group>
<field name="link_ids" widget="one2many" nolabel="1">
<tree editable="top">
<tree editable="top">
<field name="name"/>
<field name="link" widget="url"/>
</tree>
@@ -8,7 +8,19 @@ class Channel(models.Model):
_inherit = 'slide.channel'
forum_id = fields.Many2one('forum.forum', 'Course Forum')
forum_total_posts = fields.Integer('Number of active forum posts', related="forum_id.total_posts")
_sql_constraints = [
('forum_uniq', 'unique (forum_id)', "Only one forum per slide channel!"),
]
def action_redirect_to_forum(self):
self.ensure_one()
action = self.env.ref('website_forum.action_forum_post').read()[0]
action['view_mode'] = 'tree'
action['context'] = {
'create': False
}
action['domain'] = [('forum_id', '=', self.forum_id.id)]
return action
@@ -5,8 +5,17 @@
<field name="model">slide.channel</field>
<field name="inherit_id" ref="website_slides.view_slide_channel_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_view_ratings']" position="after">
<button name="action_redirect_to_forum"
type="object"
class="oe_stat_button"
attrs="{'invisible': [('forum_id', '=', False)]}"
icon="fa-comment">
<field string="Forum Posts" name="forum_total_posts" widget="statinfo"/>
</button>
</xpath>
<xpath expr="//field[@name='allow_comment']" position="after">
<field name="forum_id"/>
<field string="Forum" name="forum_id"/>
</xpath>
</field>
</record>
@@ -8,5 +8,3 @@ class Channel(models.Model):
_inherit = 'slide.channel'
nbr_certification = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True)
@@ -45,6 +45,18 @@ class Slide(models.Model):
('check_certification_preview', "CHECK(slide_type != 'certification' OR is_preview = False)", "A slide of type certification cannot be previewed."),
]
@api.onchange('survey_id')
def _on_change_survey_id(self):
if self.survey_id:
self.slide_type = 'certification'
@api.model
def create(self, values):
rec = super(Slide, self).create(values)
if rec.survey_id:
rec.slide_type = 'certification'
return rec
def _generate_certification_url(self):
""" get a map of certification url for certification slide from `self`. The url will come from the survey user input:
1/ existing and not done user_input for member of the course
@@ -5,8 +5,18 @@
<field name="model">slide.channel</field>
<field name="inherit_id" ref="website_slides.view_slide_channel_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='total_slides']" position="before">
<field name="nbr_certification"/>
<xpath expr="//span[@name='stat_label']" position="replace">
<span class="o_stat_text" attrs="{'invisible': [('nbr_certification', '>', 0)]}">Finished</span>
<span class="o_stat_text" attrs="{'invisible': [('nbr_certification', '=', 0)]}">Certified</span>
</xpath>
<xpath expr="//field[@name='nbr_webpage']" position="after">
<field string="Certifications" name="nbr_certification"/>
</xpath>
<xpath expr="//field[@name='slide_type']" position="after">
<field name="survey_id"/>
</xpath>
<xpath expr="//create[@name='add_slide_lesson']" position="after">
<create name="add_slide_certificate" string="Add Certification" groups="survey.group_survey_user" context="{'default_slide_type': 'certification'}"/>
</xpath>
</field>
</record>