summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJérome Maes <jem@openerp.com>2016-07-15 13:02:13 +0200
committerJérome Maes <jem@openerp.com>2016-07-15 13:02:13 +0200
commiteee72c838072d376488cddea8be8796b95f2daf7 (patch)
tree1b9bc444b64b3e578a41521521cb4349f3564f76
parentc12a5fb204cab28e0e434ad6b62437e966d6cc88 (diff)
parent35e7e93316b45c85766f865f1e7685982fee8d3d (diff)
[MERGE] survey: migrate to new API
Purely merge, line per line code to new api. No refactoring was done here. Migrated modules are 'survey' and 'survey_crm'
-rw-r--r--addons/survey/__init__.py2
-rw-r--r--addons/survey/__openerp__.py1
-rw-r--r--addons/survey/controllers/main.py149
-rw-r--r--addons/survey/data/survey_cron.xml19
-rw-r--r--addons/survey/models/__init__.py5
-rw-r--r--addons/survey/models/ir_autovacuum.py13
-rw-r--r--addons/survey/models/survey.py1066
-rw-r--r--addons/survey/survey.py1232
-rw-r--r--addons/survey/tests/test_survey.py118
-rw-r--r--addons/survey/tests/test_survey_ui.py8
-rw-r--r--addons/survey/views/survey_views.xml7
-rw-r--r--addons/survey/wizard/survey_email_compose_message.py168
-rw-r--r--addons/survey/wizard/survey_email_compose_message.xml4
-rw-r--r--addons/survey_crm/__init__.py2
-rw-r--r--addons/survey_crm/__openerp__.py4
-rw-r--r--addons/survey_crm/models/__init__.py4
-rw-r--r--addons/survey_crm/models/survey.py (renamed from addons/survey_crm/survey.py)18
-rw-r--r--addons/survey_crm/views/survey_crm_views.xml (renamed from addons/survey_crm/crm_view.xml)6
18 files changed, 1292 insertions, 1534 deletions
diff --git a/addons/survey/__init__.py b/addons/survey/__init__.py
index c895bdaf188..4b19b6b7cd2 100644
--- a/addons/survey/__init__.py
+++ b/addons/survey/__init__.py
@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import survey
import controllers
+import models
import wizard
import tests
diff --git a/addons/survey/__openerp__.py b/addons/survey/__openerp__.py
index afd17264b61..b297292aa71 100644
--- a/addons/survey/__openerp__.py
+++ b/addons/survey/__openerp__.py
@@ -26,7 +26,6 @@ sent mails with personal token for the invitation of the survey.
'views/survey_result.xml',
'wizard/survey_email_compose_message.xml',
'data/survey_stages.xml',
- 'data/survey_cron.xml'
],
'demo': ['data/survey_demo_user.xml',
'data/survey_demo_feedback.xml',
diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py
index b84f91979a0..7c0f42c558b 100644
--- a/addons/survey/controllers/main.py
+++ b/addons/survey/controllers/main.py
@@ -4,30 +4,26 @@
import json
import logging
import werkzeug
-import werkzeug.utils
from datetime import datetime
from math import ceil
-from openerp import SUPERUSER_ID
-from openerp.addons.web import http
-from openerp.addons.web.http import request
-from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF, ustr
-
+from odoo import fields, http, SUPERUSER_ID
+from odoo.http import request
+from odoo.tools import ustr
_logger = logging.getLogger(__name__)
class WebsiteSurvey(http.Controller):
+ # HELPER METHODS #
- ## HELPER METHODS ##
-
- def _check_bad_cases(self, cr, uid, request, survey_obj, survey, user_input_obj, token=None, context=None):
+ def _check_bad_cases(self, survey, token=None):
# In case of bad survey, redirect to surveys list
- if survey_obj.exists(cr, SUPERUSER_ID, survey.id, context=context) == []:
+ if not survey.sudo().exists():
return werkzeug.utils.redirect("/survey/")
# In case of auth required, block public user
- if survey.auth_required and uid == request.website.user_id.id:
+ if survey.auth_required and request.env.user == request.website.user_id:
return request.website.render("survey.auth_required", {'survey': survey, 'token': token})
# In case of non open surveys
@@ -41,16 +37,16 @@ class WebsiteSurvey(http.Controller):
# Everything seems to be ok
return None
- def _check_deadline(self, cr, uid, user_input, context=None):
+ def _check_deadline(self, user_input):
'''Prevent opening of the survey if the deadline has turned out
! This will NOT disallow access to users who have already partially filled the survey !'''
- if user_input.deadline:
- dt_deadline = datetime.strptime(user_input.deadline, DTF)
+ deadline = user_input.deadline
+ if deadline:
+ dt_deadline = fields.Datetime.from_string(deadline)
dt_now = datetime.now()
if dt_now > dt_deadline: # survey is not open anymore
return request.website.render("survey.notopen")
-
return None
## ROUTES HANDLERS ##
@@ -60,41 +56,34 @@ class WebsiteSurvey(http.Controller):
'/survey/start/<model("survey.survey"):survey>/<string:token>'],
type='http', auth='public', website=True)
def start_survey(self, survey, token=None, **post):
- cr, uid, context = request.cr, request.uid, request.context
- survey_obj = request.registry['survey.survey']
- user_input_obj = request.registry['survey.user_input']
+ UserInput = request.env['survey.user_input']
# Test mode
if token and token == "phantom":
_logger.info("[survey] Phantom mode")
- user_input_id = user_input_obj.create(cr, uid, {'survey_id': survey.id, 'test_entry': True}, context=context)
- user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
+ user_input = UserInput.create({'survey_id': survey.id, 'test_entry': True})
data = {'survey': survey, 'page': None, 'token': user_input.token}
return request.website.render('survey.survey_init', data)
# END Test mode
# Controls if the survey can be displayed
- errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, token=token, context=context)
+ errpage = self._check_bad_cases(survey, token=token)
if errpage:
return errpage
# Manual surveying
if not token:
vals = {'survey_id': survey.id}
- if request.website.user_id.id != uid:
- vals['partner_id'] = request.registry['res.users'].browse(cr, uid, uid, context=context).partner_id.id
- user_input_id = user_input_obj.create(cr, uid, vals, context=context)
- user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0]
+ if request.website.user_id != request.env.user:
+ vals['partner_id'] = request.env.user.partner_id.id
+ user_input = UserInput.create(vals)
else:
- try:
- user_input_id = user_input_obj.search(cr, SUPERUSER_ID, [('token', '=', token)], context=context)[0]
- except IndexError: # Invalid token
+ user_input = UserInput.sudo().search([('token', '=', token)], limit=1)
+ if not user_input:
return request.website.render("website.403")
- else:
- user_input = user_input_obj.browse(cr, SUPERUSER_ID, [user_input_id], context=context)[0]
# Do not open expired survey
- errpage = self._check_deadline(cr, uid, user_input, context=context)
+ errpage = self._check_deadline(user_input)
if errpage:
return errpage
@@ -111,32 +100,29 @@ class WebsiteSurvey(http.Controller):
type='http', auth='public', website=True)
def fill_survey(self, survey, token, prev=None, **post):
'''Display and validates a survey'''
- cr, uid, context = request.cr, request.uid, request.context
- survey_obj = request.registry['survey.survey']
- user_input_obj = request.registry['survey.user_input']
+ Survey = request.env['survey.survey']
+ UserInput = request.env['survey.user_input']
# Controls if the survey can be displayed
- errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context)
+ errpage = self._check_bad_cases(survey)
if errpage:
return errpage
# Load the user_input
try:
- user_input_id = user_input_obj.search(cr, SUPERUSER_ID, [('token', '=', token)])[0]
+ user_input = UserInput.sudo().search([('token', '=', token)], limit=1)
except IndexError: # Invalid token
return request.website.render("website.403")
- else:
- user_input = user_input_obj.browse(cr, SUPERUSER_ID, [user_input_id], context=context)[0]
# Do not display expired survey (even if some pages have already been
# displayed -- There's a time for everything!)
- errpage = self._check_deadline(cr, uid, user_input, context=context)
+ errpage = self._check_deadline(user_input)
if errpage:
return errpage
# Select the right page
if user_input.state == 'new': # First page
- page, page_nr, last = survey_obj.next_page(cr, uid, user_input, 0, go_back=False, context=context)
+ 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 last:
data.update({'last': True})
@@ -147,11 +133,11 @@ class WebsiteSurvey(http.Controller):
'user_input': user_input})
elif user_input.state == 'skip':
flag = (True if prev and prev == 'prev' else False)
- page, page_nr, last = survey_obj.next_page(cr, uid, user_input, user_input.last_displayed_page_id.id, go_back=flag, context=context)
+ page, page_nr, last = Survey.next_page(user_input, user_input.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_obj.next_page(cr, uid, user_input, user_input.last_displayed_page_id.id, go_back=True, context=context)
+ page, page_nr, last = Survey.next_page(user_input, user_input.last_displayed_page_id.id, go_back=True)
data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token}
if last:
@@ -165,16 +151,14 @@ class WebsiteSurvey(http.Controller):
'/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):
- cr, uid, context = request.cr, request.uid, request.context
- user_input_line_obj = request.registry['survey.user_input_line']
+ UserInputLine = request.env['survey.user_input_line']
ret = {}
# Fetch previous answers
if page:
- ids = user_input_line_obj.search(cr, SUPERUSER_ID, [('user_input_id.token', '=', token), ('page_id', '=', page.id)], context=context)
+ previous_answers = UserInputLine.sudo().search([('user_input_id.token', '=', token), ('page_id', '=', page.id)])
else:
- ids = user_input_line_obj.search(cr, SUPERUSER_ID, [('user_input_id.token', '=', token)], context=context)
- previous_answers = user_input_line_obj.browse(cr, uid, ids, context=context)
+ previous_answers = UserInputLine.sudo().search([('user_input_id.token', '=', token)])
# Return non empty answers in a JSON compatible format
for answer in previous_answers:
@@ -199,7 +183,7 @@ class WebsiteSurvey(http.Controller):
answer_tag = "%s_%s" % (answer_tag, answer.value_suggested_row.id)
answer_value = answer.value_suggested.id
if answer_value:
- dict_soft_update(ret, answer_tag, answer_value)
+ ret.setdefault(answer_tag, []).append(answer_value)
else:
_logger.warning("[survey] No answer has been found for question %s marked as non skipped" % answer_tag)
return json.dumps(ret)
@@ -208,13 +192,10 @@ class WebsiteSurvey(http.Controller):
@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):
- cr, uid, context = request.cr, request.uid, request.context
- user_input_line_obj = request.registry['survey.user_input_line']
ret = {}
# Fetch answers
- ids = user_input_line_obj.search(cr, SUPERUSER_ID, [('user_input_id.token', '=', token)], context=context)
- previous_answers = user_input_line_obj.browse(cr, uid, ids, context=context)
+ previous_answers = request.env['survey.user_input_line'].sudo().search([('user_input_id.token', '=', token)])
# Compute score for each question
for answer in previous_answers:
@@ -223,50 +204,42 @@ class WebsiteSurvey(http.Controller):
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)
+ @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)
page_id = int(post['page_id'])
- cr, uid, context = request.cr, request.uid, request.context
- survey_obj = request.registry['survey.survey']
- questions_obj = request.registry['survey.question']
- questions_ids = questions_obj.search(cr, uid, [('page_id', '=', page_id)], context=context)
- questions = questions_obj.browse(cr, uid, questions_ids, context=context)
+ questions = request.env['survey.question'].search([('page_id', '=', page_id)])
# Answer validation
errors = {}
for question in questions:
answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
- errors.update(questions_obj.validate_question(cr, uid, question, post, answer_tag, context=context))
+ errors.update(question.validate_question(post, answer_tag))
ret = {}
- if (len(errors) != 0):
+ if len(errors):
# Return errors messages to webpage
ret['errors'] = errors
else:
# Store answers into database
- user_input_obj = request.registry['survey.user_input']
-
- user_input_line_obj = request.registry['survey.user_input_line']
try:
- user_input_id = user_input_obj.search(cr, SUPERUSER_ID, [('token', '=', post['token'])], context=context)[0]
+ user_input = request.env['survey.user_input'].sudo().search([('token', '=', post['token'])], limit=1)
except KeyError: # Invalid token
return request.website.render("website.403")
- user_input = user_input_obj.browse(cr, SUPERUSER_ID, user_input_id, context=context)
- user_id = uid if user_input.type != 'link' else SUPERUSER_ID
+ user_id = request.env.user.id if user_input.type != 'link' else SUPERUSER_ID
+
for question in questions:
answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id)
- user_input_line_obj.save_lines(cr, user_id, user_input_id, question, post, answer_tag, context=context)
+ request.env['survey.user_input_line'].sudo(user=user_id).save_lines(user_input.id, question, post, answer_tag)
go_back = post['button_submit'] == 'previous'
- next_page, _, last = survey_obj.next_page(cr, uid, user_input, page_id, go_back=go_back, context=context)
+ next_page, _, last = request.env['survey.survey'].next_page(user_input, 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_obj.write(cr, user_id, user_input_id, vals, context=context)
+ user_input.sudo(user=user_id).write(vals)
ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, post['token'])
if go_back:
ret['redirect'] += '/prev'
@@ -289,12 +262,11 @@ class WebsiteSurvey(http.Controller):
type='http', auth='user', website=True)
def survey_reporting(self, survey, token=None, **post):
'''Display survey Results & Statistics for given survey.'''
- result_template ='survey.result'
+ result_template = 'survey.result'
current_filters = []
filter_display_data = []
filter_finish = False
- survey_obj = request.registry['survey.survey']
if not survey.user_input_ids or not [input_id.id for input_id in survey.user_input_ids if input_id.state != 'new']:
result_template = 'survey.no_result'
if 'finished' in post:
@@ -302,8 +274,8 @@ class WebsiteSurvey(http.Controller):
filter_finish = True
if post or filter_finish:
filter_data = self.get_filter_data(post)
- current_filters = survey_obj.filter_input_ids(request.cr, request.uid, survey, filter_data, filter_finish, context=request.context)
- filter_display_data = survey_obj.get_filter_display_data(request.cr, request.uid, filter_data, context=request.context)
+ current_filters = survey.filter_input_ids(filter_data, filter_finish)
+ filter_display_data = survey.get_filter_display_data(filter_data)
return request.website.render(result_template,
{'survey': survey,
'survey_dict': self.prepare_result_dict(survey, current_filters),
@@ -350,15 +322,21 @@ class WebsiteSurvey(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_obj = request.registry['survey.survey']
+ Survey = request.env['survey.survey']
result = {'page_ids': []}
for page in survey.page_ids:
page_dict = {'page': page, 'question_ids': []}
for question in page.question_ids:
- question_dict = {'question':question, 'input_summary':survey_obj.get_input_summary(request.cr, request.uid, question, current_filters, context=request.context), 'prepare_result':survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context), 'graph_data': self.get_graph_data(question, current_filters)}
+ question_dict = {
+ '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),
+ }
+
page_dict['question_ids'].append(question_dict)
result['page_ids'].append(page_dict)
return result
@@ -384,28 +362,19 @@ class WebsiteSurvey(http.Controller):
'''Returns formatted data required by graph library on basis of filter'''
# TODO refactor this terrible method and merge it with prepare_result_dict
current_filters = current_filters if current_filters else []
- survey_obj = request.registry['survey.survey']
+ Survey = request.env['survey.survey']
result = []
if question.type == 'multiple_choice':
result.append({'key': ustr(question.question),
- 'values': survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context)['answers']
+ 'values': Survey.prepare_result(question, current_filters)['answers']
})
if question.type == 'simple_choice':
- result = survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context)['answers']
+ result = Survey.prepare_result(question, current_filters)['answers']
if question.type == 'matrix':
- data = survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context)
+ data = Survey.prepare_result(question, current_filters)
for answer in data['answers']:
values = []
for row in data['rows']:
values.append({'text': data['rows'].get(row), 'count': data['result'].get((row, answer))})
result.append({'key': data['answers'].get(answer), 'values': values})
return json.dumps(result)
-
-def dict_soft_update(dictionary, key, value):
- ''' Insert the pair <key>: <value> into the <dictionary>. If <key> is
- already present, this function will append <value> to the list of
- existing data (instead of erasing it) '''
- if key in dictionary:
- dictionary[key].append(value)
- else:
- dictionary.update({key: [value]})
diff --git a/addons/survey/data/survey_cron.xml b/addons/survey/data/survey_cron.xml
deleted file mode 100644
index 7f22bbcb5ff..00000000000
--- a/addons/survey/data/survey_cron.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0"?>
-<openerp>
- <data noupdate="1">
- <!-- Remove unused user inputs -->
- <record forcecreate="True" id="ir_cron_clean_empty_surveys"
- model="ir.cron">
- <field name="name">Run Clean empty surveys</field>
- <field name="active" eval="True"/>
- <field name="user_id" ref="base.user_root"/>
- <field name="interval_number">1</field>
- <field name="interval_type">hours</field>
- <field name="numbercall">-1</field>
- <field name="doall" eval="False"/>
- <field name="model" eval="'survey.user_input'"/>
- <field name="function" eval="'do_clean_emptys'"/>
- <field name="args" eval="'()'"/>
- </record>
- </data>
-</openerp>
diff --git a/addons/survey/models/__init__.py b/addons/survey/models/__init__.py
new file mode 100644
index 00000000000..4a40a7d2ae0
--- /dev/null
+++ b/addons/survey/models/__init__.py
@@ -0,0 +1,5 @@
+# -*- encoding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import survey
+import ir_autovacuum
diff --git a/addons/survey/models/ir_autovacuum.py b/addons/survey/models/ir_autovacuum.py
new file mode 100644
index 00000000000..466cfda6f0d
--- /dev/null
+++ b/addons/survey/models/ir_autovacuum.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+
+class AutoVacuum(models.AbstractModel):
+ _inherit = 'ir.autovacuum'
+
+ @api.model
+ def power_on(self, *args, **kwargs):
+ self.env['survey.user_input'].do_clean_emptys()
+ return super(AutoVacuum, self).power_on(*args, **kwargs)
diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py
new file mode 100644
index 00000000000..8697577c42f
--- /dev/null
+++ b/addons/survey/models/survey.py
@@ -0,0 +1,1066 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import datetime
+import logging
+import re
+import uuid
+from urlparse import urljoin
+from collections import Counter, OrderedDict
+from itertools import product
+
+from odoo import api, fields, models, tools, _
+from odoo.exceptions import UserError, ValidationError
+
+from odoo.addons.website.models.website import slug
+
+email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+")
+_logger = logging.getLogger(__name__)
+
+
+def dict_keys_startswith(dictionary, string):
+ """Returns a dictionary containing the elements of <dict> whose keys start with <string>.
+ .. note::
+ This function uses dictionary comprehensions (Python >= 2.7)
+ """
+ matched_keys = [key for key in dictionary.keys() if key.startswith(string)]
+ return dict((k, dictionary[k]) for k in matched_keys)
+
+
+class SurveyStage(models.Model):
+ """Stages for Kanban view of surveys"""
+
+ _name = 'survey.stage'
+ _description = 'Survey Stage'
+ _order = 'sequence,id'
+
+ name = fields.Char(required=True, translate=True)
+ sequence = fields.Integer(default=1)
+ closed = fields.Boolean(help="If closed, people won't be able to answer to surveys in this column.")
+ fold = fields.Boolean(string="Folded in kanban view")
+
+ _sql_constraints = [
+ ('positive_sequence', 'CHECK(sequence >= 0)', 'Sequence number MUST be a natural')
+ ]
+
+
+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.
+ """
+
+ _name = 'survey.survey'
+ _description = 'Survey'
+ _rec_name = 'title'
+ _inherit = ['mail.thread', 'ir.needaction_mixin']
+
+ def _default_stage(self):
+ return self.env['survey.stage'].search([], limit=1).id
+
+ 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="set null", copy=False)
+ 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')
+
+ def _is_designed(self):
+ 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
+
+ @api.multi
+ def _compute_survey_statistic(self):
+ UserInput = self.env['survey.user_input']
+
+ sent_survey = UserInput.search([('survey_id', 'in', self.ids), ('type', '=', 'link')])
+ start_survey = UserInput.search(['&', ('survey_id', 'in', self.ids), '|', ('state', '=', 'skip'), ('state', '=', 'done')])
+ complete_survey = UserInput.search([('survey_id', 'in', self.ids), ('state', '=', 'done')])
+
+ for survey in self:
+ survey.tot_sent_survey = len(sent_survey.filtered(lambda user_input: user_input.survey_id == survey))
+ survey.tot_start_survey = len(start_survey.filtered(lambda user_input: user_input.survey_id == survey))
+ survey.tot_comp_survey = len(complete_survey.filtered(lambda user_input: user_input.survey_id == survey))
+
+ 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'].get_param('web.base.url')
+ for survey in self:
+ survey.public_url = urljoin(base_url, "survey/start/%s" % (slug(survey)))
+ survey.print_url = urljoin(base_url, "survey/print/%s" % (slug(survey)))
+ survey.result_url = urljoin(base_url, "survey/results/%s" % (slug(survey)))
+ survey.public_url_html = '<a href="%s">%s</a>' % (survey.public_url, _("Click here to start survey"))
+
+ @api.multi
+ def _read_group_stage_ids(self, domain, read_group_order=None, access_rights_uid=None):
+ """ Read group customization in order to display all the stages in the
+ kanban view, even if they are empty
+ """
+ SurveyStage = self.env['survey.stage']
+ order = SurveyStage._order
+ access_rights_uid = access_rights_uid or self.env.user.id
+
+ if read_group_order == 'stage_id desc':
+ order = '%s desc' % order
+
+ stage_ids = SurveyStage._search([], order=order, access_rights_uid=access_rights_uid)
+ stages = SurveyStage.sudo(user=access_rights_uid).browse(stage_ids)
+ result = stages.name_get()
+ return result, {stage.id: stage.fold for stage in stages}
+
+ _group_by_full = {
+ 'stage_id': _read_group_stage_ids
+ }
+
+ # Public methods #
+ def copy_data(self, default=None):
+ title = _("%s (copy)") % (self.title)
+ default = dict(default or {}, title=title)
+ return super(Survey, self).copy_data(default)
+
+ @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
+ of the last displayed page.
+
+ If page_id == 0, it will always return the first page of the survey.
+
+ If all the pages have been displayed and go_back == False, it will
+ return None
+
+ If go_back == True, it will return the *previous* page instead of the
+ next page.
+
+ .. note::
+ It is assumed here that a careful user will not try to set go_back
+ to True if she knows that the page to display is the first one!
+ (doing this will probably cause a giant worm to eat her house)
+ """
+ survey = user_input.survey_id
+ pages = list(enumerate(survey.page_ids))
+
+ # First page
+ if page_id == 0:
+ return (pages[0][1], 0, len(pages) == 1)
+
+ current_page_index = pages.index((filter(lambda p: p[1].id == page_id, pages))[0])
+
+ # All the pages have been displayed
+ if current_page_index == len(pages) - 1 and not go_back:
+ return (None, -1, False)
+ # Let's get back, baby!
+ elif go_back and survey.users_can_go_back:
+ return (pages[current_page_index - 1][1], current_page_index - 1, False)
+ else:
+ # This will show the last page
+ if current_page_index == len(pages) - 2:
+ return (pages[current_page_index + 1][1], current_page_index + 1, True)
+ # This will show a regular page
+ else:
+ return (pages[current_page_index + 1][1], current_page_index + 1, False)
+
+ @api.multi
+ def filter_input_ids(self, filters, finished=False):
+ """If user applies any filters, then this function returns list of
+ filtered user_input_id and label's strings for display data in web.
+ :param filters: list of dictionary (having: row_id, ansewr_id)
+ :param finished: True for completely filled survey,Falser otherwise.
+ :returns list of filtered user_input_ids.
+ """
+ self.ensure_one()
+ if filters:
+ domain_filter, choice = [], []
+ for current_filter in filters:
+ row_id, answer_id = current_filter['row_id'], current_filter['answer_id']
+ if row_id == 0:
+ choice.append(answer_id)
+ else:
+ domain_filter.extend(['|', ('value_suggested_row.id', '=', row_id), ('value_suggested.id', '=', answer_id)])
+ if choice:
+ domain_filter.insert(0, ('value_suggested.id', 'in', choice))
+ else:
+ domain_filter = domain_filter[1:]
+ input_lines = self.env['survey.user_input_line'].search(domain_filter)
+ filtered_input_ids = [input_line.user_input_id.id for input_line in input_lines]
+ else:
+ filtered_input_ids = []
+ if finished:
+ UserInput = self.env['survey.user_input']
+ if not filtered_input_ids:
+ user_inputs = UserInput.search([('survey_id', '=', self.id)])
+ else:
+ user_inputs = UserInput.browse(filtered_input_ids)
+ return user_inputs.filtered(lambda input_item: input_item.state == 'done').ids
+ return filtered_input_ids
+
+ @api.model
+ def get_filter_display_data(self, filters):
+ """Returns data to display current filters
+ :param filters: list of dictionary (having: row_id, answer_id)
+ :returns list of dict having data to display filters.
+ """
+ filter_display_data = []
+ if filters:
+ Label = self.env['survey.label']
+ for current_filter in filters:
+ row_id, answer_id = current_filter['row_id'], current_filter['answer_id']
+ label = Label.browse(answer_id)
+ question = label.question_id
+ if row_id == 0:
+ labels = label
+ else:
+ labels = Label.browse([row_id, answer_id])
+ filter_display_data.append({'question_text': question.question,
+ 'labels': labels.mapped('value')})
+ return filter_display_data
+
+ @api.model
+ def prepare_result(self, question, current_filters=None):
+ """ Compute statistical data for questions by counting number of vote per choice on basis of filter """
+ current_filters = current_filters if current_filters else []
+ result_summary = {}
+
+ # Calculate and return statistics for choice
+ if question.type in ['simple_choice', 'multiple_choice']:
+ answers = {}
+ comments = []
+ [answers.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids]
+ for input_line in question.user_input_line_ids:
+ if input_line.answer_type == 'suggestion' and answers.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
+ answers[input_line.value_suggested.id]['count'] += 1
+ if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters):
+ comments.append(input_line)
+ result_summary = {'answers': answers.values(), 'comments': comments}
+
+ # Calculate and return statistics for matrix
+ if question.type == 'matrix':
+ rows = OrderedDict()
+ answers = OrderedDict()
+ res = dict()
+ comments = []
+ [rows.update({label.id: label.value}) for label in question.labels_ids_2]
+ [answers.update({label.id: label.value}) for label in question.labels_ids]
+ for cell in product(rows.keys(), answers.keys()):
+ res[cell] = 0
+ for input_line in question.user_input_line_ids:
+ if input_line.answer_type == 'suggestion' and (not(current_filters) or input_line.user_input_id.id in current_filters) and input_line.value_suggested_row:
+ res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1
+ if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters):
+ comments.append(input_line)
+ result_summary = {'answers': answers, 'rows': rows, 'result': res, 'comments': comments}
+
+ # Calculate and return statistics for free_text, textbox, datetime
+ if question.type in ['free_text', 'textbox', 'datetime']:
+ result_summary = []
+ for input_line in question.user_input_line_ids:
+ if not(current_filters) or input_line.user_input_id.id in current_filters:
+ result_summary.append(input_line)
+
+ # Calculate and return statistics for numerical_box
+ if question.type == 'numerical_box':
+ result_summary = {'input_lines': []}
+ all_inputs = []
+ for input_line in question.user_input_line_ids:
+ if not(current_filters) or input_line.user_input_id.id in current_filters:
+ all_inputs.append(input_line.value_number)
+ result_summary['input_lines'].append(input_line)
+ if all_inputs:
+ result_summary.update({'average': round(sum(all_inputs) / len(all_inputs), 2),
+ 'max': round(max(all_inputs), 2),
+ 'min': round(min(all_inputs), 2),
+ 'sum': sum(all_inputs),
+ 'most_common': Counter(all_inputs).most_common(5)})
+ return result_summary
+
+ @api.model
+ def get_input_summary(self, question, current_filters=None):
+ """ Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter """
+ current_filters = current_filters if current_filters else []
+ result = {}
+ if question.survey_id.user_input_ids:
+ total_input_ids = current_filters or [input_id.id for input_id in question.survey_id.user_input_ids if input_id.state != 'new']
+ result['total_inputs'] = len(total_input_ids)
+ question_input_ids = []
+ for user_input in question.user_input_line_ids:
+ if not user_input.skipped:
+ question_input_ids.append(user_input.user_input_id.id)
+ result['answered'] = len(set(question_input_ids) & set(total_input_ids))
+ result['skipped'] = result['total_inputs'] - result['answered']
+ return result
+
+ # Actions
+
+ @api.multi
+ def action_start_survey(self):
+ """ Open the website page with the survey form """
+ self.ensure_one()
+ token = self.env.context.get('survey_token')
+ trail = "/%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
+ }
+
+ @api.multi
+ def action_send_survey(self):
+ """ Open a window to compose an email, pre-filled with the survey message """
+ # Ensure that this survey has at least one page with at least one question.
+ if not self.page_ids or not [page.question_ids for page in self.page_ids if page.question_ids]:
+ raise UserError(_('You cannot send an invitation for a survey that has no questions.'))
+
+ 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)
+
+ 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'
+ )
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'survey.mail.compose.message',
+ 'target': 'new',
+ 'context': local_context,
+ }
+
+ @api.multi
+ def action_print_survey(self):
+ """ Open the website page with the survey printable view """
+ self.ensure_one()
+ token = self.env.context.get('survey_token')
+ trail = "/" + 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
+ }
+
+ @api.multi
+ def action_result_survey(self):
+ """ Open the website page with the survey results view """
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Results of the Survey",
+ 'target': 'self',
+ 'url': self.with_context(relative_url=True).result_url
+ }
+
+ @api.multi
+ def action_test_survey(self):
+ """ Open the website page with the survey form into test mode"""
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Results of the Survey",
+ 'target': 'self',
+ 'url': self.with_context(relative_url=True).public_url + "/phantom"
+ }
+
+ @api.multi
+ def action_survey_user_input(self):
+ action_rec = self.env.ref('survey.action_survey_user_input')
+ action = action_rec.read()[0]
+ ctx = dict(self.env.context)
+ ctx.update({'search_default_survey_id': self.ids[0],
+ 'search_default_completed': 1})
+ action['context'] = ctx
+ return action
+
+
+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.
+ """
+ _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)
+ sequence = fields.Integer('Page number', default=10)
+ description = fields.Html('Description', translate=True, oldname="note", help="An introductory text to your page")
+
+
+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...).
+ """
+
+ _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'))
+ survey_id = fields.Many2one('survey.survey', related='page_id.survey_id', string='Survey')
+ 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
+ type = fields.Selection([
+ ('free_text', 'Multiple Lines Text Box'),
+ ('textbox', 'Single Line Text Box'),
+ ('numerical_box', 'Numerical Value'),
+ ('datetime', 'Date and Time'),
+ ('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)
+ 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
+
+ # 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')
+
+ # Comments
+ comments_allowed = fields.Boolean('Show Comments Field',
+ oldname="allow_comment")
+ 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')
+
+ # Validation
+ validation_required = fields.Boolean('Validate entry', oldname='is_validation_require')
+ 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')
+ validation_min_float_value = fields.Float('Minimum value')
+ validation_max_float_value = fields.Float('Maximum value')
+ validation_min_date = fields.Datetime('Minimum Date')
+ validation_max_date = fields.Datetime('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."))
+
+ # 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)])
+
+ _sql_constraints = [
+ ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'),
+ ('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'),
+ ('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'),
+ ('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'),
+ ('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!')
+ ]
+
+ @api.onchange('validation_email')
+ def onchange_validation_email(self):
+ if self.validation_email:
+ self.validation_required = False
+
+ # Validation methods
+
+ @api.multi
+ def validate_question(self, post, answer_tag):
+ """ Validate question, depending on question type and parameters """
+ self.ensure_one()
+ try:
+ checker = getattr(self, 'validate_' + self.type)
+ except AttributeError:
+ _logger.warning(self.type + ": This type of question has no validation method")
+ return {}
+ else:
+ return checker(post, answer_tag)
+
+ @api.multi
+ def validate_free_text(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ answer = post[answer_tag].strip()
+ # Empty answer to mandatory question
+ if self.constr_mandatory and not answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ return errors
+
+ @api.multi
+ def validate_textbox(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ answer = post[answer_tag].strip()
+ # Empty answer to mandatory question
+ if self.constr_mandatory and not answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ # Email format validation
+ # Note: this validation is very basic:
+ # all the strings of the form
+ # <something>@<anything>.<extension>
+ # will be accepted
+ if answer and self.validation_email:
+ if not email_validator.match(answer):
+ errors.update({answer_tag: _('This answer must be an email address')})
+ # Answer validation (if properly defined)
+ # Length of the answer must be in a range
+ if answer and self.validation_required:
+ if not (self.validation_length_min <= len(answer) <= self.validation_length_max):
+ errors.update({answer_tag: self.validation_error_msg})
+ return errors
+
+ @api.multi
+ def validate_numerical_box(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ answer = post[answer_tag].strip()
+ # Empty answer to mandatory question
+ if self.constr_mandatory and not answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ # Checks if user input is a number
+ if answer:
+ try:
+ floatanswer = float(answer)
+ except ValueError:
+ errors.update({answer_tag: _('This is not a number')})
+ # Answer validation (if properly defined)
+ if answer and self.validation_required:
+ # Answer is not in the right range
+ with tools.ignore(Exception):
+ floatanswer = float(answer) # check that it is a float has been done hereunder
+ if not (self.validation_min_float_value <= floatanswer <= self.validation_max_float_value):
+ errors.update({answer_tag: self.validation_error_msg})
+ return errors
+
+ @api.multi
+ def validate_datetime(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ answer = post[answer_tag].strip()
+ # Empty answer to mandatory question
+ if self.constr_mandatory and not answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ # Checks if user input is a datetime
+ if answer:
+ try:
+ dateanswer = fields.Datetime.from_string(answer)
+ except ValueError:
+ errors.update({answer_tag: _('This is not a date/time')})
+ return errors
+ # Answer validation (if properly defined)
+ if answer and self.validation_required:
+ # Answer is not in the right range
+ try:
+ datetime_from_string = fields.Datetime.from_string
+ dateanswer = datetime_from_string(answer)
+ min_date = datetime_from_string(self.validation_min_date)
+ max_date = datetime_from_string(self.validation_max_date)
+
+ if min_date and max_date and not (min_date <= dateanswer <= max_date):
+ # If Minimum and Maximum Date are entered
+ errors.update({answer_tag: self.validation_error_msg})
+ elif min_date and not min_date <= dateanswer:
+ # If only Minimum Date is entered and not Define Maximum Date
+ errors.update({answer_tag: self.validation_error_msg})
+ elif max_date and not dateanswer <= max_date:
+ # If only Maximum Date is entered and not Define Minimum Date
+ errors.update({answer_tag: self.validation_error_msg})
+ except ValueError: # check that it is a datetime has been done hereunder
+ pass
+ return errors
+
+ @api.multi
+ def validate_simple_choice(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ if self.comments_allowed:
+ comment_tag = "%s_%s" % (answer_tag, 'comment')
+ # Empty answer to mandatory self
+ if self.constr_mandatory and answer_tag not in post:
+ errors.update({answer_tag: self.constr_error_msg})
+ if self.constr_mandatory and answer_tag in post and not post[answer_tag].strip():
+ errors.update({answer_tag: self.constr_error_msg})
+ # Answer is a comment and is empty
+ if self.constr_mandatory and answer_tag in post and post[answer_tag] == "-1" and self.comment_count_as_answer and comment_tag in post and not post[comment_tag].strip():
+ errors.update({answer_tag: self.constr_error_msg})
+ return errors
+
+ @api.multi
+ def validate_multiple_choice(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ if self.constr_mandatory:
+ answer_candidates = dict_keys_startswith(post, answer_tag)
+ comment_flag = answer_candidates.pop(("%s_%s" % (answer_tag, -1)), None)
+ if self.comments_allowed:
+ comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ # Preventing answers with blank value
+ if all([True if not answer.strip() else False for answer in answer_candidates.values()]):
+ errors.update({answer_tag: self.constr_error_msg})
+ # There is no answer neither comments (if comments count as answer)
+ if not answer_candidates and self.comment_count_as_answer and (not comment_flag or not comment_answer):
+ errors.update({answer_tag: self.constr_error_msg})
+ # There is no answer at all
+ if not answer_candidates and not self.comment_count_as_answer:
+ errors.update({answer_tag: self.constr_error_msg})
+ return errors
+
+ @api.multi
+ def validate_matrix(self, post, answer_tag):
+ self.ensure_one()
+ errors = {}
+ if self.constr_mandatory:
+ lines_number = len(self.labels_ids_2)
+ answer_candidates = dict_keys_startswith(post, answer_tag)
+ answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ # Number of lines that have been answered
+ if self.matrix_subtype == 'simple':
+ answer_number = len(answer_candidates)
+ elif self.matrix_subtype == 'multiple':
+ answer_number = len(set([sk.rsplit('_', 1)[0] for sk in answer_candidates.keys()]))
+ else:
+ raise RuntimeError("Invalid matrix subtype")
+ # Validate that each line has been answered
+ if answer_number != lines_number:
+ 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'
+ _description = 'Survey Label'
+
+ question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade')
+ question_id_2 = fields.Many2one('survey.question', string='Question 2', ondelete='cascade')
+ sequence = fields.Integer('Label Sequence order', default=10)
+ value = fields.Char('Suggested value', translate=True, required=True)
+ quizz_mark = fields.Float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
+
+ @api.constrains('question_id', 'question_id_2')
+ def _check_question_not_empty(self):
+ """Ensure that field question_id XOR field question_id_2 is not null"""
+ if not bool(self.question_id) != bool(self.question_id_2):
+ raise ValidationError("A label must be attached to one and only one question")
+
+
+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'
+ _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")
+ type = fields.Selection([('manually', 'Manually'), ('link', 'Link')], string='Answer Type', default='manually', required=True, readonly=True, oldname="response_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)
+ 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 !
+ 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')
+ print_url = fields.Char("Public link to the empty survey", related='survey_id.print_url')
+
+ quizz_score = fields.Float("Score for the quiz", compute="_compute_quizz_score", default=0.0)
+
+ @api.depends('user_input_line_ids.quizz_mark')
+ def _compute_quizz_score(self):
+ for user_input in self:
+ user_input.quizz_score = sum(user_input.user_input_line_ids.mapped('quizz_mark'))
+
+ _sql_constraints = [
+ ('unique_token', 'UNIQUE (token)', 'A token must be unique!'),
+ ('deadline_in_the_past', 'CHECK (deadline >= date_create)', 'The deadline cannot be in the past')
+ ]
+
+ @api.model
+ def do_clean_emptys(self):
+ """ Remove empty user inputs that have been created manually
+ (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([('type', '=', 'manually'), ('state', '=', 'new'),
+ ('date_create', '<', 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()
+
+ @api.multi
+ def action_view_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
+ }
+
+
+class SurveyUserInputLine(models.Model):
+ _name = 'survey.user_input_line'
+ _description = 'Survey User Input Line'
+ _rec_name = 'date_create'
+
+ 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)
+ page_id = fields.Many2one(related='question_id.page_id', string="Page")
+ survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True)
+ date_create = fields.Datetime('Create Date', default=fields.Datetime.now, required=True)
+ skipped = fields.Boolean('Skipped')
+ answer_type = fields.Selection([
+ ('text', 'Text'),
+ ('number', 'Number'),
+ ('date', 'Date'),
+ ('free_text', 'Free Text'),
+ ('suggestion', 'Suggestion')], string='Answer Type')
+ value_text = fields.Char('Text answer')
+ value_number = fields.Float('Numerical answer')
+ value_date = fields.Datetime('Date answer')
+ value_free_text = fields.Text('Free Text answer')
+ value_suggested = fields.Many2one('survey.label', string="Suggested answer")
+ value_suggested_row = fields.Many2one('survey.label', string="Row answer")
+ quizz_mark = fields.Float('Score given for this choice')
+
+ @api.constrains('skipped', 'answer_type')
+ def _answered_or_skipped(self):
+ for uil in self:
+ if not uil.skipped != bool(uil.answer_type):
+ raise ValidationError(_('A question cannot be unanswered and skipped'))
+
+ @api.constrains('answer_type')
+ def _check_answer_type(self):
+ for uil in self:
+ fields_type = {
+ 'text': bool(uil.value_text),
+ 'number': (bool(uil.value_number) or uil.value_number == 0),
+ 'date': bool(uil.value_date),
+ 'free_text': bool(uil.value_free_text),
+ 'suggestion': bool(uil.value_suggested)
+ }
+ if not fields_type.get(uil.answer_type, True):
+ raise ValidationError(_('The answer must be in the right type'))
+
+ def _get_mark(self, value_suggested):
+ label = self.env['survey.label'].browse(int(value_suggested))
+ mark = label.quizz_mark if label.exists() else 0.0
+ return mark
+
+ @api.model
+ def create(self, vals):
+ value_suggested = vals.get('value_suggested')
+ if value_suggested:
+ vals.update({'quizz_mark': self._get_mark(value_suggested)})
+ return super(SurveyUserInputLine, self).create(vals)
+
+ @api.multi
+ def write(self, vals):
+ value_suggested = vals.get('value_suggested')
+ if value_suggested:
+ vals.update({'quizz_mark': self._get_mark(value_suggested)})
+ return super(SurveyUserInputLine, self).write(vals)
+
+ @api.model
+ def save_lines(self, user_input_id, question, post, answer_tag):
+ """ Save answers to questions, depending on question type
+
+ If an answer already exists for question and user_input_id, it will be
+ overwritten (in order to maintain data consistency).
+ """
+ try:
+ saver = getattr(self, 'save_line_' + question.type)
+ except AttributeError:
+ _logger.error(question.type + ": This type of question has no saving function")
+ return False
+ else:
+ saver(user_input_id, question, post, answer_tag)
+
+ @api.model
+ def save_line_free_text(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False,
+ }
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'free_text', 'value_free_text': post[answer_tag]})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ if old_uil:
+ old_uil.write(vals)
+ else:
+ old_uil.create(vals)
+ return True
+
+ @api.model
+ def save_line_textbox(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'text', 'value_text': post[answer_tag]})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ if old_uil:
+ old_uil.write(vals)
+ else:
+ old_uil.create(vals)
+ return True
+
+ @api.model
+ def save_line_numerical_box(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'number', 'value_number': float(post[answer_tag])})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ if old_uil:
+ old_uil.write(vals)
+ else:
+ old_uil.create(vals)
+ return True
+
+ @api.model
+ def save_line_datetime(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'date', 'value_date': post[answer_tag]})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ if old_uil:
+ old_uil.write(vals)
+ else:
+ old_uil.create(vals)
+ return True
+
+ @api.model
+ def save_line_simple_choice(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ old_uil.sudo().unlink()
+
+ if answer_tag in post and post[answer_tag].strip():
+ vals.update({'answer_type': 'suggestion', 'value_suggested': post[answer_tag]})
+ else:
+ vals.update({'answer_type': None, 'skipped': True})
+
+ # '-1' indicates 'comment count as an answer' so do not need to record it
+ if post.get(answer_tag) and post.get(answer_tag) != '-1':
+ self.create(vals)
+
+ comment_answer = post.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ if comment_answer:
+ vals.update({'answer_type': 'text', 'value_text': comment_answer, 'skipped': False, 'value_suggested': False})
+ self.create(vals)
+
+ return True
+
+ @api.model
+ def save_line_multiple_choice(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ old_uil.sudo().unlink()
+
+ ca_dict = dict_keys_startswith(post, answer_tag)
+ comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ if len(ca_dict) > 0:
+ for key in ca_dict:
+ # '-1' indicates 'comment count as an answer' so do not need to record it
+ if key != ('%s_%s' % (answer_tag, '-1')):
+ vals.update({'answer_type': 'suggestion', 'value_suggested': ca_dict[key]})
+ self.create(vals)
+ if comment_answer:
+ vals.update({'answer_type': 'text', 'value_text': comment_answer, 'value_suggested': False})
+ self.create(vals)
+ if not ca_dict and not comment_answer:
+ vals.update({'answer_type': None, 'skipped': True})
+ self.create(vals)
+ return True
+
+ @api.model
+ def save_line_matrix(self, user_input_id, question, post, answer_tag):
+ vals = {
+ 'user_input_id': user_input_id,
+ 'question_id': question.id,
+ 'survey_id': question.survey_id.id,
+ 'skipped': False
+ }
+ old_uil = self.search([
+ ('user_input_id', '=', user_input_id),
+ ('survey_id', '=', question.survey_id.id),
+ ('question_id', '=', question.id)
+ ])
+ old_uil.sudo().unlink()
+
+ no_answers = True
+ ca_dict = dict_keys_startswith(post, answer_tag)
+
+ comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
+ if comment_answer:
+ vals.update({'answer_type': 'text', 'value_text': comment_answer})
+ self.create(vals)
+ no_answers = False
+
+ if question.matrix_subtype == 'simple':
+ for row in question.labels_ids_2:
+ a_tag = "%s_%s" % (answer_tag, row.id)
+ if a_tag in ca_dict:
+ no_answers = False
+ vals.update({'answer_type': 'suggestion', 'value_suggested': ca_dict[a_tag], 'value_suggested_row': row.id})
+ self.create(vals)
+
+ elif question.matrix_subtype == 'multiple':
+ for col in question.labels_ids:
+ for row in question.labels_ids_2:
+ a_tag = "%s_%s_%s" % (answer_tag, row.id, col.id)
+ if a_tag in ca_dict:
+ no_answers = False
+ vals.update({'answer_type': 'suggestion', 'value_suggested': col.id, 'value_suggested_row': row.id})
+ self.create(vals)
+ if no_answers:
+ vals.update({'answer_type': None, 'skipped': True})
+ self.create(vals)
+ return True
diff --git a/addons/survey/survey.py b/addons/survey/survey.py
deleted file mode 100644
index 17693c685f8..00000000000
--- a/addons/survey/survey.py
+++ /dev/null
@@ -1,1232 +0,0 @@
-# -*- encoding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from openerp.osv import fields, osv
-from openerp.tools.translate import _
-from openerp import SUPERUSER_ID
-from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DF
-from openerp.addons.website.models.website import slug
-from urlparse import urljoin
-from itertools import product
-from collections import Counter
-from collections import OrderedDict
-from openerp.exceptions import UserError
-
-import datetime
-import logging
-import re
-import uuid
-
-_logger = logging.getLogger(__name__)
-
-class survey_stage(osv.Model):
- """Stages for Kanban view of surveys"""
-
- _name = 'survey.stage'
- _description = 'Survey Stage'
- _order = 'sequence,id'
-
- _columns = {
- 'name': fields.char(string="Name", required=True, translate=True),
- 'sequence': fields.integer(string="Sequence"),
- 'closed': fields.boolean(string="Closed", help="If closed, people won't be able to answer to surveys in this column."),
- 'fold': fields.boolean(string="Folded in kanban view")
- }
- _defaults = {
- 'sequence': 1,
- 'closed': False
- }
- _sql_constraints = [
- ('positive_sequence', 'CHECK(sequence >= 0)', 'Sequence number MUST be a natural')
- ]
-
-
-class survey_survey(osv.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.
- '''
-
- _name = 'survey.survey'
- _description = 'Survey'
- _rec_name = 'title'
- _inherit = ['mail.thread', 'ir.needaction_mixin']
-
- # Protected methods #
-
- def _has_questions(self, cr, uid, ids, context=None):
- """ Ensure that this survey has at least one page with at least one
- question. """
- for survey in self.browse(cr, uid, ids, context=context):
- if not survey.page_ids or not [page.question_ids
- for page in survey.page_ids if page.question_ids]:
- return False
- return True
-
- ## Function fields ##
-
- def _is_designed(self, cr, uid, ids, name, arg, context=None):
- res = dict()
- for survey in self.browse(cr, uid, ids, context=context):
- if not survey.page_ids or not [page.question_ids
- for page in survey.page_ids if page.question_ids]:
- res[survey.id] = False
- else:
- res[survey.id] = True
- return res
-
- def _get_tot_sent_survey(self, cr, uid, ids, name, arg, context=None):
- """ Returns the number of invitations sent for this survey, be they
- (partially) completed or not """
- res = dict((id, 0) for id in ids)
- sur_res_obj = self.pool.get('survey.user_input')
- for id in ids:
- res[id] = sur_res_obj.search(cr, uid, # SUPERUSER_ID,
- [('survey_id', '=', id), ('type', '=', 'link')],
- context=context, count=True)
- return res
-
- def _get_tot_start_survey(self, cr, uid, ids, name, arg, context=None):
- """ Returns the number of started instances of this survey, be they
- completed or not """
- res = dict((id, 0) for id in ids)
- sur_res_obj = self.pool.get('survey.user_input')
- for id in ids:
- res[id] = sur_res_obj.search(cr, uid, # SUPERUSER_ID,
- ['&', ('survey_id', '=', id), '|', ('state', '=', 'skip'), ('state', '=', 'done')],
- context=context, count=True)
- return res
-
- def _get_tot_comp_survey(self, cr, uid, ids, name, arg, context=None):
- """ Returns the number of completed instances of this survey """
- res = dict((id, 0) for id in ids)
- sur_res_obj = self.pool.get('survey.user_input')
- for id in ids:
- res[id] = sur_res_obj.search(cr, uid, # SUPERUSER_ID,
- [('survey_id', '=', id), ('state', '=', 'done')],
- context=context, count=True)
- return res
-
- def _get_public_url(self, cr, uid, ids, name, arg, context=None):
- """ Computes a public URL for the survey """
- if context and context.get('relative_url'):
- base_url = '/'
- else:
- base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
- res = {}
- for survey in self.browse(cr, uid, ids, context=context):
- res[survey.id] = urljoin(base_url, "survey/start/%s" % slug(survey))
- return res
-
- def _get_public_url_html(self, cr, uid, ids, name, arg, context=None):
- """ Computes a public URL for the survey (html-embeddable version)"""
- urls = self._get_public_url(cr, uid, ids, name, arg, context=context)
- for id, url in urls.iteritems():
- urls[id] = '<a href="%s">%s</a>' % (url, _("Click here to start survey"))
- return urls
-
- def _get_print_url(self, cr, uid, ids, name, arg, context=None):
- """ Computes a printing URL for the survey """
- if context and context.get('relative_url'):
- base_url = '/'
- else:
- base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
- res = {}
- for survey in self.browse(cr, uid, ids, context=context):
- res[survey.id] = urljoin(base_url, "survey/print/%s" % slug(survey))
- return res
-
- def _get_result_url(self, cr, uid, ids, name, arg, context=None):
- """ Computes an URL for the survey results """
- if context and context.get('relative_url'):
- base_url = '/'
- else:
- base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
- res = {}
- for survey in self.browse(cr, uid, ids, context=context):
- res[survey.id] = urljoin(base_url, "survey/results/%s" % slug(survey))
- return res
-
- # Model fields #
-
- _columns = {
- 'title': fields.char('Title', required=1, translate=True),
- 'page_ids': fields.one2many('survey.page', 'survey_id', 'Pages', copy=True),
- 'stage_id': fields.many2one('survey.stage', string="Stage", ondelete="set null", copy=False),
- '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.function(_get_tot_sent_survey,
- string="Number of sent surveys", type="integer"),
- 'tot_start_survey': fields.function(_get_tot_start_survey,
- string="Number of started surveys", type="integer"),
- 'tot_comp_survey': fields.function(_get_tot_comp_survey,
- string="Number of completed surveys", type="integer"),
- 'description': fields.html('Description', translate=True,
- oldname="description", help="A long description of the purpose of the survey"),
- 'color': fields.integer('Color Index'),
- 'user_input_ids': fields.one2many('survey.user_input', 'survey_id',
- 'User responses', readonly=1),
- 'designed': fields.function(_is_designed, string="Is designed?",
- type="boolean"),
- 'public_url': fields.function(_get_public_url,
- string="Public link", type="char"),
- 'public_url_html': fields.function(_get_public_url_html,
- string="Public link (html version)", type="char"),
- 'print_url': fields.function(_get_print_url,
- string="Print link", type="char"),
- 'result_url': fields.function(_get_result_url,
- string="Results link", type="char"),
- 'email_template_id': fields.many2one('mail.template',
- 'Email Template', ondelete='set null'),
- 'thank_you_message': fields.html('Thank you message', translate=True,
- help="This message will be displayed when survey is completed"),
- 'quizz_mode': fields.boolean(string='Quiz mode'),
- 'active': fields.boolean(string="Active"),
- 'is_closed': fields.related('stage_id', 'closed', type='boolean'),
- }
-
- def _default_stage(self, cr, uid, context=None):
- ids = self.pool['survey.stage'].search(cr, uid, [], limit=1, context=context)
- if ids:
- return ids[0]
- return False
-
- _defaults = {
- 'color': 0,
- 'stage_id': lambda self, *a, **kw: self._default_stage(*a, **kw),
- 'active': True,
- }
-
- def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
- """ Read group customization in order to display all the stages in the
- kanban view, even if they are empty """
- stage_obj = self.pool.get('survey.stage')
- order = stage_obj._order
- access_rights_uid = access_rights_uid or uid
-
- if read_group_order == 'stage_id desc':
- order = '%s desc' % order
-
- stage_ids = stage_obj._search(cr, uid, [], order=order, access_rights_uid=access_rights_uid, context=context)
- result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
-
- # restore order of the search
- result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
-
- fold = {}
- for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
- fold[stage.id] = stage.fold or False
- return result, fold
-
- _group_by_full = {
- 'stage_id': _read_group_stage_ids
- }
-
- # Public methods #
-
- def copy_data(self, cr, uid, id, default=None, context=None):
- current_rec = self.read(cr, uid, id, fields=['title'], context=context)
- title = _("%s (copy)") % (current_rec.get('title'))
- default = dict(default or {}, title=title)
- return super(survey_survey, self).copy_data(cr, uid, id, default,
- context=context)
-
- def next_page(self, cr, uid, user_input, page_id, go_back=False, context=None):
- '''The next page to display to the user, knowing that page_id is the id
- of the last displayed page.
-
- If page_id == 0, it will always return the first page of the survey.
-
- If all the pages have been displayed and go_back == False, it will
- return None
-
- If go_back == True, it will return the *previous* page instead of the
- next page.
-
- .. note::
- It is assumed here that a careful user will not try to set go_back
- to True if she knows that the page to display is the first one!
- (doing this will probably cause a giant worm to eat her house)'''
- survey = user_input.survey_id
- pages = list(enumerate(survey.page_ids))
-
- # First page
- if page_id == 0:
- return (pages[0][1], 0, len(pages) == 1)
-
- current_page_index = pages.index((filter(lambda p: p[1].id == page_id, pages))[0])
-
- # All the pages have been displayed
- if current_page_index == len(pages) - 1 and not go_back:
- return (None, -1, False)
- # Let's get back, baby!
- elif go_back and survey.users_can_go_back:
- return (pages[current_page_index - 1][1], current_page_index - 1, False)
- else:
- # This will show the last page
- if current_page_index == len(pages) - 2:
- return (pages[current_page_index + 1][1], current_page_index + 1, True)
- # This will show a regular page
- else:
- return (pages[current_page_index + 1][1], current_page_index + 1, False)
-
- def filter_input_ids(self, cr, uid, survey, filters, finished=False, context=None):
- '''If user applies any filters, then this function returns list of
- filtered user_input_id and label's strings for display data in web.
- :param filters: list of dictionary (having: row_id, ansewr_id)
- :param finished: True for completely filled survey,Falser otherwise.
- :returns list of filtered user_input_ids.
- '''
- context = context if context else {}
- if filters:
- input_line_obj = self.pool.get('survey.user_input_line')
- domain_filter, choice, filter_display_data = [], [], []
- for filter in filters:
- row_id, answer_id = filter['row_id'], filter['answer_id']
- if row_id == 0:
- choice.append(answer_id)
- else:
- domain_filter.extend(['|', ('value_suggested_row.id', '=', row_id), ('value_suggested.id', '=', answer_id)])
- if choice:
- domain_filter.insert(0, ('value_suggested.id', 'in', choice))
- else:
- domain_filter = domain_filter[1:]
- line_ids = input_line_obj.search(cr, uid, domain_filter, context=context)
- filtered_input_ids = [input.user_input_id.id for input in input_line_obj.browse(cr, uid, line_ids, context=context)]
- else:
- filtered_input_ids, filter_display_data = [], []
- if finished:
- user_input = self.pool.get('survey.user_input')
- if not filtered_input_ids:
- current_filters = user_input.search(cr, uid, [('survey_id', '=', survey.id)], context=context)
- user_input_objs = user_input.browse(cr, uid, current_filters, context=context)
- else:
- user_input_objs = user_input.browse(cr, uid, filtered_input_ids, context=context)
- return [input.id for input in user_input_objs if input.state == 'done']
- return filtered_input_ids
-
- def get_filter_display_data(self, cr, uid, filters, context):
- '''Returns data to display current filters
- :param filters: list of dictionary (having: row_id, answer_id)
- :param finished: True for completely filled survey, False otherwise.
- :returns list of dict having data to display filters.
- '''
- filter_display_data = []
- if filters:
- question_obj = self.pool.get('survey.question')
- label_obj = self.pool.get('survey.label')
- for filter in filters:
- row_id, answer_id = filter['row_id'], filter['answer_id']
- question_id = label_obj.browse(cr, uid, answer_id, context=context).question_id.id
- question = question_obj.browse(cr, uid, question_id, context=context)
- if row_id == 0:
- labels = label_obj.browse(cr, uid, [answer_id], context=context)
- else:
- labels = label_obj.browse(cr, uid, [row_id, answer_id], context=context)
- filter_display_data.append({'question_text': question.question, 'labels': [label.value for label in labels]})
- return filter_display_data
-
- def prepare_result(self, cr, uid, question, current_filters=None, context=None):
- ''' Compute statistical data for questions by counting number of vote per choice on basis of filter '''
- current_filters = current_filters if current_filters else []
- context = context if context else {}
- result_summary = {}
-
- #Calculate and return statistics for choice
- if question.type in ['simple_choice', 'multiple_choice']:
- answers = {}
- comments = []
- [answers.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids]
- for input_line in question.user_input_line_ids:
- if input_line.answer_type == 'suggestion' and answers.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
- answers[input_line.value_suggested.id]['count'] += 1
- if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters):
- comments.append(input_line)
- result_summary = {'answers': answers.values(), 'comments': comments}
-
- #Calculate and return statistics for matrix
- if question.type == 'matrix':
- rows = OrderedDict()
- answers = OrderedDict()
- res = dict()
- comments = []
- [rows.update({label.id: label.value}) for label in question.labels_ids_2]
- [answers.update({label.id: label.value}) for label in question.labels_ids]
- for cell in product(rows.keys(), answers.keys()):
- res[cell] = 0
- for input_line in question.user_input_line_ids:
- if input_line.answer_type == 'suggestion' and (not(current_filters) or input_line.user_input_id.id in current_filters) and input_line.value_suggested_row:
- res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1
- if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters):
- comments.append(input_line)
- result_summary = {'answers': answers, 'rows': rows, 'result': res, 'comments': comments}
-
- #Calculate and return statistics for free_text, textbox, datetime
- if question.type in ['free_text', 'textbox', 'datetime']:
- result_summary = []
- for input_line in question.user_input_line_ids:
- if not(current_filters) or input_line.user_input_id.id in current_filters:
- result_summary.append(input_line)
-
- #Calculate and return statistics for numerical_box
- if question.type == 'numerical_box':
- result_summary = {'input_lines': []}
- all_inputs = []
- for input_line in question.user_input_line_ids:
- if not(current_filters) or input_line.user_input_id.id in current_filters:
- all_inputs.append(input_line.value_number)
- result_summary['input_lines'].append(input_line)
- if all_inputs:
- result_summary.update({'average': round(sum(all_inputs) / len(all_inputs), 2),
- 'max': round(max(all_inputs), 2),
- 'min': round(min(all_inputs), 2),
- 'sum': sum(all_inputs),
- 'most_common': Counter(all_inputs).most_common(5)})
- return result_summary
-
- def get_input_summary(self, cr, uid, question, current_filters=None, context=None):
- ''' Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter '''
- current_filters = current_filters if current_filters else []
- context = context if context else {}
- result = {}
- if question.survey_id.user_input_ids:
- total_input_ids = current_filters or [input_id.id for input_id in question.survey_id.user_input_ids if input_id.state != 'new']
- result['total_inputs'] = len(total_input_ids)
- question_input_ids = []
- for user_input in question.user_input_line_ids:
- if not user_input.skipped:
- question_input_ids.append(user_input.user_input_id.id)
- result['answered'] = len(set(question_input_ids) & set(total_input_ids))
- result['skipped'] = result['total_inputs'] - result['answered']
- return result
-
- # Actions
-
- def action_start_survey(self, cr, uid, ids, context=None):
- ''' Open the website page with the survey form '''
- trail = ""
- context = dict(context or {}, relative_url=True)
- if 'survey_token' in context:
- trail = "/" + context['survey_token']
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Start Survey",
- 'target': 'self',
- 'url': self.read(cr, uid, ids, ['public_url'], context=context)[0]['public_url'] + trail
- }
-
- def action_send_survey(self, cr, uid, ids, context=None):
- ''' Open a window to compose an email, pre-filled with the survey
- message '''
- if not self._has_questions(cr, uid, ids, context=None):
- raise UserError(_('You cannot send an invitation for a survey that has no questions.'))
-
- survey_browse = self.pool.get('survey.survey').browse(cr, uid, ids,
- context=context)[0]
- if survey_browse.stage_id.closed:
- raise UserError(_("You cannot send invitations for closed surveys."))
-
- assert len(ids) == 1, 'This option should only be used for a single \
- survey at a time.'
- ir_model_data = self.pool.get('ir.model.data')
- templates = ir_model_data.get_object_reference(cr, uid,
- 'survey', 'email_template_survey')
- template_id = templates[1] if len(templates) > 0 else False
- ctx = dict(context)
-
- ctx.update({'default_model': 'survey.survey',
- 'default_res_id': ids[0],
- 'default_survey_id': ids[0],
- 'default_use_template': bool(template_id),
- 'default_template_id': template_id,
- 'default_composition_mode': 'comment'}
- )
- return {
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'survey.mail.compose.message',
- 'target': 'new',
- 'context': ctx,
- }
-
- def action_print_survey(self, cr, uid, ids, context=None):
- ''' Open the website page with the survey printable view '''
- trail = ""
- context = dict(context or {}, relative_url=True)
- if 'survey_token' in context:
- trail = "/" + context['survey_token']
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Print Survey",
- 'target': 'self',
- 'url': self.read(cr, uid, ids, ['print_url'], context=context)[0]['print_url'] + trail
- }
-
- def action_result_survey(self, cr, uid, ids, context=None):
- ''' Open the website page with the survey results view '''
- context = dict(context or {}, relative_url=True)
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Results of the Survey",
- 'target': 'self',
- 'url': self.read(cr, uid, ids, ['result_url'], context=context)[0]['result_url']
- }
-
- def action_test_survey(self, cr, uid, ids, context=None):
- ''' Open the website page with the survey form into test mode'''
- context = dict(context or {}, relative_url=True)
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Results of the Survey",
- 'target': 'self',
- 'url': self.read(cr, uid, ids, ['public_url'], context=context)[0]['public_url'] + "/phantom"
- }
-
- def action_survey_user_input(self, cr, uid, ids, context=None):
- action_rec = self.pool['ir.model.data'].xmlid_to_object(cr, uid, 'survey.action_survey_user_input', context=context)
- action = action_rec.read()[0]
- ctx = dict(context)
- ctx.update({'search_default_survey_id': ids[0],
- 'search_default_completed': 1})
- action['context'] = ctx
- return action
-
-class survey_page(osv.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. '''
-
- _name = 'survey.page'
- _description = 'Survey Page'
- _rec_name = 'title'
- _order = 'sequence,id'
-
- # Model Fields #
-
- _columns = {
- 'title': fields.char('Page Title', required=1,
- translate=True),
- 'survey_id': fields.many2one('survey.survey', 'Survey',
- ondelete='cascade', required=True),
- 'question_ids': fields.one2many('survey.question', 'page_id',
- 'Questions', copy=True),
- 'sequence': fields.integer('Page number'),
- 'description': fields.html('Description',
- help="An introductory text to your page", translate=True,
- oldname="note"),
- }
- _defaults = {
- 'sequence': 10
- }
-
-
-class survey_question(osv.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...).'''
- _name = 'survey.question'
- _description = 'Survey Question'
- _rec_name = 'question'
- _order = 'sequence,id'
-
- # Model fields #
-
- _columns = {
- # Question metadata
- 'page_id': fields.many2one('survey.page', 'Survey page',
- ondelete='cascade', required=1),
- 'survey_id': fields.related('page_id', 'survey_id', type='many2one',
- relation='survey.survey', string='Survey'),
- 'sequence': fields.integer(string='Sequence'),
-
- # Question
- 'question': fields.char('Question Name', required=1, translate=True),
- 'description': fields.html('Description', help="Use this field to add \
- additional explanations about your question", translate=True,
- oldname='descriptive_text'),
-
- # Answer
- 'type': fields.selection([('free_text', 'Multiple Lines Text Box'),
- ('textbox', 'Single Line Text Box'),
- ('numerical_box', 'Numerical Value'),
- ('datetime', 'Date and Time'),
- ('simple_choice', 'Multiple choice: only one answer'),
- ('multiple_choice', 'Multiple choice: multiple answers allowed'),
- ('matrix', 'Matrix')], 'Type of Question', size=15, required=1),
- 'matrix_subtype': fields.selection([('simple', 'One choice per row'),
- ('multiple', 'Multiple choices per row')], 'Matrix Type'),
- 'labels_ids': fields.one2many('survey.label',
- 'question_id', 'Types of answers', oldname='answer_choice_ids', copy=True),
- 'labels_ids_2': fields.one2many('survey.label',
- 'question_id_2', '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
-
- # Display options
- 'column_nb': fields.selection([('12', '1'),
- ('6', '2'),
- ('4', '3'),
- ('3', '4'),
- ('2', '6')],
- 'Number of columns'),
- # These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap
- 'display_mode': fields.selection([('columns', 'Radio Buttons'),
- ('dropdown', 'Selection Box')],
- 'Display mode'),
-
- # Comments
- 'comments_allowed': fields.boolean('Show Comments Field',
- oldname="allow_comment"),
- 'comments_message': fields.char('Comment Message', translate=True),
- 'comment_count_as_answer': fields.boolean('Comment Field is an Answer Choice',
- oldname='make_comment_field'),
-
- # Validation
- 'validation_required': fields.boolean('Validate entry',
- oldname='is_validation_require'),
- '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'),
- 'validation_min_float_value': fields.float('Minimum value'),
- 'validation_max_float_value': fields.float('Maximum value'),
- 'validation_min_date': fields.datetime('Minimum Date'),
- 'validation_max_date': fields.datetime('Maximum Date'),
- 'validation_error_msg': fields.char('Validation Error message',
- oldname='validation_valid_err_msg',
- translate=True),
-
- # 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),
- 'user_input_line_ids': fields.one2many('survey.user_input_line',
- 'question_id', 'Answers',
- domain=[('skipped', '=', False)]),
- }
-
- _defaults = {
- 'page_id': lambda self, cr, uid, context: context.get('page_id'),
- 'sequence': 10,
- 'type': 'free_text',
- 'matrix_subtype': 'simple',
- 'column_nb': '12',
- 'display_mode': 'columns',
- 'constr_error_msg': lambda s, cr, uid, c: _('This question requires an answer.'),
- 'validation_error_msg': lambda s, cr, uid, c: _('The answer you entered has an invalid format.'),
- 'validation_required': False,
- 'comments_message': lambda s, cr, uid, c: _('If other, please specify:'),
- }
-
- _sql_constraints = [
- ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'),
- ('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'),
- ('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'),
- ('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'),
- ('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!')
- ]
-
- def onchange_validation_email(self, cr, uid, ids, validation_email, context=None):
- return {'value': {'validation_required': False}} if validation_email else {}
-
- # Validation methods
-
- def validate_question(self, cr, uid, question, post, answer_tag, context=None):
- ''' Validate question, depending on question type and parameters '''
- try:
- checker = getattr(self, 'validate_' + question.type)
- except AttributeError:
- _logger.warning(question.type + ": This type of question has no validation method")
- return {}
- else:
- return checker(cr, uid, question, post, answer_tag, context=context)
-
- def validate_free_text(self, cr, uid, question, post, answer_tag, context=None):
- errors = {}
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if question.constr_mandatory and not answer:
- errors.update({answer_tag: question.constr_error_msg})
- return errors
-
- def validate_textbox(self, cr, uid, question, post, answer_tag, context=None):
- errors = {}
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if question.constr_mandatory and not answer:
- errors.update({answer_tag: question.constr_error_msg})
- # Email format validation
- # Note: this validation is very basic:
- # all the strings of the form
- # <something>@<anything>.<extension>
- # will be accepted
- if answer and question.validation_email:
- if not re.match(r"[^@]+@[^@]+\.[^@]+", answer):
- errors.update({answer_tag: _('This answer must be an email address')})
- # Answer validation (if properly defined)
- # Length of the answer must be in a range
- if answer and question.validation_required:
- if not (question.validation_length_min <= len(answer) <= question.validation_length_max):
- errors.update({answer_tag: question.validation_error_msg})
- return errors
-
- def validate_numerical_box(self, cr, uid, question, post, answer_tag, context=None):
- errors = {}
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if question.constr_mandatory and not answer:
- errors.update({answer_tag: question.constr_error_msg})
- # Checks if user input is a number
- if answer:
- try:
- floatanswer = float(answer)
- except ValueError:
- errors.update({answer_tag: _('This is not a number')})
- # Answer validation (if properly defined)
- if answer and question.validation_required:
- # Answer is not in the right range
- try:
- floatanswer = float(answer) # check that it is a float has been done hereunder
- if not (question.validation_min_float_value <= floatanswer <= question.validation_max_float_value):
- errors.update({answer_tag: question.validation_error_msg})
- except ValueError:
- pass
- return errors
-
- def validate_datetime(self, cr, uid, question, post, answer_tag, context=None):
- errors = {}
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if question.constr_mandatory and not answer:
- errors.update({answer_tag: question.constr_error_msg})
- # Checks if user input is a datetime
- if answer:
- try:
- dateanswer = datetime.datetime.strptime(answer, DF)
- except ValueError:
- errors.update({answer_tag: _('This is not a date/time')})
- return errors
- # Answer validation (if properly defined)
- if answer and question.validation_required:
- # Answer is not in the right range
- try:
- dateanswer = datetime.datetime.strptime(answer, DF)
- min_date = question.validation_min_date and datetime.datetime.strptime(question.validation_min_date, DF) or False
- max_date = question.validation_max_date and datetime.datetime.strptime(question.validation_max_date, DF) or False
-
- if (min_date and max_date and not(min_date <= dateanswer <= max_date)):
- # If Minimum and Maximum Date are entered
- errors.update({answer_tag: question.validation_error_msg})
- elif (min_date and not(min_date <= dateanswer)):
- # If only Minimum Date is entered and not Define Maximum Date
- errors.update({answer_tag: question.validation_error_msg})
- elif (max_date and not(dateanswer <= max_date)):
- # If only Maximum Date is entered and not Define Minimum Date
- errors.update({answer_tag: question.validation_error_msg})
- except ValueError: # check that it is a datetime has been done hereunder
- pass
- return errors
-
- def validate_simple_choice(self, cr, uid, question, post, answer_tag, context=None):
- errors = {}
- if question.comments_allowed:
- comment_tag = "%s_%s" % (answer_tag, 'comment')
- # Empty answer to mandatory question
- if question.constr_mandatory and answer_tag not in post:
- errors.update({answer_tag: question.constr_error_msg})
- if question.constr_mandatory and answer_tag in post and post[answer_tag].strip() == '':
- errors.update({answer_tag: question.constr_error_msg})
- # Answer is a comment and is empty
- if question.constr_mandatory and answer_tag in post and post[answer_tag] == "-1" and question.comment_count_as_answer and comment_tag in post and not post[comment_tag].strip():
- errors.update({answer_tag: question.constr_error_msg})
- return errors
-
- def validate_multiple_choice(self, cr, uid, question, post, answer_tag, context=None):
- errors = {}
- if question.constr_mandatory:
- answer_candidates = dict_keys_startswith(post, answer_tag)
- comment_flag = answer_candidates.pop(("%s_%s" % (answer_tag, -1)), None)
- if question.comments_allowed:
- comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- # Preventing answers with blank value
- if all([True if answer.strip() == '' else False for answer in answer_candidates.values()]):
- errors.update({answer_tag: question.constr_error_msg})
- # There is no answer neither comments (if comments count as answer)
- if not answer_candidates and question.comment_count_as_answer and (not comment_flag or not comment_answer):
- errors.update({answer_tag: question.constr_error_msg})
- # There is no answer at all
- if not answer_candidates and not question.comment_count_as_answer:
- errors.update({answer_tag: question.constr_error_msg})
- return errors
-
- def validate_matrix(self, cr, uid, question, post, answer_tag, context=None):
- errors = {}
- if question.constr_mandatory:
- lines_number = len(question.labels_ids_2)
- answer_candidates = dict_keys_startswith(post, answer_tag)
- comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- # Number of lines that have been answered
- if question.matrix_subtype == 'simple':
- answer_number = len(answer_candidates)
- elif question.matrix_subtype == 'multiple':
- answer_number = len(set([sk.rsplit('_', 1)[0] for sk in answer_candidates.keys()]))
- else:
- raise RuntimeError("Invalid matrix subtype")
- # Validate that each line has been answered
- if answer_number != lines_number:
- errors.update({answer_tag: question.constr_error_msg})
- return errors
-
-
-class survey_label(osv.Model):
- ''' A suggested answer for a question '''
- _name = 'survey.label'
- _rec_name = 'value'
- _order = 'sequence,id'
- _description = 'Survey Label'
-
- def _check_question_not_empty(self, cr, uid, ids, context=None):
- '''Ensure that field question_id XOR field question_id_2 is not null'''
- for label in self.browse(cr, uid, ids, context=context):
- # 'bool()' is required in order to make '!=' act as XOR with objects
- return bool(label.question_id) != bool(label.question_id_2)
-
- _columns = {
- 'question_id': fields.many2one('survey.question', 'Question',
- ondelete='cascade'),
- 'question_id_2': fields.many2one('survey.question', 'Question 2',
- ondelete='cascade'),
- 'sequence': fields.integer('Label Sequence order'),
- 'value': fields.char("Suggested value", translate=True,
- required=True),
- 'quizz_mark': fields.float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer"),
- }
- _defaults = {
- 'sequence': 10,
- }
- _constraints = [
- (_check_question_not_empty, "A label must be attached to one and only one question", ['question_id', 'question_id_2'])
- ]
-
-
-class survey_user_input(osv.Model):
- ''' Metadata for a set of one user's answers to a particular survey '''
- _name = "survey.user_input"
- _rec_name = 'date_create'
- _description = 'Survey User Input'
-
- def _quizz_get_score(self, cr, uid, ids, name, args, context=None):
- ret = dict()
- for user_input in self.browse(cr, uid, ids, context=context):
- ret[user_input.id] = sum([uil.quizz_mark for uil in user_input.user_input_line_ids] or [0.0])
- return ret
-
- _columns = {
- 'survey_id': fields.many2one('survey.survey', 'Survey', required=True,
- readonly=1, ondelete='restrict'),
- 'date_create': fields.datetime('Creation Date', required=True,
- readonly=1, copy=False),
- 'deadline': fields.datetime("Deadline",
- help="Date by which the person can open the survey and submit answers",
- oldname="date_deadline"),
- 'type': fields.selection([('manually', 'Manually'), ('link', 'Link')],
- 'Answer Type', required=1, readonly=1,
- oldname="response_type"),
- 'state': fields.selection([('new', 'Not started yet'),
- ('skip', 'Partially completed'),
- ('done', 'Completed')],
- 'Status',
- readonly=True),
- 'test_entry': fields.boolean('Test entry', readonly=1),
- 'token': fields.char("Identification token", readonly=1, required=1, copy=False),
-
- # Optional Identification data
- 'partner_id': fields.many2one('res.partner', 'Partner', readonly=1),
- 'email': fields.char("E-mail", readonly=1),
-
- # Displaying data
- 'last_displayed_page_id': fields.many2one('survey.page',
- 'Last displayed page'),
- # The answers !
- 'user_input_line_ids': fields.one2many('survey.user_input_line',
- 'user_input_id', 'Answers', copy=True),
-
- # URLs used to display the answers
- 'result_url': fields.related('survey_id', 'result_url', type='char',
- string="Public link to the survey results"),
- 'print_url': fields.related('survey_id', 'print_url', type='char',
- string="Public link to the empty survey"),
-
- 'quizz_score': fields.function(_quizz_get_score, type="float", string="Score for the quiz")
- }
- _defaults = {
- 'date_create': fields.datetime.now,
- 'type': 'manually',
- 'state': 'new',
- 'token': lambda s, cr, uid, c: uuid.uuid4().__str__(),
- 'quizz_score': 0.0,
- }
-
- _sql_constraints = [
- ('unique_token', 'UNIQUE (token)', 'A token must be unique!'),
- ('deadline_in_the_past', 'CHECK (deadline >= date_create)', 'The deadline cannot be in the past')
- ]
-
- def do_clean_emptys(self, cr, uid, automatic=False, context=None):
- ''' Remove empty user inputs that have been created manually
- (used as a cronjob declared in data/survey_cron.xml) '''
- empty_user_input_ids = self.search(cr, uid, [('type', '=', 'manually'),
- ('state', '=', 'new'),
- ('date_create', '<', (datetime.datetime.now() - datetime.timedelta(hours=1)).strftime(DF))],
- context=context)
- if empty_user_input_ids:
- self.unlink(cr, uid, empty_user_input_ids, context=context)
-
- def action_survey_resent(self, cr, uid, ids, context=None):
- ''' Sent again the invitation '''
- record = self.browse(cr, uid, ids[0], context=context)
- context = dict(context or {})
- context.update({
- 'survey_resent_token': True,
- 'default_partner_ids': record.partner_id and [record.partner_id.id] or [],
- 'default_multi_email': record.email or "",
- 'default_public': 'email_private',
- })
- return self.pool.get('survey.survey').action_send_survey(cr, uid,
- [record.survey_id.id], context=context)
-
- def action_view_answers(self, cr, uid, ids, context=None):
- ''' Open the website page with the survey form '''
- user_input = self.read(cr, uid, ids, ['print_url', 'token'], context=context)[0]
- return {
- 'type': 'ir.actions.act_url',
- 'name': "View Answers",
- 'target': 'self',
- 'url': '%s/%s' % (user_input['print_url'], user_input['token'])
- }
-
- def action_survey_results(self, cr, uid, ids, context=None):
- ''' Open the website page with the survey results '''
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Survey Results",
- 'target': 'self',
- 'url': self.read(cr, uid, ids, ['result_url'], context=context)[0]['result_url']
- }
-
-
-class survey_user_input_line(osv.Model):
- _name = 'survey.user_input_line'
- _description = 'Survey User Input Line'
- _rec_name = 'date_create'
-
- def _answered_or_skipped(self, cr, uid, ids, context=None):
- for uil in self.browse(cr, uid, ids, context=context):
- # 'bool()' is required in order to make '!=' act as XOR with objects
- return uil.skipped != bool(uil.answer_type)
-
- def _check_answer_type(self, cr, uid, ids, context=None):
- for uil in self.browse(cr, uid, ids, context=None):
- if uil.answer_type:
- if uil.answer_type == 'text':
- # 'bool()' is required in order to make '!=' act as XOR with objects
- return bool(uil.value_text)
- elif uil.answer_type == 'number':
- return (uil.value_number == 0) or (uil.value_number != False)
- elif uil.answer_type == 'date':
- return bool(uil.value_date)
- elif uil.answer_type == 'free_text':
- return bool(uil.value_free_text)
- elif uil.answer_type == 'suggestion':
- return bool(uil.value_suggested)
- return True
-
- _columns = {
- 'user_input_id': fields.many2one('survey.user_input', 'User Input',
- ondelete='cascade', required=1),
- 'question_id': fields.many2one('survey.question', 'Question',
- ondelete='restrict', required=1),
- 'page_id': fields.related('question_id', 'page_id', type='many2one',
- relation='survey.page', string="Page"),
- 'survey_id': fields.related('user_input_id', 'survey_id',
- type="many2one", relation="survey.survey",
- string='Survey', store=True),
- 'date_create': fields.datetime('Create Date', required=1),
- 'skipped': fields.boolean('Skipped'),
- 'answer_type': fields.selection([('text', 'Text'),
- ('number', 'Number'),
- ('date', 'Date'),
- ('free_text', 'Free Text'),
- ('suggestion', 'Suggestion')],
- 'Answer Type'),
- 'value_text': fields.char("Text answer"),
- 'value_number': fields.float("Numerical answer"),
- 'value_date': fields.datetime("Date answer"),
- 'value_free_text': fields.text("Free Text answer"),
- 'value_suggested': fields.many2one('survey.label', "Suggested answer"),
- 'value_suggested_row': fields.many2one('survey.label', "Row answer"),
- 'quizz_mark': fields.float("Score given for this choice")
- }
-
- _defaults = {
- 'skipped': False,
- 'date_create': fields.datetime.now()
- }
- _constraints = [
- (_answered_or_skipped, "A question cannot be unanswered and skipped", ['skipped', 'answer_type']),
- (_check_answer_type, "The answer must be in the right type", ['answer_type', 'text', 'number', 'date', 'free_text', 'suggestion'])
- ]
-
- def __get_mark(self, cr, uid, value_suggested, context=None):
- try:
- mark = self.pool.get('survey.label').browse(cr, uid, int(value_suggested), context=context).quizz_mark
- except AttributeError:
- mark = 0.0
- except KeyError:
- mark = 0.0
- except ValueError:
- mark = 0.0
- return mark
-
- def create(self, cr, uid, vals, context=None):
- value_suggested = vals.get('value_suggested')
- if value_suggested:
- vals.update({'quizz_mark': self.__get_mark(cr, uid, value_suggested)})
- return super(survey_user_input_line, self).create(cr, uid, vals, context=context)
-
- def write(self, cr, uid, ids, vals, context=None):
- value_suggested = vals.get('value_suggested')
- if value_suggested:
- vals.update({'quizz_mark': self.__get_mark(cr, uid, value_suggested)})
- return super(survey_user_input_line, self).write(cr, uid, ids, vals, context=context)
-
- def save_lines(self, cr, uid, user_input_id, question, post, answer_tag,
- context=None):
- ''' Save answers to questions, depending on question type
-
- If an answer already exists for question and user_input_id, it will be
- overwritten (in order to maintain data consistency). '''
- try:
- saver = getattr(self, 'save_line_' + question.type)
- except AttributeError:
- _logger.error(question.type + ": This type of question has no saving function")
- return False
- else:
- saver(cr, uid, user_input_id, question, post, answer_tag, context=context)
-
- def save_line_free_text(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'page_id': question.page_id.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False,
- }
- if answer_tag in post and post[answer_tag].strip() != '':
- vals.update({'answer_type': 'free_text', 'value_free_text': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)],
- context=context)
- if old_uil:
- self.write(cr, uid, old_uil[0], vals, context=context)
- else:
- self.create(cr, uid, vals, context=context)
- return True
-
- def save_line_textbox(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'page_id': question.page_id.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip() != '':
- vals.update({'answer_type': 'text', 'value_text': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)],
- context=context)
- if old_uil:
- self.write(cr, uid, old_uil[0], vals, context=context)
- else:
- self.create(cr, uid, vals, context=context)
- return True
-
- def save_line_numerical_box(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'page_id': question.page_id.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip() != '':
- vals.update({'answer_type': 'number', 'value_number': float(post[answer_tag])})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)],
- context=context)
- if old_uil:
- self.write(cr, uid, old_uil[0], vals, context=context)
- else:
- self.create(cr, uid, vals, context=context)
- return True
-
- def save_line_datetime(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'page_id': question.page_id.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip() != '':
- vals.update({'answer_type': 'date', 'value_date': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)],
- context=context)
- if old_uil:
- self.write(cr, uid, old_uil[0], vals, context=context)
- else:
- self.create(cr, uid, vals, context=context)
- return True
-
- def save_line_simple_choice(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'page_id': question.page_id.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)],
- context=context)
- if old_uil:
- self.unlink(cr, SUPERUSER_ID, old_uil, context=context)
-
- if answer_tag in post and post[answer_tag].strip() != '':
- vals.update({'answer_type': 'suggestion', 'value_suggested': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
-
- # '-1' indicates 'comment count as an answer' so do not need to record it
- if post.get(answer_tag) and post.get(answer_tag) != '-1':
- self.create(cr, uid, vals, context=context)
-
- comment_answer = post.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- if comment_answer:
- vals.update({'answer_type': 'text', 'value_text': comment_answer, 'skipped': False, 'value_suggested': False})
- self.create(cr, uid, vals, context=context)
-
- return True
-
- def save_line_multiple_choice(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'page_id': question.page_id.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)],
- context=context)
- if old_uil:
- self.unlink(cr, SUPERUSER_ID, old_uil, context=context)
-
- ca = dict_keys_startswith(post, answer_tag)
- comment_answer = ca.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- if len(ca) > 0:
- for a in ca:
- # '-1' indicates 'comment count as an answer' so do not need to record it
- if a != ('%s_%s' % (answer_tag, '-1')):
- vals.update({'answer_type': 'suggestion', 'value_suggested': ca[a]})
- self.create(cr, uid, vals, context=context)
- if comment_answer:
- vals.update({'answer_type': 'text', 'value_text': comment_answer, 'value_suggested': False})
- self.create(cr, uid, vals, context=context)
- if not ca and not comment_answer:
- vals.update({'answer_type': None, 'skipped': True})
- self.create(cr, uid, vals, context=context)
- return True
-
- def save_line_matrix(self, cr, uid, user_input_id, question, post, answer_tag, context=None):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'page_id': question.page_id.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)],
- context=context)
- if old_uil:
- self.unlink(cr, SUPERUSER_ID, old_uil, context=context)
-
- no_answers = True
- ca = dict_keys_startswith(post, answer_tag)
-
- comment_answer = ca.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- if comment_answer:
- vals.update({'answer_type': 'text', 'value_text': comment_answer})
- self.create(cr, uid, vals, context=context)
- no_answers = False
-
- if question.matrix_subtype == 'simple':
- for row in question.labels_ids_2:
- a_tag = "%s_%s" % (answer_tag, row.id)
- if a_tag in ca:
- no_answers = False
- vals.update({'answer_type': 'suggestion', 'value_suggested': ca[a_tag], 'value_suggested_row': row.id})
- self.create(cr, uid, vals, context=context)
-
- elif question.matrix_subtype == 'multiple':
- for col in question.labels_ids:
- for row in question.labels_ids_2:
- a_tag = "%s_%s_%s" % (answer_tag, row.id, col.id)
- if a_tag in ca:
- no_answers = False
- vals.update({'answer_type': 'suggestion', 'value_suggested': col.id, 'value_suggested_row': row.id})
- self.create(cr, uid, vals, context=context)
- if no_answers:
- vals.update({'answer_type': None, 'skipped': True})
- self.create(cr, uid, vals, context=context)
- return True
-
-
-def dict_keys_startswith(dictionary, string):
- '''Returns a dictionary containing the elements of <dict> whose keys start
- with <string>.
-
- .. note::
- This function uses dictionary comprehensions (Python >= 2.7)'''
- return {k: dictionary[k] for k in filter(lambda key: key.startswith(string), dictionary.keys())}
diff --git a/addons/survey/tests/test_survey.py b/addons/survey/tests/test_survey.py
index 72d36427846..a827fc29ee7 100644
--- a/addons/survey/tests/test_survey.py
+++ b/addons/survey/tests/test_survey.py
@@ -7,10 +7,10 @@ from collections import Counter
from itertools import product
from urlparse import urljoin
-from openerp import _
-from openerp.exceptions import UserError
-from openerp.tests.common import TransactionCase
-from openerp.addons.website.models.website import slug
+from odoo import _
+from odoo.exceptions import UserError
+from odoo.tests.common import TransactionCase
+from odoo.addons.website.models.website import slug
class TestSurvey(TransactionCase):
@@ -31,19 +31,11 @@ class TestSurvey(TransactionCase):
'name': 'Wout Janssens', 'login': 'Wout', 'alias_name': 'wout', 'email': 'wout.janssens@example.com',
'groups_id': [(6, 0, [self.ref('base.group_public')])]})
- self.Partner = self.env['res.partner']
- self.Survey = self.env['survey.survey']
- self.Question = self.env['survey.question']
- self.Stage = self.env['survey.stage']
- self.SurveyMailMessage = self.env['survey.mail.compose.message']
- self.UserInput = self.env['survey.user_input']
- self.UserInputLine = self.env['survey.user_input_line']
-
- self.survey1 = self.Survey.sudo(self.survey_manager).create({'title': "S0", 'page_ids': [(0, 0, {'title': "P0"})]})
+ self.survey1 = self.env['survey.survey'].sudo(self.survey_manager).create({'title': "S0", 'page_ids': [(0, 0, {'title': "P0"})]})
self.page1 = self.survey1.page_ids[0]
def test_00_create_minimal_survey(self):
- question = self.Question.sudo(self.survey_manager).create({'page_id': self.page1.id, 'question': 'Q0'})
+ question = self.env['survey.question'].sudo(self.survey_manager).create({'page_id': self.page1.id, 'question': 'Q0'})
self.assertEqual(self.survey1.title, "S0", msg="Title of the survey is somehow modified.")
self.assertEqual(len(self.survey1.page_ids), 1, msg="Additional Pages are linked with the survey after creation.")
self.assertEqual(self.page1.title, "P0", msg="Title of the page is somehow modified.")
@@ -51,88 +43,88 @@ class TestSurvey(TransactionCase):
self.assertEqual(question.question, "Q0", msg="Title of the Question is somehow modified.")
def test_01_question_type_validation_save_line_function(self):
- for (question_type, text) in self.Question._columns['type'].selection:
+ for (question_type, text) in self.env['survey.question']._columns['type'].selection:
# Each question ype must have validation function.
- self.assertTrue(hasattr(self.Question, 'validate_' + question_type), msg="Question must have a validation method in\
+ self.assertTrue(hasattr(self.env['survey.question'], 'validate_' + question_type), msg="Question must have a validation method in\
the form of 'validate_' followed by the name of the type.")
# Survey Input Lines must have validation function for each line.
- self.assertTrue(hasattr(self.UserInputLine, 'save_line_' + question_type), msg="Inputline must have Save method in \
+ self.assertTrue(hasattr(self.env['survey.user_input_line'], 'save_line_' + question_type), msg="Inputline must have Save method in \
the form of 'save_line_' followed by the name of the type.")
def test_02_question_answer_required(self):
- for (question_type, text) in self.Question._columns['type'].selection:
+ for (question_type, text) in self.env['survey.question']._columns['type'].selection:
# Blank value of field is not accepted for mandatory questions.
if question_type == 'multiple_choice':
- question = self.Question.sudo(self.survey_manager).create({
+ question = self.env['survey.question'].sudo(self.survey_manager).create({
'page_id': self.page1.id, 'question': 'Q0', 'type': 'multiple_choice',
'constr_mandatory': True, 'constr_error_msg': 'Error',
'labels_ids': [(0, 0, {'value': "MChoice0", "quizz_mark": 0}), (0, 0, {'value': "MChoice1", "quizz_mark": 0})]})
elif question_type == 'matrix':
- question = self.Question.sudo(self.survey_manager).create({
+ question = self.env['survey.question'].sudo(self.survey_manager).create({
'page_id': self.page1.id, 'question': 'Q0', 'type': 'matrix', 'matrix_subtype': 'simple',
'constr_mandatory': True, 'constr_error_msg': 'Error',
'labels_ids': [(0, 0, {'value': "Column0", "quizz_mark": 0}), (0, 0, {'value': "Column1", "quizz_mark": 0})],
'labels_ids_2': [(0, 0, {'value': "Row0", "quizz_mark": 0}), (0, 0, {'value': "Row1", "quizz_mark": 0})]})
else:
- question = self.Question.sudo(self.survey_manager).create({
+ question = self.env['survey.question'].sudo(self.survey_manager).create({
'page_id': self.page1.id, 'question': 'Q0', 'type': question_type, 'constr_mandatory': True, 'constr_error_msg': 'Error'})
answer_tag = '%s_%s_%s' % (self.survey1.id, self.page1.id, question.id)
- self.assertDictEqual({answer_tag: "Error"}, self.Question.validate_question(question, {answer_tag: ''}, answer_tag),
+ self.assertDictEqual({answer_tag: "Error"}, question.validate_question({answer_tag: ''}, answer_tag),
msg=("Validation function for type %s is unable to generate error if it is mandatory and answer is blank." % question_type))
def test_03_question_textbox(self):
questions = [
- self.Question.sudo(self.survey_manager).create({
+ self.env['survey.question'].sudo(self.survey_manager).create({
'page_id': self.page1.id, 'question': 'Q0', 'type': 'textbox', 'validation_email': True}),
- self.Question.sudo(self.survey_manager).create({
+ self.env['survey.question'].sudo(self.survey_manager).create({
'page_id': self.page1.id, 'question': 'Q1', 'type': 'textbox', 'validation_required': True,
'validation_length_min': 2, 'validation_length_max': 8, 'validation_error_msg': "Error"})]
results = [('test @ testcom', _('This answer must be an email address')), ('t', 'Error')]
for i in range(len(questions)):
answer_tag = '%s_%s_%s' % (self.survey1.id, self.page1.id, questions[i].id)
- self.assertEqual(self.Question.validate_question(questions[i], {answer_tag: results[i][0]}, answer_tag), {answer_tag: results[i][1]}, msg="\
+ self.assertEqual(questions[i].validate_question({answer_tag: results[i][0]}, answer_tag), {answer_tag: results[i][1]}, msg="\
Validation function for textbox is unable to notify if answer is violating the validation rules")
def test_04_question_numerical_box(self):
- question = self.Question.sudo(self.survey_manager).create({
+ question = self.env['survey.question'].sudo(self.survey_manager).create({
'page_id': self.page1.id, 'question': 'Q0', 'type': 'numerical_box', 'validation_required': True,
'validation_min_float_value': 2.1, 'validation_max_float_value': 3.0, 'validation_error_msg': "Error"})
answer_tag = '%s_%s_%s' % (self.survey1.id, self.page1.id, question.id)
results = [('aaa', _('This is not a number')), ('4.5', 'Error'), ('0.1', 'Error')]
for i in range(len(results)):
- self.assertEqual(self.Question.validate_question(question, {answer_tag: results[i][0]}, answer_tag), {answer_tag: results[i][1]}, msg="\
+ self.assertEqual(question.validate_question({answer_tag: results[i][0]}, answer_tag), {answer_tag: results[i][1]}, msg="\
Validation function for type numerical_box is unable to notify if answer is violating the validation rules")
def test_05_question_datetime(self):
- question = self.Question.sudo(self.survey_manager).create({
+ question = self.env['survey.question'].sudo(self.survey_manager).create({
'page_id': self.page1.id, 'question': 'Q0', 'type': 'datetime', 'validation_required': True,
'validation_min_date': '2015-03-20 00:00:00', 'validation_max_date': '2015-03-25 00:00:00', 'validation_error_msg': "Error"})
answer_tag = '%s_%s_%s' % (self.survey1.id, self.page1.id, question.id)
results = [('2015-55-10', _('This is not a date/time')), ('2015-03-19 00:00:00', 'Error'), ('2015-03-26 00:00:00', 'Error')]
for i in range(len(results)):
- self.assertEqual(self.Question.validate_question(question, {answer_tag: results[i][0]}, answer_tag), {answer_tag: results[i][1]}, msg="\
+ self.assertEqual(question.validate_question({answer_tag: results[i][0]}, answer_tag), {answer_tag: results[i][1]}, msg="\
Validation function for type datetime is unable to notify if answer is violating the validation rules")
def test_06_survey_sharing(self):
# Case-1: Executing action with correct data.
- correct_survey = self.Survey.sudo(self.survey_manager).create({
- 'title': "S0", 'stage_id': self.Stage.search([('sequence', '=', 1)]).id,
+ correct_survey = self.env['survey.survey'].sudo(self.survey_manager).create({
+ 'title': "S0", 'stage_id': self.env['survey.stage'].search([('sequence', '=', 1)]).id,
'page_ids': [(0, 0, {'title': "P0", 'question_ids': [(0, 0, {'question': "Q0", 'type': 'free_text'})]})]})
action = correct_survey.action_send_survey()
- templates = self.env['ir.model.data'].get_object_reference('survey', 'email_template_survey')
- template_id = templates[1] if len(templates) > 0 else False
- ctx = dict(self.env.context)
- ctx.update({
- 'default_model': 'survey.survey',
- 'default_res_id': correct_survey.id,
- 'default_survey_id': correct_survey.id,
- 'default_use_template': bool(template_id),
- 'default_template_id': template_id,
- 'default_composition_mode': 'comment'})
+ template = self.env.ref('survey.email_template_survey', raise_if_not_found=False)
+
+ ctx = dict(
+ self.env.context,
+ default_model='survey.survey',
+ default_res_id=correct_survey.id,
+ default_survey_id=correct_survey.id,
+ default_use_template=bool(template),
+ default_template_id=template and template.id or False,
+ default_composition_mode='comment')
self.assertDictEqual(action, {
'type': 'ir.actions.act_window',
@@ -145,26 +137,26 @@ class TestSurvey(TransactionCase):
# Case-2: Executing action with incorrect data.
surveys = [
- self.Survey.sudo(self.survey_manager).create({ # Survey without any page or question.
+ self.env['survey.survey'].sudo(self.survey_manager).create({ # Survey without any page or question.
'title': "Test survey"}),
- self.Survey.sudo(self.survey_manager).create({ # Closed Survey.
- 'title': "S0", 'stage_id': self.Stage.search([('closed', '=', True)]).id, # Getting Closed stage id.
+ self.env['survey.survey'].sudo(self.survey_manager).create({ # Closed Survey.
+ 'title': "S0", 'stage_id': self.env['survey.stage'].search([('closed', '=', True)]).id, # Getting Closed stage id.
'page_ids': [(0, 0, {'title': "P0", 'question_ids': [(0, 0, {'question': "Q0", 'type': 'free_text'})]})]})]
for survey in surveys:
self.assertRaises(UserError, survey.action_send_survey)
def test_07_survey_email_message(self):
# Case-1: Executing send_mail with correct data.
- partner = self.Partner.create({'name': 'Marie De Cock', 'email': 'marie.de.cock@gmail.com'})
- survey_mail_message = self.SurveyMailMessage.sudo(self.survey_manager).create({
+ partner = self.env['res.partner'].create({'name': 'Marie De Cock', 'email': 'marie.de.cock@gmail.com'})
+ survey_mail_message = self.env['survey.mail.compose.message'].sudo(self.survey_manager).create({
'survey_id': self.survey1.id, 'public': 'email_public_link', 'body': '__URL__', 'partner_ids': [(4, partner.id)]})
survey_mail_message.send_mail()
# Case-2: Executing send_mail with incorrect data.
mail_messages = [
- self.SurveyMailMessage.sudo(self.survey_manager).create({ # Mail Message without __URL__ in body.
+ self.env['survey.mail.compose.message'].sudo(self.survey_manager).create({ # Mail Message without __URL__ in body.
'survey_id': self.survey1.id, 'public': 'email_public_link'}),
- self.SurveyMailMessage.sudo(self.survey_manager).create({ # Mail Message without recipents.
+ self.env['survey.mail.compose.message'].sudo(self.survey_manager).create({ # Mail Message without recipents.
'survey_id': self.survey1.id, 'public': 'email_public_link', 'body': "__URL__"})]
for message in mail_messages:
self.assertRaises(UserError, message.send_mail)
@@ -198,20 +190,20 @@ class TestSurvey(TransactionCase):
self.assertEqual(url_html % ('/' + url), getattr(self.survey1.with_context({'relative_url': True}), urltype + '_url_html'), msg="Public URL is incorrect.")
def test_09_answer_survey(self):
- question = self.Question.sudo(self.survey_manager).create({'page_id': self.page1.id, 'question': 'Q0'})
- input_portal = self.UserInput.sudo(self.survey_user).create({
+ question = self.env['survey.question'].sudo(self.survey_manager).create({'page_id': self.page1.id, 'question': 'Q0'})
+ input_portal = self.env['survey.user_input'].sudo(self.survey_user).create({
'survey_id': self.survey1.id,
'partner_id': self.survey_user.partner_id.id,
'user_input_line_ids': [(0, 0, {
'skipped': False, 'answer_type': 'free_text', 'value_free_text': "Test Answer",
- 'survey_id': self.survey1.id, 'page_id': self.page1, 'question_id': question.id})]})
+ 'survey_id': self.survey1.id, 'question_id': question.id})]})
- input_public = self.UserInput.sudo(self.user_public).create({
+ input_public = self.env['survey.user_input'].sudo(self.user_public).create({
'survey_id': self.survey1.id,
'partner_id': self.survey_user.partner_id.id,
'user_input_line_ids': [(0, 0, {
'skipped': False, 'answer_type': 'free_text', 'value_free_text': "Test Answer",
- 'survey_id': self.survey1.id, 'page_id': self.page1, 'question_id': question.id})]})
+ 'survey_id': self.survey1.id, 'question_id': question.id})]})
answers = [input_portal.user_input_line_ids[0], input_public.user_input_line_ids[0]]
expected_values = {'answer_type': 'free_text', 'value_free_text': "Test Answer"}
@@ -220,52 +212,52 @@ class TestSurvey(TransactionCase):
self.assertEqual(getattr(answer, field), value, msg="Unable to answer the survey. Expected behaviour of %s is not proper." % (field))
def test_10_survey_result_simple_multiple_choice(self):
- question = self.Question.sudo(self.survey_manager).create({
+ question = self.env['survey.question'].sudo(self.survey_manager).create({
'page_id': self.page1.id, 'question': 'Q0', 'type': 'simple_choice',
'labels_ids': [(0, 0, {'value': "Choice0", 'quizz_mark': 0}), (0, 0, {'value': "Choice1", 'quizz_mark': 0})]})
for i in range(3):
- self.UserInput.sudo(self.user_public).create({'survey_id': self.survey1.id, 'user_input_line_ids': [(0, 0, {
+ self.env['survey.user_input'].sudo(self.user_public).create({'survey_id': self.survey1.id, 'user_input_line_ids': [(0, 0, {
'question_id': question.id,
'answer_type': 'suggestion',
'value_suggested': random.choice(question.labels_ids.ids)})]})
lines = [line.value_suggested.id for line in question.user_input_line_ids]
answers = [{'text': label.value, 'count': lines.count(label.id), 'answer_id': label.id} for label in question.labels_ids]
- prp_result = self.Survey.prepare_result(question)['answers']
+ prp_result = self.env['survey.survey'].prepare_result(question)['answers']
answers.sort()
prp_result.sort()
self.assertEqual(prp_result, answers, msg="Statistics of simple, multiple choice questions are different from expectation")
def test_11_survey_result_matrix(self):
- question = self.Question.sudo(self.survey_manager).create({
+ question = self.env['survey.question'].sudo(self.survey_manager).create({
'page_id': self.page1.id, 'question': 'Q0', 'type': 'matrix', 'matrix_subtype': 'simple',
'labels_ids': [(0, 0, {'value': "Column0", "quizz_mark": 0}), (0, 0, {'value': "Column1", "quizz_mark": 0})],
'labels_ids_2': [(0, 0, {'value': "Row0", "quizz_mark": 0}), (0, 0, {'value': "Row1", "quizz_mark": 0})]})
for i in range(3):
- self.UserInput.sudo(self.user_public).create({'survey_id': self.survey1.id, 'user_input_line_ids': [(0, 0, {
+ self.env['survey.user_input'].sudo(self.user_public).create({'survey_id': self.survey1.id, 'user_input_line_ids': [(0, 0, {
'question_id': question.id, 'answer_type': 'suggestion', 'value_suggested': random.choice(question.labels_ids.ids),
'value_suggested_row': random.choice(question.labels_ids_2.ids)})]})
lines = [(line.value_suggested_row.id, line.value_suggested.id) for line in question.user_input_line_ids]
res = {}
for i in product(question.labels_ids_2.ids, question.labels_ids.ids):
res[i] = lines.count((i))
- self.assertEqual(self.Survey.prepare_result(question)['result'], res, msg="Statistics of matrix type questions are different from expectations")
+ self.assertEqual(self.env['survey.survey'].prepare_result(question)['result'], res, msg="Statistics of matrix type questions are different from expectations")
def test_12_survey_result_numeric_box(self):
- question = self.Question.sudo(self.survey_manager).create({'page_id': self.page1.id, 'question': 'Q0', 'type': 'numerical_box'})
+ question = self.env['survey.question'].sudo(self.survey_manager).create({'page_id': self.page1.id, 'question': 'Q0', 'type': 'numerical_box'})
num = map(float, random.sample(range(1, 100), 3))
nsum = sum(num)
for i in range(3):
- self.UserInput.sudo(self.user_public).create({'survey_id': self.survey1.id, 'user_input_line_ids': [(0, 0, {
+ self.env['survey.user_input'].sudo(self.user_public).create({'survey_id': self.survey1.id, 'user_input_line_ids': [(0, 0, {
'question_id': question.id, 'answer_type': 'number', 'value_number': num[i]})]})
exresult = {
'average': round((nsum / len(num)), 2), 'max': round(max(num), 2),
'min': round(min(num), 2), 'sum': nsum, 'most_common': Counter(num).most_common(5)}
- result = self.Survey.prepare_result(question)
+ result = self.env['survey.survey'].prepare_result(question)
for key in exresult.keys():
self.assertEqual(result[key], exresult[key], msg="Statistics of numeric box type questions are different from expectations")
def test_13_survey_actions(self):
- self.Question.sudo(self.survey_manager).create({'page_id': self.page1.id, 'question': 'Q0', 'type': 'numerical_box'})
+ self.env['survey.question'].sudo(self.survey_manager).create({'page_id': self.page1.id, 'question': 'Q0', 'type': 'numerical_box'})
actions = {
'start': {'method': 'public', 'token': '/test', 'text': 'Start'},
diff --git a/addons/survey/tests/test_survey_ui.py b/addons/survey/tests/test_survey_ui.py
index 79a85c3ef31..bea83b1ea7e 100644
--- a/addons/survey/tests/test_survey_ui.py
+++ b/addons/survey/tests/test_survey_ui.py
@@ -1,10 +1,10 @@
-import openerp.tests
+import odoo.tests
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-@openerp.tests.common.at_install(False)
-@openerp.tests.common.post_install(True)
-class TestUi(openerp.tests.HttpCase):
+@odoo.tests.common.at_install(False)
+@odoo.tests.common.post_install(True)
+class TestUi(odoo.tests.HttpCase):
def test_01_admin_survey_tour(self):
self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('test_survey', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.test_survey", login="admin")
diff --git a/addons/survey/views/survey_views.xml b/addons/survey/views/survey_views.xml
index a85100f4f5b..b5a8edcbbe3 100644
--- a/addons/survey/views/survey_views.xml
+++ b/addons/survey/views/survey_views.xml
@@ -29,7 +29,7 @@
<field name="arch" type="xml">
<form string="Survey User inputs" create="false">
<header>
- <button name="action_survey_resent" string="Sent Invitation Again" type="object" class="oe_highlight" attrs="{'invisible': ['|', ('type','=','manually'), ('state', '=', 'done')]}"/>
+ <button name="action_survey_resend" string="Send Invitation Again" type="object" class="oe_highlight" attrs="{'invisible': ['|', ('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" />
<field name="state" widget="statusbar"/>
@@ -401,7 +401,7 @@
</group>
<notebook>
<page string="Answers">
- <field name="validation_email" attrs="{'invisible': [('type', '!=', 'textbox')]}" on_change="onchange_validation_email(validation_email)"/>
+ <field name="validation_email" attrs="{'invisible': [('type', '!=', 'textbox')]}"/>
<label for="validation_email" attrs="{'invisible': [('type', '!=', 'textbox')]}"/>
<separator />
<field name="labels_ids" string="Type of answers" context="{'default_question_id': active_id}" attrs="{'invisible': [('type', 'not in', ['simple_choice', 'multiple_choice', 'matrix'])]}">
@@ -622,8 +622,7 @@
<field name="state">code</field>
<field name="code">
if context.get('active_ids'):
- test_answers = self.pool['survey.user_input'].search(cr, uid, [('survey_id', 'in', context['active_ids']), ('test_entry', '=', 'True')], context=context)
- self.pool['survey.user_input'].unlink(cr, uid, test_answers, context=context)
+ test_answers = env['survey.user_input'].search([('survey_id', 'in', context['active_ids']), ('test_entry', '=', 'True')]).unlink()
</field>
</record>
diff --git a/addons/survey/wizard/survey_email_compose_message.py b/addons/survey/wizard/survey_email_compose_message.py
index 55e326b3750..658edbef72e 100644
--- a/addons/survey/wizard/survey_email_compose_message.py
+++ b/addons/survey/wizard/survey_email_compose_message.py
@@ -1,121 +1,85 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import osv
-from openerp.osv import fields
-from openerp.tools.translate import _
-from datetime import datetime
-from openerp.exceptions import UserError
-
import re
import uuid
import urlparse
-emails_split = re.compile(r"[;,\n\r]+")
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+emails_split = re.compile(r"[;,\n\r]+")
+email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+")
-class survey_mail_compose_message(osv.TransientModel):
+class SurveyMailComposeMessage(models.TransientModel):
_name = 'survey.mail.compose.message'
_inherit = 'mail.compose.message'
_description = 'Email composition wizard for Survey'
- _log_access = True
- def _get_public_url(self, cr, uid, ids, name, arg, context=None):
- res = dict((id, 0) for id in ids)
- survey_obj = self.pool.get('survey.survey')
- for wizard in self.browse(cr, uid, ids, context=context):
- res[wizard.id] = wizard.survey_id.public_url
- return res
-
- def _get_public_url_html(self, cr, uid, ids, name, arg, context=None):
- """ Compute if the message is unread by the current user """
- urls = self._get_public_url(cr, uid, ids, name, arg, context=context)
- for key, url in urls.items():
- urls[key] = '<a href="%s">%s</a>' % (url, _("Click here to start survey"))
- return urls
-
- _columns = {
- 'survey_id': fields.many2one('survey.survey', 'Survey', 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', required=True),
- 'public_url': fields.function(_get_public_url, string="Public url", type="char"),
- 'public_url_html': fields.function(_get_public_url_html, string="Public HTML web link", type="char"),
- 'partner_ids': fields.many2many('res.partner',
- 'survey_mail_compose_message_res_partner_rel',
- 'wizard_id', 'partner_id', 'Existing contacts'),
- 'attachment_ids': fields.many2many('ir.attachment',
- 'survey_mail_compose_message_ir_attachments_rel',
- 'wizard_id', 'attachment_id', 'Attachments'),
- 'multi_email': fields.text(string='List of emails', help="This list of emails of recipients will not converted in contacts. Emails separated by commas, semicolons or newline."),
- '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."),
- }
-
- _defaults = {
- 'public': 'public_link',
- 'survey_id': lambda self, cr, uid, ctx={}: ctx.get('model') == 'survey.survey' and ctx.get('res_id') or None
- }
-
- def default_get(self, cr, uid, fields, context=None):
- res = super(survey_mail_compose_message, self).default_get(cr, uid, fields, context=context)
+ def default_survey_id(self):
+ context = self.env.context
+ if context.get('model') == 'survey.survey':
+ return context.get('res_id')
+
+ 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")
+ partner_ids = fields.Many2many('res.partner', 'survey_mail_compose_message_res_partner_rel', 'wizard_id', 'partner_id', string='Existing contacts')
+ attachment_ids = fields.Many2many('ir.attachment', 'survey_mail_compose_message_ir_attachments_rel', 'wizard_id', 'attachment_id', string='Attachments')
+ multi_email = fields.Text(string='List of emails', help="This list of emails of recipients will not be converted in contacts.\
+ Emails must be separated by commas, semicolons or newline.")
+ 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.")
+
+ @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(SurveyMailComposeMessage, 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.get('active_ids')})
+ res.update({'partner_ids': context['active_ids']})
return res
- def onchange_multi_email(self, cr, uid, ids, multi_email, context=None):
- emails = list(set(emails_split.split(multi_email or "")))
+ @api.onchange('multi_email')
+ def onchange_multi_email(self):
+ emails = list(set(emails_split.split(self.multi_email or "")))
emails_checked = []
error_message = ""
for email in emails:
email = email.strip()
if email:
- if not re.search(r"^[^@]+@[^@]+$", email):
+ if not email_validator.match(email):
error_message += "\n'%s'" % email
else:
emails_checked.append(email)
if error_message:
- raise UserError(_("One email at least is incorrect: %s") % error_message)
+ raise UserError(_("Incorrect Email Address: %s") % error_message)
emails_checked.sort()
- values = {'multi_email': '\n'.join(emails_checked)}
- return {'value': values}
-
- def onchange_survey_id(self, cr, uid, ids, survey_id, context=None):
- """ Compute if the message is unread by the current user. """
- if survey_id:
- survey = self.pool.get('survey.survey').browse(cr, uid, survey_id, context=context)
- return {
- 'value': {
- 'subject': survey.title,
- 'public_url': survey.public_url,
- 'public_url_html': '<a href="%s">%s</a>' % (survey.public_url, _("Click here to take survey")),
- }}
- else:
- txt = _("Please select a survey")
- return {
- 'value': {
- 'public_url': txt,
- 'public_url_html': txt,
- }}
+ self.multi_email = '\n'.join(emails_checked)
#------------------------------------------------------
# Wizard validation and send
#------------------------------------------------------
-
- def send_mail(self, cr, uid, ids, auto_commit=False, context=None):
+ @api.multi
+ def send_mail(self, auto_commit=False):
""" Process the wizard content and proceed with sending the related
email(s), rendering any template patterns on the fly if needed """
- if context is None:
- context = {}
- survey_response_obj = self.pool.get('survey.user_input')
- partner_obj = self.pool.get('res.partner')
- mail_mail_obj = self.pool.get('mail.mail')
- try:
- model, anonymous_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'portal', 'group_anonymous')
- except ValueError:
- anonymous_id = None
+ SurveyUserInput = self.env['survey.user_input']
+ Partner = self.env['res.partner']
+ Mail = self.env['mail.mail']
+ anonymous_group = self.env.ref('portal.group_anonymous', raise_if_not_found=False)
def create_response_and_send_mail(wizard, token, partner_id, email):
""" Create one mail by recipients and replace __URL__ by link with identification token """
@@ -143,37 +107,38 @@ class survey_mail_compose_message(osv.TransientModel):
values['recipient_ids'] = [(4, partner_id)]
else:
values['email_to'] = email
- mail_id = mail_mail_obj.create(cr, uid, values, context=context)
- mail_mail_obj.send(cr, uid, [mail_id], context=context)
+ Mail.create(values).send()
def create_token(wizard, partner_id, email):
if context.get("survey_resent_token"):
- response_ids = survey_response_obj.search(cr, uid, [('survey_id', '=', wizard.survey_id.id), ('state', 'in', ['new', 'skip']), '|', ('partner_id', '=', partner_id), ('email', '=', email)], context=context)
- if response_ids:
- return survey_response_obj.read(cr, uid, response_ids, ['token'], context=context)[0]['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 = uuid.uuid4().__str__()
# create response with token
- survey_response_obj.create(cr, uid, {
+ survey_user_input = SurveyUserInput.create({
'survey_id': wizard.survey_id.id,
'deadline': wizard.date_deadline,
- 'date_create': datetime.now(),
+ 'date_create': fields.Datetime.now(),
'type': 'link',
'state': 'new',
'token': token,
'partner_id': partner_id,
- 'email': email},
- context=context)
- return token
+ 'email': email})
+ return survey_user_input.token
- for wizard in self.browse(cr, uid, ids, context=context):
+ 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')
@@ -181,16 +146,16 @@ class survey_mail_compose_message(osv.TransientModel):
# quick check of email list
emails_list = []
if wizard.multi_email:
- emails = list(set(emails_split.split(wizard.multi_email)) - set([partner.email for partner in wizard.partner_ids]))
+ emails = set(emails_split.split(wizard.multi_email)) - set(wizard.partner_ids.mapped('email'))
for email in emails:
email = email.strip()
- if re.search(r"^[^@]+@[^@]+$", email):
+ if email_validator.match(email):
emails_list.append(email)
# remove public anonymous access
partner_list = []
for partner in wizard.partner_ids:
- if not anonymous_id or not partner.user_ids or anonymous_id not in [x.id for x in partner.user_ids[0].groups_id]:
+ if not anonymous_group or not partner.user_ids or anonymous_group not in partner.user_ids[0].groups_id:
partner_list.append({'id': partner.id, 'email': partner.email})
if not len(emails_list) and not len(partner_list):
@@ -199,10 +164,9 @@ class survey_mail_compose_message(osv.TransientModel):
raise UserError(_("Please enter at least one valid recipient."))
for email in emails_list:
- partner_id = partner_obj.search(cr, uid, [('email', '=', email)], context=context)
- partner_id = partner_id and partner_id[0] or None
- token = create_token(wizard, partner_id, email)
- create_response_and_send_mail(wizard, token, partner_id, email)
+ 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'])
diff --git a/addons/survey/wizard/survey_email_compose_message.xml b/addons/survey/wizard/survey_email_compose_message.xml
index cf88509fc2a..d1ca0a67ec8 100644
--- a/addons/survey/wizard/survey_email_compose_message.xml
+++ b/addons/survey/wizard/survey_email_compose_message.xml
@@ -30,7 +30,7 @@
<field name="res_id" invisible="1"/>
<field name="parent_id" invisible="1"/>
<group col="2">
- <field name="survey_id" readonly="context.get('default_survey_id')" on_change="onchange_survey_id(survey_id)" />
+ <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">
@@ -45,7 +45,7 @@
<group col="1" attrs="{'invisible':['|',('public','=',False),('public','in',['public_link'])]}">
<group col="2">
<field invisible="context.get('survey_resent_token')" name="partner_ids" widget="many2many_tags_email" placeholder="Add list of existing contacts..." context="{'force_email':True, 'show_email':True}" />
- <field invisible="context.get('survey_resent_token')" name="multi_email" placeholder="Add list of email of recipients (will not converted in partner), separated by commas, semicolons or newline..." on_change="onchange_multi_email(multi_email)" />
+ <field invisible="context.get('survey_resent_token')" name="multi_email" placeholder="Add list of email of recipients (will not converted in partner), separated by commas, semicolons or newline..."/>
<field name="subject" placeholder="Subject..."/>
</group>
<field name="body" options="{'style-inline': true}"/>
diff --git a/addons/survey_crm/__init__.py b/addons/survey_crm/__init__.py
index 0a274c37955..81fc81d29c9 100644
--- a/addons/survey_crm/__init__.py
+++ b/addons/survey_crm/__init__.py
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import survey
+import models
diff --git a/addons/survey_crm/__openerp__.py b/addons/survey_crm/__openerp__.py
index 62938fd0e2e..845bb9e3c33 100644
--- a/addons/survey_crm/__openerp__.py
+++ b/addons/survey_crm/__openerp__.py
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-
{
'name': 'Survey CRM',
'version': '2.0',
@@ -15,7 +13,7 @@ This module adds a Survey mass mailing button inside the more option of lead/cus
""",
'depends': ['crm', 'survey'],
'data': [
- 'crm_view.xml',
+ 'views/survey_crm_views.xml',
],
'installable': True,
'auto_install': True
diff --git a/addons/survey_crm/models/__init__.py b/addons/survey_crm/models/__init__.py
new file mode 100644
index 00000000000..0a274c37955
--- /dev/null
+++ b/addons/survey_crm/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import survey
diff --git a/addons/survey_crm/survey.py b/addons/survey_crm/models/survey.py
index 4ee163098ea..ea7db12ffcf 100644
--- a/addons/survey_crm/survey.py
+++ b/addons/survey_crm/models/survey.py
@@ -1,18 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import osv
+from odoo import api, models
-class survey_mail_compose_message(osv.TransientModel):
+class SurveyComposeMessage(models.TransientModel):
+
_inherit = 'survey.mail.compose.message'
- def default_get(self, cr, uid, fields, context=None):
- res = super(survey_mail_compose_message, self).default_get(cr, uid, fields, context=context)
- if context.get('active_model') == 'crm.lead' and context.get('active_ids'):
+ @api.model
+ def default_get(self, fields):
+ result = super(SurveyComposeMessage, self).default_get(fields)
+ if self._context.get('active_model') == 'crm.lead' and self._context.get('active_ids'):
partner_ids = []
emails_list = []
- for lead in self.pool.get('crm.lead').browse(cr, uid, context.get('active_ids'), context=context):
+ for lead in self.env['crm.lead'].browse(self._context.get('active_ids')):
if lead.partner_id:
partner_ids.append(lead.partner_id.id)
else:
@@ -21,5 +23,5 @@ class survey_mail_compose_message(osv.TransientModel):
emails_list.append(email)
multi_email = "\n".join(emails_list)
- res.update({'partner_ids': list(set(partner_ids)), 'multi_email': multi_email})
- return res
+ result.update({'partner_ids': list(set(partner_ids)), 'multi_email': multi_email})
+ return result
diff --git a/addons/survey_crm/crm_view.xml b/addons/survey_crm/views/survey_crm_views.xml
index d28840643f6..4b37cd91a4d 100644
--- a/addons/survey_crm/crm_view.xml
+++ b/addons/survey_crm/views/survey_crm_views.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<openerp>
- <data>
+<odoo>
<!-- Replace the default mass-mailing wizard in base with the composition wizard -->
<act_window name="Partner Survey Mailing"
res_model="survey.mail.compose.message"
@@ -14,5 +13,4 @@
'default_use_template': True,
'default_template_id': ref('survey.email_template_survey'),
}" />
- </data>
-</openerp>
+</odoo>