[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:
Odoo's Mergebot
2020-08-24 17:22:37 +02:00
committed by GitHub
8 changed files with 163 additions and 51 deletions
@@ -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 -1
View File
@@ -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',
+32
View File
@@ -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>
+26 -3
View File
@@ -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):
+64 -11
View File
@@ -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">