[MERGE] website_slides: ease mailing of members having finished courses
PURPOSE Before this commit, it was possible to send an email to all attendees only. After this commit, it will be possible to send an email to attendees that completed the course. SPECIFICATION Adds a searchable o2m field in the res.partner model to find all the completed courses linked to that partner. Adds a new 'Completion Email' template field in the slide.channel model that will be sent automatically after an attendee completes a course. LINKS Task ID-2216352 PR #49592 Upgrade PR #1084 Related: odoo/upgrade#1084 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -5,16 +5,11 @@
|
||||
<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_done_members']" position="before">
|
||||
<button class="oe_stat_button" type="object"
|
||||
name="action_mass_mailing_attendees"
|
||||
icon="fa-envelope-o"
|
||||
attrs="{'invisible': [('members_count', '=', 0)]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Mail Attendees</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
<button name="action_channel_invite" position="after">
|
||||
<button name="action_mass_mailing_attendees" string="Contact Attendees" type="object"
|
||||
class="oe_highlight" attrs="{'invisible': [('members_count', '=', 0)]}"
|
||||
groups="mass_mailing.group_mass_mailing_user"/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -23,10 +18,13 @@
|
||||
<field name="model">slide.channel</field>
|
||||
<field name="inherit_id" ref="website_slides.slide_channel_view_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//a[@name='action_channel_invite']" position="after">
|
||||
<a class="dropdown-item" name="action_mass_mailing_attendees" role="menuitem" type="object">
|
||||
Mail Attendees
|
||||
</a>
|
||||
<xpath expr="//div[@name='action_channel_invite']" position="after">
|
||||
<div role="menuitem">
|
||||
<a name="action_mass_mailing_attendees" type="object"
|
||||
groups="mass_mailing.group_mass_mailing_user">
|
||||
Contact Attendees
|
||||
</a>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'eLearning',
|
||||
'version': '2.2',
|
||||
'version': '2.3',
|
||||
'sequence': 125,
|
||||
'summary': 'Manage and publish an eLearning platform',
|
||||
'website': 'https://www.odoo.com/page/slides',
|
||||
|
||||
@@ -63,6 +63,38 @@
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Completed Channel Message -->
|
||||
<record id="mail_template_channel_completed" model="mail.template">
|
||||
<field name="name">Completed Course</field>
|
||||
<field name="model_id" ref="model_slide_channel_partner"/>
|
||||
<field name="subject">Congratulation! You completed ${object.channel_id.name}</field>
|
||||
<field name="email_from">${(object.channel_id.user_id.email_formatted or object.channel_id.user_id.company_id.catchall_formatted) | safe}</field>
|
||||
<field name="partner_to">${object.partner_id.id}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
<p style="margin: 0px;">Hello ${object.partner_id.name},</p><br/>
|
||||
<p><b>Congratulations!</b></p>
|
||||
<p>You've completed the course <b>${object.channel_id.name}</b></p>
|
||||
<p>Check out the other available courses.</p><br/>
|
||||
|
||||
<div style="margin: 16px 8px 16px 8px; text-align: center;">
|
||||
<a href="/slides/all" style="background-color: #875a7b; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px;">
|
||||
Explore courses
|
||||
</a>
|
||||
</div>
|
||||
Enjoy this exclusive content!
|
||||
% if object.channel_id.user_id.signature
|
||||
<br />
|
||||
${object.channel_id.user_id.signature | safe}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="lang">${object.partner_id.lang}</field>
|
||||
</record>
|
||||
|
||||
<!-- Channel subtypes -->
|
||||
<record id="mt_channel_slide_published" model="mail.message.subtype">
|
||||
<field name="name">Presentation Published</field>
|
||||
|
||||
@@ -9,9 +9,32 @@ class ResPartner(models.Model):
|
||||
|
||||
slide_channel_ids = fields.Many2many(
|
||||
'slide.channel', 'slide_channel_partner', 'partner_id', 'channel_id',
|
||||
string='eLearning Courses')
|
||||
slide_channel_count = fields.Integer('Course Count', compute='_compute_slide_channel_count')
|
||||
slide_channel_company_count = fields.Integer('Company Course Count', compute='_compute_slide_channel_company_count')
|
||||
string='eLearning Courses', groups="website_slides.group_website_slides_officer")
|
||||
slide_channel_completed_ids = fields.One2many(
|
||||
'slide.channel', string='Completed Courses',
|
||||
compute='_compute_slide_channel_completed_ids',
|
||||
search='_search_slide_channel_completed_ids',
|
||||
groups="website_slides.group_website_slides_officer")
|
||||
slide_channel_count = fields.Integer(
|
||||
'Course Count', compute='_compute_slide_channel_count',
|
||||
groups="website_slides.group_website_slides_officer")
|
||||
slide_channel_company_count = fields.Integer(
|
||||
'Company Course Count', compute='_compute_slide_channel_company_count',
|
||||
groups="website_slides.group_website_slides_officer")
|
||||
|
||||
def _compute_slide_channel_completed_ids(self):
|
||||
for partner in self:
|
||||
partner.slide_channel_completed_ids = self.env['slide.channel.partner'].search([
|
||||
('partner_id', '=', partner.id),
|
||||
('completed', '=', True)
|
||||
]).mapped('channel_id')
|
||||
|
||||
def _search_slide_channel_completed_ids(self, operator, value):
|
||||
cp_done = self.env['slide.channel.partner'].sudo().search([
|
||||
('channel_id', operator, value),
|
||||
('completed', '=', True)
|
||||
])
|
||||
return [('id', 'in', cp_done.partner_id.ids)]
|
||||
|
||||
@api.depends('is_company')
|
||||
def _compute_slide_channel_count(self):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -11,6 +12,8 @@ from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.osv import expression
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChannelUsersRelation(models.Model):
|
||||
_name = 'slide.channel.partner'
|
||||
@@ -38,21 +41,16 @@ class ChannelUsersRelation(models.Model):
|
||||
mapped_data.setdefault(item['channel_id'][0], dict())
|
||||
mapped_data[item['channel_id'][0]][item['partner_id'][0]] = item['__count']
|
||||
|
||||
partner_karma = dict.fromkeys(self.mapped('partner_id').ids, 0)
|
||||
completed_records = self.env['slide.channel.partner']
|
||||
for record in self:
|
||||
record.completed_slides_count = mapped_data.get(record.channel_id.id, dict()).get(record.partner_id.id, 0)
|
||||
record.completion = 100.0 if record.completed else round(100.0 * record.completed_slides_count / (record.channel_id.total_slides or 1))
|
||||
if not record.completed and record.completed_slides_count >= record.channel_id.total_slides:
|
||||
record.completed = True
|
||||
partner_karma[record.partner_id.id] += record.channel_id.karma_gen_channel_finish
|
||||
completed_records += record
|
||||
|
||||
partner_karma = {partner_id: karma_to_add
|
||||
for partner_id, karma_to_add in partner_karma.items() if karma_to_add > 0}
|
||||
|
||||
if partner_karma:
|
||||
users = self.env['res.users'].sudo().search([('partner_id', 'in', list(partner_karma.keys()))])
|
||||
for user in users:
|
||||
users.add_karma(partner_karma[user.partner_id.id])
|
||||
if completed_records:
|
||||
completed_records._set_as_completed()
|
||||
completed_records._send_completed_mail()
|
||||
|
||||
def unlink(self):
|
||||
"""
|
||||
@@ -71,6 +69,58 @@ class ChannelUsersRelation(models.Model):
|
||||
self.env['slide.slide.partner'].search(removed_slide_partner_domain).unlink()
|
||||
return super(ChannelUsersRelation, self).unlink()
|
||||
|
||||
def _set_as_completed(self):
|
||||
""" Set record as completed and compute karma gains """
|
||||
partner_karma = dict.fromkeys(self.mapped('partner_id').ids, 0)
|
||||
for record in self:
|
||||
record.completed = True
|
||||
partner_karma[record.partner_id.id] += record.channel_id.karma_gen_channel_finish
|
||||
|
||||
partner_karma = {
|
||||
partner_id: karma_to_add
|
||||
for partner_id, karma_to_add in partner_karma.items() if karma_to_add > 0
|
||||
}
|
||||
|
||||
if partner_karma:
|
||||
users = self.env['res.users'].sudo().search([('partner_id', 'in', list(partner_karma.keys()))])
|
||||
for user in users:
|
||||
users.add_karma(partner_karma[user.partner_id.id])
|
||||
|
||||
def _send_completed_mail(self):
|
||||
""" Send an email to the attendee when he has successfully completed a course. """
|
||||
template_to_records = dict()
|
||||
for record in self:
|
||||
template = record.channel_id.completed_template_id
|
||||
if template:
|
||||
records = template_to_records.setdefault(template, self.env['slide.channel.partner'])
|
||||
records += record
|
||||
|
||||
for template, records in template_to_records.items():
|
||||
record_email_values = template.generate_email(self.ids, ['subject', 'body_html', 'email_from', 'partner_to'])
|
||||
|
||||
mail_mail_values = []
|
||||
for record in self:
|
||||
email_values = record_email_values.get(record.id)
|
||||
if not email_values or not email_values.get('partner_ids'):
|
||||
continue
|
||||
|
||||
email_values.update(
|
||||
author_id=self.channel_id.user_id.partner_id.id or self.env.company.partner_id.id,
|
||||
auto_delete=True,
|
||||
recipient_ids=[(4, pid) for pid in email_values['partner_ids']],
|
||||
)
|
||||
email_values['body_html'] = template._render_encapsulate(
|
||||
'mail.mail_notification_light', email_values['body_html'],
|
||||
add_context={
|
||||
'message': self.env['mail.message'].sudo().new(dict(body=email_values['body_html'], record_name=record.channel_id.name)),
|
||||
'model_description': _('Completed Course') # tde fixme: translate into partner lang
|
||||
}
|
||||
)
|
||||
mail_mail_values.append(email_values)
|
||||
|
||||
if mail_mail_values:
|
||||
self.env['mail.mail'].sudo().create(mail_mail_values)
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
""" A channel is a container of slides. """
|
||||
@@ -146,12 +196,15 @@ class Channel(models.Model):
|
||||
" * post comment and review on training course;")
|
||||
publish_template_id = fields.Many2one(
|
||||
'mail.template', string='New Content Email',
|
||||
help="Email template to send slide publication through email",
|
||||
help="Email attendees once a new content is published",
|
||||
default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_published'))
|
||||
share_template_id = fields.Many2one(
|
||||
'mail.template', string='Share Template',
|
||||
help="Email template used when sharing a slide",
|
||||
default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.slide_template_shared'))
|
||||
completed_template_id = fields.Many2one(
|
||||
'mail.template', string='Completion Email', help="Email attendees once they've finished the course",
|
||||
default=lambda self: self.env['ir.model.data'].xmlid_to_res_id('website_slides.mail_template_channel_completed'))
|
||||
enroll = fields.Selection([
|
||||
('public', 'Public'), ('invite', 'On Invitation')],
|
||||
default='public', string='Enroll Policy', required=True,
|
||||
|
||||
@@ -7,6 +7,11 @@ $o-kanban-large-record-width: 400px;
|
||||
.o_kanban_record {
|
||||
width: $o-kanban-large-record-width;
|
||||
}
|
||||
.o_kanban_slides_card_manage_pane {
|
||||
> div {
|
||||
@include o-kanban-dashboard-dropdown-link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.o_section_list_view tr.o_data_row.o_is_section {
|
||||
|
||||
@@ -13,13 +13,6 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="slide_channel_partner_action" model="ir.actions.act_window">
|
||||
<field name="name">Attendees</field>
|
||||
<field name="res_model">slide.channel.partner</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="search_view_id" ref="website_slides.slide_channel_partner_view_search"/>
|
||||
</record>
|
||||
|
||||
<record id="slide_channel_partner_view_tree" model="ir.ui.view">
|
||||
<field name="name">slide.channel.partner.tree</field>
|
||||
<field name="model">slide.channel.partner</field>
|
||||
@@ -39,6 +32,7 @@
|
||||
<field name="name">Attendees</field>
|
||||
<field name="res_model">slide.channel.partner</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="website_slides.slide_channel_partner_view_search"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -102,8 +102,9 @@
|
||||
<group>
|
||||
<group name="communication" string="Communication">
|
||||
<field string="Allow Rating" name="allow_comment"/>
|
||||
<field name="publish_template_id" domain="[('model','=','slide.slide')]" groups="base.group_no_one"/>
|
||||
<field name="publish_template_id" domain="[('model','=','slide.slide')]"/>
|
||||
<field name="share_template_id" domain="[('model','=','slide.slide')]" groups="base.group_no_one"/>
|
||||
<field name="completed_template_id"/>
|
||||
</group>
|
||||
<group name="display" string="Display">
|
||||
<field name="visibility" widget="radio"/>
|
||||
@@ -214,20 +215,26 @@
|
||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||
<span class="fa fa-ellipsis-v" aria-hidden="false"/>
|
||||
</a>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<ul class="oe_kanban_colorpicker" data-field="color"/>
|
||||
<t t-if="widget.deletable">
|
||||
<a class="dropdown-item" role="menuitem" type="delete">Delete</a>
|
||||
</t>
|
||||
<a class="dropdown-item" role="menuitem" type="edit">
|
||||
Edit
|
||||
</a>
|
||||
<a class="dropdown-item" name="action_view_slides" role="menuitem" type="object">
|
||||
Lessons
|
||||
</a>
|
||||
<a class="dropdown-item" name="action_channel_invite" role="menuitem" type="object">
|
||||
Invite
|
||||
</a>
|
||||
<div class="o_kanban_card_manage_pane dropdown-menu" role="menu">
|
||||
<div role="menuitem" aria-haspopup="true" class="o_no_padding_kanban_colorpicker">
|
||||
<ul class="oe_kanban_colorpicker" data-field="color" role="popup"/>
|
||||
</div>
|
||||
<div class="o_kanban_slides_card_manage_pane">
|
||||
<t t-if="widget.deletable">
|
||||
<div role="menuitem">
|
||||
<a class="dropdown-item" type="delete">Delete</a>
|
||||
</div>
|
||||
</t>
|
||||
<div role="menuitem">
|
||||
<a type="edit">Edit</a>
|
||||
</div>
|
||||
<div role="menuitem">
|
||||
<a name="action_view_slides" type="object">Lessons</a>
|
||||
</div>
|
||||
<div role="menuitem" name="action_channel_invite">
|
||||
<a name="action_channel_invite" type="object">Invite</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_card_header">
|
||||
|
||||
Reference in New Issue
Block a user