[REM] website_mail_channel: remove the module "website_mail_channel"

Rationale
=========

Currently, mail channels have 2 modes
- They can be used like Discuss channel (chat, livechat, group)
- They can be used like a Mailing List (with the "email_send" field)

The mix of both feature in the same model cause some code complexity.
Several fields are not used in both cases (moderation related field)
and the future "Discord like" Discuss will even push the mail channel
further than the usage of the mailing list.

Because of all those reasons, we want to split the 2 mains features of
the mail channels into 2 different modules and models.

Purpose
=======

Remove the module "website_mail_channel" because we will merge it into
a new one, "mail_group" and "website_mail_group".

This new module "website_mail_group" will contains the website snippets
but this snippets will only work the the new <mail.group> model and
not with <mail.channel> like before.

Links
=====

Task-2510267
See odoo/odoo/pull/71599
See odoo/enterprise/pull/19296
See odoo/upgrade/pull/2600
This commit is contained in:
std-odoo
2021-07-09 11:16:38 +00:00
committed by Thibault Delavallée
parent 9183a86fde
commit 583cc5d439
20 changed files with 0 additions and 1271 deletions
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Website Mail Channels',
'category': 'Website/Website',
'summary': 'Allow visitors to join public mail channels',
'description': """
Visitors can join public mail channels managed in the Discuss app in order to get regular updates or reach out with your community.
""",
'depends': ['website_mail'],
'data': [
'data/mail_template_data.xml',
'views/snippets/s_channel.xml',
'views/snippets/snippets.xml',
'views/website_mail_channel_templates.xml',
],
'assets': {
'web.assets_frontend': [
'website_mail_channel/static/src/css/website_mail_channel.css',
'website_mail_channel/static/src/js/website_mail_channel.js',
],
'website.assets_wysiwyg': [
'website_mail_channel/static/src/snippets/s_channel/options.js',
],
}
}
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
@@ -1,279 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug
from datetime import datetime
from dateutil import relativedelta
from odoo import http, fields, tools, _
from odoo.http import request
from odoo.addons.http_routing.models.ir_http import slug
class MailGroup(http.Controller):
_thread_per_page = 20
_replies_per_page = 10
def _get_archives(self, group_id):
MailMessage = request.env['mail.message']
# use sudo to avoid side-effects due to custom ACLs
groups = MailMessage.sudo()._read_group_raw(
[('model', '=', 'mail.channel'), ('res_id', '=', group_id), ('message_type', '!=', 'notification')],
['subject', 'date'],
groupby=["date"], orderby="date desc")
for group in groups:
(r, label) = group['date']
start, end = r.split('/')
group['date'] = label
group['date_begin'] = self._to_date(start)
group['date_end'] = self._to_date(end)
return groups
def _to_date(self, dt):
""" date is (of course) a datetime so start and end are datetime
strings, but we just want date strings
"""
return (datetime
.strptime(dt, tools.DEFAULT_SERVER_DATETIME_FORMAT)
.date() # may be unnecessary?
.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
@http.route("/groups", type='http', auth="public", website=True, sitemap=True)
def view(self, **post):
groups = request.env['mail.channel'].search([('alias_id.alias_name', '!=', False)])
# compute statistics
month_date = datetime.today() - relativedelta.relativedelta(months=1)
# use sudo to avoid side-effects due to custom ACLs
messages = request.env['mail.message'].sudo().read_group([
('model', '=', 'mail.channel'),
('date', '>=', fields.Datetime.to_string(month_date)),
('message_type', '!=', 'notification'),
('res_id', 'in', groups.ids),
], ['res_id'], ['res_id'])
message_data = dict((message['res_id'], message['res_id_count']) for message in messages)
group_data = dict(
(group.id, {'monthly_message_nbr': message_data.get(group.id, 0),
'members_count': len(group.channel_partner_ids)})
for group in groups.sudo())
return request.render('website_mail_channel.mail_channels', {'groups': groups, 'group_data': group_data})
@http.route(["/groups/is_member"], type='json', auth="public", website=True)
def is_member(self, channel_id=0, **kw):
""" Determine if the current user is member of the given channel_id
:param channel_id : the channel_id to check
"""
current_user = request.env.user
session_partner_id = request.session.get('partner_id')
public_user = request.website.user_id
partner = None
# find the current partner
if current_user != public_user:
partner = current_user.partner_id
elif session_partner_id:
partner = request.env['res.partner'].sudo().browse(session_partner_id)
values = {
'is_user': current_user != public_user,
'email': partner.email if partner else "",
'is_member': False,
'alias_name': False,
}
# check if the current partner is member or not
channel = request.env['mail.channel'].browse(int(channel_id))
if channel.exists() and partner is not None:
values['is_member'] = bool(partner in channel.sudo().channel_partner_ids)
return values
@http.route(["/groups/subscription"], type='json', auth="public", website=True)
def subscription(self, channel_id=0, subscription="on", email='', **kw):
""" Subscribe to a mailing list : this will create a partner with its email address (if public user not
registered yet) and add it as channel member
:param channel_id : the channel id to join/quit
:param subscription : 'on' to unsubscribe the user, 'off' to subscribe
"""
unsubscribe = subscription == 'on'
channel = request.env['mail.channel'].browse(int(channel_id))
if not channel.exists():
return False
partner_ids = []
# search partner_id
if request.env.user != request.website.user_id:
# connected users are directly (un)subscribed
partner_ids = request.env.user.partner_id.ids
# add or remove channel members
if unsubscribe:
channel.check_access_rule('read')
channel.sudo().write({'channel_partner_ids': [(3, partner_id) for partner_id in partner_ids]})
return "off"
else: # add partner to the channel
request.session['partner_id'] = partner_ids[0]
channel.check_access_rule('read')
channel.sudo().write({'channel_partner_ids': [(4, partner_id) for partner_id in partner_ids]})
return "on"
else:
# public users will recieve confirmation email
partner_ids = [p.id for p in request.env['mail.thread'].sudo()._mail_find_partner_from_emails([email], records=channel.sudo()) if p]
if not partner_ids or not partner_ids[0]:
name = email.split('@')[0]
partner_ids = [request.env['res.partner'].sudo().create({'name': name, 'email': email}).id]
channel.sudo()._send_confirmation_email(partner_ids, unsubscribe)
return "email"
@http.route([
'''/groups/<model('mail.channel', "[('channel_type', '=', 'channel')]"):group>''',
'''/groups/<model('mail.channel'):group>/page/<int:page>'''
], type='http', auth="public", website=True, sitemap=True)
def thread_headers(self, group, page=1, mode='thread', date_begin=None, date_end=None, **post):
if group.channel_type != 'channel':
raise werkzeug.exceptions.NotFound()
Message = request.env['mail.message']
domain = [('model', '=', 'mail.channel'), ('res_id', '=', group.id), ('message_type', '!=', 'notification')]
if mode == 'thread':
domain += [('parent_id', '=', False)]
if date_begin and date_end:
domain += [('date', '>=', date_begin), ('date', '<=', date_end)]
pager = request.website.pager(
url='/groups/%s' % slug(group),
total=Message.search_count(domain),
page=page,
step=self._thread_per_page,
url_args={'mode': mode, 'date_begin': date_begin or '', 'date_end': date_end or ''},
)
messages = Message.search(domain, limit=self._thread_per_page, offset=pager['offset'])
values = {
'messages': messages,
'group': group,
'pager': pager,
'mode': mode,
'archives': self._get_archives(group.id),
'date_begin': date_begin,
'date_end': date_end,
'replies_per_page': self._replies_per_page,
}
return request.render('website_mail_channel.group_messages', values)
@http.route([
'''/groups/<model('mail.channel', "[('channel_type', '=', 'channel')]"):group>/<model('mail.message', "[('model','=','mail.channel'), ('res_id','=',group.id)]"):message>''',
], type='http', auth="public", website=True, sitemap=True)
def thread_discussion(self, group, message, mode='thread', date_begin=None, date_end=None, **post):
if group.channel_type != 'channel':
raise werkzeug.exceptions.NotFound()
Message = request.env['mail.message']
if mode == 'thread':
base_domain = [('model', '=', 'mail.channel'), ('res_id', '=', group.id), ('parent_id', '=', message.parent_id and message.parent_id.id or False)]
else:
base_domain = [('model', '=', 'mail.channel'), ('res_id', '=', group.id)]
next_message = Message.search(base_domain + [('date', '<', message.date)], order="date DESC", limit=1) or None
prev_message = Message.search(base_domain + [('date', '>', message.date)], order="date", limit=1) or None
# handle access to attachments for public / portal
if request.env.user.has_group('base.group_user'):
attachments = message.attachment_ids
else:
# portal / external people cannot access attachments -> grant access
# as message is ok, generate access tokens
attachments = message.sudo().attachment_ids
attachments.generate_access_token()
values = {
'message': message,
'attachments': attachments,
'group': group,
'mode': mode,
'archives': self._get_archives(group.id),
'date_begin': date_begin,
'date_end': date_end,
'replies_per_page': self._replies_per_page,
'next_message': next_message,
'prev_message': prev_message,
}
return request.render('website_mail_channel.group_message', values)
@http.route(
'''/groups/<model('mail.channel', "[('channel_type', '=', 'channel')]"):group>/<model('mail.message', "[('model','=','mail.channel'), ('res_id','=',group.id)]"):message>/get_replies''',
type='json', auth="public", methods=['POST'], website=True)
def render_messages(self, group, message, **post):
if group.channel_type != 'channel':
return False
last_displayed_id = post.get('last_displayed_id')
if not last_displayed_id:
return False
replies_domain = [('id', '<', int(last_displayed_id)), ('parent_id', '=', message.id)]
messages = request.env['mail.message'].search(replies_domain, limit=self._replies_per_page)
message_count = request.env['mail.message'].search_count(replies_domain)
values = {
'group': group,
'thread_header': message,
'messages': messages,
'msg_more_count': message_count - self._replies_per_page,
'replies_per_page': self._replies_per_page,
}
return request.env.ref('website_mail_channel.messages_short')._render(values, engine='ir.qweb')
@http.route("/groups/<int:group_id>/get_alias_info", type='json', auth='public', website=True)
def get_alias_info(self, group_id, **post):
group = request.env['mail.channel'].search([('id', '=', group_id)])
if not group: # doesn't exist or doesn't have the right to access it
return {}
return {
'alias_name': group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain and '%s@%s' % (group.alias_id.alias_name, group.alias_id.alias_domain) or False
}
@http.route("/groups/subscribe/<model('mail.channel'):channel>/<int:partner_id>/<string:token>", type='http', auth='public', website=True)
def confirm_subscribe(self, channel, partner_id, token, **kw):
subscriber = request.env['mail.channel.partner'].search([('channel_id', '=', channel.id), ('partner_id', '=', partner_id)])
if subscriber:
# already registered, maybe clicked twice
return request.render('website_mail_channel.invalid_token_subscription')
subscriber_token = channel._generate_action_token(partner_id, action='subscribe')
if token != subscriber_token:
return request.render('website_mail_channel.invalid_token_subscription')
# add partner
channel.sudo().write({'channel_partner_ids': [(4, partner_id)]})
return request.render("website_mail_channel.confirmation_subscription", {'subscribing': True})
@http.route("/groups/unsubscribe/<model('mail.channel'):channel>/<int:partner_id>/<string:token>", type='http', auth='public', website=True)
def confirm_unsubscribe(self, channel, partner_id, token, **kw):
subscriber = request.env['mail.channel.partner'].search([('channel_id', '=', channel.id), ('partner_id', '=', partner_id)])
if not subscriber:
partner = request.env['res.partner'].browse(partner_id).sudo().exists()
# FIXME: remove try/except in master
try:
response = request.render(
'website_mail_channel.not_subscribed',
{'partner_id': partner})
# make sure the rendering (and thus error if template is
# missing) happens inside the try block
response.flatten()
return response
except ValueError:
return _("The address %s is already unsubscribed or was never subscribed to any mailing list") % (
partner.email
)
subscriber_token = channel._generate_action_token(partner_id, action='unsubscribe')
if token != subscriber_token:
return request.render('website_mail_channel.invalid_token_subscription')
# remove partner
channel.sudo().write({'channel_partner_ids': [(3, partner_id)]})
return request.render("website_mail_channel.confirmation_subscription", {'subscribing': False})
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="mail_template_list_subscribe" model="mail.template">
<field name="name">Channel: Mailing list subscription</field>
<field name="model_id" ref="mail.model_mail_channel"/>
<field name="subject">Confirm subscription to ${object.name}</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
Hello,<br/><br/>
You have requested to be subscribed to the mailing list <strong>${object.name}</strong>.
<br/><br/>
To confirm, please visit the following link: <strong><a href="${ctx['token_url']}">${ctx['token_url']}</a></strong>
<br/><br/>
If this was a mistake or you did not requested this action, please ignore this message.
% if user.signature
<br/>
${user.signature | safe}
% endif
</div></field>
<field name="auto_delete" eval="True"/>
</record>
<record id="mail_template_list_unsubscribe" model="mail.template">
<field name="name">Channel: Mailing list unsubscription</field>
<field name="model_id" ref="mail.model_mail_channel"/>
<field name="subject">Confirm unsubscription to ${object.name}</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
Hello,<br/><br/>
You have requested to be unsubscribed to the mailing list <strong>${object.name}</strong>.
<br/><br/>
To confirm, please visit the following link: <strong><a href="${ctx['token_url']}">${ctx['token_url']}</a></strong>.
<br/><br/>
If this was a mistake or you did not requested this action, please ignore this message.
% if user.signature:
<br/>
${user.signature | safe}
% endif
</div></field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail_channel
from . import mail_mail
from . import website
@@ -1,63 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug import urls
from odoo import models, tools
from odoo.addons.http_routing.models.ir_http import slug
class MailGroup(models.Model):
_inherit = 'mail.channel'
def _notify_email_header_dict(self):
headers = super(MailGroup, self)._notify_email_header_dict()
base_url = self.get_base_url()
headers['List-Archive'] = '<%s/groups/%s>' % (base_url, slug(self))
headers['List-Subscribe'] = '<%s/groups>' % (base_url)
headers['List-Unsubscribe'] = '<%s/groups?unsubscribe>' % (base_url,)
return headers
def _send_confirmation_email(self, partner_ids, unsubscribe=False):
website = self.env['website'].get_current_website()
base_url = website.get_base_url()
route = "/groups/%(action)s/%(channel)s/%(partner)s/%(token)s"
if unsubscribe:
template = self.env.ref('website_mail_channel.mail_template_list_unsubscribe')
action = 'unsubscribe'
else:
template = self.env.ref('website_mail_channel.mail_template_list_subscribe')
action = 'subscribe'
for partner_id in partner_ids:
# generate a new token per subscriber
token = self._generate_action_token(partner_id, action=action)
token_url = urls.url_join(base_url, route % {
'action': action,
'channel': self.id,
'partner': partner_id,
'token': token,
})
template.with_context(token_url=token_url).send_mail(
self.id,
force_send=True,
email_values={
'recipient_ids': [(4, partner_id)],
'email_from': website.company_id.email_formatted,
'author_id': self.create_uid.partner_id.id,
'message_type': 'user_notification',
},
notif_layout='mail.mail_notification_light',
)
return True
def _generate_action_token(self, partner_id, action='unsubscribe'):
self.ensure_one()
data = '$'.join([
str(self.id),
str(partner_id),
action])
return tools.hmac(self.env(su=True), 'website_mail_channel-email-subscription', data)
@@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, tools, _
from odoo.addons.http_routing.models.ir_http import slug
class MailMail(models.Model):
_inherit = 'mail.mail'
def _send_prepare_body(self):
""" Short-circuit parent method for mail groups, replace the default
footer with one appropriate for mailing-lists."""
if self.model == 'mail.channel' and self.res_id:
# no super() call on purpose, no private links that could be quoted!
channel = self.env['mail.channel'].browse(self.res_id)
base_url = channel.get_base_url()
vals = {
'maillist': _('Mailing-List'),
'post_to': _('Post to'),
'unsub': _('Unsubscribe'),
'mailto': 'mailto:%s@%s' % (channel.alias_name, channel.alias_domain),
'group_url': '%s/groups/%s' % (base_url, slug(channel)),
'unsub_url': '%s/groups?unsubscribe' % (base_url,),
}
footer = """_______________________________________________
%(maillist)s: %(group_url)s
%(post_to)s: %(mailto)s
%(unsub)s: %(unsub_url)s
""" % vals
body = tools.append_content_to_html(self.body, footer, container_tag='div')
return body
else:
return super(MailMail, self)._send_prepare_body()
@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
from odoo.addons.http_routing.models.ir_http import url_for
class Website(models.Model):
_inherit = "website"
def get_suggested_controllers(self):
suggested_controllers = super(Website, self).get_suggested_controllers()
suggested_controllers.append((_('Mailing Lists'), url_for('/groups'), 'website_mail_channel'))
return suggested_controllers
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#CD7690"/><stop offset="100%" stop-color="#CA5377"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M42.474 69H4c-2 0-4-.146-4-4.075V39.178L21 15h30l-2 6.113h3l-3 5.095h6l-5 10.188 4.213 1.034L53 54.736 42.474 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" d="M15 36.646h12.813l2.562 3.416h10.25l3.417-3.416H56l-2.563 19.646H17.563L15 36.646zm.854-10.25h39.292v7.687h-5.98v-3.416H20.98v3.416h-5.125v-7.687zm2.563-5.125h34.166v3.416H18.417v-3.416zM20.979 17h29.896v2.563H20.979V17z" opacity=".3"/><path fill="#FFF" d="M15 34.646h12.813l2.562 3.416h10.25l3.417-3.416H56l-2.563 19.646H17.563L15 34.646zm.854-10.25h39.292v7.687h-5.98v-3.416H20.98v3.416h-5.125v-7.687zm2.563-5.125h34.166v3.416H18.417v-3.416zM20.979 15h29.896v2.563H20.979V15z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,7 +0,0 @@
.o_mg_link_show {
display: none;
}
.o_mg_link_content {
display: none;
}
@@ -1,71 +0,0 @@
odoo.define('website_mail_channel', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
publicWidget.registry.websiteMailChannel = publicWidget.Widget.extend({
selector: '#wrapwrap',
events: {
'click .o_mg_link_hide': '_onHideLinkClick',
'click .o_mg_link_show': '_onShowLinkClick',
'click button.o_mg_read_more': '_onReadMoreClick',
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Event} ev
*/
_onHideLinkClick: function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $link = $(ev.currentTarget);
var $container = $link.parents('div').first();
$container.find('.o_mg_link_hide').first().hide();
$container.find('.o_mg_link_show').first().show();
$container.find('.o_mg_link_content').first().show();
},
/**
* @private
* @param {Event} ev
*/
_onShowLinkClick: function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $link = $(ev.currentTarget);
var $container = $link.parents('div').first();
$container.find('.o_mg_link_hide').first().show();
$container.find('.o_mg_link_show').first().hide();
$container.find('.o_mg_link_content').first().hide();
},
/**
* @private
* @param {Event} ev
*/
_onReadMoreClick: function (ev) {
var $link = $(ev.target);
this._rpc({
route: $link.data('href'),
params: {
last_displayed_id: $link.data('msg-id'),
},
}).then(function (data) {
if (!data) {
return;
}
var $threadContainer = $link.parents('.o_mg_replies').first().find('ul.list-unstyled');
if ($threadContainer) {
var $lastMsg = $threadContainer.find('li.media').last();
$(data).find('li.media').insertAfter($lastMsg);
$(data).find('.o_mg_read_more').parent().appendTo($threadContainer);
}
var $showMore = $link.parent();
$showMore.remove();
return;
});
},
});
});
@@ -1,148 +0,0 @@
odoo.define('website_mail_channel.s_channel', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
publicWidget.registry.Channel = publicWidget.Widget.extend({
selector: '.s_channel',
disabledInEditableMode: false,
read_events: {
'click .js_follow_btn, .js_unfollow_btn': '_onFollowUnFollowBtnClick',
'click .js_follow_btn': '_onFollowBtnClick',
},
/**
* @override
*/
start: function () {
var self = this;
this.is_user = false;
var unsubscribePage = window.location.search.slice(1).split('&').indexOf("unsubscribe") >= 0;
var always = function (data) {
self.is_user = data.is_user;
self.email = data.email;
self.$target.find('.js_mg_link').attr('href', '/groups/' + self.$target.data('id'));
if (unsubscribePage && self.is_user) {
self.$target.find(".js_mg_follow_form").remove();
}
self._toggleSubscription(data.is_member ? 'on' : 'off', data.email);
self.$target.removeClass('d-none');
};
this._rpc({
route: '/groups/is_member',
params: {
model: this.$target.data('object'),
channel_id: this.$target.data('id'),
get_alias_info: true,
},
}).then(always).guardedCatch(always);
// not if editable mode to allow designer to edit alert field
if (!this.editableMode) {
this.$('> .alert').addClass('d-none');
this.$('> .input-group-append.d-none').removeClass('d-none');
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_getAliasInfo: function () {
var self = this;
if (! this.$target.data('id')) {
return Promise.resolve();
}
return this._rpc({route: '/groups/' + this.$target.data('id') + '/get_alias_info'}).then(function (data) {
if (data.alias_name) {
self.$target.find('.js_mg_email').attr('href', 'mailto:' + data.alias_name);
self.$target.find('.js_mg_email').removeClass('d-none');
} else {
self.$target.find('.js_mg_email').addClass('d-none');
}
});
},
/**
* @private
*/
_toggleSubscription: function (follow, email) {
// .js_mg_follow_form contains subscribe form
// .js_mg_details contains send/archives/unsubscribe links
// .js_mg_confirmation contains message warning has been sent
var aliasDone = this._getAliasInfo();
if (follow === "on") {
// user is connected and can unsubscribe
this.$target.find(".js_mg_follow_form").addClass('d-none');
this.$target.find(".js_mg_details").removeClass('d-none');
this.$target.find(".js_mg_confirmation").addClass('d-none');
} else if (follow === "off") {
// user is connected and can subscribe
this.$target.find(".js_mg_follow_form").removeClass('d-none');
this.$target.find(".js_mg_details").addClass('d-none');
this.$target.find(".js_mg_confirmation").addClass('d-none');
} else if (follow === "email") {
// a confirmation email has been sent
this.$target.find(".js_mg_follow_form").addClass('d-none');
this.$target.find(".js_mg_details").addClass('d-none');
this.$target.find(".js_mg_confirmation").removeClass('d-none');
} else {
console.error("Unknown subscription action", follow);
}
this.$target.find('input.js_follow_email')
.val(email ? email : "")
.attr("disabled", follow === "on" || (email.length && this.is_user) ? "disabled" : false);
this.$target.attr("data-follow", follow);
return Promise.resolve(aliasDone);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onFollowBtnClick: function (ev) {
if ($(ev.currentTarget).closest('.js_mg_follow_form').length) {
this.$('.js_follow_email').val($(ev.currentTarget).closest('.js_mg_follow_form').find('.js_follow_email').val());
}
},
/**
* @private
*/
_onFollowUnFollowBtnClick: function (ev) {
ev.preventDefault();
var self = this;
var $email = this.$target.find(".js_follow_email");
if ($email.length && !$email.val().match(/.+@.+/)) {
this.$target.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
return false;
}
this.$target.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
var subscriptionAction = this.$target.attr("data-follow") || "off";
if (window.location.search.slice(1).split('&').indexOf("unsubscribe") >= 0) {
// force unsubscribe mode via URI /groups?unsubscribe
subscriptionAction = 'on';
}
this._rpc({
route: '/groups/subscription',
params: {
'channel_id': +this.$target.data('id'),
'object': this.$target.data('object'),
'subscription': subscriptionAction,
'email': $email.length ? $email.val() : false,
},
}).then(function (follow) {
self._toggleSubscription(follow, self.email);
});
},
});
});
@@ -1,102 +0,0 @@
odoo.define('website_mail_channel.s_channel_options', function (require) {
'use strict';
var core = require('web.core');
var options = require('web_editor.snippets.options');
var wUtils = require('website.utils');
var _t = core._t;
options.registry.Channel = options.Class.extend({
/**
* @override
*/
async start() {
await this._super(...arguments);
this.publicChannels = await this._getPublicChannels();
},
/**
* @override
*/
cleanForSave: function () {
this.$target.addClass('d-none');
},
/**
* If we have already created channels => select the first one
* else => modal prompt (create a new channel)
*
* @override
*/
onBuilt() {
if (this.publicChannels.length) {
this.$target[0].dataset.id = this.publicChannels[0][0];
} else {
const widget = this._requestUserValueWidgets('create_mail_channel_opt')[0];
widget.$el.click();
}
},
//--------------------------------------------------------------------------
// Options
//--------------------------------------------------------------------------
/**
* Creates a new mail.channel through a modal prompt.
*
* @see this.selectClass for parameters
*/
createChannel: function (previewMode, widgetValue, params) {
var self = this;
return wUtils.prompt({
id: "editor_new_mail_channel_subscribe",
window_title: _t("New Mail Channel"),
input: _t("Name"),
}).then(function (result) {
var name = result.val;
if (!name) {
return;
}
return self._rpc({
model: 'mail.channel',
method: 'create',
args: [{
name: name,
public: 'public',
}],
}).then(function (id) {
self.$target.attr("data-id", id);
return self._rerenderXML();
});
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
async _renderCustomXML(uiFragment) {
const channels = await this._getPublicChannels();
const menuEl = uiFragment.querySelector('.select_discussion_list');
for (const channel of channels) {
const el = document.createElement('we-button');
el.dataset.selectDataAttribute = channel[0];
el.textContent = channel[1];
menuEl.appendChild(el);
}
},
/**
* @private
* @return {Promise}
*/
_getPublicChannels() {
return this._rpc({
model: 'mail.channel',
method: 'name_search',
args: ['', [['public', '=', 'public']]],
});
},
});
});
@@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
from . import test_unsubscribe
@@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
from odoo.tests import common, tagged
from odoo.tools.misc import mute_logger, ustr
@tagged('-at_install', 'post_install')
class TestConfirmUnsubscribe(common.HttpCase):
def setUp(self):
super(TestConfirmUnsubscribe, self).setUp()
self.partner = self.env['res.partner'].create({
'name': 'Bob',
'email': 'bob@bob.bob'
})
self.mailing_list = self.env['mail.channel'].create({
'name': 'Test Mailing List',
'public': 'public',
})
self.token = self.mailing_list._generate_action_token(self.partner.id, action='unsubscribe')
def test_not_subscribed(self):
"""Test warning works"""
self._unsubscribe_check("The address %s is already unsubscribed" % self.partner.email)
@mute_logger('odoo.addons.website.models.ir_ui_view')
def test_not_subscribed_no_template(self):
""" Test warning works on db without template (code update w/o module update) """
self.env.ref('website_mail_channel.not_subscribed').unlink()
self.assertEqual(
self.env['ir.model.data'].search_count([
('module', '=', 'website_mail_channel'),
('name', '=', 'not_subscribed'),
]), 0, 'XID for template should have been deleted')
self._unsubscribe_check("The address %s is already unsubscribed or was never subscribed to any mailing list" % self.partner.email)
def test_wrong_token(self):
self.mailing_list.sudo().write({
'channel_partner_ids': [(4, self.partner.id, False)]
})
self.token = 'XXX'
self._unsubscribe_check("Invalid or expired confirmation link.")
def test_successful_unsubscribe(self):
self.mailing_list.sudo().write({
'channel_partner_ids': [(4, self.partner.id, False)]
})
self._unsubscribe_check("You have been correctly unsubscribed")
def _unsubscribe_check(self, text):
url = "/groups/unsubscribe/{}/{}/{}".format(
self.mailing_list.id, self.partner.id,
self.token
)
r = self.url_open(url)
body = ustr(r.content)
# normalize space to make matching simpler
self.assertIn(text, u' '.join(body.split()))
@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="s_channel" name="Discussion Group">
<div class="s_channel"
data-id="0" data-object="mail.channel" data-follow="off">
<div class="input-group js_mg_follow_form">
<input type="email" name="email" placeholder="your email..."
class="js_follow_email form-control"/>
<span class="input-group-append">
<button href="#" class="btn btn-primary js_follow_btn">Subscribe</button>
</span>
</div>
<p class="js_mg_details d-none">
<span class="js_mg_email d-none"><a href="#" class="js_mg_email"><i class="fa fa-envelope-o"/> send mail</a> - </span>
<a href="#" class="js_mg_link"><i class="fa fa-file-o"/> archives</a> -
<a role="button" href="#" class="js_unfollow_btn"><i class="fa fa-times"/> unsubscribe</a>
</p>
<p class="js_mg_confirmation d-none">
a confirmation email has been sent.
</p>
</div>
</template>
<template id="s_channel_options" inherit_id="website.snippet_options">
<xpath expr="." position="inside">
<div data-js='Channel'
data-selector=".s_channel"
data-drop-near="p, h1, h2, h3, blockquote, .card">
<we-row>
<we-select class="select_discussion_list" data-attribute-name="id" data-no-preview="true">
<!-- 'we-button' added programmatically with DB data -->
</we-select>
<we-button class="fa fa-fw fa-plus" title="Create a public discussion group in your backend"
data-create-channel="" data-no-preview="true" data-name="create_mail_channel_opt"/>
</we-row>
</div>
</xpath>
</template>
<record id="website_mail_channel.s_channel_000_js" model="ir.asset">
<field name="name">Channel 000 JS</field>
<field name="bundle">web.assets_frontend</field>
<field name="path">website_mail_channel/static/src/snippets/s_channel/000.js</field>
</record>
</odoo>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="remove_external_snippets" inherit_id="website.external_snippets">
<xpath expr="//t[@t-install='website_mail_channel']" position="replace"/>
</template>
<template id="snippets" inherit_id="website.snippets" name="Snippet Subscribe">
<xpath expr="//t[@id='mail_channel_discussion_group_hook']" position="replace">
<t t-snippet="website_mail_channel.s_channel" t-thumbnail="/website/static/src/img/snippets_thumbs/s_channel.svg"/>
</xpath>
</template>
</odoo>
@@ -1,345 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="mail_channels" name="Mailing Lists">
<t t-call="website.layout">
<div id="wrap" class="oe_structure oe_empty">
<section class="bg-primary jumbotron mt0 mb0">
<div class="container">
<h1>Stay in touch with our Community</h1>
<p>Alone we can do so little, together we can do so much</p>
</div>
</section>
</div>
<div class="container mt32">
<div t-if="'unsubscribe' in request.params" class="offset-lg-9 col-lg-3 alert alert-info" role="status">
<h3>Need to unsubscribe? It's right here! <span class="fa fa-2x fa-arrow-down float-right" role="img" aria-label="" title="Read this !"></span></h3>
</div>
<div class="row mt8" t-foreach="groups" t-as="group">
<div class="col-lg-3">
<img t-att-src="website.image_url(group, 'image_128')" class="o_image_64_cover float-left" alt="Group"/>
<strong><a t-attf-href="/groups/#{ slug(group) }" t-esc="group.name"/></strong><br />
<t t-if="group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain">
<i class='fa fa-envelope-o' role="img" aria-label="Alias" title="Alias"/>
<a t-attf-href="mailto:#{group.alias_id.alias_name}@#{group.alias_id.alias_domain}"><span t-field="group.alias_id"/></a>
</t>
</div>
<div class="col-lg-4">
<div t-esc="group.description" class="text-muted"/>
</div>
<div class="col-lg-2">
<i class='fa fa-fw fa-user' role="img" aria-label="Recipients" title="Recipients"/> <t t-esc="group_data[group.id]['members_count']"/> members<br />
<i class='fa fa-fw fa-envelope-o' role="img" aria-label="Traffic" title="Traffic"/> <t t-esc="group_data[group.id]['monthly_message_nbr']"/> messages / month
</div>
<div class="col-lg-3">
<!--<t t-call="website_mail.follow"><t t-set="object" t-value="group"/></t>-->
<div class="s_channel"
t-att-data-id="group.id"
data-object="mail.channel"
t-att-data-follow="'on' if 'unsubscribe' in request.params else 'off'"
data-snippet="s_channel">
<div class="input-group js_mg_follow_form">
<input
type="email"
name="email"
class="js_follow_email form-control"
placeholder="your email..."/>
<div t-if="'unsubscribe' not in request.params" class="input-group-append">
<button href="#" class="btn btn-primary js_follow_btn">Subscribe</button>
</div>
<div t-if="'unsubscribe' in request.params" class="input-group-append">
<button href="#" class="btn btn-primary js_follow_btn">Unsubscribe</button>
</div>
</div>
<p class="js_mg_details d-none">
<span class="js_mg_email d-none"><a href="#" class="js_mg_email"><i class="fa fa-envelope-o"/> send mail</a> - </span>
<a href="#" class="js_mg_link"><i class="fa fa-file-o"/> archives</a> -
<a role="button" href="#" class="js_unfollow_btn"><i class="fa fa-times"/> unsubscribe</a>
</p>
</div>
</div>
</div>
</div>
</t>
</template>
<template id="group_messages" name="Message Threads">
<t t-call="website.layout">
<section class="container">
<div class="mt8">
<ol class="breadcrumb float-left">
<li class="breadcrumb-item"><a href="/groups">Mailing Lists</a></li>
<li class="breadcrumb-item">
<a t-attf-href="/groups/#{slug(group)}?#{mode and 'mode=%s' % mode or ''}#{date_begin and '&amp;date_begin=%s' % date_begin or ''}#{date_end and '&amp;date_end=%s' % date_end or ''}"><t t-esc="group.name"/></a>
</li>
</ol>
</div>
<div class="row">
<div class="col-lg-12">
<h1 class="text-center">
<t t-esc="group.name"/> mailing list archives
</h1><h4 class="text-center text-muted" t-if="group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain">
<i class='fa fa-envelope-o' role="img" aria-label="Alias" title="Alias"/>
<a t-attf-href="mailto:#{group.alias_id.alias_name}@#{group.alias_id.alias_domain}"><span t-field="group.alias_id"/></a>
</h4>
</div>
<div class="col-lg-3">
<h2>Archives</h2>
<ul class="nav nav-pills flex-column" id="group_mode">
<li class="nav-item">
<a t-attf-href="/groups/#{ slug(group) }?mode=thread" t-attf-class="nav-link#{mode=='thread' and ' active' or ''}">By thread</a>
</li>
<li class="nav-item">
<a t-attf-href="/groups/#{ slug(group) }?mode=date" t-attf-class="nav-link#{mode=='date' and not date_begin and ' active' or ''}">By date</a>
<ul class="nav nav-pills flex-column" style="margin-left: 8px;">
<t t-foreach="archives" t-as="month_archive">
<li class="nav-item">
<a t-ignore="True" t-attf-href="/groups/#{ slug(group) }?mode=date&amp;date_begin=#{ month_archive['date_begin'] }&amp;date_end=#{month_archive['date_end']}"
t-attf-class="nav-link#{month_archive['date_begin'] == date_begin and ' active' or ''}">
<t t-esc="month_archive['date']"/>
<span class="float-right badge badge-pill" t-esc="month_archive['date_count']"/>
</a>
</li>
</t>
</ul>
</li>
</ul>
</div>
<div class="col-lg-9">
<div>
<t t-call="website.pager"/>
</div>
<t t-call="website_mail_channel.messages_short">
<t t-set="messages" t-value="messages"/>
<t t-set="msg_more_count" t-value="0"/>
<t t-set="thread_header" t-value="None"/>
</t>
<div>
<t t-call="website.pager"/>
</div>
</div>
</div>
</section>
</t>
</template>
<template id="group_message">
<t t-call="website.layout">
<t t-set="additional_title"><t t-esc="message.description"/></t>
<section class="container">
<div class="row mt8">
<ol class="breadcrumb float-left">
<li class="breadcrumb-item"><a href="/groups">Mailing Lists</a></li>
<li class="breadcrumb-item">
<a t-attf-href="/groups/#{slug(group)}?#{mode and 'mode=%s' % mode or ''}#{date_begin and '&amp;date_begin=%s' % date_begin or ''}#{date_end and '&amp;date_end=%s' % date_end or ''}"><t t-esc="group.name"/></a>
</li>
<li t-if="message" class="breadcrumb-item active"><t t-esc="message.description"/></li>
</ol>
</div>
<div class="row">
<h1 class="text-center">
<t t-esc="group.name"/> mailing list archives
</h1><h4 class="text-center text-muted" t-if="group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain">
<i class='fa fa-envelope-o' role="img" aria-label="Alias" title="Alias"/>
<a t-attf-href="mailto:#{group.alias_id.alias_name}@#{group.alias_id.alias_domain}"><span t-field="group.alias_id"/></a>
</h4>
</div>
<div class="row">
<div class="col-lg-3">
<h4>Browse archives</h4>
<ul class="nav nav-pills flex-column" id="group_mode">
<li class="nav-item">
<a t-attf-href="/groups/#{ slug(group) }?mode=thread" t-attf-class="nav-link#{mode=='thread' and ' active' or ''}">By thread</a>
</li>
<li class="nav-item">
<a t-attf-href="/groups/#{ slug(group) }?mode=date" t-attf-class="nav-link#{mode=='date' and not date_begin and ' active' or ''}">By date</a>
<ul class="nav nav-pills flex-column" style="margin-left: 8px;">
<t t-foreach="archives" t-as="month_archive">
<li class="nav-item">
<a t-ignore="True" t-attf-href="/groups/#{ slug(group) }?mode=date&amp;date_begin=#{ month_archive['date_begin'] }&amp;date_end=#{month_archive['date_end']}"
t-attf-class="nav-link#{month_archive['date_begin'] == date_begin and ' active' or ''}">
<t t-esc="month_archive['date']"/>
<span class="float-right badge badge-pill" t-esc="month_archive['date_count']"/>
</a>
</li>
</t>
</ul>
</li>
</ul>
</div>
<div class="col-lg-9">
<div class="row">
<h4 class="col-lg-6">
<t t-if="prev_message"><a t-attf-href='/groups/#{slug(group)}/#{slug(prev_message)}?#{mode and "mode=%s" % mode or ""}'>
<i class="fa fa-arrow-left" role="img" aria-label="Previous message" title="Previous message"/> <t t-esc="prev_message.description"/>
</a></t>
</h4>
<h4 class="col-lg-6">
<t t-if="next_message"><a class="float-right" t-attf-href='/groups/#{slug(group)}/#{slug(next_message)}?#{mode and "mode=%s" % mode or ""}'>
<t t-esc="next_message.description"/> <i class="fa fa-arrow-right" role="img" aria-label="Next message" title="Next message"/>
</a></t>
</h4>
</div>
<div class="media">
<img class="rounded mt0 o_image_40_cover"
t-att-src="website.image_url(message, 'author_avatar')" alt="Avatar"/>
<div class="media-body">
<h4 t-esc="message.description"/>
<small>
by
<t t-if="message.author_id">
<span t-field="message.author_id" style="display: inline-block;" t-options='{
"widget": "contact",
"fields": ["name"]
}'/>
</t>
<t t-if="not message.author_id"><t t-esc="message.email_from"/></t>
- <i class="fa fa-calendar" role="img" aria-label="Date" title="Date"/> <span t-field="message.date"/>
</small>
<div t-out="message.body"/>
<div>
<p t-if="attachments" class="mt8">
<a href="#" class="o_mg_link_hide">
<i class="fa fa-chevron-right" role="img" aria-label="Hide attachments" title="Hide attachments"/> <t t-esc="len(attachments)"/> attachments
</a>
<a href="#" class="o_mg_link_show">
<i class="fa fa-chevron-down" role="img" aria-label="Show attachments" title="Show attachments"/> <t t-esc="len(attachments)"/> attachments
</a>
</p>
<div class="o_mg_link_content">
<div class="col-lg-2 col-md-3 text-center" t-foreach='attachments' t-as='attachment'>
<t t-if="attachment.access_token">
<a t-attf-href="/web/content/#{attachment.id}?download=true&amp;access_token=#{attachment.access_token}" target="_blank">
<div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80"/>
<div class='oe_attachment_name'><t t-esc='attachment.name' /></div>
</a>
</t>
<t t-else="">
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
<div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80"/>
<div class='oe_attachment_name'><t t-esc='attachment.name' /></div>
</a>
</t>
</div>
</div>
</div>
</div>
</div>
<div t-if="message.child_ids" class="o_mg_replies">
<h4 class="o_page_header">Follow-Ups</h4>
<t t-call="website_mail_channel.messages_short">
<t t-set="messages" t-value="message.child_ids[:replies_per_page]"/>
<t t-set="msg_more_count" t-value="len(message.child_ids) - replies_per_page"/>
<t t-set="thread_header" t-value="message"/>
</t>
</div>
<div t-if="message.parent_id">
<h4 class="o_page_header">Reference</h4>
<t t-call="website_mail_channel.messages_short">
<t t-set="messages" t-value="[message.parent_id]"/>
</t>
</div>
</div>
</div>
</section>
</t>
</template>
<template id="messages_short">
<div>
<ul class="list-unstyled">
<li t-foreach="messages" t-as="thread" class="media mt-3">
<img class="rounded mt-0 o_image_40_cover" alt="Avatar"
t-att-src="website.image_url(thread, 'author_avatar')"/>
<div class="media-body">
<h4>
<a t-attf-href="/groups/#{slug(group)}/#{slug(thread)}?mode=#{mode}&amp;date_begin=#{date_begin}&amp;date_end=#{date_end}" t-esc="thread.description"/>
</h4>
<small>
by
<t t-if="thread.author_id">
<span t-field="thread.author_id" style="display: inline-block;" t-options='{
"widget": "contact",
"fields": ["name"]
}'/>
</t>
<t t-if="not thread.author_id"><t t-esc="thread.email_from"/></t>
- <i class="fa fa-calendar" role="img" aria-label="Date" title="Date"/> <span t-field="thread.date"/>
- <i class="fa fa-paperclip" role="img" aria-label="Attachments" title="Attachments"/> <t t-esc="len(thread.attachment_ids)"/>
</small>
<p t-if="thread.child_ids" class="mt8">
<a href="#" class="o_mg_link_hide">
<i class="fa fa-chevron-right" role="img" aria-label="Hide replies" title="Hide replies"/> <t t-esc="len(thread.child_ids)"/> replies
</a>
<a href="#" class="o_mg_link_show">
<i class="fa fa-chevron-down" role="img" aria-label="Show replies" title="Show replies"/> <t t-esc="len(thread.child_ids)"/> replies
</a>
</p>
<div class="o_mg_link_content o_mg_replies">
<t t-call="website_mail_channel.messages_short">
<t t-set="messages" t-value="thread.child_ids[:replies_per_page]"/>
<t t-set="msg_more_count" t-value="len(thread.child_ids) - replies_per_page"/>
<t t-set="thread_header" t-value="thread"/>
</t>
</div>
</div>
</li>
</ul>
<p t-if="messages and (msg_more_count or 0) > 0 and thread_header">
<button class="fa btn-link o_mg_read_more"
t-attf-data-href="/groups/#{slug(group)}/#{slug(thread_header)}/get_replies"
t-attf-data-msg-id="#{messages[-1].id}">
show <t t-esc="msg_more_count"/> more replies
</button>
</p>
</div>
</template>
<template id="confirmation_subscription" name="Mailing List Confirmation">
<t t-call="website.layout">
<div id="wrap" class="oe_structure oe_empty">
<div class="container">
<p>
You have been correctly
<t t-if="subscribing">subscribed</t>
<t t-if="not subscribing">unsubscribed</t>
to the mailing list.
</p>
</div>
</div>
</t>
</template>
<template id="invalid_token_subscription" name="Invalid Token Submitted">
<t t-call="website.layout">
<div id="wrap" class="oe_structure oe_empty">
<div class="container">
<p>
Invalid or expired confirmation link.
</p>
</div>
</div>
</t>
</template>
<template id="not_subscribed" name="Email address was not subscribed">
<t t-call="website.layout">
<div id="wrap" class="oe_structure oe_empty">
<div class="container">
<p>
The address <t t-esc="partner_id.email"/> is already
unsubscribed or was never subscribed to the mailing
list, you may want to check that the address was
correct.
</p>
</div>
</div>
</t>
</template>
</odoo>