[MERGE] website_slides: re-order templates and finalize model cleaning
Purpose of this merge is to perform a quick linting after having merged several tasks and PR adding all requested eLearning features. Templates are split for better understanding now that main templates for homepage, course and lessons are in. A model cleaning is performed as well as a-bit-late additions to stick to specifications, notably splitting short and long description fields on course. See sub commits for more details. Merge linked to task ID 1941250 and PR #31272.
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -25,16 +25,18 @@ Share and Publish Videos, Presentations and Documents'
|
||||
'data': [
|
||||
'views/assets.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/website_slides_templates_homepage.xml',
|
||||
'views/website_slides_templates.xml',
|
||||
'views/website_slides_embed_templates.xml',
|
||||
'views/slide_slide_views.xml',
|
||||
'views/slide_channel_partner_views.xml',
|
||||
'views/slide_channel_views.xml',
|
||||
'views/slide_channel_tag_views.xml',
|
||||
'views/slide_channel_invite_views.xml',
|
||||
'views/website_slides_menu_views.xml',
|
||||
'views/website_slides_templates_homepage.xml',
|
||||
'views/website_slides_templates_course.xml',
|
||||
'views/website_slides_templates_lesson.xml',
|
||||
'views/website_slides_templates_lesson_embed.xml',
|
||||
'views/website_slides_templates_profile.xml',
|
||||
'views/website_slides_templates_utils.xml',
|
||||
'wizard/slide_channel_invite_views.xml',
|
||||
'data/ir_data.xml',
|
||||
'data/gamification_data.xml',
|
||||
'data/mail_data.xml',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail
|
||||
from . import main
|
||||
|
||||
@@ -53,8 +53,8 @@ class WebsiteSlides(WebsiteProfile):
|
||||
return True
|
||||
|
||||
def _get_slide_detail(self, slide):
|
||||
most_viewed_slides = slide.get_most_viewed_slides(self._slides_per_list)
|
||||
related_slides = slide.get_related_slides(self._slides_per_list)
|
||||
most_viewed_slides = slide._get_most_viewed_slides(self._slides_per_list)
|
||||
related_slides = slide._get_related_slides(self._slides_per_list)
|
||||
values = {
|
||||
'slide': slide,
|
||||
'most_viewed_slides': most_viewed_slides,
|
||||
@@ -119,7 +119,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
domain = expression.AND([domain, [('tag_ids', 'in', tags.ids)]])
|
||||
|
||||
if slide_type and 'nbr_%ss' % slide_type in request.env['slide.channel']:
|
||||
domain = expression.AND([domain, [('nbr_%ss' % slide_type, '>', 0)]])
|
||||
domain = expression.AND([domain, [('nbr_%s' % slide_type, '>', 0)]])
|
||||
return domain
|
||||
|
||||
def _prepare_channel_values(self, **kwargs):
|
||||
@@ -521,7 +521,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
@http.route(['/slides/slide/send_share_email'], type='json', auth='user', website=True)
|
||||
def slide_send_share_email(self, slide_id, email):
|
||||
slide = request.env['slide.slide'].browse(int(slide_id))
|
||||
result = slide.send_share_email(email)
|
||||
result = slide._send_share_email(email)
|
||||
return result
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
<record id="website_slides.channel_public" model="slide.channel">
|
||||
<field name="name">Basics of Gardening</field>
|
||||
<field name="channel_type">training</field>
|
||||
<field name="promote_strategy">latest</field>
|
||||
<field name="promote_strategy">most_voted</field>
|
||||
<field name="tag_ids" eval="[(5, 0),
|
||||
(4, ref('website_slides.slide_channel_tag_level_basic')),
|
||||
(4, ref('website_slides.slide_channel_tag_role_gardener')),
|
||||
(4, ref('website_slides.slide_channel_tag_other_0'))]"/>
|
||||
<field name="image" type="base64" file="website_slides/static/src/img/channel_demo_gardening.jpg"/>
|
||||
<field name="description" type="html">
|
||||
<div>Learn the basics of gardening !</div></field>
|
||||
<field name="description">Learn the basics of gardening !</field>
|
||||
</record>
|
||||
<record id="slide_category_demo_0_0" model="slide.category">
|
||||
<field name="name">Requirements</field>
|
||||
@@ -34,8 +33,7 @@
|
||||
<field name="website_published" eval="True"/>
|
||||
<field name="enroll">public</field>
|
||||
<field name="image" type="base64" file="website_slides/static/src/img/channel_demo_gardening_2.jpg"/>
|
||||
<field name="description" type="html">
|
||||
<div>In this training you will learn how to take care of trees: when to plant, potted trees, ...</div></field>
|
||||
<field name="description">Learn how to take care of your favorite trees. Learn when to plant, how to manage potted trees, ...</field>
|
||||
</record>
|
||||
<record id="slide_category_demo_1_0" model="slide.category">
|
||||
<field name="name">Interesting Facts</field>
|
||||
@@ -60,8 +58,7 @@
|
||||
<field name="website_published" eval="True"/>
|
||||
<field name="enroll">public</field>
|
||||
<field name="image" type="base64" file="website_slides/static/src/img/channel_demo_flowers.jpg"/>
|
||||
<field name="description" type="html">
|
||||
<div>A lot of nice documentation: trees, wood, gardens. After that you will know everything.</div></field>
|
||||
<field name="description">A lot of nice documentation: trees, wood, gardens. A gold mine for references.</field>
|
||||
</record>
|
||||
<record id="slide_category_demo_2_0" model="slide.category">
|
||||
<field name="name">Trees</field>
|
||||
@@ -87,9 +84,8 @@
|
||||
<field name="website_published" eval="True"/>
|
||||
<field name="enroll">public</field>
|
||||
<field name="image" type="base64" file="website_slides/static/src/img/channel_demo_flowers.jpg"/>
|
||||
<field name="description" type="html">
|
||||
<div>Knowing which kind of wood to use depending on your application is important. In this course you
|
||||
will learn the basics of wood characteristics.</div></field>
|
||||
<field name="description">Knowing which kind of wood to use depending on your application is important. In this course you
|
||||
will learn the basics of wood characteristics.</field>
|
||||
</record>
|
||||
<record id="slide_category_demo_3_0" model="slide.category">
|
||||
<field name="name">Introduction</field>
|
||||
@@ -107,9 +103,7 @@ will learn the basics of wood characteristics.</div></field>
|
||||
<field name="website_published" eval="True"/>
|
||||
<field name="enroll">invite</field>
|
||||
<field name="image" type="base64" file="website_slides/static/src/img/channel_demo_furniture.jpg"/>
|
||||
<field name="description" type="html">
|
||||
<div>We're also Santa Claus! Belligerent and numerous. I am the man with no name,
|
||||
Zapp Brannigan! </div></field>
|
||||
<field name="description">We're also Santa Claus! Belligerent and numerous. I am the man with no name, Zapp Brannigan!</field>
|
||||
</record>
|
||||
<record id="slide_category_demo_4_0" model="slide.category">
|
||||
<field name="name">Introduction</field>
|
||||
@@ -129,7 +123,7 @@ will learn the basics of wood characteristics.</div></field>
|
||||
<field name="website_published" eval="True"/>
|
||||
<field name="enroll">invite</field>
|
||||
<field name="image" type="base64" file="website_slides/static/src/img/channel_demo_furniture.jpg"/>
|
||||
<field name="description" type="html"><div>All you need to know about furniture creation.</div></field>
|
||||
<field name="description">All you need to know about furniture creation.</field>
|
||||
</record>
|
||||
<record id="slide_category_demo_5_0" model="slide.category">
|
||||
<field name="name">Introduction</field>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import gamification_challenge
|
||||
from . import slide_slide
|
||||
from . import slide_question
|
||||
from . import slide_channel
|
||||
from . import slide_channel_tag
|
||||
from . import slide_channel_invite
|
||||
from . import res_config_settings
|
||||
from . import website
|
||||
from . import res_users
|
||||
|
||||
@@ -6,7 +6,6 @@ import uuid
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.tools.translate import html_translate
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
|
||||
@@ -63,7 +62,10 @@ class Channel(models.Model):
|
||||
""" A channel is a container of slides. """
|
||||
_name = 'slide.channel'
|
||||
_description = 'Slide Channel'
|
||||
_inherit = ['mail.thread', 'website.seo.metadata', 'website.published.multi.mixin', 'rating.mixin']
|
||||
_inherit = [
|
||||
'mail.thread', 'rating.mixin',
|
||||
'image.mixin',
|
||||
'website.seo.metadata', 'website.published.multi.mixin']
|
||||
_order = 'sequence, id'
|
||||
|
||||
def _default_access_token(self):
|
||||
@@ -72,7 +74,8 @@ class Channel(models.Model):
|
||||
# description
|
||||
name = fields.Char('Name', translate=True, required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
description = fields.Html('Description', translate=html_translate, sanitize_attributes=False)
|
||||
description = fields.Text('Short Description', translate=True)
|
||||
description_html = fields.Html('Description', translate=tools.html_translate, sanitize_attributes=False)
|
||||
channel_type = fields.Selection([
|
||||
('documentation', 'Documentation'), ('training', 'Training')],
|
||||
string="Course type", default="documentation", required=True)
|
||||
@@ -82,10 +85,6 @@ class Channel(models.Model):
|
||||
'slide.channel.tag', 'slide_channel_tag_rel', 'channel_id', 'tag_id',
|
||||
string='Tags', help='Used to categorize and filter displayed channels/courses')
|
||||
category_ids = fields.One2many('slide.category', 'channel_id', string="Categories")
|
||||
image = fields.Binary("Image", attachment=True)
|
||||
image_large = fields.Binary("Large image", attachment=True)
|
||||
image_medium = fields.Binary("Medium image", attachment=True)
|
||||
image_small = fields.Binary("Small image", attachment=True)
|
||||
# slides: promote, statistics
|
||||
slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides")
|
||||
slide_last_update = fields.Date('Last Update', compute='_compute_slide_last_update', store=True)
|
||||
@@ -96,12 +95,12 @@ 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_presentations = fields.Integer('Number of Presentations', compute='_compute_slides_statistics', store=True)
|
||||
nbr_documents = fields.Integer('Number of Documents', compute='_compute_slides_statistics', store=True)
|
||||
nbr_videos = fields.Integer('Number of Videos', compute='_compute_slides_statistics', store=True)
|
||||
nbr_infographics = fields.Integer('Number of Infographics', compute='_compute_slides_statistics', store=True)
|
||||
nbr_webpages = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True)
|
||||
nbr_quizs = fields.Integer("Number of quizzes", compute="_compute_slides_statistics", store=True)
|
||||
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_quiz = fields.Integer("Number of Quizs", compute='_compute_slides_statistics', store=True)
|
||||
total_slides = fields.Integer('# Slides', compute='_compute_slides_statistics', store=True, oldname='total')
|
||||
total_views = fields.Integer('# Views', compute='_compute_slides_statistics', store=True)
|
||||
total_votes = fields.Integer('# Votes', compute='_compute_slides_statistics', store=True)
|
||||
@@ -126,7 +125,7 @@ class Channel(models.Model):
|
||||
help='Condition to enroll: everyone, on invite, on payment (sale bridge).')
|
||||
enroll_msg = fields.Html(
|
||||
'Enroll Message', help="Message explaining the enroll process",
|
||||
default=False, translate=html_translate, sanitize_attributes=False)
|
||||
default=False, translate=tools.html_translate, sanitize_attributes=False)
|
||||
enroll_group_ids = fields.Many2many('res.groups', string='Auto Enroll Groups', help="Members of those groups are automatically added as members of the channel.")
|
||||
visibility = fields.Selection([
|
||||
('public', 'Public'), ('members', 'Members')],
|
||||
@@ -180,7 +179,7 @@ class Channel(models.Model):
|
||||
@api.depends('slide_ids.slide_type', 'slide_ids.is_published', 'slide_ids.completion_time',
|
||||
'slide_ids.likes', 'slide_ids.dislikes', 'slide_ids.total_views')
|
||||
def _compute_slides_statistics(self):
|
||||
result = dict((cid, dict(total_slides=0, total_views=0, total_votes=0, total_time=0)) for cid in self.ids)
|
||||
result = dict((cid, dict(total_views=0, total_votes=0, total_time=0)) for cid in self.ids)
|
||||
read_group_res = self.env['slide.slide'].read_group(
|
||||
[('is_published', '=', True),('channel_id', 'in', self.ids)],
|
||||
['channel_id', 'slide_type', 'likes', 'dislikes', 'total_views', 'completion_time'],
|
||||
@@ -188,7 +187,6 @@ class Channel(models.Model):
|
||||
lazy=False)
|
||||
for res_group in read_group_res:
|
||||
cid = res_group['channel_id'][0]
|
||||
result[cid]['total_slides'] += res_group['__count']
|
||||
result[cid]['total_views'] += res_group.get('total_views', 0)
|
||||
result[cid]['total_votes'] += res_group.get('likes', 0)
|
||||
result[cid]['total_votes'] -= res_group.get('dislikes', 0)
|
||||
@@ -202,16 +200,16 @@ class Channel(models.Model):
|
||||
record.update(result[record.id])
|
||||
|
||||
def _compute_slides_statistics_type(self, read_group_res):
|
||||
""" Can be overridden to compute stats on added slide_types """
|
||||
result = dict((cid, dict(nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0, nbr_quizs=0)) for cid in self.ids)
|
||||
""" Compute statistics based on all existing slide types """
|
||||
slide_types = self.env['slide.slide']._fields['slide_type'].get_values(self.env)
|
||||
keys = ['nbr_%s' % slide_type for slide_type in slide_types]
|
||||
keys.append('total_slides')
|
||||
result = dict((cid, dict((key, 0) for key in keys)) for cid in self.ids)
|
||||
for res_group in read_group_res:
|
||||
cid = res_group['channel_id'][0]
|
||||
result[cid]['nbr_presentations'] += res_group.get('slide_type', '') == 'presentation' and res_group['__count'] or 0
|
||||
result[cid]['nbr_documents'] += res_group.get('slide_type', '') == 'document' and res_group['__count'] or 0
|
||||
result[cid]['nbr_videos'] += res_group.get('slide_type', '') == 'video' and res_group['__count'] or 0
|
||||
result[cid]['nbr_infographics'] += res_group.get('slide_type', '') == 'infographic' and res_group['__count'] or 0
|
||||
result[cid]['nbr_webpages'] += res_group.get('slide_type', '') == 'webpage' and res_group['__count'] or 0
|
||||
result[cid]['nbr_quizs'] += res_group.get('slide_type', '') == 'quiz' and res_group['__count'] or 0
|
||||
for slide_type in slide_types:
|
||||
result[cid]['nbr_%s' % slide_type] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0
|
||||
result[cid]['total_slides'] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0
|
||||
return result
|
||||
|
||||
@api.depends('slide_partner_ids')
|
||||
@@ -259,41 +257,6 @@ class Channel(models.Model):
|
||||
if channel.id: # avoid to perform a slug on a not yet saved record in case of an onchange.
|
||||
channel.website_url = '%s/slides/%s' % (base_url, slug(channel))
|
||||
|
||||
@api.multi
|
||||
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)]
|
||||
if len(self) == 1:
|
||||
action['context'] = {'default_channel_id': self.id}
|
||||
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
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(
|
||||
self.env.context,
|
||||
default_channel_id=self.id,
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template and template.id or False,
|
||||
notif_layout='mail.mail_notification_light',
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'slide.channel.invite',
|
||||
'target': 'new',
|
||||
'context': local_context,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# ORM Overrides
|
||||
# ---------------------------------------------------------
|
||||
@@ -322,9 +285,8 @@ class Channel(models.Model):
|
||||
vals['channel_partner_ids'] = [(0, 0, {
|
||||
'partner_id': self.env.user.partner_id.id
|
||||
})]
|
||||
if 'image' in vals:
|
||||
tools.image_resize_images(vals, return_large=True)
|
||||
channel = super(Channel, self.with_context(mail_create_nosubscribe=True)).create(vals)
|
||||
|
||||
if channel.user_id:
|
||||
channel._action_add_members(channel.user_id.partner_id)
|
||||
if 'enroll_group_ids' in vals:
|
||||
@@ -333,8 +295,6 @@ class Channel(models.Model):
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if 'image' in vals:
|
||||
tools.image_resize_images(vals, return_large=True)
|
||||
res = super(Channel, self).write(vals)
|
||||
if vals.get('user_id'):
|
||||
self._action_add_members(self.env['res.users'].sudo().browse(vals['user_id']).partner_id)
|
||||
@@ -364,6 +324,41 @@ class Channel(models.Model):
|
||||
# Business / Actions
|
||||
# ---------------------------------------------------------
|
||||
|
||||
@api.multi
|
||||
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)]
|
||||
if len(self) == 1:
|
||||
action['context'] = {'default_channel_id': self.id}
|
||||
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
def action_channel_invite(self):
|
||||
self.ensure_one()
|
||||
|
||||
if self.visibility != '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(
|
||||
self.env.context,
|
||||
default_channel_id=self.id,
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template and template.id or False,
|
||||
notif_layout='mail.mail_notification_light',
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'slide.channel.invite',
|
||||
'target': 'new',
|
||||
'context': local_context,
|
||||
}
|
||||
|
||||
def action_add_member(self, **member_values):
|
||||
""" Adds the logged in user in the channel members.
|
||||
(see '_action_add_members' for more info)
|
||||
@@ -413,11 +408,6 @@ class Channel(models.Model):
|
||||
for channel in self:
|
||||
channel._action_add_members(channel.mapped('enroll_group_ids.users.partner_id'))
|
||||
|
||||
def list_all(self):
|
||||
return {
|
||||
'channels': [{'id': channel.id, 'name': channel.name, 'website_url': channel.website_url} for channel in self.search([])]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Rating Mixin API
|
||||
# ---------------------------------------------------------
|
||||
@@ -439,12 +429,12 @@ class Category(models.Model):
|
||||
channel_id = fields.Many2one('slide.channel', string="Channel", required=True, ondelete='cascade')
|
||||
sequence = fields.Integer(default=10, help='Display order')
|
||||
slide_ids = fields.One2many('slide.slide', 'category_id', string="Slides")
|
||||
nbr_presentations = fields.Integer("Number of Presentations", compute='_count_presentations', store=True)
|
||||
nbr_documents = fields.Integer("Number of Documents", compute='_count_presentations', store=True)
|
||||
nbr_videos = fields.Integer("Number of Videos", compute='_count_presentations', store=True)
|
||||
nbr_infographics = fields.Integer("Number of Infographics", compute='_count_presentations', store=True)
|
||||
nbr_webpages = fields.Integer("Number of Webpages", compute='_count_presentations', store=True)
|
||||
nbr_quizs = fields.Integer("Number of quizzes", compute="_count_presentations", store=True)
|
||||
nbr_presentation = fields.Integer("Number of Presentations", compute='_count_presentations', store=True)
|
||||
nbr_document = fields.Integer("Number of Documents", compute='_count_presentations', store=True)
|
||||
nbr_video = fields.Integer("Number of Videos", compute='_count_presentations', store=True)
|
||||
nbr_infographic = fields.Integer("Number of Infographics", compute='_count_presentations', store=True)
|
||||
nbr_webpage = fields.Integer("Number of Webpages", compute='_count_presentations', store=True)
|
||||
nbr_quiz = fields.Integer("Number of Quizs", compute="_count_presentations", store=True)
|
||||
total_slides = fields.Integer(compute='_count_presentations', store=True, oldname='total')
|
||||
|
||||
@api.depends('slide_ids.slide_type', 'slide_ids.is_published')
|
||||
@@ -454,29 +444,23 @@ class Category(models.Model):
|
||||
[('is_published', '=', True), ('category_id', 'in', self.ids)],
|
||||
['category_id', 'slide_type'], ['category_id', 'slide_type'],
|
||||
lazy=False)
|
||||
for res_group in res:
|
||||
result[res_group['category_id'][0]][res_group['slide_type']] = result[res_group['category_id'][0]].get(res_group['slide_type'], 0) + res_group['__count']
|
||||
|
||||
type_stats = self._compute_slides_statistics_type(res)
|
||||
for cid, cdata in type_stats.items():
|
||||
result[cid].update(cdata)
|
||||
|
||||
for record in self:
|
||||
record.update(self._extract_count_presentations_type(result, record.id))
|
||||
record.update(result[record.id])
|
||||
|
||||
def _extract_count_presentations_type(self, result, record_id):
|
||||
""" Can be overridden to compute stats on added slide_types """
|
||||
statistics = {
|
||||
'nbr_presentations': result[record_id].get('presentation', 0),
|
||||
'nbr_documents': result[record_id].get('document', 0),
|
||||
'nbr_videos': result[record_id].get('video', 0),
|
||||
'nbr_infographics': result[record_id].get('infographic', 0),
|
||||
'nbr_webpages': result[record_id].get('webpage', 0),
|
||||
'nbr_quizs': result[record_id].get('quiz', 0),
|
||||
'total_slides': 0
|
||||
}
|
||||
|
||||
statistics['total_slides'] += statistics['nbr_presentations']
|
||||
statistics['total_slides'] += statistics['nbr_documents']
|
||||
statistics['total_slides'] += statistics['nbr_videos']
|
||||
statistics['total_slides'] += statistics['nbr_infographics']
|
||||
statistics['total_slides'] += statistics['nbr_webpages']
|
||||
statistics['total_slides'] += statistics['nbr_quizs']
|
||||
|
||||
return statistics
|
||||
def _compute_slides_statistics_type(self, read_group_res):
|
||||
""" Compute statistics based on all existing slide types """
|
||||
slide_types = self.env['slide.slide']._fields['slide_type'].get_values(self.env)
|
||||
keys = ['nbr_%s' % slide_type for slide_type in slide_types]
|
||||
keys.append('total_slides')
|
||||
result = dict((cid, dict((key, 0) for key in keys)) for cid in self.ids)
|
||||
for res_group in read_group_res:
|
||||
cid = res_group['category_id'][0]
|
||||
for slide_type in slide_types:
|
||||
result[cid]['nbr_%s' % slide_type] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0
|
||||
result[cid]['total_slides'] += res_group.get('slide_type', '') == slide_type and res_group['__count'] or 0
|
||||
return result
|
||||
|
||||
@@ -8,14 +8,11 @@ from odoo.exceptions import ValidationError
|
||||
class SlideQuestion(models.Model):
|
||||
_name = "slide.question"
|
||||
_rec_name = "question"
|
||||
_description = """
|
||||
Question for a slide. A slide can have multiple questions and each question
|
||||
must have at least 2 possible answers and only one good answer.
|
||||
"""
|
||||
_description = "Slide Quiz Question"
|
||||
|
||||
sequence = fields.Integer("Sequence", default=10)
|
||||
question = fields.Char("Question Name", required=True)
|
||||
slide_id = fields.Many2one('slide.slide', string="Slide")
|
||||
question = fields.Char("Question Name", required=True, translate=True)
|
||||
slide_id = fields.Many2one('slide.slide', string="Slide", required=True)
|
||||
answer_ids = fields.One2many('slide.answer', 'question_id', string="Answer")
|
||||
|
||||
@api.constrains('answer_ids')
|
||||
@@ -44,8 +41,9 @@ class SlideQuestion(models.Model):
|
||||
class SlideAnswer(models.Model):
|
||||
_name = "slide.answer"
|
||||
_rec_name = "text_value"
|
||||
_description = """Answer for a slide question"""
|
||||
_description = "Answer for a slide question"
|
||||
_order = 'question_id, id'
|
||||
|
||||
question_id = fields.Many2one('slide.question', string="Question")
|
||||
text_value = fields.Char("Answer", required=True,)
|
||||
is_correct = fields.Boolean("Is correct answer", default=False)
|
||||
question_id = fields.Many2one('slide.question', string="Question", required=True)
|
||||
text_value = fields.Char("Answer", required=True, translate=True)
|
||||
is_correct = fields.Boolean("Is correct answer")
|
||||
|
||||
@@ -11,7 +11,7 @@ import uuid
|
||||
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.exceptions import Warning, UserError
|
||||
from odoo.http import request
|
||||
@@ -77,19 +77,11 @@ class SlideTag(models.Model):
|
||||
|
||||
|
||||
class Slide(models.Model):
|
||||
""" This model represents actual presentations. Those must be one of four
|
||||
types:
|
||||
|
||||
- Presentation
|
||||
- Document
|
||||
- Infographic
|
||||
- Video
|
||||
- Webpage
|
||||
|
||||
Slide has various statistics like view count, embed count, like, dislikes """
|
||||
|
||||
_name = 'slide.slide'
|
||||
_inherit = ['mail.thread', 'website.seo.metadata', 'website.published.mixin', 'rating.mixin']
|
||||
_inherit = [
|
||||
'mail.thread', 'rating.mixin',
|
||||
'image.mixin',
|
||||
'website.seo.metadata', 'website.published.mixin']
|
||||
_description = 'Slides'
|
||||
_mail_post_access = 'read'
|
||||
_order_by_strategy = {
|
||||
@@ -99,10 +91,6 @@ class Slide(models.Model):
|
||||
}
|
||||
_order = 'category_sequence asc, sequence asc'
|
||||
|
||||
_sql_constraints = [
|
||||
('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.")
|
||||
]
|
||||
|
||||
def _default_access_token(self):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
@@ -119,10 +107,6 @@ class Slide(models.Model):
|
||||
access_token = fields.Char("Security Token", copy=False, default=_default_access_token)
|
||||
is_preview = fields.Boolean('Always visible', default=False)
|
||||
completion_time = fields.Float('# Hours', default=1, digits=(10, 4))
|
||||
image = fields.Binary("Image", attachment=True)
|
||||
image_large = fields.Binary("Large image", attachment=True)
|
||||
image_medium = fields.Binary("Medium image", attachment=True)
|
||||
image_small = fields.Binary("Small image", attachment=True)
|
||||
# subscribers
|
||||
partner_ids = fields.Many2many('res.partner', 'slide_slide_partner', 'slide_id', 'partner_id',
|
||||
string='Subscribers', groups='base.group_website_publisher')
|
||||
@@ -161,13 +145,17 @@ class Slide(models.Model):
|
||||
likes = fields.Integer('Likes', compute='_compute_user_info', store=True)
|
||||
dislikes = fields.Integer('Dislikes', compute='_compute_user_info', store=True)
|
||||
user_vote = fields.Integer('User vote', compute='_compute_user_info')
|
||||
embed_code = fields.Text('Embed Code', readonly=True, compute='_get_embed_code')
|
||||
embed_code = fields.Text('Embed Code', readonly=True, compute='_compute_embed_code')
|
||||
# views
|
||||
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)
|
||||
|
||||
_sql_constraints = [
|
||||
('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('slide_views', 'public_views')
|
||||
def _compute_total(self):
|
||||
for record in self:
|
||||
@@ -216,7 +204,8 @@ class Slide(models.Model):
|
||||
self.env['slide.slide.partner']
|
||||
)
|
||||
|
||||
def _get_embed_code(self):
|
||||
@api.depends('document_id', 'slide_type', 'mime_type')
|
||||
def _compute_embed_code(self):
|
||||
base_url = request and request.httprequest.url_root or self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
if base_url[-1] == '/':
|
||||
base_url = base_url[:-1]
|
||||
@@ -235,7 +224,7 @@ class Slide(models.Model):
|
||||
record.embed_code = False
|
||||
|
||||
@api.onchange('url')
|
||||
def on_change_url(self):
|
||||
def _on_change_url(self):
|
||||
self.ensure_one()
|
||||
if self.url:
|
||||
res = self._parse_document_url(self.url)
|
||||
@@ -250,6 +239,7 @@ class Slide(models.Model):
|
||||
@api.multi
|
||||
@api.depends('name')
|
||||
def _compute_website_url(self):
|
||||
# TDE FIXME: clena this link.tracker strange stuff
|
||||
super(Slide, self)._compute_website_url()
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for slide in self:
|
||||
@@ -296,9 +286,6 @@ class Slide(models.Model):
|
||||
for key, value in doc_data.items():
|
||||
values.setdefault(key, value)
|
||||
|
||||
if 'image' in values:
|
||||
tools.image_resize_images(values, return_large=True)
|
||||
|
||||
slide = super(Slide, self).create(values)
|
||||
|
||||
if slide.website_published:
|
||||
@@ -312,15 +299,16 @@ class Slide(models.Model):
|
||||
for key, value in doc_data.items():
|
||||
values.setdefault(key, value)
|
||||
|
||||
if 'image' in values:
|
||||
tools.image_resize_images(values, return_large=True)
|
||||
|
||||
res = super(Slide, self).write(values)
|
||||
if values.get('website_published'):
|
||||
self.date_published = datetime.datetime.now()
|
||||
self._post_publication()
|
||||
return res
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Mail/Rating
|
||||
# ---------------------------------------------------------
|
||||
|
||||
@api.multi
|
||||
def get_access_action(self, access_uid=None):
|
||||
""" Instead of the classic form view, redirect to website if it is published. """
|
||||
@@ -335,10 +323,6 @@ class Slide(models.Model):
|
||||
}
|
||||
return super(Slide, self).get_access_action(access_uid)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Business Methods
|
||||
# ---------------------------------------------------------
|
||||
|
||||
@api.multi
|
||||
def _notify_get_groups(self, message, groups):
|
||||
""" Add access button to everyone if the document is active. """
|
||||
@@ -350,7 +334,12 @@ class Slide(models.Model):
|
||||
|
||||
return groups
|
||||
|
||||
def get_related_slides(self, limit=20):
|
||||
# ---------------------------------------------------------
|
||||
# Business Methods
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def _get_related_slides(self, limit=20):
|
||||
# TDE FIXME: should build a smarter domain
|
||||
domain = request.website.website_domain()
|
||||
domain += [('website_published', '=', True), ('id', '!=', self.id)]
|
||||
if self.category_id:
|
||||
@@ -358,7 +347,8 @@ class Slide(models.Model):
|
||||
for record in self.search(domain, limit=limit):
|
||||
yield record
|
||||
|
||||
def get_most_viewed_slides(self, limit=20):
|
||||
def _get_most_viewed_slides(self, limit=20):
|
||||
# TDE FIXME: should build a smarter domain
|
||||
domain = request.website.website_domain()
|
||||
domain += [('website_published', '=', True), ('id', '!=', self.id)]
|
||||
for record in self.search(domain, limit=limit, order='total_views desc'):
|
||||
@@ -387,10 +377,13 @@ class Slide(models.Model):
|
||||
self.write({'access_token': self._default_access_token()})
|
||||
return self._sign_token(partner_id)
|
||||
|
||||
@api.one
|
||||
def send_share_email(self, email):
|
||||
def _send_share_email(self, email):
|
||||
# TDE FIXME: template to check
|
||||
mail_ids = []
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
return self.channel_id.share_template_id.with_context(email=email, base_url=base_url).send_mail(self.id, notif_layout='mail.mail_notification_light')
|
||||
for record in self:
|
||||
mail_ids.append(self.channel_id.share_template_id.with_context(email=email, base_url=base_url).send_mail(record.id, notif_layout='mail.mail_notification_light'))
|
||||
return mail_ids
|
||||
|
||||
def action_like(self):
|
||||
self.check_access_rights('read')
|
||||
|
||||
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 139 KiB |
@@ -37,10 +37,10 @@ class TestStatistics(common.SlidesCase):
|
||||
channel_publisher = self.channel.sudo(self.user_publisher)
|
||||
# slide type computation
|
||||
self.assertEqual(channel_publisher.total_slides, len(channel_publisher.slide_ids))
|
||||
self.assertEqual(channel_publisher.nbr_infographics, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'infographic')))
|
||||
self.assertEqual(channel_publisher.nbr_presentations, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'presentation')))
|
||||
self.assertEqual(channel_publisher.nbr_documents, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'document')))
|
||||
self.assertEqual(channel_publisher.nbr_videos, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'video')))
|
||||
self.assertEqual(channel_publisher.nbr_infographic, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'infographic')))
|
||||
self.assertEqual(channel_publisher.nbr_presentation, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'presentation')))
|
||||
self.assertEqual(channel_publisher.nbr_document, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'document')))
|
||||
self.assertEqual(channel_publisher.nbr_video, len(channel_publisher.slide_ids.filtered(lambda s: s.slide_type == 'video')))
|
||||
# slide statistics computation
|
||||
self.assertEqual(float_compare(channel_publisher.total_time, sum(s.completion_time for s in channel_publisher.slide_ids), 3), 0)
|
||||
# members computation
|
||||
|
||||
@@ -114,10 +114,11 @@
|
||||
<page string="Statistics" groups="base.group_no_one">
|
||||
<group>
|
||||
<group>
|
||||
<field name="nbr_presentations"/>
|
||||
<field name="nbr_documents"/>
|
||||
<field name="nbr_videos"/>
|
||||
<field name="nbr_infographics"/>
|
||||
<field name="nbr_presentation"/>
|
||||
<field name="nbr_document"/>
|
||||
<field name="nbr_video"/>
|
||||
<field name="nbr_infographic"/>
|
||||
<field name="nbr_webpage"/>
|
||||
<field name="total_slides"/>
|
||||
</group>
|
||||
<group>
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo><data>
|
||||
|
||||
<!-- Channels sub-template: header -->
|
||||
<template id="course_nav" name="Course Navigation Header">
|
||||
<div class="o_wslides_course_nav">
|
||||
<div class="container">
|
||||
<div class="row align-items-center justify-content-between">
|
||||
<!-- nav -->
|
||||
<nav aria-label="breadcrumb" class="col-6">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/slides">Home</a>
|
||||
</li>
|
||||
<li t-att-class="'breadcrumb-item %s' % ('active' if not search_category and not search_tag and not search_slide_type else '')">
|
||||
<a t-att-href="'/slides/%s' % slug(channel)"><span t-esc="channel.name"/></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_category" t-if="search_category">
|
||||
<a t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))"><span t-esc="search_category.name"/></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_tag" t-if="search_tag">
|
||||
<a t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))"><span t-esc="search_tag.name"/></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type">
|
||||
<a t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)"><span t-esc="slide_types[search_slide_type]"/></a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="col-6 d-flex flex-row align-items-center">
|
||||
<!-- karma / profile -->
|
||||
<a t-if="not is_public_user" t-att-href="'/profile/user/%s?channel_id=%s' % (user.id, channel.id)" class="font-weight-bold mr-3">
|
||||
<i class="fa fa-diamond" /> <t t-esc="user_id.karma"/>
|
||||
</a>
|
||||
<!-- search -->
|
||||
<form t-attf-action="/slides/#{slug(channel)}" role="search" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="search" t-attf-placeholder="Search in #{channel.name}" t-att-value="search"/>
|
||||
<span class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit" aria-label="Search" title="Search">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<!-- sort / filter -->
|
||||
<small class="dropdown float-right ml-3">
|
||||
<h6 class="m-0">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="slidesChannelDropdownSort"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<t t-if="sorting == 'most_voted'">Most Voted</t>
|
||||
<t t-elif="sorting == 'most_viewed'">Most Viewed</t>
|
||||
<t t-else="">Newest</t>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="slidesChannelDropdownSort" role="menu">
|
||||
<div role="separator" class="dropdown-header">Sort by</div>
|
||||
<a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='latest'))"
|
||||
t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'latest' else '')">Newest</a>
|
||||
<a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='most_voted'))"
|
||||
t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'most_voted' else '')">Most Voted</a>
|
||||
<a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='most_viewed'))"
|
||||
t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'most_viewed' else '')">Most Viewed</a>
|
||||
</div>
|
||||
</h6>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Channel main template -->
|
||||
<template id='course_main' name="Course Main">
|
||||
<t t-set="head">
|
||||
<t t-call-assets="web.pdf_js_lib" t-css="false"/>
|
||||
<script type="text/javascript" src="/website_slides/static/lib/pdfslidesviewer/PDFSlidesViewer.js"></script>
|
||||
</t>
|
||||
<t t-call="website.layout">
|
||||
<div class="wrap o_wslides_wrap">
|
||||
<t t-set="main_object" t-value="channel"/>
|
||||
<t t-call="website_slides.course_nav"/>
|
||||
|
||||
<div class="row o_wslides_course_header">
|
||||
<div class="col-lg-12 o_wslides_container o_wslides_course_header_container">
|
||||
<div class="row align-items-end">
|
||||
<div class="col o_wslides_course_header_aside main_image">
|
||||
<div t-field="channel.image" widget="image" t-options="{'widget': 'image'}"/>
|
||||
</div>
|
||||
<div class="col-8 o_wslides_course_header_content">
|
||||
<h3 class="row"><span class="font-weight-bold col" t-field="channel.name"/></h3>
|
||||
<div class="row">
|
||||
<div class="col-12 o_wslides_course_description" t-field="channel.description"/>
|
||||
<div class="col-6 mt8 mb64">
|
||||
<t t-call="website_rating.rating_stars_static_popup_composer">
|
||||
<t t-set="rating_avg" t-value="rating_avg"/>
|
||||
<t t-set="rating_total" t-value="rating_count"/>
|
||||
<t t-set="object" t-value="channel"/>
|
||||
<t t-set="hash" t-value="message_post_hash"/>
|
||||
<t t-set="pid" t-value="message_post_pid"/>
|
||||
<t t-set="default_message_id" t-value="last_message_id"/>
|
||||
<t t-set="default_message" t-value="last_message"/>
|
||||
<t t-set="default_rating_value" t-value="last_rating_value"/>
|
||||
<t t-set="force_submit_url" t-value="'/slides/mail/update_comment' if last_message_id else False"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row o_wslides_course_content">
|
||||
<div class="col-lg-12 o_wslides_container o_wslides_content_container">
|
||||
<div class="row justify-content-end o_wslides_course_content_nav">
|
||||
<div class="col-8">
|
||||
<ul class="nav nav-tabs o_wslides_nav_tabs" role="tablist" id="profile_extra_info_tablist">
|
||||
<li class="nav-item o_wslides_nav_tabs_item_home">
|
||||
<a t-att-href="'/slides/%s%s?%s' % (slug(channel), '/%s' % slug(category) if category else '', keep_query('debug'))"
|
||||
t-att-class="'nav-link o_wslides_navlink %s' % ('' if search_slide_type else 'active')">
|
||||
<i class="fa fa-home"/> Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a t-att-href="'/slides/%s' % (slug(channel))"
|
||||
t-att-class="'nav-link o_wslides_navlink'">Review</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row o_wslides_course_content_main">
|
||||
<!-- Channel presentation, aka general information + CTAs -->
|
||||
<div class="col-4 o_wslides_course_content_aside">
|
||||
<div class="row justify-content-center mt8 o_wslides_join_channel">
|
||||
<a t-if="not channel.is_member and channel.enroll == 'public'" role="button"
|
||||
class="col-10 btn btn-primary o_wslides_join_channel_link"
|
||||
title="Start Course" aria-label="Start Course Channel"
|
||||
t-att-href="'#'"
|
||||
t-att-data-channel-id="channel.id">
|
||||
<span class="cta-title text_small_caps">
|
||||
<t t-if="channel.channel_type == 'documentation'">Start Course</t>
|
||||
<t t-else="">Join Course</t>
|
||||
</span>
|
||||
</a>
|
||||
<t t-if="not channel.is_member and channel.enroll == 'invite'">
|
||||
<a role="button" class="col-10 btn btn-light" t-att-href="'#'">
|
||||
<span class="cta-title text_small_caps">Private Course</span>
|
||||
</a>
|
||||
<div t-if="channel.enroll_msg" t-field="channel.enroll_msg"/>
|
||||
<span t-if="not channel.enroll_msg">Contact website administrator.</span>
|
||||
</t>
|
||||
<span t-if="channel.is_member" class="col-10 btn btn-primary">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
<span class="cta-title text_small_caps">Already member</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
<span class="col-5">Last Update</span>
|
||||
<span class="col"><t t-esc="channel.slide_last_update" t-options="{'widget': 'date'}"/></span>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
<span class="col-5">Members</span>
|
||||
<span class="col" t-esc="channel.members_count"/>
|
||||
</div>
|
||||
<div class="row mt8"><div class="col"><hr/></div></div>
|
||||
<t t-set="slide_type_keys" t-value="slide_types.keys()"/>
|
||||
<t t-foreach="slide_type_keys" t-as="slide_type_key">
|
||||
<div class="row mt8 align-items-center">
|
||||
<span class="col-5">
|
||||
<t t-if="search_category">
|
||||
<a t-att-href="'/slides/%s/category/%s?%s' % (slug(channel), slug(search_category), keep_query('debug', slide_type=slide_type_key))"
|
||||
t-att-class="'%s' % ('active' if search_slide_type == slide_type_key else '')">
|
||||
<t t-esc="slide_types[slide_type_key]"/>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', slide_type=slide_type_key))"
|
||||
t-att-class="'%s' % ('active' if search_slide_type == slide_type_key else '')">
|
||||
<t t-esc="slide_types[slide_type_key]"/>
|
||||
</a>
|
||||
</t>
|
||||
</span>
|
||||
<span class="col">
|
||||
<t t-if="search_category">
|
||||
<span class="badge badge-pill badge-info" t-esc="search_category['nbr_%s' % slide_type_key]"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge badge-pill badge-info" t-esc="channel['nbr_%s' % slide_type_key]"/>
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<div class="row mt8"><div class="col"><hr/></div></div>
|
||||
<div class="row justify-content-center mt8">
|
||||
<button role="button" class="btn btn-primary col-10" title="Share Channel"
|
||||
aria-label="Share Channel"
|
||||
data-toggle="modal" data-target="#slideChannelShareModal">
|
||||
<i class="fa fa-share-square"></i> Share
|
||||
</button>
|
||||
<div class="modal fade" id="slideChannelShareModal" tabindex="-1" role="dialog" aria-labelledby="slideChannelShareModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="slideChannelShareModalLabel">
|
||||
Share
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span>&times;</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>Share on Social Networks</h5>
|
||||
<t t-call="website_slides.slides_share">
|
||||
<t t-set="record" t-value="channel"/>
|
||||
</t>
|
||||
<h5 class="mt16">Share Link</h5>
|
||||
<input type="text" class="form-control" readonly="readonly" onclick="this.select();"
|
||||
t-att-value="channel.website_url"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div t-if="channel.can_upload" class="row justify-content-center mt8 mb8">
|
||||
<a role="button" class="btn btn-primary oe_slide_js_upload col-10" title="Upload Presentation"
|
||||
aria-label="Upload Presentation" href="#"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-can-upload="channel.can_upload"
|
||||
t-att-data-can-publish="channel.can_publish">
|
||||
<i class="fa fa-cloud-upload"></i> Upload
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="channel.channel_type == 'training'" t-call="website_slides.course_content_training"/>
|
||||
<t t-if="channel.channel_type == 'documentation'" t-call="website_slides.course_content_documentation"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="course_content_training" name="Content of a training channel">
|
||||
<div class="col-8 d-flex flex-column ml-4">
|
||||
<div class="course-content oe_js_course_slides_list w-100 mt8">
|
||||
<ul>
|
||||
<t t-set="i" t-value="1"/>
|
||||
<t t-set="j" t-value="0"/>
|
||||
<t t-foreach="channel.category_ids" t-as="category">
|
||||
<li t-attf-class="#{'section-draggable' if user == user_id else ''}" t-attf-category_id="#{category.id}">
|
||||
<div t-attf-category_id="#{category.id}" class="course-section text-muted font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center">
|
||||
<div style="width:50%;" class="d-flex align-items-center">
|
||||
<i t-if="channel.user_id == user" class="fa fa-arrows mr-3 text-muted"></i>
|
||||
<div class="mr-2">Section <span class="section-index" t-esc="i"/>:</div>
|
||||
<span t-field="category.name"/>
|
||||
</div>
|
||||
<!-- <a t-if="channel.user_id == user" title="Add content to this section" class="mr-2 p-0 oe_slide_js_add_slide" href="#" t-attf-channel_id="#{channel.id}" t-attf-category_id="#{category.id}">+</a> -->
|
||||
<a class="mr-2 oe_slide_js_upload"
|
||||
role="button"
|
||||
aria-label="Upload Presentation"
|
||||
href="#"
|
||||
style="font-size: 1.5rem;text-decoration: none;"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-category-id="category.id"
|
||||
t-att-data-can-upload="channel.can_upload"
|
||||
t-att-data-can-publish="channel.can_publish">+</a>
|
||||
<t t-set="i" t-value="i+1"/>
|
||||
</div>
|
||||
<ul t-attf-category_id="#{category.id}" >
|
||||
<t t-foreach="category.slide_ids" t-as="slide">
|
||||
<li t-att-index="j" t-attf-slide_id="#{slide.id}" t-attf-category_id="#{category.id}" t-attf-class="#{'content-slide slide-draggable d-flex justify-content-between align-items-center p-2' if channel.user_id == user else 'content-slide d-flex justify-content-between align-items-center p-2'}">
|
||||
<div class="content-slide-infos ml-2">
|
||||
<i t-if="channel.user_id == user" class="fa fa-arrows mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'video'" class="fa fa-play-circle mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'link'" class="fa fa-file-code-o mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'webpage'" class="fa fa-file-text mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'certification'" class="fa fa-trophy mr-2 text-muted"></i>
|
||||
<a class="link-to-slide" t-attf-href="/slides/slide/#{slug(slide)}"><span t-field="slide.name"/></a>
|
||||
</div>
|
||||
<div class="content-slide-controls mr-2">
|
||||
<i t-if="not slide.id in user_progress or not user_progress[slide.id].completed" class="check-done fa fa-check-circle text-muted mr-1"></i>
|
||||
<i t-if="slide.id in user_progress and user_progress[slide.id].completed" class="check-done text-primary fa fa-check-circle mr-1"></i>
|
||||
<a t-if="channel.user_id == user and not slide.slide_type == 'webpage'" t-attf-href="/web#id=#{slide.id}&action=#{slide_action}&model=slide.slide&view_type=form"><i class="fa fa-pencil text-muted mr-1"></i></a>
|
||||
<a t-if="channel.user_id == user and slide.slide_type == 'webpage'" t-attf-href="/slides/slide/#{slug(slide)}?enable_editor=1"><i class="fa fa-pencil text-muted mr-1"></i></a>
|
||||
<i t-if="channel.user_id == user" t-attf-slide_id="#{slide.id}" class="fa fa-trash text-muted oe_slide_js_delete_slide"></i>
|
||||
</div>
|
||||
</li>
|
||||
<t t-set="j" t-value="j+1"/>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
<div t-if="channel.user_id == user" class="content-actions">
|
||||
<a class="mr-2 oe_slide_js_upload"
|
||||
role="button"
|
||||
aria-label="Upload Presentation"
|
||||
href="#"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-can-upload="channel.can_upload"
|
||||
t-att-data-can-publish="channel.can_publish">Add Content</a>
|
||||
<a class="oe_slide_js_add_section" t-attf-channel_id="#{channel.id}" href="#">Add Section</a>
|
||||
</div>
|
||||
</div>
|
||||
<div t-field="channel.description_html"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="course_content_documentation" name="Content of a training channel">
|
||||
<div class="col-8 mt24 ml32 o_wslides_channel_content_promoted">
|
||||
<div class="row mt16 align-items-center">
|
||||
<div class="col">
|
||||
<h3><i class="fa fa-plus-square"/> Featured lesson</h3>
|
||||
<hr/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" t-if="slide_promoted">
|
||||
<div class="col-4">
|
||||
<img class="img img-fluid" style=""
|
||||
t-att-src="'/slides/slide/%s/get_image?field=image_large' % slide_promoted.id"
|
||||
t-att-alt="slide_promoted.name"/>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="row">
|
||||
<h3 class="col-6" t-att-title="slide_promoted.name">
|
||||
<t t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher">
|
||||
<a t-attf-href="/slides/slide/#{slug(slide_promoted)}" class="font-weight-bold text-muted" t-field="slide_promoted.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="font-weight-bold text-muted" t-field="slide_promoted.name"/>
|
||||
</t>
|
||||
</h3>
|
||||
<div class="col-12" t-field="slide_promoted.description"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="tab-content mt16" id="slideChannelContent">
|
||||
<div class="tab-pane active" role="tabpanel" aria-labelledby="profile-tab" id="slideChannelContentAbout">
|
||||
<t t-foreach="category_data" t-as="category">
|
||||
<div class="row mt16 align-items-center">
|
||||
<div class="col">
|
||||
<a t-att-href="'/slides/%s/category/%s' % (slug(channel), category['id'])" class="float-right">
|
||||
View all
|
||||
</a>
|
||||
<h3><i class="fa fa-plus-square"/> <t t-esc="category['name']"/></h3>
|
||||
<hr/>
|
||||
</div>
|
||||
</div>
|
||||
<t t-call="website_slides.course_lessons_grid">
|
||||
<t t-set="slides" t-value="category['slides']"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-field="channel.description_html"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Embedded template: display a list of slides in a grid view -->
|
||||
<template id='course_lessons_grid' name="Course: Lessons Grid View">
|
||||
<div class="row o_wslides_row mt8">
|
||||
<div class="col-lg-3 col-md-6 col-12" t-foreach="slides" t-as="slide">
|
||||
<div class="card o_wslides_lesson_card">
|
||||
<img class="card-img-top img-fuild" style="max-height: 160px;"
|
||||
t-att-src="'/slides/slide/%s/get_image?field=image_large' % slide.id"
|
||||
t-att-alt="slide.name"/>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" t-att-title="slide.name">
|
||||
<t t-if="slide.is_preview or channel.is_member or is_slides_publisher">
|
||||
<a t-attf-href="/slides/slide/#{slug(slide)}" t-esc="slide.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="text-muted" t-esc="slide.name"/>
|
||||
</t>
|
||||
</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">
|
||||
<t t-if="user.has_group('website.group_website_publisher')">
|
||||
<i class="fa fa-globe float-right o_wslides_slide_manage_publish"></i>
|
||||
<i class="fa fa-pencil float-right o_wslides_slide_manage_edit"></i>
|
||||
<i class="fa fa-trash float-right o_wslides_slide_manage_unlink"></i>
|
||||
</t>
|
||||
<t t-if="slide.is_preview">
|
||||
<span class="badge badge-info">Free preview</span>
|
||||
</t>
|
||||
<t t-if="not slide.is_published and user.has_group('website.group_website_publisher')">
|
||||
<span class="badge badge-danger">Unpublished</span>
|
||||
</t>
|
||||
</h6>
|
||||
<div class="card-text">
|
||||
<div class="text-muted oe_no_empty" t-field="slide.description"/>
|
||||
<div t-if="slide.tag_ids">
|
||||
<t t-foreach="slide.tag_ids" t-as="tag">
|
||||
<a t-att-href="'/slides/%s/tag/%s' % (slug(slide.channel_id), slug(tag))" class="badge badge-secondary">
|
||||
<t t-esc="tag.name"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<!-- CHECKME: like only if logged -->
|
||||
<div class="float-right text-right o_wslides_like">
|
||||
<span class="o_wslides_like_up" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
|
||||
<i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i>
|
||||
<span t-esc="slide.likes"/>
|
||||
</span>
|
||||
<span class="o_wslides_like_down" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
|
||||
<i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
|
||||
<span t-esc="slide.dislikes"/>
|
||||
</span>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<t t-call="website_slides.slides_misc_float_time">
|
||||
<t t-set="value" t-value="slide.completion_time"/>
|
||||
</t>
|
||||
<!-- <t t-esc="slide.total_views"/> Views
|
||||
<timeago class="timeago" t-att-datetime="slide.create_date"></timeago> -->
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</data></odoo>
|
||||
@@ -1,454 +1,7 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Tools template: share on social networkds -->
|
||||
<template id='slides_share' name="Slides Media Share">
|
||||
<div>
|
||||
<a t-attf-href="https://www.facebook.com/sharer/sharer.php?u=#{record.website_url}" class="o_slides_social_share" social-key="facebook" aria-label="Share on Facebook" title="Share on Facebook"><i class="fa fa-facebook-square fa-2x"/></a>
|
||||
<a t-attf-href="https://twitter.com/intent/tweet?text=#{record.name}&url=#{record.website_url}" class="o_slides_social_share" social-key="twitter" aria-label="Share on Twitter" title="Share on Twitter"><i class="fa fa-twitter fa-2x"/></a>
|
||||
<a t-attf-href="http://www.linkedin.com/shareArticle?mini=true&url=#{record.website_url}&title=#{record.name}&" social-key="linkedin" class="o_slides_social_share" aria-label="Share on LinkedIn" title="Share on LinkedIn"><i class="fa fa-linkedin fa-2x"/></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Channels: no channel found -->
|
||||
<template id="channel_not_found" name="Private Presentation">
|
||||
<t t-call="website.layout">
|
||||
<div class="container">
|
||||
<div class="oe_structure">
|
||||
<t t-if="search_term">
|
||||
<h2>No channel found when searching "<i><t t-esc="search_term"/></i>".</h2>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<h2>No channel created or published yet.</h2>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Channels sub-template: header -->
|
||||
<template id="course_nav" name="Course Navigation Header">
|
||||
<div class="o_wslides_course_nav">
|
||||
<div class="container">
|
||||
<div class="row align-items-center justify-content-between">
|
||||
<!-- nav -->
|
||||
<nav aria-label="breadcrumb" class="col-6">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/slides">Home</a>
|
||||
</li>
|
||||
<li t-att-class="'breadcrumb-item %s' % ('active' if not search_category and not search_tag and not search_slide_type else '')">
|
||||
<a t-att-href="'/slides/%s' % slug(channel)"><span t-esc="channel.name"/></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_category" t-if="search_category">
|
||||
<a t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))"><span t-esc="search_category.name"/></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_tag" t-if="search_tag">
|
||||
<a t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))"><span t-esc="search_tag.name"/></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type">
|
||||
<a t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)"><span t-esc="slide_types[search_slide_type]"/></a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="col-6 d-flex flex-row align-items-center">
|
||||
<!-- karma / profile -->
|
||||
<a t-if="not is_public_user" t-att-href="'/profile/user/%s?channel_id=%s' % (user.id, channel.id)" class="font-weight-bold mr-3">
|
||||
<i class="fa fa-diamond" /> <t t-esc="user_id.karma"/>
|
||||
</a>
|
||||
<!-- search -->
|
||||
<form t-attf-action="/slides/#{slug(channel)}" role="search" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="search" t-attf-placeholder="Search in #{channel.name}" t-att-value="search"/>
|
||||
<span class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit" aria-label="Search" title="Search">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<!-- sort / filter -->
|
||||
<small class="dropdown float-right ml-3">
|
||||
<h6 class="m-0">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="slidesChannelDropdownSort"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<t t-if="sorting == 'most_voted'">Most Voted</t>
|
||||
<t t-elif="sorting == 'most_viewed'">Most Viewed</t>
|
||||
<t t-else="">Newest</t>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="slidesChannelDropdownSort" role="menu">
|
||||
<div role="separator" class="dropdown-header">Sort by</div>
|
||||
<a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='latest'))"
|
||||
t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'latest' else '')">Newest</a>
|
||||
<a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='most_voted'))"
|
||||
t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'most_voted' else '')">Most Voted</a>
|
||||
<a role="menuitem" t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', 'slide_type', sorting='most_viewed'))"
|
||||
t-att-class="'dropdown-item %s' % ('active' if sorting and sorting == 'most_viewed' else '')">Most Viewed</a>
|
||||
</div>
|
||||
</h6>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Channel main template -->
|
||||
<template id='course_main' name="Course Main">
|
||||
<t t-set="head">
|
||||
<t t-call-assets="web.pdf_js_lib" t-css="false"/>
|
||||
<script type="text/javascript" src="/website_slides/static/lib/pdfslidesviewer/PDFSlidesViewer.js"></script>
|
||||
</t>
|
||||
<t t-call="website.layout">
|
||||
<div class="wrap o_wslides_wrap">
|
||||
<t t-set="main_object" t-value="channel"/>
|
||||
<t t-call="website_slides.course_nav"/>
|
||||
|
||||
<div class="row o_wslides_course_header">
|
||||
<div class="col-lg-12 o_wslides_container o_wslides_course_header_container">
|
||||
<div class="row align-items-end">
|
||||
<div class="col o_wslides_course_header_aside main_image">
|
||||
<div t-field="channel.image" widget="image" t-options="{'widget': 'image'}"/>
|
||||
</div>
|
||||
<div class="col-8 o_wslides_course_header_content">
|
||||
<h3 class="row"><span class="font-weight-bold col" t-field="channel.name"/></h3>
|
||||
<div class="row">
|
||||
<div class="col-12 o_wslides_course_description" t-field="channel.description"/>
|
||||
<div class="col-6 mt8 mb64">
|
||||
<t t-call="website_rating.rating_stars_static_popup_composer">
|
||||
<t t-set="rating_avg" t-value="rating_avg"/>
|
||||
<t t-set="rating_total" t-value="rating_count"/>
|
||||
<t t-set="object" t-value="channel"/>
|
||||
<t t-set="hash" t-value="message_post_hash"/>
|
||||
<t t-set="pid" t-value="message_post_pid"/>
|
||||
<t t-set="default_message_id" t-value="last_message_id"/>
|
||||
<t t-set="default_message" t-value="last_message"/>
|
||||
<t t-set="default_rating_value" t-value="last_rating_value"/>
|
||||
<t t-set="force_submit_url" t-value="'/slides/mail/update_comment' if last_message_id else False"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row o_wslides_course_content">
|
||||
<div class="col-lg-12 o_wslides_container o_wslides_content_container">
|
||||
<div class="row justify-content-end o_wslides_course_content_nav">
|
||||
<div class="col-8">
|
||||
<ul class="nav nav-tabs o_wslides_nav_tabs" role="tablist" id="profile_extra_info_tablist">
|
||||
<li class="nav-item o_wslides_nav_tabs_item_home">
|
||||
<a t-att-href="'/slides/%s%s?%s' % (slug(channel), '/%s' % slug(category) if category else '', keep_query('debug'))"
|
||||
t-att-class="'nav-link o_wslides_navlink %s' % ('' if search_slide_type else 'active')">
|
||||
<i class="fa fa-home"/> Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a t-att-href="'/slides/%s' % (slug(channel))"
|
||||
t-att-class="'nav-link o_wslides_navlink'">Review</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row o_wslides_course_content_main">
|
||||
<!-- Channel presentation, aka general information + CTAs -->
|
||||
<div class="col-4 o_wslides_course_content_aside">
|
||||
<div class="row justify-content-center mt8 o_wslides_join_channel">
|
||||
<a t-if="not channel.is_member and channel.enroll == 'public'" role="button"
|
||||
class="col-10 btn btn-primary o_wslides_join_channel_link"
|
||||
title="Start Course" aria-label="Start Course Channel"
|
||||
t-att-href="'#'"
|
||||
t-att-data-channel-id="channel.id">
|
||||
<span class="cta-title text_small_caps">
|
||||
<t t-if="channel.channel_type == 'documentation'">Start Course</t>
|
||||
<t t-else="">Join Course</t>
|
||||
</span>
|
||||
</a>
|
||||
<t t-if="not channel.is_member and channel.enroll == 'invite'">
|
||||
<a role="button" class="col-10 btn btn-light" t-att-href="'#'">
|
||||
<span class="cta-title text_small_caps">Private Course</span>
|
||||
</a>
|
||||
<div t-if="channel.enroll_msg" t-field="channel.enroll_msg"/>
|
||||
<span t-if="not channel.enroll_msg">Contact website administrator.</span>
|
||||
</t>
|
||||
<span t-if="channel.is_member" class="col-10 btn btn-primary">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
<span class="cta-title text_small_caps">Already member</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
<span class="col-5">Last Update</span>
|
||||
<span class="col"><t t-esc="channel.slide_last_update" t-options="{'widget': 'date'}"/></span>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
<span class="col-5">Members</span>
|
||||
<span class="col" t-esc="channel.members_count"/>
|
||||
</div>
|
||||
<div class="row mt8"><div class="col"><hr/></div></div>
|
||||
<t t-set="slide_type_keys" t-value="slide_types.keys()"/>
|
||||
<t t-foreach="slide_type_keys" t-as="slide_type_key">
|
||||
<div class="row mt8 align-items-center">
|
||||
<span class="col-5">
|
||||
<t t-if="search_category">
|
||||
<a t-att-href="'/slides/%s/category/%s?%s' % (slug(channel), slug(search_category), keep_query('debug', slide_type=slide_type_key))"
|
||||
t-att-class="'%s' % ('active' if search_slide_type == slide_type_key else '')">
|
||||
<t t-esc="slide_types[slide_type_key]"/>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a t-att-href="'/slides/%s?%s' % (slug(channel), keep_query('debug', slide_type=slide_type_key))"
|
||||
t-att-class="'%s' % ('active' if search_slide_type == slide_type_key else '')">
|
||||
<t t-esc="slide_types[slide_type_key]"/>
|
||||
</a>
|
||||
</t>
|
||||
</span>
|
||||
<span class="col">
|
||||
<t t-if="search_category">
|
||||
<span class="badge badge-pill badge-info" t-esc="search_category['nbr_%ss' % slide_type_key]"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge badge-pill badge-info" t-esc="channel['nbr_%ss' % slide_type_key]"/>
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<div class="row mt8"><div class="col"><hr/></div></div>
|
||||
<div class="row justify-content-center mt8">
|
||||
<button role="button" class="btn btn-primary col-10" title="Share Channel"
|
||||
aria-label="Share Channel"
|
||||
data-toggle="modal" data-target="#slideChannelShareModal">
|
||||
<i class="fa fa-share-square"></i> Share
|
||||
</button>
|
||||
<div class="modal fade" id="slideChannelShareModal" tabindex="-1" role="dialog" aria-labelledby="slideChannelShareModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="slideChannelShareModalLabel">
|
||||
Share
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span>&times;</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>Share on Social Networks</h5>
|
||||
<t t-call="website_slides.slides_share">
|
||||
<t t-set="record" t-value="channel"/>
|
||||
</t>
|
||||
<h5 class="mt16">Share Link</h5>
|
||||
<input type="text" class="form-control" readonly="readonly" onclick="this.select();"
|
||||
t-att-value="channel.website_url"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div t-if="channel.can_upload" class="row justify-content-center mt8 mb8">
|
||||
<a role="button" class="btn btn-primary oe_slide_js_upload col-10" title="Upload Presentation"
|
||||
aria-label="Upload Presentation" href="#"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-can-upload="channel.can_upload"
|
||||
t-att-data-can-publish="channel.can_publish">
|
||||
<i class="fa fa-cloud-upload"></i> Upload
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- training mode ==============================================-->
|
||||
<div t-if="channel.channel_type == 'training'" style="width:70%;" class="d-flex align-items-center ml-4 justify-content-center">
|
||||
<div class="course-content oe_js_course_slides_list" style="width: 100%">
|
||||
<ul>
|
||||
<t t-set="i" t-value="1"/>
|
||||
<t t-set="j" t-value="0"/>
|
||||
<t t-foreach="channel.category_ids" t-as="category">
|
||||
<li t-attf-class="#{'section-draggable' if user == user_id else ''}" t-attf-category_id="#{category.id}">
|
||||
<div t-attf-category_id="#{category.id}" class="course-section text-muted font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center">
|
||||
<div style="width:50%;" class="d-flex align-items-center">
|
||||
<i t-if="channel.user_id == user" class="fa fa-arrows mr-3 text-muted"></i>
|
||||
<div class="mr-2">Section <span class="section-index" t-esc="i"/>:</div>
|
||||
<span t-field="category.name"/>
|
||||
</div>
|
||||
<!-- <a t-if="channel.user_id == user" title="Add content to this section" class="mr-2 p-0 oe_slide_js_add_slide" href="#" t-attf-channel_id="#{channel.id}" t-attf-category_id="#{category.id}">+</a> -->
|
||||
<a class="mr-2 oe_slide_js_upload"
|
||||
role="button"
|
||||
aria-label="Upload Presentation"
|
||||
href="#"
|
||||
style="font-size: 1.5rem;text-decoration: none;"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-category-id="category.id"
|
||||
t-att-data-can-upload="channel.can_upload"
|
||||
t-att-data-can-publish="channel.can_publish">+</a>
|
||||
<t t-set="i" t-value="i+1"/>
|
||||
</div>
|
||||
<ul t-attf-category_id="#{category.id}" >
|
||||
<t t-foreach="category.slide_ids" t-as="slide">
|
||||
<li t-att-index="j" t-attf-slide_id="#{slide.id}" t-attf-category_id="#{category.id}" t-attf-class="#{'content-slide slide-draggable d-flex justify-content-between align-items-center p-2' if channel.user_id == user else 'content-slide d-flex justify-content-between align-items-center p-2'}">
|
||||
<div class="content-slide-infos ml-2">
|
||||
<i t-if="channel.user_id == user" class="fa fa-arrows mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'video'" class="fa fa-play-circle mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'link'" class="fa fa-file-code-o mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'webpage'" class="fa fa-file-text mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text-muted"></i>
|
||||
<i t-if="slide.slide_type == 'certification'" class="fa fa-trophy mr-2 text-muted"></i>
|
||||
<a class="link-to-slide" t-attf-href="/slides/slide/#{slug(slide)}"><span t-field="slide.name"/></a>
|
||||
</div>
|
||||
<div class="content-slide-controls mr-2">
|
||||
<i t-if="not slide.id in user_progress or not user_progress[slide.id].completed" class="check-done fa fa-check-circle text-muted mr-1"></i>
|
||||
<i t-if="slide.id in user_progress and user_progress[slide.id].completed" class="check-done text-primary fa fa-check-circle mr-1"></i>
|
||||
<a t-if="channel.user_id == user and not slide.slide_type == 'webpage'" t-attf-href="/web#id=#{slide.id}&action=#{slide_action}&model=slide.slide&view_type=form"><i class="fa fa-pencil text-muted mr-1"></i></a>
|
||||
<a t-if="channel.user_id == user and slide.slide_type == 'webpage'" t-attf-href="/slides/slide/#{slug(slide)}?enable_editor=1"><i class="fa fa-pencil text-muted mr-1"></i></a>
|
||||
<i t-if="channel.user_id == user" t-attf-slide_id="#{slide.id}" class="fa fa-trash text-muted oe_slide_js_delete_slide"></i>
|
||||
</div>
|
||||
</li>
|
||||
<t t-set="j" t-value="j+1"/>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
<div t-if="channel.user_id == user" class="content-actions">
|
||||
<a class="mr-2 oe_slide_js_upload"
|
||||
role="button"
|
||||
aria-label="Upload Presentation"
|
||||
href="#"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-can-upload="channel.can_upload"
|
||||
t-att-data-can-publish="channel.can_publish">Add Content</a>
|
||||
<a class="oe_slide_js_add_section" t-attf-channel_id="#{channel.id}" href="#">Add Section</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Channel content, aka slides (lessons in documentation mode) -->
|
||||
<div t-if="channel.channel_type == 'documentation'" class="col-8 mt24 ml32 o_wslides_channel_content_promoted">
|
||||
<div class="row mt16 align-items-center">
|
||||
<div class="col">
|
||||
<h3><i class="fa fa-plus-square"/> Featured lesson</h3>
|
||||
<hr/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" t-if="slide_promoted">
|
||||
<div class="col-4">
|
||||
<img class="img img-fluid" style=""
|
||||
t-att-src="'/slides/slide/%s/get_image?field=image_large' % slide_promoted.id"
|
||||
t-att-alt="slide_promoted.name"/>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="row">
|
||||
<h3 class="col-6" t-att-title="slide_promoted.name">
|
||||
<t t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher">
|
||||
<a t-attf-href="/slides/slide/#{slug(slide_promoted)}" class="font-weight-bold text-muted" t-field="slide_promoted.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="font-weight-bold text-muted" t-field="slide_promoted.name"/>
|
||||
</t>
|
||||
</h3>
|
||||
<div class="col-12" t-field="slide_promoted.description"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Channel content, aka slides (lessons in documentation mode) -->
|
||||
<div t-if="channel.channel_type == 'documentation'" class="col-12">
|
||||
<div class="tab-content mt16" id="slideChannelContent">
|
||||
<div class="tab-pane active" role="tabpanel" aria-labelledby="profile-tab" id="slideChannelContentAbout">
|
||||
<t t-foreach="category_data" t-as="category">
|
||||
<div class="row mt16 align-items-center">
|
||||
<div class="col">
|
||||
<a t-att-href="'/slides/%s/category/%s' % (slug(channel), category['id'])" class="float-right">
|
||||
View all
|
||||
</a>
|
||||
<h3><i class="fa fa-plus-square"/> <t t-esc="category['name']"/></h3>
|
||||
<hr/>
|
||||
</div>
|
||||
</div>
|
||||
<t t-call="website_slides.slides_grid_view">
|
||||
<t t-set="slides" t-value="category['slides']"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Embedded template: display a list of slides in a grid view -->
|
||||
<template id='slides_grid_view' name="Slides (Grid View)">
|
||||
<div class="row o_wslides_row mt8">
|
||||
<div class="col-lg-3 col-md-6 col-12" t-foreach="slides" t-as="slide">
|
||||
<div class="card o_wslides_lesson_card">
|
||||
<img class="card-img-top img-fuild" style="max-height: 160px;"
|
||||
t-att-src="'/slides/slide/%s/get_image?field=image_large' % slide.id"
|
||||
t-att-alt="slide.name"/>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" t-att-title="slide.name">
|
||||
<t t-if="slide.is_preview or channel.is_member or is_slides_publisher">
|
||||
<a t-attf-href="/slides/slide/#{slug(slide)}" t-esc="slide.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="text-muted" t-esc="slide.name"/>
|
||||
</t>
|
||||
</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">
|
||||
<t t-if="user.has_group('website.group_website_publisher')">
|
||||
<i class="fa fa-globe float-right o_wslides_slide_manage_publish"></i>
|
||||
<i class="fa fa-pencil float-right o_wslides_slide_manage_edit"></i>
|
||||
<i class="fa fa-trash float-right o_wslides_slide_manage_unlink"></i>
|
||||
</t>
|
||||
<t t-if="slide.is_preview">
|
||||
<span class="badge badge-info">Free preview</span>
|
||||
</t>
|
||||
<t t-if="not slide.is_published and user.has_group('website.group_website_publisher')">
|
||||
<span class="badge badge-danger">Unpublished</span>
|
||||
</t>
|
||||
</h6>
|
||||
<div class="card-text">
|
||||
<div class="text-muted oe_no_empty" t-field="slide.description"/>
|
||||
<div t-if="slide.tag_ids">
|
||||
<t t-foreach="slide.tag_ids" t-as="tag">
|
||||
<a t-att-href="'/slides/%s/tag/%s' % (slug(slide.channel_id), slug(tag))" class="badge badge-secondary">
|
||||
<t t-esc="tag.name"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<!-- CHECKME: like only if logged -->
|
||||
<div class="float-right text-right o_wslides_like">
|
||||
<span class="o_wslides_like_up" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
|
||||
<i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i>
|
||||
<span t-esc="slide.likes"/>
|
||||
</span>
|
||||
<span class="o_wslides_like_down" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
|
||||
<i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
|
||||
<span t-esc="slide.dislikes"/>
|
||||
</span>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<t t-call="website_slides.slides_misc_float_time">
|
||||
<t t-set="value" t-value="slide.completion_time"/>
|
||||
</t>
|
||||
<!-- <t t-esc="slide.total_views"/> Views
|
||||
<timeago class="timeago" t-att-datetime="slide.create_date"></timeago> -->
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<odoo><data>
|
||||
|
||||
<!-- Slide: main template: detailed view -->
|
||||
<template id="slide_edit_options" inherit_id="website.user_navbar" name="Edit Slide Options">
|
||||
<xpath expr="//li[@id='edit-page-menu']" position="after">
|
||||
<t t-if="main_object._name == 'slide.slide'" t-set="action" t-value="'website_slides.action_slides_slides'" />
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="slide_detail_view" name="Slide Detailed View">
|
||||
<t t-call="website.layout">
|
||||
<t t-set="main_object" t-value="slide"/>
|
||||
@@ -1004,19 +557,6 @@
|
||||
</li>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- Slide sub-template: share: social media -->
|
||||
<template id='slide_social_media' name="Share on Social Networks">
|
||||
<h4 class="mt0">Share on Social Networks</h4>
|
||||
<t t-call="website_slides.slides_share">
|
||||
<t t-set="record" t-value="slide"/>
|
||||
</t>
|
||||
<h4 class="mt-3">Share Link</h4>
|
||||
<input type="text" class="form-control" t-att-value="slide.website_url" readonly="readonly" onClick="this.select();"/>
|
||||
<span class="form-text">Use permanent link to share in social media</span>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- Slide sub-template: share: send by email -->
|
||||
<template id='slide_social_email' name="Share by Email">
|
||||
<h4 class="mt-3">Share with a friend</h4>
|
||||
@@ -1057,15 +597,4 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- User Navbar -->
|
||||
<template id="user_navbar_inherit_website_slides" inherit_id="website.user_navbar">
|
||||
<xpath expr="//div[@id='o_new_content_menu_choices']//div[@name='module_website_slides']" position="attributes">
|
||||
<attribute name="name"/>
|
||||
<attribute name="t-att-data-module-id"/>
|
||||
<attribute name="t-att-data-module-shortdesc"/>
|
||||
<attribute name="groups">website.group_website_designer</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
</data></odoo>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo><data>
|
||||
|
||||
<!-- Share on social networks -->
|
||||
<template id='slides_share' name="Slides Media Share">
|
||||
<div>
|
||||
<a t-attf-href="https://www.facebook.com/sharer/sharer.php?u=#{record.website_url}" class="o_slides_social_share" social-key="facebook" aria-label="Share on Facebook" title="Share on Facebook"><i class="fa fa-facebook-square fa-2x"/></a>
|
||||
<a t-attf-href="https://twitter.com/intent/tweet?text=#{record.name}&url=#{record.website_url}" class="o_slides_social_share" social-key="twitter" aria-label="Share on Twitter" title="Share on Twitter"><i class="fa fa-twitter fa-2x"/></a>
|
||||
<a t-attf-href="http://www.linkedin.com/shareArticle?mini=true&url=#{record.website_url}&title=#{record.name}&" social-key="linkedin" class="o_slides_social_share" aria-label="Share on LinkedIn" title="Share on LinkedIn"><i class="fa fa-linkedin fa-2x"/></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Share: social media -->
|
||||
<template id='slide_social_media' name="Share on Social Networks">
|
||||
<h4 class="mt0">Share on Social Networks</h4>
|
||||
<t t-call="website_slides.slides_share">
|
||||
<t t-set="record" t-value="slide"/>
|
||||
</t>
|
||||
<h4 class="mt-3">Share Link</h4>
|
||||
<input type="text" class="form-control" t-att-value="slide.website_url" readonly="readonly" onClick="this.select();"/>
|
||||
<span class="form-text">Use permanent link to share in social media</span>
|
||||
</template>
|
||||
|
||||
<!-- Website edit page -->
|
||||
<template id="slide_edit_options" inherit_id="website.user_navbar" name="Edit Slide Options">
|
||||
<xpath expr="//li[@id='edit-page-menu']" position="after">
|
||||
<t t-if="main_object._name == 'slide.slide'" t-set="action" t-value="'website_slides.action_slides_slides'" />
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- User Navbar -->
|
||||
<template id="user_navbar_inherit_website_slides" inherit_id="website.user_navbar">
|
||||
<xpath expr="//div[@id='o_new_content_menu_choices']//div[@name='module_website_slides']" position="attributes">
|
||||
<attribute name="name"/>
|
||||
<attribute name="t-att-data-module-id"/>
|
||||
<attribute name="t-att-data-module-shortdesc"/>
|
||||
<attribute name="groups">website.group_website_designer</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</data></odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import slide_channel_invite
|
||||
@@ -7,26 +7,10 @@ from odoo import fields, models
|
||||
class Channel(models.Model):
|
||||
_inherit = 'slide.channel'
|
||||
|
||||
nbr_certifications = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True)
|
||||
|
||||
def _compute_slides_statistics_type(self, read_group_res):
|
||||
result = super(Channel, self)._compute_slides_statistics_type(read_group_res)
|
||||
for cid in result:
|
||||
result[cid]['nbr_certifications'] = 0
|
||||
for res_group in read_group_res:
|
||||
cid = res_group['channel_id'][0]
|
||||
result[cid]['nbr_certifications'] += res_group.get('slide_type', '') == 'certification' and res_group['__count'] or 0
|
||||
return result
|
||||
nbr_certification = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True)
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
_inherit = 'slide.category'
|
||||
|
||||
nbr_certifications = fields.Integer("Number of Certifications", compute='_count_presentations', store=True)
|
||||
|
||||
def _extract_count_presentations_type(self, result, record_id):
|
||||
statistics = super(Category, self)._extract_count_presentations_type(result, record_id)
|
||||
statistics['nbr_certifications'] = result[record_id].get('certification', 0)
|
||||
statistics['total_slides'] += statistics['nbr_certifications']
|
||||
|
||||
return statistics
|
||||
nbr_certification = fields.Integer("Number of Certifications", compute='_count_presentations', store=True)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<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_certifications"/>
|
||||
<field name="nbr_certification"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||