Files
odoo_source/addons/survey/wizard/survey_invite.py
T
Thibault Delavallée 84bb9c748d [REF] survey: remove unused or unnecessary fields to clean models
PURPOSE

As new features are about to land in survey, notably live interactions [1]
and new survey building [2] performing a pre cleaning is necessary. In this
PR we clean survey models by: removing unnecessary fields, cleaning some code
and finally renaming models.

SPECIFICATIONS: QUESTION FIELD ON SURVEY.SURVEY

On survey.survey, remove question field, unnecessary related on title.

Question model holds two fields for its title: ``title`` and ``question``.
Question is simply a related on title, making the two fields completely
redundant. It is mainly due to historical reasons, when updating models for
certifications and eLearning. In this commit we keep only title field and
remove the question related field as it adds unnecessary complexity to the
model.

SPECIFICATIONS: INPUT_TYPE FIELD ON SURVEY.USER_INPUT

On survey.user_input, remove input_type and its garbage collect.

``input_type`` field exists on user input model to tell whether answer has
been created through invite or through manual click on a survey page. It
has been added a long time ago when surveys were either open to everyone,
either closed and on invite only.

Since eLearning and certification surveys access mode on surveys has evolved.
Notably being able to distinguish invite from manual survey user input is not
necessary anymore. Indeed what is important is the way people can reach the
survey, not how they created their user input.

Invitation creates token and this can be used if people effectively want to
find invitation-related user inputs.

Since 09ea5c7d49 manual entries still in draft are garbage collected.
Reason is still unclear as it is not obvious that tons of unnecessary entries
will be created. As this seems like unnecessary optimization this commit
removes that feature along with the input_type field.

SPECIFICATIONS: REPLACE URLS FIELDS BY METHODS ON SURVEY.{SURVEY, USER_INPUT}

On survey.{survey, user_input}, remove url fields replaced by methods

In this commit we remove some remaining of URL fields that are better found
using methods. Both survey and user input holds a "start" url field that is
replaced by a method call ``get_start_url`` on both survey (generic) and
user input (token specific) models. We also introduced a ``get_print_url``
method doing the same for the printable version of survey / user input.

SPECIFICATIONS: REMOVE CATEGORY FIELD ON SURVEY.SURVEY

On survey.survey, remove unused category field.

Survey model holds a ``category`` field whose purpose is to be able to somehow
categorize surveys according to their use. However using this field is not
easy as it is hidden and is a simple selection field. Module should add their
own key. Its sole use is in ``hr_recruitment_survey`` which is a niche module.
Let us clean models and lessen model complexity.

LINKS

[0] Related to Task ID 2061901 (survey models cleaning and preparation)
[1] Task ID 1972640 (live interactions)
[2] Task ID 2119587 (new frontend for building surveys)

PR #40765
2019-12-05 15:21:28 +00:00

267 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import re
import werkzeug
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
emails_split = re.compile(r"[;,\n\r]+")
class SurveyInvite(models.TransientModel):
_name = 'survey.invite'
_description = 'Survey Invitation Wizard'
@api.model
def _get_default_from(self):
if self.env.user.email:
return tools.formataddr((self.env.user.name, self.env.user.email))
raise UserError(_("Unable to post message, please configure the sender's email address."))
@api.model
def _get_default_author(self):
return self.env.user.partner_id
# composer content
subject = fields.Char('Subject')
body = fields.Html('Contents', default='', sanitize_style=True)
attachment_ids = fields.Many2many(
'ir.attachment', 'survey_mail_compose_message_ir_attachments_rel', 'wizard_id', 'attachment_id',
string='Attachments')
template_id = fields.Many2one(
'mail.template', 'Use template', index=True,
domain="[('model', '=', 'survey.user_input')]")
# origin
email_from = fields.Char('From', default=_get_default_from, help="Email address of the sender.")
author_id = fields.Many2one(
'res.partner', 'Author', index=True,
ondelete='set null', default=_get_default_author,
help="Author of the message.")
# recipients
partner_ids = fields.Many2many(
'res.partner', 'survey_invite_partner_ids', 'invite_id', 'partner_id', string='Recipients',
domain="""[
'|', (survey_users_can_signup, '=', 1),
'|', (not survey_users_login_required, '=', 1),
('user_ids', '!=', False),
]"""
)
existing_partner_ids = fields.Many2many(
'res.partner', compute='_compute_existing_partner_ids', readonly=True, store=False)
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_emails = fields.Text(
'Existing emails', compute='_compute_existing_emails',
readonly=True, store=False)
existing_mode = fields.Selection([
('new', 'New invite'), ('resend', 'Resend invite')],
string='Handle existing', default='resend', required=True)
existing_text = fields.Text('Resend Comment', compute='_compute_existing_text', readonly=True, store=False)
# technical info
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
# survey
survey_id = fields.Many2one('survey.survey', string='Survey', required=True)
survey_start_url = fields.Char('Survey URL', compute='_compute_survey_start_url')
survey_access_mode = fields.Selection(related="survey_id.access_mode", readonly=True)
survey_users_login_required = fields.Boolean(related="survey_id.users_login_required", readonly=True)
survey_users_can_signup = fields.Boolean(related='survey_id.users_can_signup')
deadline = fields.Datetime(string="Answer deadline")
@api.depends('partner_ids', 'survey_id')
def _compute_existing_partner_ids(self):
existing_answers = self.survey_id.user_input_ids
self.existing_partner_ids = existing_answers.mapped('partner_id') & self.partner_ids
@api.depends('emails', 'survey_id')
def _compute_existing_emails(self):
emails = list(set(emails_split.split(self.emails or "")))
existing_emails = self.survey_id.mapped('user_input_ids.email')
self.existing_emails = '\n'.join(email for email in emails if email in existing_emails)
@api.depends('existing_partner_ids', 'existing_emails')
def _compute_existing_text(self):
existing_text = False
if self.existing_partner_ids:
existing_text = '%s: %s.' % (
_('The following customers have already received an invite'),
', '.join(self.mapped('existing_partner_ids.name'))
)
if self.existing_emails:
existing_text = '%s\n' % existing_text if existing_text else ''
existing_text += '%s: %s.' % (
_('The following emails have already received an invite'),
self.existing_emails
)
self.existing_text = existing_text
@api.depends('survey_id.access_token')
def _compute_survey_start_url(self):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for invite in self:
invite.survey_start_url = werkzeug.urls.url_join(base_url, invite.survey_id.get_start_url()) if invite.survey_id else False
@api.onchange('emails')
def _onchange_emails(self):
if self.emails and (self.survey_users_login_required 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_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)
@api.onchange('partner_ids')
def _onchange_partner_ids(self):
if self.survey_users_login_required 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')
def _onchange_template_id(self):
""" UPDATE ME """
if self.template_id:
self.subject = self.template_id.subject
self.body = self.template_id.body_html
@api.model
def create(self, values):
if values.get('template_id') and not (values.get('body') or values.get('subject')):
template = self.env['mail.template'].browse(values['template_id'])
if not values.get('subject'):
values['subject'] = template.subject
if not values.get('body'):
values['body'] = template.body_html
return super(SurveyInvite, self).create(values)
# ------------------------------------------------------
# Wizard validation and send
# ------------------------------------------------------
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 == 'resend':
partners_done = existing_answers.mapped('partner_id')
emails_done = existing_answers.mapped('email')
# only add the last answer for each user of each type (partner_id & email)
# to have only one mail sent per user
for partner_done in partners_done:
answers |= next(existing_answer for existing_answer in
existing_answers.sorted(lambda answer: answer.create_date, reverse=True)
if existing_answer.partner_id == partner_done)
for email_done in emails_done:
answers |= next(existing_answer for existing_answer in
existing_answers.sorted(lambda answer: answer.create_date, reverse=True)
if existing_answer.email == email_done)
for new_partner in partners - partners_done:
answers |= self.survey_id._create_answer(partner=new_partner, check_attempts=False, **self._get_answers_values())
for new_email in [email for email in emails if email not in emails_done]:
answers |= self.survey_id._create_answer(email=new_email, check_attempts=False, **self._get_answers_values())
return answers
def _get_answers_values(self):
return {
'deadline': self.deadline,
}
def _send_mail(self, answer):
""" Create mail specific for recipient containing notably its access token """
subject = self.env['mail.template']._render_template(self.subject, 'survey.user_input', answer.id, post_process=True)
body = self.env['mail.template']._render_template(self.body, 'survey.user_input', answer.id, post_process=True)
# post the message
mail_values = {
'email_from': self.email_from,
'author_id': self.author_id.id,
'model': None,
'res_id': None,
'subject': subject,
'body_html': body,
'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.company,
}
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)
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 """
self.ensure_one()
Partner = self.env['res.partner']
# 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:
email_formatted = tools.email_split_and_format(email)
if email_formatted:
valid_emails.extend(email_formatted)
if not valid_partners and not valid_emails:
raise UserError(_("Please enter at least one valid recipient."))
answers = self._prepare_answers(valid_partners, valid_emails)
for answer in answers:
self._send_mail(answer)
return {'type': 'ir.actions.act_window_close'}