[REF] survey: refactor invitation wizard

Purpose is to clean and simplify invite wizard on surveys. Main purpose is
to simplify options.

Public link is displayed for public surveys, allowing to quickly share
it. When sending emails a token is now generated for each recipient. There is
no "batch public link" sending option anymore as it is not really necessary.

Check is done on recipients and emails to match the access mode option. Having
a restricted survey gives the same limitations on people to invite (users,
employees, ...).

Code is cleaned to ease its understanding and prepare future evolution notably
with appraisals.

An existing mode field is added on invite model. All options are currently
not used but it prepares future addition to survey, notably for certifications.
Its purpose is to know what to do with existing tokens when inviting new
recipients. It is used notably to resend tokens through the user_input form
view instead of relying on a custom context key.

This commit is linked to task ID 1911586 and PR #28986.
This commit is contained in:
Thibault Delavallée
2019-01-14 15:22:18 +00:00
parent 1c5371a966
commit 6fa4688693
7 changed files with 360 additions and 208 deletions
+5 -1
View File
@@ -16,7 +16,11 @@ sent mails with personal token for the invitation of the survey.
""",
'summary': 'Create surveys and analyze answers',
'website': 'https://www.odoo.com/page/survey',
'depends': ['http_routing', 'mail', 'web_tour'],
'depends': [
'auth_signup',
'http_routing',
'mail',
'web_tour'],
'data': [
'data/mail_template_data.xml',
'data/survey_data.xml',
-3
View File
@@ -345,12 +345,9 @@ class Survey(models.Model):
local_context = dict(
self.env.context,
default_model='survey.survey',
default_res_id=self.id,
default_survey_id=self.id,
default_use_template=bool(template),
default_template_id=template and template.id or False,
default_composition_mode='comment',
notif_layout='mail.mail_notification_light',
)
return {
+15 -11
View File
@@ -45,7 +45,7 @@ class SurveyUserInput(models.Model):
email = fields.Char('E-mail', readonly=True)
# answers
user_input_line_ids = fields.One2many('survey.user_input_line', 'user_input_id', string='Answers', copy=True)
deadline = fields.Datetime('Deadline', help="Date by which the person can open the survey and submit answers")
deadline = fields.Datetime('Deadline', help="Datetime until customer can open the survey and submit answers")
last_displayed_page_id = fields.Many2one('survey.page', string='Last displayed page')
quizz_score = fields.Float("Score for the quiz", compute="_compute_quizz_score", default=0.0)
@@ -69,16 +69,20 @@ class SurveyUserInput(models.Model):
('create_date', '<', an_hour_ago)]).unlink()
@api.multi
def action_survey_resend(self):
""" Send again the invitation """
self.ensure_one()
local_context = {
'survey_resent_token': True,
'default_partner_ids': self.partner_id and [self.partner_id.id] or [],
'default_multi_email': self.email or "",
'default_public': 'email_private',
}
return self.survey_id.with_context(local_context).action_send_survey()
def action_resend(self):
partners = self.env['res.partner']
emails = []
for user_answer in self:
if user_answer.partner_id:
partners |= user_answer.partner_id
elif user_answer.email:
emails.append(user_answer.email)
return self.survey_id.with_context(
default_existing_mode='resend',
default_partner_ids=partners.ids,
default_emails=','.join(emails)
).action_send_survey()
@api.multi
def action_print_answers(self):
+161
View File
@@ -1,13 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.addons.survey.tests import common
from odoo.exceptions import UserError
from odoo.tests import Form
from odoo.tests.common import users
class TestSurveyInvite(common.SurveyCase):
def setUp(self):
res = super(TestSurveyInvite, self).setUp()
# by default signup not allowed
self.env["ir.config_parameter"].set_param('auth_signup.invitation_scope', 'b2b')
return res
@users('survey_manager')
def test_survey_invite_action(self):
# Check correctly configured survey returns an invite wizard action
@@ -30,3 +41,153 @@ class TestSurveyInvite(common.SurveyCase):
for survey in surveys:
with self.assertRaises(UserError):
survey.action_send_survey()
@users('survey_manager')
def test_survey_invite(self):
Answer = self.env['survey.user_input']
deadline = fields.Datetime.now() + relativedelta(months=1)
self.survey.write({'access_mode': 'public'})
action = self.survey.action_send_survey()
invite_form = Form(self.env[action['res_model']].with_context(action['context']))
# some lowlevel checks that action is correctly configured
self.assertEqual(Answer.search([('survey_id', '=', self.survey.id)]), self.env['survey.user_input'])
self.assertEqual(invite_form.survey_id, self.survey)
invite_form.partner_ids.add(self.customer)
invite_form.deadline = fields.Datetime.to_string(deadline)
invite = invite_form.save()
invite.action_invite()
answers = Answer.search([('survey_id', '=', self.survey.id)])
self.assertEqual(len(answers), 1)
self.assertEqual(
set(answers.mapped('email')),
set([self.customer.email]))
self.assertEqual(answers.mapped('partner_id'), self.customer)
self.assertEqual(set(answers.mapped('deadline')), set([deadline]))
@users('survey_manager')
def test_survey_invite_authentication_nosignup(self):
Answer = self.env['survey.user_input']
self.survey.write({'access_mode': 'authentication'})
action = self.survey.action_send_survey()
invite_form = Form(self.env[action['res_model']].with_context(action['context']))
with self.assertRaises(UserError): # do not allow to add customer (partner without user)
invite_form.partner_ids.add(self.customer)
invite_form.partner_ids.clear()
invite_form.partner_ids.add(self.user_portal.partner_id)
invite_form.partner_ids.add(self.user_emp.partner_id)
with self.assertRaises(UserError):
invite_form.emails = 'test1@example.com, Raoulette Vignolette <test2@example.com>'
invite_form.emails = False
invite = invite_form.save()
invite.action_invite()
answers = Answer.search([('survey_id', '=', self.survey.id)])
self.assertEqual(len(answers), 2)
self.assertEqual(
set(answers.mapped('email')),
set([self.user_emp.email, self.user_portal.email]))
self.assertEqual(answers.mapped('partner_id'), self.user_emp.partner_id | self.user_portal.partner_id)
@users('survey_manager')
def test_survey_invite_authentication_signup(self):
self.env["ir.config_parameter"].sudo().set_param('auth_signup.invitation_scope', 'b2c')
self.survey.invalidate_cache()
Answer = self.env['survey.user_input']
self.survey.write({'access_mode': 'authentication'})
action = self.survey.action_send_survey()
invite_form = Form(self.env[action['res_model']].with_context(action['context']))
invite_form.partner_ids.add(self.customer)
invite_form.partner_ids.add(self.user_portal.partner_id)
invite_form.partner_ids.add(self.user_emp.partner_id)
# TDE FIXME: not sure for emails in authentication + signup
# invite_form.emails = 'test1@example.com, Raoulette Vignolette <test2@example.com>'
invite = invite_form.save()
invite.action_invite()
answers = Answer.search([('survey_id', '=', self.survey.id)])
self.assertEqual(len(answers), 3)
self.assertEqual(
set(answers.mapped('email')),
set([self.customer.email, self.user_emp.email, self.user_portal.email]))
self.assertEqual(answers.mapped('partner_id'), self.customer | self.user_emp.partner_id | self.user_portal.partner_id)
@users('survey_manager')
def test_survey_invite_internal(self):
Answer = self.env['survey.user_input']
self.survey.write({'access_mode': 'internal'})
action = self.survey.action_send_survey()
invite_form = Form(self.env[action['res_model']].with_context(action['context']))
with self.assertRaises(UserError): # do not allow to add customer (partner without user)
invite_form.partner_ids.add(self.customer)
with self.assertRaises(UserError): # do not allow to add portal user
invite_form.partner_ids.add(self.user_portal.partner_id)
invite_form.partner_ids.clear()
invite_form.partner_ids.add(self.user_emp.partner_id)
with self.assertRaises(UserError):
invite_form.emails = 'test1@example.com, Raoulette Vignolette <test2@example.com>'
invite_form.emails = False
invite = invite_form.save()
invite.action_invite()
answers = Answer.search([('survey_id', '=', self.survey.id)])
self.assertEqual(len(answers), 1)
self.assertEqual(
set(answers.mapped('email')),
set([self.user_emp.email]))
self.assertEqual(answers.mapped('partner_id'), self.user_emp.partner_id)
@users('survey_manager')
def test_survey_invite_public(self):
Answer = self.env['survey.user_input']
self.survey.write({'access_mode': 'public'})
action = self.survey.action_send_survey()
invite_form = Form(self.env[action['res_model']].with_context(action['context']))
invite_form.partner_ids.add(self.customer)
invite_form.emails = 'test1@example.com, Raoulette Vignolette <test2@example.com>'
invite = invite_form.save()
invite.action_invite()
answers = Answer.search([('survey_id', '=', self.survey.id)])
self.assertEqual(len(answers), 3)
self.assertEqual(
set(answers.mapped('email')),
set(['test1@example.com', 'Raoulette Vignolette <test2@example.com>', self.customer.email]))
self.assertEqual(answers.mapped('partner_id'), self.customer)
@users('survey_manager')
def test_survey_invite_token(self):
Answer = self.env['survey.user_input']
self.survey.write({'access_mode': 'token'})
action = self.survey.action_send_survey()
invite_form = Form(self.env[action['res_model']].with_context(action['context']))
invite_form.partner_ids.add(self.customer)
invite_form.emails = 'test1@example.com, Raoulette Vignolette <test2@example.com>'
invite = invite_form.save()
invite.action_invite()
answers = Answer.search([('survey_id', '=', self.survey.id)])
self.assertEqual(len(answers), 3)
self.assertEqual(
set(answers.mapped('email')),
set(['test1@example.com', 'Raoulette Vignolette <test2@example.com>', self.customer.email]))
self.assertEqual(answers.mapped('partner_id'), self.customer)
+3 -2
View File
@@ -29,8 +29,9 @@
<field name="arch" type="xml">
<form string="Survey User inputs" create="false">
<header>
<button name="action_survey_resend" string="Send Invitation Again" type="object" class="oe_highlight" attrs="{'invisible': ['|', ('input_type','=','manually'), ('state', '=', 'done')]}"/>
<button name="action_print_answers" states="done" string="Print" type="object" class="oe_highlight" />
<button name="action_resend" string="Resend Invitation" type="object" class="oe_highlight"
attrs="{'invisible': ['|', ('state', '=', 'done'), '&amp;', ('partner_id', '=', False), ('email', '=', False)]}"/>
<button name="action_print_answers" states="done" string="Print" type="object" class="oe_highlight"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
+151 -152
View File
@@ -3,29 +3,21 @@
import logging
import re
import uuid
from email.utils import formataddr
from werkzeug import urls
from odoo import api, fields, models, _
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
emails_split = re.compile(r"[;,\n\r]+")
email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+")
class SurveyInvite(models.TransientModel):
_name = 'survey.invite'
_description = 'Survey Invitation Wizard'
def default_survey_id(self):
context = self.env.context
if context.get('model') == 'survey.survey':
return context.get('res_id')
@api.model
def _get_default_from(self):
if self.env.user.email:
@@ -52,52 +44,69 @@ class SurveyInvite(models.TransientModel):
ondelete='set null', default=_get_default_author,
help="Author of the message.")
# recipients
partner_ids = fields.Many2many('res.partner', 'survey_mail_compose_message_res_partner_rel', 'wizard_id', 'partner_id', string='Existing contacts')
multi_email = fields.Text(string='List of emails', help="This list of emails of recipients will not be converted in contacts.\
partner_ids = fields.Many2many(
'res.partner', 'survey_mail_compose_message_res_partner_rel', 'wizard_id', 'partner_id', string='Recipients')
emails = fields.Text(string='Additional emails', help="This list of emails of recipients will not be converted in contacts.\
Emails must be separated by commas, semicolons or newline.")
existing_mode = fields.Selection([
('skip', 'Skip'), ('resend', 'Resend'), ('prevent', 'Prevent')],
string='Existing', default='skip')
# technical info
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
# survey
survey_id = fields.Many2one('survey.survey', string='Survey', default=default_survey_id, required=True)
public = fields.Selection([('public_link', 'Share the public web link to your audience.'),
('email_public_link', 'Send by email the public web link to your audience.'),
('email_private', 'Send private invitation to your audience (only one response per recipient and per invitation).')],
string='Share options', default='public_link', required=True)
public_url = fields.Char(compute="_compute_survey_url", string="Public url")
date_deadline = fields.Date(string="Deadline to which the invitation to respond is valid",
help="Deadline to which the invitation to respond for this survey is valid. If the field is empty,\
the invitation is still valid.")
survey_id = fields.Many2one('survey.survey', string='Survey', required=True)
survey_url = fields.Char(related="survey_id.public_url", readonly=True)
survey_access_mode = fields.Selection(related="survey_id.access_mode", readonly=True)
deadline = fields.Datetime(string="Answer deadline")
@api.depends('survey_id')
def _compute_survey_url(self):
for wizard in self:
wizard.public_url = wizard.survey_id.public_url
@api.model
def default_get(self, fields):
res = super(SurveyInvite, self).default_get(fields)
context = self.env.context
if context.get('active_model') == 'res.partner' and context.get('active_ids'):
res.update({'partner_ids': context['active_ids']})
return res
@api.onchange('multi_email')
def onchange_multi_email(self):
emails = list(set(emails_split.split(self.multi_email or "")))
emails_checked = []
error_message = ""
@api.onchange('emails')
def _onchange_emails(self):
if self.emails and (self.survey_access_mode == 'internal' or (self.survey_access_mode == 'authentication' and not self.survey_id.users_can_signup)):
raise UserError(_('This survey does not allow external people to participate. You should create user accounts or update survey access mode accordingly.'))
if not self.emails:
return
valid, error = [], []
emails = list(set(emails_split.split(self.emails or "")))
for email in emails:
email = email.strip()
if email:
if not email_validator.match(email):
error_message += "\n'%s'" % email
else:
emails_checked.append(email)
if error_message:
raise UserError(_("Incorrect Email Address: %s") % error_message)
email_check = tools.email_split_and_format(email)
if not email_check:
error.append(email)
else:
valid.extend(email_check)
if error:
raise UserError(_("Some emails you just entered are incorrect: %s") % (', '.join(error)))
self.emails = '\n'.join(valid)
emails_checked.sort()
self.multi_email = '\n'.join(emails_checked)
@api.onchange('survey_access_mode')
def _onchange_survey_access_mode(self):
if self.survey_access_mode == 'internal':
return {'domain': {
'partner_ids': [('user_ids.groups_id', 'in', [self.env.ref('base.group_user').id])]
}}
elif self.survey_access_mode == 'authentication':
if not self.survey_id.users_can_signup:
return {'domain': {
'partner_ids': [('user_ids.groups_id', 'in', [self.env.ref('base.group_user').id, self.env.ref('base.group_portal').id])]
}}
@api.onchange('partner_ids')
def _onchange_partner_ids(self):
if self.survey_access_mode == 'internal' and self.partner_ids:
invalid_partners = self.partner_ids.filtered(lambda partner: not partner.user_ids or not all(user.has_group('base.group_user') for user in partner.user_ids))
if invalid_partners:
raise UserError(
_('The following recipients are not valid users belonging to the employee group: %s. You should create user accounts for them.' %
(','.join(invalid_partners.mapped('name')))))
elif self.survey_access_mode == 'authentication' and self.partner_ids:
if not self.survey_id.users_can_signup:
invalid_partners = self.env['res.partner'].search([
('user_ids', '=', False),
('id', 'in', self.partner_ids.ids)
])
if invalid_partners:
raise UserError(
_('The following recipients have no user account: %s. You should create user accounts for them or allow external signup in configuration.' %
(','.join(invalid_partners.mapped('name')))))
@api.onchange('template_id', 'survey_id')
def _onchange_template_id(self):
@@ -146,119 +155,109 @@ class SurveyInvite(models.TransientModel):
# Wizard validation and send
#------------------------------------------------------
@api.multi
def send_mail_action(self):
return self.send_mail()
def _prepare_answers(self, partners, emails):
answers = self.env['survey.user_input']
existing_answers = self.env['survey.user_input'].search([
'&', ('survey_id', '=', self.survey_id.id),
'|',
('partner_id', 'in', partners.ids),
('email', 'in', emails)
])
partners_done = self.env['res.partner']
emails_done = []
if existing_answers:
if self.existing_mode == 'prevent':
raise UserError(_('Some of recipients already started survey. Please update recipients list accordingly.'))
if self.existing_mode in ['skip', 'reset']:
partners_done = existing_answers.mapped('partner_id')
emails_done = existing_answers.mapped('email')
if self.existing_mode == 'reset':
existing_answers.write({
'deadline': self.deadline,
'state': 'new',
'user_input_line_ids': [(5, 0)],
})
answers |= existing_answers
for new_partner in partners - partners_done:
answers |= self.survey_id._create_answer(partner=new_partner, deadline=self.deadline)
for new_email in [email for email in emails if email not in emails_done]:
answers |= self.survey_id._create_answer(email=new_email, deadline=self.deadline)
return answers
def _send_mail(self, answer):
""" Create mail specific for recipient containing notably its access token """
url = '%s?token=%s' % (self.survey_id.public_url, answer.token)
# post the message
mail_values = {
'email_from': self.email_from,
'author_id': self.author_id.id,
'model': None,
'res_id': None,
'subject': self.subject,
'body_html': self.body.replace("__URL__", url),
'attachment_ids': [(4, att.id) for att in self.attachment_ids],
'auto_delete': True,
}
if answer.partner_id:
mail_values['recipient_ids'] = [(4, answer.partner_id.id)]
else:
mail_values['email_to'] = answer.email
# optional support of notif_layout in context
notif_layout = self.env.context.get('notif_layout', self.env.context.get('custom_layout'))
if notif_layout:
try:
template = self.env.ref(notif_layout, raise_if_not_found=True)
except ValueError:
_logger.warning('QWeb template %s not found when sending survey mails. Sending without layouting.' % (notif_layout))
else:
template_ctx = {
'message': self.env['mail.message'].sudo().new(dict(body=mail_values['body_html'], record_name=self.survey_id.title)),
'model_description': self.env['ir.model']._get('survey.survey').display_name,
'company': self.env.user.company_id,
}
body = template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
mail_values['body_html'] = self.env['mail.thread']._replace_local_links(body)
return self.env['mail.mail'].sudo().create(mail_values)
@api.multi
def send_mail(self, auto_commit=False):
def action_invite(self):
""" Process the wizard content and proceed with sending the related
email(s), rendering any template patterns on the fly if needed """
SurveyUserInput = self.env['survey.user_input']
self.ensure_one()
Partner = self.env['res.partner']
Mail = self.env['mail.mail']
notif_layout = self.env.context.get('notif_layout', self.env.context.get('custom_layout'))
def create_response_and_send_mail(wizard, token, partner_id, email):
""" Create one mail by recipients and replace __URL__ by link with identification token """
#set url
url = wizard.survey_id.public_url
# check if __URL__ is in the text
if self.body.find("__URL__") < 0:
raise UserError(_("The content of the text don't contain '__URL__'. \
__URL__ is automaticaly converted into the special url of the survey."))
if token:
url = url + '/' + token
# post the message
values = {
'model': None,
'res_id': None,
'subject': wizard.subject,
'body': wizard.body.replace("__URL__", url),
'body_html': wizard.body.replace("__URL__", url),
'parent_id': None,
'attachment_ids': wizard.attachment_ids and [(6, 0, wizard.attachment_ids.ids)] or None,
'email_from': wizard.email_from or None,
'auto_delete': True,
}
if partner_id:
values['recipient_ids'] = [(4, partner_id)]
# compute partners and emails, try to find partners for given emails
valid_partners = self.partner_ids
valid_emails = []
for email in emails_split.split(self.emails or ''):
partner = False
email_normalized = tools.email_normalize(email)
if email_normalized:
partner = Partner.search([('email_normalized', '=', email_normalized)])
if partner:
valid_partners |= partner
else:
values['email_to'] = email
email_formatted = tools.email_split_and_format(email)
if email_formatted:
valid_emails.extend(email_formatted)
# optional support of notif_layout in context
if notif_layout:
try:
template = self.env.ref(notif_layout, raise_if_not_found=True)
except ValueError:
_logger.warning('QWeb template %s not found when sending survey mails. Sending without layouting.' % (notif_layout))
else:
template_ctx = {
'message': self.env['mail.message'].sudo().new(dict(body=values['body_html'], record_name=wizard.survey_id.title)),
'model_description': self.env['ir.model']._get('survey.survey').display_name,
'company': self.env.user.company_id,
}
body = template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
values['body_html'] = self.env['mail.thread']._replace_local_links(body)
if not valid_partners and not valid_emails:
raise UserError(_("Please enter at least one valid recipient."))
Mail.create(values).send()
def create_token(wizard, partner_id, email):
if context.get("survey_resent_token"):
survey_user_input = SurveyUserInput.search([('survey_id', '=', wizard.survey_id.id),
('state', 'in', ['new', 'skip']), '|', ('partner_id', '=', partner_id),
('email', '=', email)], limit=1)
if survey_user_input:
return survey_user_input.token
if wizard.public != 'email_private':
return None
else:
token = str(uuid.uuid4())
# create response with token
survey_user_input = SurveyUserInput.create({
'survey_id': wizard.survey_id.id,
'deadline': wizard.date_deadline,
'input_type': 'link',
'state': 'new',
'token': token,
'partner_id': partner_id,
'email': email})
return survey_user_input.token
for wizard in self:
# check if __URL__ is in the text
if wizard.body.find("__URL__") < 0:
raise UserError(_("The content of the text don't contain '__URL__'. \
__URL__ is automaticaly converted into the special url of the survey."))
context = self.env.context
if not wizard.multi_email and not wizard.partner_ids and (context.get('default_partner_ids') or context.get('default_multi_email')):
wizard.multi_email = context.get('default_multi_email')
wizard.partner_ids = context.get('default_partner_ids')
# quick check of email list
emails_list = []
if wizard.multi_email:
emails = set(emails_split.split(wizard.multi_email)) - set(wizard.partner_ids.mapped('email'))
for email in emails:
email = email.strip()
if email_validator.match(email):
emails_list.append(email)
# remove public anonymous access
partner_list = []
for partner in wizard.partner_ids:
partner_list.append({'id': partner.id, 'email': partner.email})
if not len(emails_list) and not len(partner_list):
raise UserError(_("Please enter at least one valid recipient."))
for email in emails_list:
partner = Partner.search([('email', '=', email)], limit=1)
token = create_token(wizard, partner.id, email)
create_response_and_send_mail(wizard, token, partner.id, email)
for partner in partner_list:
token = create_token(wizard, partner['id'], partner['email'])
create_response_and_send_mail(wizard, token, partner['id'], partner['email'])
answers = self._prepare_answers(valid_partners, valid_emails)
for answer in answers:
self._send_mail(answer)
return {'type': 'ir.actions.act_window_close'}
+25 -39
View File
@@ -1,60 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Composition form (context['survey_resent_token'], if True, hide share option, emails list and partner_ids) -->
<record model="ir.ui.view" id="survey_invite_view_form">
<field name="name">survey.invite.view.form</field>
<field name="model">survey.invite</field>
<field name="arch" type="xml">
<form string="Compose Email">
<group col="2">
<field name="survey_id" readonly="context.get('default_survey_id')"/>
<field name="public" widget="radio" invisible="context.get('survey_resent_token')" />
</group>
<group col="1" invisible="context.get('survey_resent_token')" attrs="{'invisible':[('public','in',['email_private', 'email_public_link'])]}" class="oe_survey_email_public">
<div>
You can share your survey web public link and/or send private invitations to your audience. People can answer once per invitation, and whenever they want with the public web link (in this case, the "Login Required" setting must be disabled).
</div>
<div>Copy, paste and share the web link below to your audience.</div>
<code><field name="public_url"/></code>
</group>
<group col="1" attrs="{'invisible':['|',('public','=',False),('public','in',['public_link'])]}">
<group col="1">
<group col="2">
<field name="survey_access_mode" invisible="1"/>
<field name="survey_id" readonly="context.get('default_survey_id')"/>
<field name="existing_mode" widget="radio" invisible="1" />
<field name="survey_url" label="Public share URL" readonly="1" widget="CopyClipboardText"
attrs="{'invisible':[('survey_access_mode', '!=', 'public')]}"
class="mb16"/>
<field name="partner_ids"
invisible="context.get('survey_resent_token')"
attrs="{'readonly': [('existing_mode', '=', 'resend')]}"
widget="many2many_tags_email"
placeholder="Add existing contacts..."
context="{'force_email':True, 'show_email':True}" />
<field name="multi_email"
invisible="context.get('survey_resent_token')"
context="{'force_email':True, 'show_email':True, 'no_create_edit': True}"/>
<field name="emails"
attrs="{
'invisible': [('survey_access_mode', 'in', ['authentication', 'internal'])],
'readonly': [('existing_mode', '=', 'resend')]
}"
placeholder="Add a list of email of recipients (will not be converted into contacts). Separated by commas, semicolons or newline..."/>
<field name="subject" placeholder="Subject..."/>
</group>
<field name="body" options="{'style-inline': true}"/>
<group col="2">
<div>
<label for="attachment_ids" />
<field name="attachment_ids" widget="many2many_binary" />
</div>
<div class="oe_survey_date_deadline">
<label for="date_deadline" />
<field name="date_deadline" />
</div>
<group>
<group>
<field name="attachment_ids" widget="many2many_binary"/>
</group>
<group>
<field name="deadline"/>
<field name="template_id" label="Use template"/>
</group>
</group>
</group>
<footer attrs="{'invisible':[('public','in',['email_private', 'email_public_link'])]}">
<button string="Close" class="btn-primary" special="cancel" />
</footer>
<footer attrs="{'invisible':['|',('public','=',False),('public','in',['public_link'])]}">
<button string="Send" name="send_mail_action" type="object" class="btn-primary" />
<button string="Cancel" class="btn-secondary" special="cancel" />
<group class="oe_right" col="1">
<div>
Use template
<field name="template_id" nolabel="1"/>
</div>
<button icon="/email_template/static/src/img/email_template_save.png" type="object" name="save_as_template" string="Save as new template" class="oe_link" help="Save as a new template" />
</group>
<footer>
<button string="Send" name="action_invite" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>