[MERGE][REF] survey: update ACLs and refactor invite wizard

Purpose of this merge is to refactor, clarify and fix surveys access rights
and rules. Indeed currently surveys access rights and rules are not very
clear. This application is currently used mainly for external and public
surveys.

In this merge we clean access on survey models. Basically only surveys officers
and managers have access to the models. Other people including employees,
portal users and anonymous people users have to use the portal view and
dedicated routes instead of accessing data through the backend.

In this merge we also expand the access options of surveys. Surveys can be
public, limited to authenticated people, limited to employees or on invitation
only. Invite wizard has been redone to simplify it and better support those
options.

For more details about the content please have a look at each subcommits.

This merge is linked to task ID 1911586 and PR #28986.
This commit is contained in:
Robot Odoo
2019-01-15 13:23:56 +01:00
committed by GitHub
32 changed files with 1417 additions and 715 deletions
+1 -1
View File
@@ -12,9 +12,9 @@
'depends': ['survey', 'hr_recruitment'],
'data': [
'security/hr_recruitment_survey_security.xml',
'security/ir.model.access.csv',
'views/hr_job_views.xml',
'views/hr_applicant_views.xml',
'views/survey_survey_views.xml',
'views/res_config_setting_views.xml',
],
'demo': [
@@ -3,7 +3,8 @@
<record id="survey_recruitment_form" model="survey.survey">
<field name="title">Recruitment Form</field>
<field name="stage_id" ref="survey.stage_permanent"/>
<field name="auth_required" eval="True"/>
<field name="access_mode">token</field>
<field name="category">hr_recruitment</field>
<field name="users_can_go_back" eval="True"/>
<field name="description" type="html">
<p>
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_job
from . import hr_applicant
from . import survey_survey
@@ -6,7 +6,7 @@ from odoo import api, fields, models
class Applicant(models.Model):
_inherit = "hr.applicant"
survey_id = fields.Many2one('survey.survey', related='job_id.survey_id', string="Survey", readonly=False)
survey_id = fields.Many2one('survey.survey', related='job_id.survey_id', string="Survey", readonly=True)
response_id = fields.Many2one('survey.user_input', "Response", ondelete="set null", oldname="response")
@api.multi
@@ -8,6 +8,7 @@ class Job(models.Model):
survey_id = fields.Many2one(
'survey.survey', "Interview Form",
domain=[('category', '=', 'hr_recruitment')],
help="Choose an interview form for this job position and you will be able to print/answer this interview from all applicants who apply for this job")
@api.multi
@@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class Survey(models.Model):
_inherit = 'survey.survey'
category = fields.Selection(selection_add=[('hr_recruitment', 'Recruitment')])
@@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_survey_hr_user,survey.hr.user,survey.model_survey_survey,hr_recruitment.group_hr_recruitment_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_survey_hr_user survey.hr.user survey.model_survey_survey hr_recruitment.group_hr_recruitment_user 1 1 1 0
@@ -6,9 +6,10 @@
<field name="inherit_id" ref="hr_recruitment.hr_job_survey"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='address_id']" position="before">
<label for="survey_id" groups="base.group_user"/>
<div groups="base.group_user" class="o_row">
<field name="survey_id"/>
<label for="survey_id" groups="survey.group_survey_user"/>
<div groups="survey.group_survey_user" class="o_row">
<field name="survey_id"
context="{'default_category': 'hr_recruitment', 'default_access_mode': 'token'}"/>
<button string="Display Interview Form" name="action_print_survey" type="object" attrs="{'invisible':[('survey_id','=',False)]}" class="oe_link"/>
</div>
</xpath>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="survey_survey_view_form" model="ir.ui.view">
<field name="name">survey.survey.view.form.inherit.hr_recruitment</field>
<field name="model">survey.survey</field>
<field name="inherit_id" ref="survey.survey_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='category']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
</field>
</record>
</data></odoo>
+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',
+215 -153
View File
@@ -8,6 +8,7 @@ from datetime import datetime
from math import ceil
from odoo import fields, http, SUPERUSER_ID
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools import ustr
@@ -16,153 +17,209 @@ _logger = logging.getLogger(__name__)
class Survey(http.Controller):
def _check_bad_cases(self, survey, token=None):
# In case of bad survey, redirect to surveys list
if not survey.sudo().exists():
return werkzeug.utils.redirect("/survey/")
def _fetch_from_access_token(self, survey_id, access_token):
""" Check that given token matches an answer from the given survey_id.
Returns a sudo-ed browse record of survey in order to avoid access rights
issues now that access is granted through token. """
survey_sudo = request.env['survey.survey'].with_context(active_test=False).sudo().browse(survey_id)
if not access_token:
answer_sudo = request.env['survey.user_input'].sudo()
else:
answer_sudo = request.env['survey.user_input'].sudo().search([
('survey_id', '=', survey_sudo.id),
('token', '=', access_token)
], limit=1)
return survey_sudo, answer_sudo
# In case of auth required, block public user
if survey.auth_required and request.env.user._is_public():
return request.render("survey.auth_required", {'survey': survey, 'token': token})
def _check_validity(self, survey_id, access_token, ensure_token=True):
""" Check survey is open and can be taken. This does not checks for
security rules, only functional / business rules. It returns a string key
allowing further manipulation of validity issues
# In case of non open surveys
if survey.stage_id.closed:
return request.render("survey.notopen")
* survey_wrong: survey does not exist;
* survey_auth: authentication is required;
* survey_closed: survey is closed and does not accept input anymore;
* survey_void: survey is void and should not be taken;
* token_wrong: given token not recognized;
* token_required: no token given although it is necessary to access the
survey;
* answer_deadline: token linked to an expired answer;
# If there is no pages
if not survey.page_ids:
return request.render("survey.nopages", {'survey': survey})
:param ensure_token: whether user input existence based on given access token
should be enforced or not, depending on the route requesting a token or
allowing external world calls;
"""
survey_sudo, answer_sudo = self._fetch_from_access_token(survey_id, access_token)
# Everything seems to be ok
return None
if not survey_sudo.exists():
return 'survey_wrong'
def _check_deadline(self, user_input):
'''Prevent opening of the survey if the deadline has turned out
if access_token and not answer_sudo:
return 'token_wrong'
! This will NOT disallow access to users who have already partially filled the survey !'''
deadline = user_input.deadline
if deadline:
dt_deadline = fields.Datetime.from_string(deadline)
if not answer_sudo and ensure_token:
return 'token_required'
if not answer_sudo and survey_sudo.access_mode == 'token':
return 'token_required'
# Public -> no auth required; Token -> token check hereabove
if survey_sudo.access_mode not in ['public', 'token'] and request.env.user._is_public():
return 'survey_auth'
if survey_sudo.is_closed or not survey_sudo.active:
return 'survey_closed'
if not survey_sudo.page_ids:
return 'survey_void'
# In case of delayed deadline # TDE FIXME
if answer_sudo and answer_sudo.deadline:
dt_now = datetime.now()
if dt_now > dt_deadline: # survey is not open anymore
return request.render("survey.notopen")
return None
if dt_now > answer_sudo.deadline:
return 'answer_deadline'
@http.route('/survey/test/<model("survey.survey"):survey>', type='http', auth='user', website=True)
def survey_test(self, survey, token=None, **kwargs):
return True
def _get_access_data(self, survey_id, access_token, ensure_token=True):
""" Get back data related to survey and user input, given the ID and access
token provided by the route.
: param ensure_token: whether user input existence should be enforced or not(see ``_check_validity``)
"""
survey_sudo, answer_sudo = request.env['survey.survey'].sudo(), request.env['survey.user_input'].sudo()
has_survey_access, can_answer = False, False
validity_code = self._check_validity(survey_id, access_token, ensure_token=ensure_token)
if validity_code != 'survey_wrong':
survey_sudo, answer_sudo = self._fetch_from_access_token(survey_id, access_token)
try:
survey_user = survey_sudo.sudo(request.env.user)
survey_user.check_access_rights(self, 'read', raise_exception=True)
survey_user.check_access_rule(self, 'read')
except:
pass
else:
has_survey_access = True
can_answer = bool(answer_sudo)
if not can_answer:
can_answer = survey_sudo.access_mode == 'public'
return {
'survey_sudo': survey_sudo,
'answer_sudo': answer_sudo,
'has_survey_access': has_survey_access,
'can_answer': can_answer,
'validity_code': validity_code,
}
def _redirect_with_error(self, access_data, error_key):
survey_sudo = access_data['survey_sudo']
answer_sudo = access_data['answer_sudo']
if error_key == 'survey_void' and access_data['can_answer']:
return request.render("survey.survey_void", {'survey': survey_sudo})
elif error_key == 'survey_closed' and access_data['can_answer']:
return request.render("survey.survey_expired", {'survey': survey_sudo})
elif error_key == 'survey_auth' and answer_sudo.token:
return request.render("survey.auth_required", {'survey': survey_sudo, 'token': answer_sudo.token})
elif error_key == 'answer_deadline' and answer_sudo.token:
return request.render("survey.survey_expired", {'survey': survey_sudo})
return werkzeug.utils.redirect("/")
@http.route('/survey/test/<int:survey_id>', type='http', auth='user', website=True)
def survey_test(self, survey_id, token=None, **kwargs):
""" Test mode for surveys: create a test answer, only for managers or officers
testing their surveys """
if request.env.user.has_group('survey.group_survey_manager') or \
request.env.user.has_group('survey.group_survey_user') and survey.create_uid == request.env.user:
user_input = request.env['survey.user_input'].create({
'survey_id': survey.id,
'test_entry': True
})
return request.redirect('/survey/start/%s/%s' % (survey.id, user_input.token))
return werkzeug.utils.redirect('/')
survey_sudo = request.env['survey.survey'].sudo().browse(survey_id)
try:
answer_sudo = survey_sudo._create_answer(user=request.env.user, test_entry=True)
except:
return werkzeug.utils.redirect('/')
return request.redirect('/survey/start/%s?token=%s' % (survey_sudo.id, answer_sudo.token))
@http.route(['/survey/start/<model("survey.survey"):survey>',
'/survey/start/<model("survey.survey"):survey>/<string:token>'],
type='http', auth='public', website=True)
def survey_start(self, survey, token=None, **post):
@http.route('/survey/start/<int:survey_id>', type='http', auth='public', website=True)
def survey_start(self, survey_id, token=None, email=False):
""" Start a survey by providing a token linked to an answer or generate
a new token if access is allowed """
UserInput = request.env['survey.user_input']
access_data = self._get_access_data(survey_id, token, ensure_token=False)
if access_data['validity_code'] is not True:
return self._redirect_with_error(access_data, access_data['validity_code'])
# Controls if the survey can be displayed
errpage = self._check_bad_cases(survey, token=token)
if errpage:
return errpage
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
if not answer_sudo:
try:
answer_sudo = survey_sudo._create_answer(user=request.env.user, email=email)
except UserError:
answer_sudo = False
# Manual surveying
if not token:
vals = {'survey_id': survey.id}
if not request.env.user._is_public():
vals['partner_id'] = request.env.user.partner_id.id
user_input = UserInput.create(vals)
else:
user_input = UserInput.sudo().search([('token', '=', token)], limit=1)
if not user_input:
return request.render("survey.403", {'survey': survey})
# Do not open expired survey
errpage = self._check_deadline(user_input)
if errpage:
return errpage
if not answer_sudo:
try:
survey_sudo.sudo(request.env.user).check_access_rights('read')
survey_sudo.sudo(request.env.user).check_access_rule('read')
except:
return werkzeug.utils.redirect("/")
else:
return request.render("survey.403", {'survey': survey_sudo})
# Select the right page
if user_input.state == 'new': # Intro page
data = {'survey': survey, 'page': None, 'token': user_input.token}
if answer_sudo.state == 'new': # Intro page
data = {'survey': survey_sudo, 'page': None, 'token': answer_sudo.token}
return request.render('survey.survey_init', data)
else:
return request.redirect('/survey/fill/%s/%s' % (survey.id, user_input.token))
return request.redirect('/survey/fill/%s/%s' % (survey_sudo.id, answer_sudo.token))
# Survey displaying
@http.route(['/survey/fill/<model("survey.survey"):survey>/<string:token>',
'/survey/fill/<model("survey.survey"):survey>/<string:token>/<string:prev>'],
type='http', auth='public', website=True)
def fill_survey(self, survey, token, prev=None, **post):
'''Display and validates a survey'''
Survey = request.env['survey.survey']
UserInput = request.env['survey.user_input']
@http.route('/survey/fill/<int:survey_id>/<string:token>', type='http', auth='public', website=True)
def survey_display_page(self, survey_id, token, prev=None):
access_data = self._get_access_data(survey_id, token, ensure_token=True)
if access_data['validity_code'] is not True:
return self._redirect_with_error(access_data, access_data['validity_code'])
# Controls if the survey can be displayed
errpage = self._check_bad_cases(survey)
if errpage:
return errpage
# Load the user_input
user_input = UserInput.sudo().search([('token', '=', token)], limit=1)
if not user_input: # Invalid token
return request.render("survey.403", {'survey': survey})
# Do not display expired survey (even if some pages have already been
# displayed -- There's a time for everything!)
errpage = self._check_deadline(user_input)
if errpage:
return errpage
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
# Select the right page
if user_input.state == 'new': # First page
page, page_nr, last = Survey.next_page(user_input, 0, go_back=False)
data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
if answer_sudo.state == 'new': # First page
page, page_nr, last = survey_sudo.next_page(answer_sudo, 0, go_back=False)
data = {'survey': survey_sudo, 'page': page, 'page_nr': page_nr, 'token': answer_sudo.token}
if last:
data.update({'last': True})
return request.render('survey.survey', data)
elif user_input.state == 'done': # Display success message
return request.render('survey.sfinished', {'survey': survey,
'token': token,
'user_input': user_input})
elif user_input.state == 'skip':
elif answer_sudo.state == 'done': # Display success message
return request.render('survey.sfinished', {'survey': survey_sudo,
'token': token,
'user_input': answer_sudo})
elif answer_sudo.state == 'skip':
flag = (True if prev and prev == 'prev' else False)
page, page_nr, last = Survey.next_page(user_input, user_input.last_displayed_page_id.id, go_back=flag)
page, page_nr, last = survey_sudo.next_page(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=flag)
#special case if you click "previous" from the last page, then leave the survey, then reopen it from the URL, avoid crash
if not page:
page, page_nr, last = Survey.next_page(user_input, user_input.last_displayed_page_id.id, go_back=True)
page, page_nr, last = survey_sudo.next_page(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=True)
data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
data = {'survey': survey_sudo, 'page': page, 'page_nr': page_nr, 'token': answer_sudo.token}
if last:
data.update({'last': True})
return request.render('survey.survey', data)
else:
return request.render("survey.403", {'survey': survey})
return request.render("survey.403", {'survey': survey_sudo})
# AJAX prefilling of a survey
@http.route(['/survey/prefill/<model("survey.survey"):survey>/<string:token>',
'/survey/prefill/<model("survey.survey"):survey>/<string:token>/<model("survey.page"):page>'],
type='http', auth='public', website=True)
def prefill(self, survey, token, page=None, **post):
UserInputLine = request.env['survey.user_input_line']
ret = {}
@http.route('/survey/prefill/<int:survey_id>/<string:token>', type='http', auth='public', website=True)
def survey_get_answers(self, survey_id, token, page_id=None):
""" TDE NOTE: original comment: # AJAX prefilling of a survey -> AJAX / http ?? """
access_data = self._get_access_data(survey_id, token, ensure_token=True)
if access_data['validity_code'] is not True:
return {}
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
# Fetch previous answers
if page:
previous_answers = UserInputLine.sudo().search([('user_input_id.token', '=', token), ('page_id', '=', page.id)])
if page_id:
previous_answers = answer_sudo.user_input_line_ids.filtered(lambda line: line.page_id.id == page_id)
else:
previous_answers = UserInputLine.sudo().search([('user_input_id.token', '=', token)])
previous_answers = answer_sudo.user_input_line_ids
# Return non empty answers in a JSON compatible format
ret = {}
for answer in previous_answers:
if not answer.skipped:
answer_tag = '%s_%s_%s' % (answer.survey_id.id, answer.page_id.id, answer.question_id.id)
@@ -190,32 +247,41 @@ class Survey(http.Controller):
_logger.warning("[survey] No answer has been found for question %s marked as non skipped" % answer_tag)
return json.dumps(ret, default=str)
# AJAX scores loading for quiz correction mode
@http.route(['/survey/scores/<model("survey.survey"):survey>/<string:token>'],
type='http', auth='public', website=True)
def get_scores(self, survey, token, page=None, **post):
ret = {}
@http.route('/survey/scores/<int:survey_id>/<string:token>', type='http', auth='public', website=True)
def survey_get_scores(self, survey_id, token, page_id=None):
""" TDE NOTE: original comment: # AJAX scores loading for quiz correction mode -> AJAX / http ?? """
access_data = self._get_access_data(survey_id, token, ensure_token=True)
if access_data['validity_code'] is not True:
return {}
# Fetch answers
previous_answers = request.env['survey.user_input_line'].sudo().search([('user_input_id.token', '=', token)])
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
# Compute score for each question
for answer in previous_answers:
ret = {}
for answer in answer_sudo.user_input_line_ids:
tmp_score = ret.get(answer.question_id.id, 0.0)
ret.update({answer.question_id.id: tmp_score + answer.quizz_mark})
return json.dumps(ret)
# AJAX submission of a page
@http.route(['/survey/submit/<model("survey.survey"):survey>'], type='http', methods=['POST'], auth='public', website=True)
def submit(self, survey, **post):
_logger.debug('Incoming data: %s', post)
@http.route('/survey/submit/<int:survey_id>/<string:token>', type='http', methods=['POST'], auth='public', website=True)
def survey_submit(self, survey_id, token, **post):
""" TDE NOTE: original comment: # AJAX submission of a page -> AJAX / http ?? """
access_data = self._get_access_data(survey_id, token, ensure_token=True)
if access_data['validity_code'] is not True:
return {}
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
page_id = int(post['page_id'])
questions = request.env['survey.question'].search([('page_id', '=', page_id)])
questions = request.env['survey.question'].sudo().search([
('survey_id', '=', survey_id),
('page_id', '=', page_id)
])
# Answer validation
errors = {}
for question in questions:
answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
answer_tag = "%s_%s_%s" % (survey_id, page_id, question.id)
errors.update(question.validate_question(post, answer_tag))
ret = {}
@@ -223,46 +289,42 @@ class Survey(http.Controller):
# Return errors messages to webpage
ret['errors'] = errors
else:
# Store answers into database
try:
user_input = request.env['survey.user_input'].sudo().search([('token', '=', post['token'])], limit=1)
except KeyError: # Invalid token
return request.render("survey.403", {'survey': survey})
user_id = request.env.user.id if user_input.input_type != 'link' else SUPERUSER_ID
for question in questions:
answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
request.env['survey.user_input_line'].sudo(user=user_id).save_lines(user_input.id, question, post, answer_tag)
answer_tag = "%s_%s_%s" % (survey_id, page_id, question.id)
request.env['survey.user_input_line'].sudo().save_lines(answer_sudo.id, question, post, answer_tag)
go_back = post['button_submit'] == 'previous'
next_page, _, last = request.env['survey.survey'].next_page(user_input, page_id, go_back=go_back)
next_page, _, last = request.env['survey.survey'].next_page(answer_sudo, page_id, go_back=go_back)
vals = {'last_displayed_page_id': page_id}
if next_page is None and not go_back:
vals.update({'state': 'done'})
else:
vals.update({'state': 'skip'})
user_input.sudo(user=user_id).write(vals)
ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, post['token'])
answer_sudo.write(vals)
ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.id, token)
if go_back:
ret['redirect'] += '/prev'
ret['redirect'] += '?prev=prev'
return json.dumps(ret)
# Printing routes
@http.route(['/survey/print/<model("survey.survey"):survey>',
'/survey/print/<model("survey.survey"):survey>/<string:token>'],
type='http', auth='public', website=True)
def print_survey(self, survey, token=None, **post):
@http.route('/survey/print/<int:survey_id>', type='http', auth='public', website=True)
def survey_print(self, survey_id, token=None, **post):
'''Display an survey in printable view; if <token> is set, it will
grab the answers of the user_input_id that has <token>.'''
return request.render('survey.survey_print',
{'survey': survey,
'token': token,
'page_nr': 0,
'quizz_correction': True if survey.quizz_mode and token else False})
access_data = self._get_access_data(survey_id, token, ensure_token=False)
if access_data['validity_code'] is not True and (
not access_data['has_survey_access'] or access_data['validity_code'] not in ['token_required', 'survey_closed', 'survey_void']):
return self._redirect_with_error(access_data, access_data['validity_code'])
@http.route(['/survey/results/<model("survey.survey"):survey>'],
type='http', auth='user', website=True)
def survey_reporting(self, survey, token=None, **post):
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
return request.render('survey.survey_print', {
'survey': survey_sudo,
'token': token,
'page_nr': 0,
'quizz_correction': True if survey_sudo.quizz_mode and token else False})
@http.route('/survey/results/<model("survey.survey"):survey>', type='http', auth='user', website=True)
def survey_report(self, survey, token=None, **post):
'''Display survey Results & Statistics for given survey.'''
result_template = 'survey.result'
current_filters = []
@@ -275,12 +337,12 @@ class Survey(http.Controller):
post.pop('finished')
filter_finish = True
if post or filter_finish:
filter_data = self.get_filter_data(post)
filter_data = self._get_filter_data(post)
current_filters = survey.filter_input_ids(filter_data, filter_finish)
filter_display_data = survey.get_filter_display_data(filter_data)
return request.render(result_template,
{'survey': survey,
'survey_dict': self.prepare_result_dict(survey, current_filters),
'survey_dict': self._prepare_result_dict(survey, current_filters),
'page_range': self.page_range,
'current_filters': current_filters,
'filter_display_data': filter_display_data,
@@ -324,7 +386,7 @@ class Survey(http.Controller):
# filter_finish: boolean => only finished surveys or not
#
def prepare_result_dict(self, survey, current_filters=None):
def _prepare_result_dict(self, survey, current_filters=None):
"""Returns dictionary having values for rendering template"""
current_filters = current_filters if current_filters else []
Survey = request.env['survey.survey']
@@ -336,14 +398,14 @@ class Survey(http.Controller):
'question': question,
'input_summary': Survey.get_input_summary(question, current_filters),
'prepare_result': Survey.prepare_result(question, current_filters),
'graph_data': self.get_graph_data(question, current_filters),
'graph_data': self._get_graph_data(question, current_filters),
}
page_dict['question_ids'].append(question_dict)
result['page_ids'].append(page_dict)
return result
def get_filter_data(self, post):
def _get_filter_data(self, post):
"""Returns data used for filtering the result"""
filters = []
for ids in post:
@@ -360,9 +422,9 @@ class Survey(http.Controller):
total = ceil(total_record / float(limit))
return range(1, int(total + 1))
def get_graph_data(self, question, current_filters=None):
def _get_graph_data(self, question, current_filters=None):
'''Returns formatted data required by graph library on basis of filter'''
# TODO refactor this terrible method and merge it with prepare_result_dict
# TODO refactor this terrible method and merge it with _prepare_result_dict
current_filters = current_filters if current_filters else []
Survey = request.env['survey.survey']
result = []
+8 -7
View File
@@ -1,22 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="email_template_survey" model="mail.template">
<record id="mail_template_user_input_invite" model="mail.template">
<field name="name">Survey: Send by email</field>
<field name="model_id" ref="model_survey_survey" />
<field name="subject">${object.title}: Survey</field>
<field name="model_id" ref="model_survey_user_input" />
<field name="subject">Participate to ${object.survey_id.title} survey</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px; font-size: 13px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello<br /><br />
We are conducting a survey, and your response would be appreciated.
Dear ${object.partner_id.name or 'participant'}<br/><br/>
We are conducting a survey and your response would be appreciated.
<div style="margin: 16px 0px 16px 0px;">
<a href="__URL__"
<a href="${('%s?token=%s' % (object.survey_id.public_url, object.token)) | safe}"
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
Start Survey
</a>
</div>
Thanks for your participation!
Please answer the appraisal for ${format_date(object.deadline)}.<br/><br/>
Thank you for your participation.
</p>
</div>
</field>
+2 -2
View File
@@ -4,7 +4,7 @@
<record model="survey.survey" id="survey_feedback">
<field name="title">User Feedback Form</field>
<field name="stage_id" ref="survey.stage_in_progress" />
<field name="auth_required" eval="False" />
<field name="access_mode">public</field>
<field name="users_can_go_back" eval="True" />
<field name="description" type="html">
<p>This survey allows you to give a feedback about your experience with our eCommerce solution.
@@ -229,4 +229,4 @@
<field name="state">skip</field>
</record>
</data></odoo>
</data></odoo>
+2 -10
View File
@@ -5,21 +5,13 @@ from odoo import fields, models
class SurveyPage(models.Model):
""" A page for a survey.
Pages are essentially containers, allowing to group questions by ordered
screens.
.. note::
A page should be deleted if the survey it belongs to is deleted.
"""
""" A page for a survey. Pages are essentially containers, allowing to group questions by ordered
screens. """
_name = 'survey.page'
_description = 'Survey Page'
_rec_name = 'title'
_order = 'sequence,id'
# Model Fields #
title = fields.Char('Page Title', required=True, translate=True)
survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade', required=True)
question_ids = fields.One2many('survey.question', 'page_id', string='Questions', copy=True)
+42 -57
View File
@@ -20,72 +20,56 @@ def dict_keys_startswith(dictionary, string):
class SurveyQuestion(models.Model):
""" Questions that will be asked in a survey.
Each question can have one of more suggested answers (eg. in case of
dropdown choices, multi-answer checkboxes, radio buttons...).
"""
""" Questions that will be asked in a survey. Each question can have one of more suggested answers (eg. in case of
dropdown choices, multi-answer checkboxes, radio buttons...). """
_name = 'survey.question'
_description = 'Survey Question'
_rec_name = 'question'
_order = 'sequence,id'
# Model fields #
# Question metadata
page_id = fields.Many2one('survey.page', string='Survey page',
ondelete='cascade', required=True, default=lambda self: self.env.context.get('page_id'))
page_id = fields.Many2one(
'survey.page', string='Survey page',
ondelete='cascade', required=True, default=lambda self: self.env.context.get('page_id'))
survey_id = fields.Many2one('survey.survey', related='page_id.survey_id', string='Survey', readonly=False)
sequence = fields.Integer('Sequence', default=10)
# Question
question = fields.Char('Question Name', required=True, translate=True)
description = fields.Html('Description', help="Use this field to add \
additional explanations about your question", translate=True,
oldname='descriptive_text')
# Answer
description = fields.Html('Description', help="Use this field to add additional explanations about your question", translate=True)
question_type = fields.Selection([
('free_text', 'Multiple Lines Text Box'),
('textbox', 'Single Line Text Box'),
('numerical_box', 'Numerical Value'),
('date', 'Date'),
('simple_choice', 'Multiple choice: only one answer'),
('multiple_choice', 'Multiple choice: multiple answers allowed'),
('matrix', 'Matrix')], string='Type of Question', default='free_text', required=True, oldname='type')
matrix_subtype = fields.Selection([('simple', 'One choice per row'),
('free_text', 'Multiple Lines Text Box'),
('textbox', 'Single Line Text Box'),
('numerical_box', 'Numerical Value'),
('date', 'Date'),
('simple_choice', 'Multiple choice: only one answer'),
('multiple_choice', 'Multiple choice: multiple answers allowed'),
('matrix', 'Matrix')], string='Type of Question',
default='free_text', required=True, oldname='type')
# simple choice / multiple choice / matrix
labels_ids = fields.One2many(
'survey.label', 'question_id', string='Types of answers', copy=True,
help='Labels used for proposed choices: simple choice, multiple choice and columns of matrix')
# matrix
matrix_subtype = fields.Selection([
('simple', 'One choice per row'),
('multiple', 'Multiple choices per row')], string='Matrix Type', default='simple')
labels_ids = fields.One2many('survey.label', 'question_id', string='Types of answers', oldname='answer_choice_ids', copy=True)
labels_ids_2 = fields.One2many('survey.label', 'question_id_2', string='Rows of the Matrix', copy=True)
# labels are used for proposed choices
# if question.type == simple choice | multiple choice
# -> only labels_ids is used
# if question.type == matrix
# -> labels_ids are the columns of the matrix
# -> labels_ids_2 are the rows of the matrix
labels_ids_2 = fields.One2many(
'survey.label', 'question_id_2', string='Rows of the Matrix', copy=True,
help='Labels used for proposed choices: rows of matrix')
# Display options
column_nb = fields.Selection([('12', '1'),
('6', '2'),
('4', '3'),
('3', '4'),
('2', '6')],
'Number of columns', default='12')
# These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap
display_mode = fields.Selection([('columns', 'Radio Buttons'),
('dropdown', 'Selection Box')],
default='columns')
column_nb = fields.Selection([
('12', '1'), ('6', '2'), ('4', '3'), ('3', '4'), ('2', '6')],
string='Number of columns', default='12',
help='These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap for dropdown-based simple and multiple choice questions.')
display_mode = fields.Selection(
[('columns', 'Radio Buttons'), ('dropdown', 'Selection Box')],
string='Display Mode', default='columns', help='Display mode of simple choice questions.')
# Comments
comments_allowed = fields.Boolean('Show Comments Field',
oldname="allow_comment")
comments_allowed = fields.Boolean('Show Comments Field')
comments_message = fields.Char('Comment Message', translate=True, default=lambda self: _("If other, please specify:"))
comment_count_as_answer = fields.Boolean('Comment Field is an Answer Choice',
oldname='make_comment_field')
comment_count_as_answer = fields.Boolean('Comment Field is an Answer Choice')
# Validation
validation_required = fields.Boolean('Validate entry', oldname='is_validation_require')
validation_required = fields.Boolean('Validate entry')
validation_email = fields.Boolean('Input must be an email')
validation_length_min = fields.Integer('Minimum Text Length')
validation_length_max = fields.Integer('Maximum Text Length')
@@ -93,13 +77,14 @@ class SurveyQuestion(models.Model):
validation_max_float_value = fields.Float('Maximum value')
validation_min_date = fields.Date('Minimum Date')
validation_max_date = fields.Date('Maximum Date')
validation_error_msg = fields.Char('Validation Error message', oldname='validation_valid_err_msg',
translate=True, default=lambda self: _("The answer you entered has an invalid format."))
validation_error_msg = fields.Char('Validation Error message', translate=True, default=lambda self: _("The answer you entered has an invalid format."))
# Constraints on number of answers (matrices)
constr_mandatory = fields.Boolean('Mandatory Answer', oldname="is_require_answer")
constr_error_msg = fields.Char('Error message', oldname='req_error_msg', translate=True, default=lambda self: _("This question requires an answer."))
user_input_line_ids = fields.One2many('survey.user_input_line', 'question_id', string='Answers', domain=[('skipped', '=', False)])
constr_mandatory = fields.Boolean('Mandatory Answer')
constr_error_msg = fields.Char('Error message', translate=True, default=lambda self: _("This question requires an answer."))
# Answer
user_input_line_ids = fields.One2many(
'survey.user_input_line', 'question_id', string='Answers',
domain=[('skipped', '=', False)], groups='survey.group_survey_user')
_sql_constraints = [
('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'),
@@ -277,9 +262,9 @@ class SurveyQuestion(models.Model):
errors.update({answer_tag: self.constr_error_msg})
return errors
class SurveyLabel(models.Model):
""" A suggested answer for a question """
_name = 'survey.label'
_rec_name = 'value'
_order = 'sequence,id'
-2
View File
@@ -5,8 +5,6 @@ from odoo import fields, models
class SurveyStage(models.Model):
"""Stages for Kanban view of surveys"""
_name = 'survey.stage'
_description = 'Survey Stage'
_order = 'sequence,id'
+98 -42
View File
@@ -11,11 +11,8 @@ from odoo.exceptions import UserError
class Survey(models.Model):
""" Settings for a multi-page/multi-question survey.
Each survey can have one or more attached pages, and each page can display
one or more questions.
"""
""" Settings for a multi-page/multi-question survey. Each survey can have one or more attached pages
and each page can display one or more questions. """
_name = 'survey.survey'
_description = 'Survey'
_rec_name = 'title'
@@ -24,36 +21,43 @@ class Survey(models.Model):
def _default_stage(self):
return self.env['survey.stage'].search([], limit=1).id
# description
title = fields.Char('Title', required=True, translate=True)
page_ids = fields.One2many('survey.page', 'survey_id', string='Pages', copy=True)
stage_id = fields.Many2one('survey.stage', string="Stage", default=_default_stage,
ondelete="restrict", copy=False, group_expand='_read_group_stage_ids')
auth_required = fields.Boolean('Login required', help="Users with a public link will be requested to login before taking part to the survey",
oldname="authenticate")
users_can_go_back = fields.Boolean('Users can go back', help="If checked, users can go back to previous pages.")
tot_sent_survey = fields.Integer("Number of sent surveys", compute="_compute_survey_statistic")
tot_start_survey = fields.Integer("Number of started surveys", compute="_compute_survey_statistic")
tot_comp_survey = fields.Integer("Number of completed surveys", compute="_compute_survey_statistic")
description = fields.Html("Description", translate=True, help="A long description of the purpose of the survey")
color = fields.Integer('Color Index', default=0)
user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True)
designed = fields.Boolean("Is designed?", compute="_is_designed")
public_url = fields.Char("Public link", compute="_compute_survey_url")
public_url_html = fields.Char("Public link (html version)", compute="_compute_survey_url")
print_url = fields.Char("Print link", compute="_compute_survey_url")
result_url = fields.Char("Results link", compute="_compute_survey_url")
email_template_id = fields.Many2one('mail.template', string='Email Template', ondelete='set null')
thank_you_message = fields.Html("Thanks Message", translate=True, help="This message will be displayed when survey is completed")
quizz_mode = fields.Boolean("Quizz Mode")
active = fields.Boolean("Active", default=True)
is_closed = fields.Boolean("Is closed", related='stage_id.closed', readonly=False)
stage_id = fields.Many2one('survey.stage', string="Stage", default=_default_stage,
ondelete="restrict", copy=False, group_expand='_read_group_stage_ids')
is_closed = fields.Boolean("Is closed", related='stage_id.closed', readonly=True)
category = fields.Selection([
('default', 'Generic Survey')], string='Category',
default='default', required=True,
help='Category is used to know in which context the survey is used. Various apps may define their own categories when they use survey like jobs recruitment or employee appraisal surveys.')
# content
page_ids = fields.One2many('survey.page', 'survey_id', string='Pages', copy=True)
user_input_ids = fields.One2many('survey.user_input', 'survey_id', string='User responses', readonly=True, groups='survey.group_survey_user')
# security / access
access_mode = fields.Selection([
('public', 'Everyone'),
('authentication', 'Login Required'),
('internal', 'Employees Only'),
('token', 'Invitation only')], string='Access Mode',
default='authentication', required=True)
users_can_go_back = fields.Boolean('Users can go back', help="If checked, users can go back to previous pages.")
users_can_signup = fields.Boolean('Users can signup', compute='_compute_users_can_signup')
public_url = fields.Char("Public link", compute="_compute_survey_url")
# statistics
tot_sent_survey = fields.Integer("Number of sent surveys", compute="_compute_survey_statistic")
tot_start_survey = fields.Integer("Number of started surveys", compute="_compute_survey_statistic")
tot_comp_survey = fields.Integer("Number of completed surveys", compute="_compute_survey_statistic")
def _is_designed(self):
@api.multi
def _compute_users_can_signup(self):
signup_allowed = self.env['res.users'].sudo()._get_signup_invitation_scope() == 'b2c'
for survey in self:
if not survey.page_ids or not [page.question_ids for page in survey.page_ids if page.question_ids]:
survey.designed = False
else:
survey.designed = True
survey.users_can_signup = signup_allowed
@api.multi
def _compute_survey_statistic(self):
@@ -70,13 +74,9 @@ class Survey(models.Model):
def _compute_survey_url(self):
""" Computes a public URL for the survey """
base_url = '/' if self.env.context.get('relative_url') else \
self.env['ir.config_parameter'].sudo().get_param('web.base.url')
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for survey in self:
survey.public_url = urls.url_join(base_url, "survey/start/%s" % (slug(survey)))
survey.print_url = urls.url_join(base_url, "survey/print/%s" % (slug(survey)))
survey.result_url = urls.url_join(base_url, "survey/results/%s" % (slug(survey)))
survey.public_url_html = '<a href="%s">%s</a>' % (survey.public_url, _("Click here to start survey"))
survey.public_url = urls.url_join(base_url, "survey/start/%s" % (survey.id))
@api.model
def _read_group_stage_ids(self, stages, domain, order):
@@ -92,6 +92,65 @@ class Survey(models.Model):
default = dict(default or {}, title=title)
return super(Survey, self).copy_data(default)
@api.multi
def _create_answer(self, user=False, partner=False, email=False, test_entry=False, **additional_vals):
""" Main entry point to get a token back or create a new one. This method
does check for current user access in order to explicitely validate
security.
:param user: target user asking for a token; it might be void or a
public user in which case an email is welcomed;
:param email: email of the person asking the token is no user exists;
"""
self.check_access_rights('read')
self.check_access_rule('read')
tokens = self.env['survey.user_input']
for survey in self:
if partner and not user and partner.user_ids:
user = partner.user_ids[0]
survey._check_answer_creation(user, partner, email, test_entry=test_entry)
answer_vals = {
'survey_id': survey.id,
'test_entry': test_entry,
}
if user and not user._is_public:
answer_vals['partner_id'] = user.partner_id.id
answer_vals['email'] = user.email
elif partner:
answer_vals['partner_id'] = partner.id
answer_vals['email'] = partner.email
else:
answer_vals['email'] = email
answer_vals.update(additional_vals)
tokens += tokens.create(answer_vals)
return tokens
@api.multi
def _check_answer_creation(self, user, partner, email, test_entry=False):
""" Ensure conditions to create new tokens are met. """
self.ensure_one()
if test_entry:
if not user.has_group('survey.group_survey_manager') or not user.has_group('survey.group_survey_user'):
raise UserError(_('Creating test token is not allowed for you.'))
else:
if not self.active:
raise UserError(_('Creating token for archived surveys is not allowed.'))
elif self.is_closed:
raise UserError(_('Creating token for closed surveys is not allowed.'))
if self.access_mode == 'authentication':
# signup possible -> should have at least a partner to create an account
if self.users_can_signup and not user and not partner:
raise UserError(_('Creating token for external people is not allowed for surveys requesting authentication.'))
# no signup possible -> should be a not public user (employee or portal users)
if not self.users_can_signup and (not user or user._is_public()):
raise UserError(_('Creating token for external people is not allowed for surveys requesting authentication.'))
if self.access_mode == 'internal' and (not user or not user.has_group('base.group_user')):
raise UserError(_('Creating token for anybody else than employees is not allowed for internal surveys.'))
@api.model
def next_page(self, user_input, page_id, go_back=False):
""" The next page to display to the user, knowing that page_id is the id
@@ -268,12 +327,12 @@ class Survey(models.Model):
""" Open the website page with the survey form """
self.ensure_one()
token = self.env.context.get('survey_token')
trail = "/%s" % token if token else ""
trail = "?token=%s" % token if token else ""
return {
'type': 'ir.actions.act_url',
'name': "Start Survey",
'target': 'self',
'url': self.with_context(relative_url=True).public_url + trail
'url': self.public_url + trail
}
@api.multi
@@ -286,16 +345,13 @@ class Survey(models.Model):
if self.stage_id.closed:
raise UserError(_("You cannot send invitations for closed surveys."))
template = self.env.ref('survey.email_template_survey', raise_if_not_found=False)
template = self.env.ref('survey.mail_template_user_input_invite', raise_if_not_found=False)
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 {
@@ -312,12 +368,12 @@ class Survey(models.Model):
""" Open the website page with the survey printable view """
self.ensure_one()
token = self.env.context.get('survey_token')
trail = "/" + token if token else ""
trail = "?token=%s" % token if token else ""
return {
'type': 'ir.actions.act_url',
'name': "Print Survey",
'target': 'self',
'url': self.with_context(relative_url=True).print_url + trail
'url': '/survey/print/%s%s' % (self.id, trail)
}
@api.multi
@@ -328,7 +384,7 @@ class Survey(models.Model):
'type': 'ir.actions.act_url',
'name': "Results of the Survey",
'target': 'self',
'url': self.with_context(relative_url=True).result_url
'url': '/survey/results/%s' % self.id
}
@api.multi
+32 -44
View File
@@ -25,33 +25,28 @@ class SurveyUserInput(models.Model):
""" Metadata for a set of one user's answers to a particular survey """
_name = "survey.user_input"
_rec_name = 'date_create'
_rec_name = 'survey_id'
_description = 'Survey User Input'
survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='restrict')
date_create = fields.Datetime('Creation Date', default=fields.Datetime.now, required=True, readonly=True, copy=False)
deadline = fields.Datetime('Deadline', help="Date by which the person can open the survey and submit answers", oldname="date_deadline")
input_type = fields.Selection([('manually', 'Manually'), ('link', 'Link')], string='Answer Type', default='manually', required=True, readonly=True, oldname="type")
# description
survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='cascade')
input_type = fields.Selection([
('manually', 'Manually'), ('link', 'Link')],
string='Answer Type', default='manually', required=True, readonly=True,
oldname="type")
state = fields.Selection([
('new', 'Not started yet'),
('skip', 'Partially completed'),
('done', 'Completed')], string='Status', default='new', readonly=True)
test_entry = fields.Boolean(readonly=True)
# identification and access
token = fields.Char('Identification token', default=lambda self: str(uuid.uuid4()), readonly=True, required=True, copy=False)
# Optional Identification data
partner_id = fields.Many2one('res.partner', string='Partner', readonly=True)
email = fields.Char('E-mail', readonly=True)
# Displaying data
last_displayed_page_id = fields.Many2one('survey.page', string='Last displayed page')
# The answers !
# answers
user_input_line_ids = fields.One2many('survey.user_input_line', 'user_input_id', string='Answers', copy=True)
# URLs used to display the answers
result_url = fields.Char("Public link to the survey results", related='survey_id.result_url', readonly=False)
print_url = fields.Char("Public link to the empty survey", related='survey_id.print_url', readonly=False)
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)
@api.depends('user_input_line_ids.quizz_mark')
@@ -69,54 +64,47 @@ class SurveyUserInput(models.Model):
(used as a cronjob declared in data/survey_cron.xml)
"""
an_hour_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(hours=1))
self.search([('input_type', '=', 'manually'), ('state', '=', 'new'),
('date_create', '<', an_hour_ago)]).unlink()
self.search([('input_type', '=', 'manually'),
('state', '=', 'new'),
('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_view_answers(self):
def action_print_answers(self):
""" Open the website page with the survey form """
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'name': "View Answers",
'target': 'self',
'url': '%s/%s' % (self.print_url, self.token)
}
@api.multi
def action_survey_results(self):
""" Open the website page with the survey results """
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'name': "Survey Results",
'target': 'self',
'url': self.result_url
'url': '/survey/print/%s?token=%s' % (self.survey_id.id, self.token)
}
class SurveyUserInputLine(models.Model):
_name = 'survey.user_input_line'
_description = 'Survey User Input Line'
_rec_name = 'date_create'
_rec_name = 'user_input_id'
user_input_id = fields.Many2one('survey.user_input', string='User Input', ondelete='cascade', required=True)
question_id = fields.Many2one('survey.question', string='Question', ondelete='restrict', required=True)
question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade', required=True)
page_id = fields.Many2one(related='question_id.page_id', string="Page", readonly=False)
survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False)
date_create = fields.Datetime('Create Date', default=fields.Datetime.now, required=True)
skipped = fields.Boolean('Skipped')
answer_type = fields.Selection([
('text', 'Text'),
+28 -21
View File
@@ -1,22 +1,29 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_survey_public,survey.survey public,model_survey_survey,,1,0,0,0
access_survey_page_public,survey.page public,model_survey_page,,1,0,0,0
access_survey_question_public,survey.question public,model_survey_question,,1,0,0,0
access_survey_label_public,survey.label public,model_survey_label,,1,0,0,0
access_survey_user_input_public,survey.user_input public,model_survey_user_input,,1,1,1,0
access_survey_user_input_line_public,survey.user_input_line public,model_survey_user_input_line,,1,1,1,0
access_survey_stage_public,survey.stage public,model_survey_stage,,1,0,0,0
access_survey_user,survey.survey user,model_survey_survey,group_survey_user,1,0,0,0
access_survey_page_user,survey.page user,model_survey_page,group_survey_user,1,0,0,0
access_survey_question_user,survey.question user,model_survey_question,group_survey_user,1,0,0,0
access_survey_label_user,survey.label user,model_survey_label,group_survey_user,1,0,0,0
access_survey_user_input_user,survey.user_input user,model_survey_user_input,group_survey_user,1,1,1,0
access_survey_user_input_line_user,survey.user_input_line user,model_survey_user_input_line,group_survey_user,1,1,1,0
access_survey_stage_user,survey.stage user,model_survey_stage,group_survey_user,1,0,0,0
access_survey_manager,survey.survey manager,model_survey_survey,group_survey_manager,1,1,1,1
access_survey_page_manager,survey.page manager,model_survey_page,group_survey_manager,1,1,1,1
access_survey_question_manager,survey.question manager,model_survey_question,group_survey_manager,1,1,1,1
access_survey_label_manager,survey.label manager,model_survey_label,group_survey_manager,1,1,1,1
access_survey_user_input_manager,survey.user_input manager,model_survey_user_input,group_survey_manager,1,1,1,1
access_survey_user_input_line_manager,survey.user_input_line manager,model_survey_user_input_line,group_survey_manager,1,1,1,1
access_survey_stage_manager,survey.stage manager,model_survey_stage,group_survey_manager,1,1,1,1
access_survey_all,survey.survey.all,model_survey_survey,,0,0,0,0
access_survey_user,survey.survey.user,model_survey_survey,base.group_user,0,0,0,0
access_survey_survey_user,survey.survey.survey.user,model_survey_survey,group_survey_user,1,1,1,1
access_survey_survey_manager,survey.survey.survey.manager,model_survey_survey,group_survey_manager,1,1,1,1
access_survey_stage_all,survey.stage.all,model_survey_stage,,0,0,0,0
access_survey_stage_user,survey.stage.user,model_survey_stage,base.group_user,0,0,0,0
access_survey_stage_survey_user,survey.stage.survey.user,model_survey_stage,group_survey_user,1,0,0,0
access_survey_stage_survey_manager,survey.stage.survey.manager,model_survey_stage,group_survey_manager,1,1,1,1
access_survey_page_all,survey.page.all,model_survey_page,,0,0,0,0
access_survey_page_user,survey.page.user,model_survey_page,base.group_user,0,0,0,0
access_survey_page_survey_user,survey.page.survey.user,model_survey_page,group_survey_user,1,1,1,1
access_survey_page_survey_manager,survey.page.survey.manager,model_survey_page,group_survey_manager,1,1,1,1
access_survey_question_all,survey.question.all,model_survey_question,,0,0,0,0
access_survey_question_user,survey.question.user,model_survey_question,base.group_user,0,0,0,0
access_survey_question_survey_user,survey.question.survey.user,model_survey_question,group_survey_user,1,1,1,1
access_survey_question_survey_manager,survey.question.survey.manager,model_survey_question,group_survey_manager,1,1,1,1
access_survey_label_all,survey.label.all,model_survey_label,,0,0,0,0
access_survey_label_user,survey.label.user,model_survey_label,base.group_user,0,0,0,0
access_survey_label_survey_user,survey.label.survey.user,model_survey_label,group_survey_user,1,1,1,1
access_survey_label_survey_manager,survey.label.survey.manager,model_survey_label,group_survey_manager,1,1,1,1
access_survey_user_input_all,survey.user_input.all,model_survey_user_input,,0,0,0,0
access_survey_user_input_user,survey.user_input.user,model_survey_user_input,base.group_user,0,0,0,0
access_survey_user_input_survey_user,survey.user_input.survey.user,model_survey_user_input,group_survey_user,1,1,1,1
access_survey_user_input_survey_manager,survey.user_input.survey.manager,model_survey_user_input,group_survey_manager,1,1,1,1
access_survey_user_input_line_all,survey.user_input_line.all,model_survey_user_input_line,,0,0,0,0
access_survey_user_input_line_user,survey.user_input_line.user,model_survey_user_input_line,base.group_user,0,0,0,0
access_survey_user_input_line_survey_user,survey.user_input_line.survey.user,model_survey_user_input_line,group_survey_user,1,1,1,1
access_survey_user_input_line_survey_manager,survey.user_input_line.survey.manager,model_survey_user_input_line,group_survey_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_survey_public access_survey_all survey.survey public survey.survey.all model_survey_survey 1 0 0 0 0
3 access_survey_page_public access_survey_user survey.page public survey.survey.user model_survey_page model_survey_survey base.group_user 1 0 0 0 0
4 access_survey_question_public access_survey_survey_user survey.question public survey.survey.survey.user model_survey_question model_survey_survey group_survey_user 1 0 1 0 1 0 1
5 access_survey_label_public access_survey_survey_manager survey.label public survey.survey.survey.manager model_survey_label model_survey_survey group_survey_manager 1 0 1 0 1 0 1
6 access_survey_user_input_public access_survey_stage_all survey.user_input public survey.stage.all model_survey_user_input model_survey_stage 1 0 1 0 1 0 0
7 access_survey_user_input_line_public access_survey_stage_user survey.user_input_line public survey.stage.user model_survey_user_input_line model_survey_stage base.group_user 1 0 1 0 1 0 0
8 access_survey_stage_public access_survey_stage_survey_user survey.stage public survey.stage.survey.user model_survey_stage group_survey_user 1 0 0 0
9 access_survey_user access_survey_stage_survey_manager survey.survey user survey.stage.survey.manager model_survey_survey model_survey_stage group_survey_user group_survey_manager 1 0 1 0 1 0 1
10 access_survey_page_user access_survey_page_all survey.page user survey.page.all model_survey_page group_survey_user 1 0 0 0 0
11 access_survey_question_user access_survey_page_user survey.question user survey.page.user model_survey_question model_survey_page group_survey_user base.group_user 1 0 0 0 0
12 access_survey_label_user access_survey_page_survey_user survey.label user survey.page.survey.user model_survey_label model_survey_page group_survey_user 1 0 1 0 1 0 1
13 access_survey_user_input_user access_survey_page_survey_manager survey.user_input user survey.page.survey.manager model_survey_user_input model_survey_page group_survey_user group_survey_manager 1 1 1 0 1
14 access_survey_user_input_line_user access_survey_question_all survey.user_input_line user survey.question.all model_survey_user_input_line model_survey_question group_survey_user 1 0 1 0 1 0 0
15 access_survey_stage_user access_survey_question_user survey.stage user survey.question.user model_survey_stage model_survey_question group_survey_user base.group_user 1 0 0 0 0
16 access_survey_manager access_survey_question_survey_user survey.survey manager survey.question.survey.user model_survey_survey model_survey_question group_survey_manager group_survey_user 1 1 1 1
17 access_survey_page_manager access_survey_question_survey_manager survey.page manager survey.question.survey.manager model_survey_page model_survey_question group_survey_manager 1 1 1 1
18 access_survey_question_manager access_survey_label_all survey.question manager survey.label.all model_survey_question model_survey_label group_survey_manager 1 0 1 0 1 0 1 0
19 access_survey_label_manager access_survey_label_user survey.label manager survey.label.user model_survey_label group_survey_manager base.group_user 1 0 1 0 1 0 1 0
20 access_survey_user_input_manager access_survey_label_survey_user survey.user_input manager survey.label.survey.user model_survey_user_input model_survey_label group_survey_manager group_survey_user 1 1 1 1
21 access_survey_user_input_line_manager access_survey_label_survey_manager survey.user_input_line manager survey.label.survey.manager model_survey_user_input_line model_survey_label group_survey_manager 1 1 1 1
22 access_survey_stage_manager access_survey_user_input_all survey.stage manager survey.user_input.all model_survey_stage model_survey_user_input group_survey_manager 1 0 1 0 1 0 1 0
23 access_survey_user_input_user survey.user_input.user model_survey_user_input base.group_user 0 0 0 0
24 access_survey_user_input_survey_user survey.user_input.survey.user model_survey_user_input group_survey_user 1 1 1 1
25 access_survey_user_input_survey_manager survey.user_input.survey.manager model_survey_user_input group_survey_manager 1 1 1 1
26 access_survey_user_input_line_all survey.user_input_line.all model_survey_user_input_line 0 0 0 0
27 access_survey_user_input_line_user survey.user_input_line.user model_survey_user_input_line base.group_user 0 0 0 0
28 access_survey_user_input_line_survey_user survey.user_input_line.survey.user model_survey_user_input_line group_survey_user 1 1 1 1
29 access_survey_user_input_line_survey_manager survey.user_input_line.survey.manager model_survey_user_input_line group_survey_manager 1 1 1 1
+195 -43
View File
@@ -19,61 +19,213 @@
<field name="groups_id" eval="[(4,ref('group_survey_manager'))]"/>
</record>
<!-- Record rules -->
<record id="survey_users_access" model="ir.rule">
<field name="name">Access to survey for regular users</field>
<field name="model_id" ref="survey.model_survey_survey"/>
<field name="domain_force">[('stage_id.closed', '=', False)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field eval="0" name="perm_unlink"/>
<field eval="0" name="perm_write"/>
<field eval="1" name="perm_read"/>
<field eval="0" name="perm_create"/>
</record>
<record id="survey_manager_access" model="ir.rule">
<field name="name">Survey Manager access rights</field>
<!-- SURVEY: SURVEY, PAGE, STAGE, QUESTION, LABEL -->
<record id="survey_survey_rule_survey_manager" model="ir.rule">
<field name="name">Survey: manager: all</field>
<field name="model_id" ref="survey.model_survey_survey"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_manager'))]"/>
<field eval="1" name="perm_unlink"/>
<field eval="1" name="perm_write"/>
<field eval="1" name="perm_read"/>
<field eval="1" name="perm_create"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_input_public_access" model="ir.rule">
<field name="name">Public access to user_input</field>
<field name="model_id" ref="survey.model_survey_user_input"/>
<record id="survey_survey_rule_survey_user_read" model="ir.rule">
<field name="name">Survey: officer: read all</field>
<field name="model_id" ref="survey.model_survey_survey"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<record id="survey_survey_rule_survey_user_cwu" model="ir.rule">
<field name="name">Survey: officer: create/write/unlink own only</field>
<field name="model_id" ref="survey.model_survey_survey"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
<field eval="0" name="perm_unlink"/>
<field eval="0" name="perm_write"/>
<field eval="1" name="perm_read"/>
<field eval="0" name="perm_create"/>
</record>
<record id="survey_input_users_access" model="ir.rule">
<field name="name">Access to user_input for regular users</field>
<field name="model_id" ref="survey.model_survey_user_input"/>
<field name="domain_force">['|', ('create_uid', '=', user.id), ('partner_id', '=', user.partner_id.id)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field eval="0" name="perm_unlink"/>
<field eval="0" name="perm_write"/>
<field eval="1" name="perm_read"/>
<field eval="0" name="perm_create"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_input_manager_access" model="ir.rule">
<field name="name">Survey Manager access rights</field>
<record id="survey_page_rule_survey_manager" model="ir.rule">
<field name="name">Survey page: manager: all</field>
<field name="model_id" ref="survey.model_survey_page"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_manager'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_page_rule_survey_user_read" model="ir.rule">
<field name="name">Survey page: officer: read all</field>
<field name="model_id" ref="survey.model_survey_page"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<record id="survey_page_rule_survey_user_cwu" model="ir.rule">
<field name="name">Survey page: officer: create/write/unlink linked to own survey only</field>
<field name="model_id" ref="survey.model_survey_page"/>
<field name="domain_force">[('survey_id.create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_stage_rule_survey_manager" model="ir.rule">
<field name="name">Survey stage: manager: all</field>
<field name="model_id" ref="survey.model_survey_stage"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_manager'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_stage_rule_survey_user_read" model="ir.rule">
<field name="name">Survey stage: officer: read all</field>
<field name="model_id" ref="survey.model_survey_page"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<record id="survey_question_rule_survey_manager" model="ir.rule">
<field name="name">Survey question: manager: all</field>
<field name="model_id" ref="survey.model_survey_question"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_manager'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_question_rule_survey_user_read" model="ir.rule">
<field name="name">Survey question: officer: read all</field>
<field name="model_id" ref="survey.model_survey_question"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<record id="survey_question_rule_survey_user_cw" model="ir.rule">
<field name="name">Survey question: officer: create/write/unlink linked to own survey only</field>
<field name="model_id" ref="survey.model_survey_question"/>
<field name="domain_force">[('survey_id.create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_label_rule_survey_manager" model="ir.rule">
<field name="name">Survey label: manager: all</field>
<field name="model_id" ref="survey.model_survey_label"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_manager'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_label_rule_survey_user_read" model="ir.rule">
<field name="name">Survey label: officer: read all</field>
<field name="model_id" ref="survey.model_survey_label"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<record id="survey_label_rule_survey_user_cw" model="ir.rule">
<field name="name">Survey label: officer: create/write/unlink linked to own survey only</field>
<field name="model_id" ref="survey.model_survey_question"/>
<field name="domain_force">[('survey_id.create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="1"/>
</record>
<!-- SURVEY: USER_INPUT, USER_INPUT_LINE -->
<record id="survey_user_input_rule_survey_manager" model="ir.rule">
<field name="name">Survey user input: manager: all</field>
<field name="model_id" ref="survey.model_survey_user_input"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_manager'))]"/>
<field eval="1" name="perm_unlink"/>
<field eval="1" name="perm_write"/>
<field eval="1" name="perm_read"/>
<field eval="1" name="perm_create"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_user_input_rule_survey_user_read" model="ir.rule">
<field name="name">Survey user input: officer: read all</field>
<field name="model_id" ref="survey.model_survey_user_input"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<record id="survey_user_input_rule_survey_user_cw" model="ir.rule">
<field name="name">Survey user input: officer: create/write/unlink linked to own survey only</field>
<field name="model_id" ref="survey.model_survey_user_input"/>
<field name="domain_force">[('survey_id.create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_user_input_line_rule_survey_manager" model="ir.rule">
<field name="name">Survey user input line: manager: all</field>
<field name="model_id" ref="survey.model_survey_user_input_line"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_manager'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="survey_user_input_line_rule_survey_user_read" model="ir.rule">
<field name="name">Survey user input line: officer: read all</field>
<field name="model_id" ref="survey.model_survey_user_input_line"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<record id="survey_user_input_line_rule_survey_user_cw" model="ir.rule">
<field name="name">Survey user input line: officer: create/write/unlink linked to own survey only</field>
<field name="model_id" ref="survey.model_survey_user_input_line"/>
<field name="domain_force">[('user_input_id.survey_id.create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_survey_user'))]"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="1"/>
</record>
</data>
</odoo>
+1
View File
@@ -5,4 +5,5 @@ from . import common
from . import test_survey
from . import test_survey_flow
from . import test_survey_invite
from . import test_survey_security
from . import test_survey_ui
+1 -1
View File
@@ -64,7 +64,7 @@ class SurveyCase(common.SavepointCase):
self.survey = self.env['survey.survey'].sudo(self.survey_manager).create({
'title': 'Test Survey',
'auth_required': True,
'access_mode': 'authentication',
'users_can_go_back': False,
})
self.page_0 = self.env['survey.page'].sudo(self.survey_manager).create({
-16
View File
@@ -175,19 +175,3 @@ class TestSurveyInternals(common.SurveyCase):
result = self.env['survey.survey'].prepare_result(question)
for key in exresult:
self.assertEqual(result[key], exresult[key])
@users('survey_manager')
def test_survey_urls(self):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
urltypes = {'public': 'start', 'print': 'print', 'result': 'results'}
for urltype, urltxt in urltypes.items():
survey_url = getattr(self.survey, urltype + '_url')
survey_url_relative = getattr(self.survey.with_context({'relative_url': True}), urltype + '_url')
url = "survey/%s/%s" % (urltxt, slug(self.survey))
full_url = urls.url_join(base_url, url)
self.assertEqual(full_url, survey_url)
self.assertEqual('/' + url, survey_url_relative)
if urltype == 'public':
url_html = '<a href="%s">Click here to start survey</a>'
self.assertEqual(url_html % full_url, getattr(self.survey, urltype + '_url_html'), msg="Public URL is incorrect")
self.assertEqual(url_html % ('/' + url), getattr(self.survey.with_context({'relative_url': True}), urltype + '_url_html'))
+5 -5
View File
@@ -17,8 +17,8 @@ class TestSurveyFlow(common.SurveyCase, HttpCase):
def _access_page(self, survey, token):
return self.url_open('/survey/fill/%s/%s' % (survey.id, token))
def _access_submit(self, survey, post_data):
return self.url_open('/survey/submit/%s' % survey.id, data=post_data)
def _access_submit(self, survey, token, post_data):
return self.url_open('/survey/submit/%s/%s' % (survey.id, token), data=post_data)
def _find_csrf_token(self, text):
csrf_token_re = re.compile("(input.+csrf_token.+value=\")([_a-zA-Z0-9]{51})", re.MULTILINE)
@@ -47,7 +47,7 @@ class TestSurveyFlow(common.SurveyCase, HttpCase):
with self.sudo(self.survey_manager):
survey = self.env['survey.survey'].create({
'title': 'Public Survey for Tarte Al Djotte',
'auth_required': False,
'access_mode': 'public',
})
# First page is about customer data
@@ -108,7 +108,7 @@ class TestSurveyFlow(common.SurveyCase, HttpCase):
page0_q1.id: {'value': [44.0]},
}
post_data = self._format_submission_data(page_0, answer_data, {'csrf_token': csrf_token, 'token': answer_token, 'button_submit': 'next'})
r = self._access_submit(survey, post_data)
r = self._access_submit(survey, answer_token, post_data)
self.assertResponse(r, 200)
answers.invalidate_cache() # TDE note: necessary as lots of sudo in controllers messing with cache
@@ -127,7 +127,7 @@ class TestSurveyFlow(common.SurveyCase, HttpCase):
page1_q0.id: {'value': [page1_q0.labels_ids.ids[0], page1_q0.labels_ids.ids[1]]},
}
post_data = self._format_submission_data(page_1, answer_data, {'csrf_token': csrf_token, 'token': answer_token, 'button_submit': 'next'})
r = self._access_submit(survey, post_data)
r = self._access_submit(survey, answer_token, post_data)
self.assertResponse(r, 200)
answers.invalidate_cache() # TDE note: necessary as lots of sudo in controllers messing with cache
+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)
+325
View File
@@ -0,0 +1,325 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.survey.tests import common
from odoo.exceptions import AccessError, UserError
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tools import mute_logger
@tagged('security')
class TestAccess(common.SurveyCase):
def setUp(self):
super(TestAccess, self).setUp()
self.answer_0 = self._add_answer(self.survey, self.customer)
self.answer_0_0 = self._add_answer_line(self.question_ft, self.answer_0, 'Test Answer')
self.answer_0_1 = self._add_answer_line(self.question_num, self.answer_0, 5)
@mute_logger('odoo.addons.base.models.ir_model')
@users('user_emp')
def test_access_survey_employee(self):
# Create: nope
with self.assertRaises(AccessError):
self.env['survey.survey'].create({'title': 'Test Survey 2'})
with self.assertRaises(AccessError):
self.env['survey.page'].create({'title': 'My Page', 'survey_id': self.survey.id})
with self.assertRaises(AccessError):
self.env['survey.question'].create({'question': 'My Question', 'page_id': self.page_0.id})
# Read: nope
with self.assertRaises(AccessError):
self.env['survey.survey'].search([('title', 'ilike', 'Test')])
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).read(['title'])
# Write: nope
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).write({'title': 'New Title'})
with self.assertRaises(AccessError):
self.page_0.sudo(self.env.user).write({'title': 'New Title'})
with self.assertRaises(AccessError):
self.question_ft.sudo(self.env.user).write({'question': 'New Title'})
# Unlink: nope
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.page_0.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.question_ft.sudo(self.env.user).unlink()
@mute_logger('odoo.addons.base.models.ir_model')
@users('user_portal')
def test_access_survey_portal(self):
# Create: nope
with self.assertRaises(AccessError):
self.env['survey.survey'].create({'title': 'Test Survey 2'})
with self.assertRaises(AccessError):
self.env['survey.page'].create({'title': 'My Page', 'survey_id': self.survey.id})
with self.assertRaises(AccessError):
self.env['survey.question'].create({'question': 'My Question', 'page_id': self.page_0.id})
# Read: nope
with self.assertRaises(AccessError):
self.env['survey.survey'].search([('title', 'ilike', 'Test')])
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).read(['title'])
# Write: nope
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).write({'title': 'New Title'})
with self.assertRaises(AccessError):
self.page_0.sudo(self.env.user).write({'title': 'New Title'})
with self.assertRaises(AccessError):
self.question_ft.sudo(self.env.user).write({'question': 'New Title'})
# Unlink: nope
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.page_0.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.question_ft.sudo(self.env.user).unlink()
@mute_logger('odoo.addons.base.models.ir_model')
@users('user_public')
def test_access_survey_public(self):
# Create: nope
with self.assertRaises(AccessError):
self.env['survey.survey'].create({'title': 'Test Survey 2'})
with self.assertRaises(AccessError):
self.env['survey.page'].create({'title': 'My Page', 'survey_id': self.survey.id})
with self.assertRaises(AccessError):
self.env['survey.question'].create({'question': 'My Question', 'page_id': self.page_0.id})
# Read: nope
with self.assertRaises(AccessError):
self.env['survey.survey'].search([('title', 'ilike', 'Test')])
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).read(['title'])
# Write: nope
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).write({'title': 'New Title'})
with self.assertRaises(AccessError):
self.page_0.sudo(self.env.user).write({'title': 'New Title'})
with self.assertRaises(AccessError):
self.question_ft.sudo(self.env.user).write({'question': 'New Title'})
# Unlink: nope
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.page_0.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.question_ft.sudo(self.env.user).unlink()
@users('survey_manager')
def test_access_survey_survey_manager(self):
# Create: all
survey = self.env['survey.survey'].create({'title': 'Test Survey 2'})
page = self.env['survey.page'].create({'title': 'My Page', 'survey_id': survey.id})
self.env['survey.question'].create({'question': 'My Question', 'page_id': page.id})
# Read: all
surveys = self.env['survey.survey'].search([('title', 'ilike', 'Test')])
self.assertEqual(surveys, self.survey | survey)
surveys.read(['title'])
# Write: all
(self.survey | survey).write({'title': 'New Title'})
# Unlink: all
(self.survey | survey).unlink()
@mute_logger('odoo.addons.base.models.ir_model')
@users('survey_user')
def test_access_survey_survey_user(self):
# Create: own only
survey = self.env['survey.survey'].create({'title': 'Test Survey 2'})
page = self.env['survey.page'].create({'title': 'My Page', 'survey_id': survey.id})
self.env['survey.question'].create({'question': 'My Question', 'page_id': page.id})
# Read: all
surveys = self.env['survey.survey'].search([('title', 'ilike', 'Test')])
self.assertEqual(surveys, self.survey | survey)
surveys.read(['title'])
# Write: own only
survey.write({'title': 'New Title'})
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).write({'title': 'New Title'})
# Unlink: own only
survey.unlink()
with self.assertRaises(AccessError):
self.survey.sudo(self.env.user).unlink()
@mute_logger('odoo.addons.base.models.ir_model')
@users('user_emp')
def test_access_answers_employee(self):
# Create: nope
with self.assertRaises(AccessError):
self.env['survey.user_input'].create({'survey_id': self.survey.id})
with self.assertRaises(AccessError):
self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': self.answer_0.id})
# Read: nope
with self.assertRaises(AccessError):
self.env['survey.user_input'].search([('survey_id', 'in', [self.survey.id])])
with self.assertRaises(AccessError):
self.env['survey.user_input_line'].search([('survey_id', 'in', [self.survey.id])])
with self.assertRaises(AccessError):
self.env['survey.user_input'].browse(self.answer_0.ids).read(['state'])
with self.assertRaises(AccessError):
self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number'])
# Write: nope
with self.assertRaises(AccessError):
self.answer_0.sudo(self.env.user).write({'state': 'done'})
# Unlink: nope
with self.assertRaises(AccessError):
self.answer_0.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.answer_0_0.sudo(self.env.user).unlink()
@mute_logger('odoo.addons.base.models.ir_model')
@users('user_portal')
def test_access_answers_portal(self):
# Create: nope
with self.assertRaises(AccessError):
self.env['survey.user_input'].create({'survey_id': self.survey.id})
with self.assertRaises(AccessError):
self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': self.answer_0.id})
# Read: nope
with self.assertRaises(AccessError):
self.env['survey.user_input'].search([('survey_id', 'in', [self.survey.id])])
with self.assertRaises(AccessError):
self.env['survey.user_input_line'].search([('survey_id', 'in', [self.survey.id])])
with self.assertRaises(AccessError):
self.env['survey.user_input'].browse(self.answer_0.ids).read(['state'])
with self.assertRaises(AccessError):
self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number'])
# Write: nope
with self.assertRaises(AccessError):
self.answer_0.sudo(self.env.user).write({'state': 'done'})
# Unlink: nope
with self.assertRaises(AccessError):
self.answer_0.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.answer_0_0.sudo(self.env.user).unlink()
@mute_logger('odoo.addons.base.models.ir_model')
@users('user_public')
def test_access_answers_public(self):
# Create: nope
with self.assertRaises(AccessError):
self.env['survey.user_input'].create({'survey_id': self.survey.id})
with self.assertRaises(AccessError):
self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': self.answer_0.id})
# Read: nope
with self.assertRaises(AccessError):
self.env['survey.user_input'].search([('survey_id', 'in', [self.survey.id])])
with self.assertRaises(AccessError):
self.env['survey.user_input_line'].search([('survey_id', 'in', [self.survey.id])])
with self.assertRaises(AccessError):
self.env['survey.user_input'].browse(self.answer_0.ids).read(['state'])
with self.assertRaises(AccessError):
self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number'])
# Write: nope
with self.assertRaises(AccessError):
self.answer_0.sudo(self.env.user).write({'state': 'done'})
# Unlink: nope
with self.assertRaises(AccessError):
self.answer_0.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.answer_0_0.sudo(self.env.user).unlink()
@mute_logger('odoo.addons.base.models.ir_model')
@users('survey_user')
def test_access_answers_survey_user(self):
survey_own = self.env['survey.survey'].create({'title': 'Other'})
page_own = self.env['survey.page'].create({'title': 'Other', 'survey_id': survey_own.id})
question_own = self.env['survey.question'].create({'question': 'Other Question', 'page_id': page_own.id})
# Create: own survey only
answer_own = self.env['survey.user_input'].create({'survey_id': survey_own.id})
answer_line_own = self.env['survey.user_input_line'].create({'question_id': question_own.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': answer_own.id})
# Read: always
answers = self.env['survey.user_input'].search([('survey_id', 'in', [survey_own.id, self.survey.id])])
self.assertEqual(answers, answer_own | self.answer_0)
answer_lines = self.env['survey.user_input_line'].search([('survey_id', 'in', [survey_own.id, self.survey.id])])
self.assertEqual(answer_lines, answer_line_own | self.answer_0_0 | self.answer_0_1)
self.env['survey.user_input'].browse(answer_own.ids).read(['state'])
self.env['survey.user_input'].browse(self.answer_0.ids).read(['state'])
self.env['survey.user_input_line'].browse(answer_line_own.ids).read(['value_number'])
self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number'])
# Create: own survey only (moved after read because DB not correctly rollbacked with assertRaises)
with self.assertRaises(AccessError):
answer_other = self.env['survey.user_input'].create({'survey_id': self.survey.id})
with self.assertRaises(AccessError):
answer_line_other = self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': self.answer_0.id})
# Write: own survey only
answer_own.write({'state': 'done'})
with self.assertRaises(AccessError):
self.answer_0.sudo(self.env.user).write({'state': 'done'})
# Unlink: own survey only
answer_own.unlink()
answer_line_own.unlink()
with self.assertRaises(AccessError):
self.answer_0.sudo(self.env.user).unlink()
with self.assertRaises(AccessError):
self.answer_0_0.sudo(self.env.user).unlink()
@users('survey_manager')
def test_access_answers_survey_manager(self):
admin = self.env.ref('base.user_admin')
with self.sudo(admin):
survey_other = self.env['survey.survey'].create({'title': 'Other'})
page_other = self.env['survey.page'].create({'title': 'Other', 'survey_id': survey_other.id})
question_other = self.env['survey.question'].create({'question': 'Other Question', 'page_id': page_other.id})
self.assertEqual(survey_other.create_uid, admin)
self.assertEqual(question_other.create_uid, admin)
# Create: always
answer_own = self.env['survey.user_input'].create({'survey_id': self.survey.id})
answer_other = self.env['survey.user_input'].create({'survey_id': survey_other.id})
answer_line_own = self.env['survey.user_input_line'].create({'question_id': self.question_num.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': answer_own.id})
answer_line_other = self.env['survey.user_input_line'].create({'question_id': question_other.id, 'answer_type': 'number', 'value_number': 3, 'user_input_id': answer_other.id})
# Read: always
answers = self.env['survey.user_input'].search([('survey_id', 'in', [survey_other.id, self.survey.id])])
self.assertEqual(answers, answer_own | answer_other | self.answer_0)
answer_lines = self.env['survey.user_input_line'].search([('survey_id', 'in', [survey_other.id, self.survey.id])])
self.assertEqual(answer_lines, answer_line_own | answer_line_other | self.answer_0_0 | self.answer_0_1)
self.env['survey.user_input'].browse(answer_own.ids).read(['state'])
self.env['survey.user_input'].browse(self.answer_0.ids).read(['state'])
self.env['survey.user_input_line'].browse(answer_line_own.ids).read(['value_number'])
self.env['survey.user_input_line'].browse(self.answer_0_0.ids).read(['value_number'])
# Write: always
answer_own.write({'state': 'done'})
answer_other.write({'partner_id': self.env.user.partner_id.id})
# Unlink: always
(answer_own | answer_other | self.answer_0).unlink()
+3 -3
View File
@@ -6,10 +6,10 @@ import odoo.tests
class TestUi(odoo.tests.HttpCase):
def test_01_admin_survey_tour(self):
self.phantom_js("/survey/start/user-feedback-form-1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="admin")
self.phantom_js("/survey/start/1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="admin")
def test_02_demo_survey_tour(self):
self.phantom_js("/survey/start/user-feedback-form-1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="demo")
self.phantom_js("/survey/start/1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready", login="demo")
def test_03_public_survey_tour(self):
self.phantom_js("/survey/start/user-feedback-form-1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready")
self.phantom_js("/survey/start/1", "odoo.__DEBUG__.services['web_tour.tour'].run('test_survey')", "odoo.__DEBUG__.services['web_tour.tour'].tours.test_survey.ready")
+33 -36
View File
@@ -9,12 +9,11 @@
<field name="id" invisible="1"/>
<field name="tot_start_survey" invisible="1"/>
<field name="is_closed" invisible="1"/>
<header groups="survey.group_survey_manager">
<button name="action_test_survey" string="Test Survey" type="object" attrs="{'invisible': [('id', '=', False)]}"/>
<button name="action_print_survey" string="Print Survey" type="object" attrs="{'invisible': [('id', '=', False)]}"/>
<button name="action_send_survey" string="Share and invite by email" type="object" class="oe_highlight" attrs="{'invisible': [('id', '=', False)]}"/>
<button name="action_result_survey" string="View results" type="object" attrs="{'invisible': ['|',('id', '=', False), ('tot_start_survey', '!=', 0)]}"/>
<button name="action_result_survey" string="View results" type="object" class="oe_highlight" attrs="{'invisible': [('tot_start_survey', '=', 0)]}"/>
<header>
<button name="action_test_survey" string="Test" type="object" attrs="{'invisible': [('id', '=', False)]}"/>
<button name="action_print_survey" string="Print" type="object" attrs="{'invisible': [('id', '=', False)]}"/>
<button name="action_send_survey" string="Share and invite" type="object" class="oe_highlight" attrs="{'invisible': [('id', '=', False)]}"/>
<button name="action_result_survey" string="View results" type="object" class="oe_highlight" attrs="{'invisible': [('id', '=', False)]}"/>
<field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/>
</header>
<sheet>
@@ -25,7 +24,7 @@
icon="fa-pencil-square-o">
<field string="Answers" name="tot_comp_survey" widget="statinfo"/>
</button>
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive" attrs="{'invisible': [('is_closed', '=', False)]}">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
@@ -33,24 +32,25 @@
<label for="title" class="oe_edit_only"/>
<h1><field name="title" placeholder="Survey Title"/></h1>
</div>
<notebook>
<page string="Edit Pages and Questions">
<field name="page_ids" mode="tree" context="{'default_survey_id': active_id}" nolabel="1">
<tree>
<field name="sequence" widget="handle"/>
<field name="title"/>
<field name="question_ids"/>
</tree>
</field>
</page>
<page string="Options">
<group class="o_label_nowrap">
<field name="users_can_go_back" string="User can come back in the previous page"/>
<field name="auth_required"/>
<field name="quizz_mode" groups="base.group_no_one"/>
</group>
</page>
</notebook>
<group>
<group>
<field name="access_mode"/>
<field name="users_can_go_back"/>
</group>
<group>
<field name="category" invisible="1"/>
<field name="quizz_mode"/>
</group>
</group>
<group string="Edit Pages and Questions">
<field name="page_ids" mode="tree" context="{'default_survey_id': active_id}" nolabel="1">
<tree>
<field name="sequence" widget="handle"/>
<field name="title"/>
<field name="question_ids"/>
</tree>
</field>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
@@ -80,14 +80,12 @@
<field name="arch" type="xml">
<kanban default_group_by="stage_id">
<field name="stage_id" />
<field name="designed" />
<field name="title" />
<field name="tot_sent_survey" />
<field name="tot_start_survey" />
<field name="tot_comp_survey" />
<field name="color" />
<field name="auth_required" />
<field name="public_url" />
<field name="access_mode"/>
<field name="activity_ids" />
<field name="activity_state" />
<templates>
@@ -110,13 +108,11 @@
</div>
<div class="o_kanban_record_bottom">
<ul class="list-unstyled">
<li id="survey_test">
<t t-if="record.designed.raw_value"><a type="object" name="action_test_survey">Test</a></t>
<t t-if="! record.designed.raw_value"><a style="color: #aaaaaa;">Test</a></t>
<li>
<a type="object" name="action_test_survey">Test</a>
</li>
<li>
<t t-if="record.designed.raw_value"><a type="object" name="action_send_survey">Share &amp;amp; Invite</a></t>
<t t-if="! record.designed.raw_value"><a style="color: #aaaaaa;">Share &amp;amp; Invite</a></t>
<a type="object" name="action_send_survey">Share &amp;amp; Invite</a>
</li>
<li>
<t t-if="record.tot_start_survey.raw_value &gt; 0"><a name="action_result_survey" type="object">Analyze Answers</a> <span t-if="record.tot_start_survey.raw_value &gt; 0">(<field name="tot_start_survey" />)</span></t>
@@ -154,6 +150,9 @@
<filter string="Upcoming Activities" name="activities_upcoming_all"
domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))
]"/>
<group expand="0" string="Group By">
<filter string="Category" name="groupby_category" context="{'group_by': 'category'}"/>
</group>
</search>
</field>
</record>
@@ -169,12 +168,10 @@
</p><p>
You can create surveys for different purposes: customer opinion, services feedback, recruitment interviews, employee's periodical evaluations, marketing campaigns, etc.
</p><p>
Design easily your survey, send invitations to answer by email and analyse answers.
Design easily your survey, send invitations to answer by email and analyze answers.
</p>
</field>
</record>
<act_window context="{'search_default_survey_id': [active_id], 'default_survey_id': active_id}" id="act_survey_pages" name="Pages" res_model="survey.page" src_model="survey.survey"/>
<act_window context="{'search_default_survey_id': [active_id], 'default_survey_id': active_id}" id="act_survey_question" name="Questions" res_model="survey.question" src_model="survey.survey"/>
<menuitem name="Surveys" id="menu_survey_form" action="action_survey_form" parent="menu_surveys" sequence="1"/>
+27 -17
View File
@@ -26,7 +26,7 @@
<h1>Thank you!</h1>
<div t-field="survey.thank_you_message" class="oe_no_empty" />
<div t-if='survey.quizz_mode'>You scored <t t-esc="user_input.quizz_score" /> points.</div>
<div>If you wish, you can <a t-att-href="'/survey/print/%s/%s' % (slug(survey), token)">review your answers</a>.</div>
<div>If you wish, you can <a t-att-href="'/survey/print/%s?token=%s' % (survey.id, token)">review your answers</a>.</div>
</div>
</div>
</div>
@@ -34,13 +34,13 @@
</template>
<!-- Message when the survey is not open -->
<template id="notopen" name="Survey not open">
<template id="survey_expired" name="Survey Expired">
<t t-call="survey.layout">
<div class="wrap">
<div class="container">
<div class="jumbotron mt32">
<h1>Not open</h1>
<p>This survey is not open. Thank you for your interest!</p>
<h1><span t-field="survey.title"/> survey expired</h1>
<p>This survey is now closed. Thank you for your interest !</p>
</div>
</div>
</div>
@@ -54,7 +54,9 @@
<div class="container">
<div class="jumbotron mt32">
<h1>Login required</h1>
<p>This survey is open only to registered people. Please <a t-attf-href="/web/login?redirect=%2Fsurvey%2Fstart%2F#{ slug(survey) }%2F#{token}">log in</a>.</p>
<p>This survey is open only to registered people. Please
<a t-att-href="'/web/login?redirect=%s' % ('/survey/start/%s?token=%s' % (survey.id, token))">log in</a>.
</p>
</div>
</div>
</div>
@@ -62,14 +64,18 @@
</template>
<!-- Message when the survey has no pages -->
<template id="nopages" name="Survey has no pages">
<template id="survey_void" name="Survey has no pages">
<t t-call="survey.layout">
<div class="wrap">
<div class="container">
<t t-call="survey.back" />
<div class="jumbotron mt32">
<h1>Not ready</h1>
<p>This survey has no pages by now!</p>
<h1><span t-field="survey.title"/> survey is void</h1>
<p>This survey has no question defined currently.<br />
<a t-att-href="'/web#view_type=form&amp;model=survey.survey&amp;id=%s&amp;action=survey.action_survey_form' % survey.id"
class="btn btn-secondary"
groups="survey.group_survey_manager">Edit in backend</a>
</p>
</div>
</div>
</div>
@@ -135,7 +141,7 @@
<div class='jumbotron mt32'>
<h1 t-field='survey.title' />
<div t-field='survey.description' class="oe_no_empty"/>
<a role="button" class="btn btn-primary btn-lg" t-att-href="'/survey/fill/%s/%s' % (slug(survey), token)">
<a role="button" class="btn btn-primary btn-lg" t-att-href="'/survey/fill/%s/%s' % (survey.id, token)">
Start Survey
</a>
</div>
@@ -167,7 +173,11 @@
<div t-field='page.description' class="oe_no_empty"/>
</div>
<form role="form" method="post" class="js_surveyform" t-att-name="'%s_%s' % (survey.id, page.id)" t-att-action="'/survey/fill/%s/%s' % (slug(survey), token)" t-att-data-prefill="'/survey/prefill/%s/%s/%s' % (slug(survey), token, slug(page))" t-att-data-validate="'/survey/validate/%s' % (slug(survey))" t-att-data-submit="'/survey/submit/%s' % (slug(survey))">
<form role="form" method="post" class="js_surveyform" t-att-name="'%s_%s' % (survey.id, page.id)"
t-att-action="'/survey/fill/%s/%s' % (survey.id, token)"
t-att-data-prefill="'/survey/prefill/%s/%s?page_id=%s' % (survey.id, token, page.id)"
t-att-data-validate="'/survey/validate/%s/%s' % (survey.id, token)"
t-att-data-submit="'/survey/submit/%s/%s' % (survey.id, token)">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="page_id" t-att-value="page.id" />
<input type="hidden" name="token" t-att-value="token" />
@@ -336,7 +346,7 @@
<h1><span t-field='survey.title'/></h1>
<t t-if="survey.description"><div t-field='survey.description' class="oe_no_empty"/></t>
</div>
<div role="form" class="js_surveyform" t-att-name="'%s' % (survey.id)" t-att-data-prefill="'/survey/prefill/%s/%s' % (slug(survey), token)">
<div role="form" class="js_surveyform" t-att-name="'%s' % (survey.id)" t-att-data-prefill="'/survey/prefill/%s/%s' % (survey.id, token)">
<t t-foreach="survey.page_ids" t-as="page">
<div class="o_page_header">
<h1 t-field='page.title' />
@@ -455,20 +465,20 @@
<tbody>
<t t-set="text_result" t-value="prepare_result"/>
<tr t-foreach="text_result" t-as="user_input">
<td><a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
<td><a t-att-href="'/survey/print/%s?token=%s' % (user_input.survey_id.id, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
<t t-if="question.question_type == 'free_text'">
<td>
<a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)" t-field="user_input.value_free_text"></a><br/>
<a t-att-href="'/survey/print/%s?token=%s' % (user_input.survey_id.id, user_input.user_input_id.token)" t-field="user_input.value_free_text"></a><br/>
</td>
</t>
<t t-if="question.question_type == 'textbox'">
<td>
<a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)" t-field="user_input.value_text"></a><br/>
<a t-att-href="'/survey/print/%s?token=%s' % (user_input.survey_id.id, user_input.user_input_id.token)" t-field="user_input.value_text"></a><br/>
</td>
</t>
<t t-if="question.question_type == 'date'">
<td>
<a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)" t-field="user_input.value_date"></a><br/>
<a t-att-href="'/survey/print/%s?token=%s' % (user_input.survey_id.id, user_input.user_input_id.token)" t-field="user_input.value_date"></a><br/>
</td>
</t>
</tr>
@@ -489,7 +499,7 @@
</thead>
<tbody>
<tr t-foreach="comments" t-as="user_input">
<td><a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
<td><a t-att-href="'/survey/print/%s?token=%s' % (user_input.survey_id.id, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
<td>
<span t-field="user_input.value_text"></span><br/>
</td>
@@ -673,7 +683,7 @@
</thead>
<tbody>
<tr class="d-none" t-foreach="number_result['input_lines']" t-as="user_input">
<td><a t-att-href="'%s/%s' % (user_input.user_input_id.print_url, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
<td><a t-att-href="'/survey/print/%s?token=%s' % (user_input.survey_id.id, user_input.user_input_id.token)"><t t-esc="user_input_index + 1"></t></a></td>
<td><span t-field="user_input.value_number"></span><br/></td>
</tr>
</tbody>
+10 -10
View File
@@ -29,9 +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_view_answers" states="done" string="Print These Answers" type="object" class="oe_highlight" />
<button name="action_survey_results" string="View Results" type="object" />
<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>
@@ -39,7 +39,7 @@
<group col="2">
<group>
<field name="survey_id"/>
<field name="date_create"/>
<field name="create_date"/>
<field name="input_type"/>
<field name="token" groups="base.group_no_one"/>
</group>
@@ -57,7 +57,7 @@
<field name="question_id"/>
<field name="answer_type"/>
<field name="skipped"/>
<field name="date_create"/>
<field name="create_date"/>
<field name="quizz_mark" groups="base.group_no_one"/>
</tree>
</field>
@@ -71,7 +71,7 @@
<field name="arch" type="xml">
<tree string="Survey User inputs" decoration-muted="test_entry == True" create="false">
<field name="survey_id"/>
<field name="date_create"/>
<field name="create_date"/>
<field name="deadline"/>
<field name="partner_id"/>
<field name="email"/>
@@ -89,7 +89,7 @@
<field name="arch" type="xml">
<kanban create="false">
<field name="survey_id"/>
<field name="date_create"/>
<field name="create_date"/>
<field name="partner_id"/>
<field name="email"/>
<field name="input_type"/>
@@ -105,7 +105,7 @@
</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<field name="date_create"/>
<field name="create_date"/>
</div>
<div class="oe_kanban_bottom_right mr4">
<field name="state" widget="kanban_label_selection" options="{'classes': {'new': 'default', 'done': 'success', 'skip':'warning'}}"/>
@@ -155,7 +155,7 @@
<sheet>
<group col="4">
<field name="question_id"/>
<field name="date_create"/>
<field name="create_date"/>
<field name="answer_type"/>
<field name="skipped" />
<field name="quizz_mark" groups="base.group_no_one"/>
@@ -181,7 +181,7 @@
<field name="user_input_id"/>
<field name="page_id"/>
<field name="question_id"/>
<field name="date_create"/>
<field name="create_date"/>
<field name="answer_type"/>
<field name="skipped"/>
<field name="quizz_mark" groups="base.group_no_one"/>
+166 -196
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:
@@ -44,7 +36,7 @@ class SurveyInvite(models.TransientModel):
string='Attachments')
template_id = fields.Many2one(
'mail.template', 'Use template', index=True,
domain="[('model', '=', 'survey.survey')]")
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(
@@ -52,216 +44,194 @@ 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")
public_url_html = fields.Char(compute="_compute_survey_url", string="Public HTML web link")
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
wizard.public_url_html = wizard.survey_id.public_url_html
@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('template_id', 'survey_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')
def _onchange_template_id(self):
""" UPDATE ME """
to_render_fields = [
'subject', 'body_html',
'email_from', 'reply_to',
'partner_to', 'email_to', 'email_cc',
'attachment_ids',
'mail_server_id'
]
returned_fields = to_render_fields + ['partner_ids', 'attachments']
for wizard in self:
if wizard.template_id:
values = {}
if self.template_id:
self.subject = self.template_id.subject
self.body = self.template_id.body_html
template_values = wizard.template_id.generate_email([wizard.survey_id.id], fields=to_render_fields)[wizard.survey_id.id]
values = dict((field, template_values[field]) for field in returned_fields if template_values.get(field))
values['body'] = values.pop('body_html', '')
# transform attachments into attachment_ids; not attached to the document because this will
# be done further in the posting process, allowing to clean database if email not send
Attachment = self.env['ir.attachment']
for attach_fname, attach_datas in values.pop('attachments', []):
data_attach = {
'name': attach_fname,
'datas': attach_datas,
'datas_fname': attach_fname,
'res_model': 'mail.compose.message', # TDE CHECKME
'res_id': 0,
'type': 'binary', # override default_type from context, possibly meant for another model!
}
values.setdefault('attachment_ids', list()).append(Attachment.create(data_attach).id)
else:
default_values = self.default_get(returned_fields)
values = dict((key, default_values[key]) for key in returned_fields if key in default_values)
# This onchange should return command instead of ids for x2many field.
# ORM handle the assignation of command list on new onchange (api.v8),
# this force the complete replacement of x2many field with
# command and is compatible with onchange api.v7
values = self._convert_to_write(values)
wizard.update(values)
@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
#------------------------------------------------------
@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, **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, **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.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
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,
'date_create': fields.Datetime.now(),
'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 -41
View File
@@ -1,62 +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>
<div>Copy and paste the HTML code below to add this web link to any webpage.</div>
<code><field name="public_url_html" /></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>