-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* TA#50025 [MIG]: survey_answer_for_partner * TA#50025 [MIG]: survey_answer_for_partner
- Loading branch information
1 parent
ef93cb9
commit 21ebd39
Showing
8 changed files
with
198 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). | ||
from . import main |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
# -*- coding: utf-8 -*- | ||
# © 2022 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). | ||
|
||
from datetime import timedelta | ||
from dateutil.relativedelta import relativedelta | ||
|
||
from odoo import http, _, fields | ||
from odoo.addons.survey.controllers.main import Survey | ||
from odoo.addons.base.models.ir_ui_view import keep_query | ||
from odoo.exceptions import UserError | ||
from odoo.http import request | ||
|
||
|
||
class SurveyCustom(Survey): | ||
|
||
@http.route('/survey/partner/<string:survey_token>', type='http', auth='user', website=True) | ||
def survey_partner(self, survey_token, answer_token=None, **kwargs): | ||
""" Test mode for surveys: create a test answer, only for managers or officers | ||
testing their surveys """ | ||
survey_sudo, answer_sudo = self._fetch_from_access_token(survey_token, answer_token) | ||
return request.redirect('/survey/start/partner/%s?%s' % (survey_sudo.access_token, keep_query('*', answer_token=answer_sudo.access_token))) | ||
|
||
@http.route('/survey/start/partner/<string:survey_token>', type='http', auth='public', website=True) | ||
def survey_start_partner(self, survey_token, answer_token=None, email=False, **post): | ||
""" Start a survey by providing | ||
* a token linked to a survey; | ||
* a token linked to an answer or generate a new token if access is allowed; | ||
""" | ||
# Get the current answer token from cookie | ||
answer_from_cookie = False | ||
if not answer_token: | ||
answer_token = request.httprequest.cookies.get('survey_%s' % survey_token) | ||
answer_from_cookie = bool(answer_token) | ||
|
||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=False) | ||
|
||
if answer_from_cookie and access_data['validity_code'] == 'token_wrong': | ||
# If the cookie had been generated for another user or does not correspond to any existing answer object | ||
# (probably because it has been deleted), ignore it and redo the check. | ||
# The cookie will be replaced by a legit value when resolving the URL, so we don't clean it further here. | ||
access_data = self._get_access_data(survey_token, None, ensure_token=False) | ||
if access_data['validity_code'] == 'token_wrong': | ||
return self._redirect_with_error(access_data, access_data['validity_code']) | ||
|
||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] | ||
if not answer_sudo: | ||
try: | ||
answer_sudo = survey_sudo._create_answer(user=request.env.user, email=email) | ||
except UserError: | ||
answer_sudo = False | ||
|
||
if not answer_sudo: | ||
try: | ||
survey_sudo.with_user(request.env.user).check_access_rights('read') | ||
survey_sudo.with_user(request.env.user).check_access_rule('read') | ||
except: | ||
return request.redirect("/") | ||
else: | ||
return request.render("survey.survey_403_page", {'survey': survey_sudo}) | ||
|
||
return request.redirect('/survey/partner/%s/%s' % (survey_sudo.access_token, answer_sudo.access_token)) | ||
|
||
@http.route('/survey/partner/<string:survey_token>/<string:answer_token>', type='http', auth='public', website=True) | ||
def survey_display_page_partner(self, survey_token, answer_token, **post): | ||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) | ||
if access_data['validity_code'] == 'token_wrong': | ||
return self._redirect_with_error(access_data, access_data['validity_code']) | ||
|
||
answer_sudo = access_data['answer_sudo'] | ||
if answer_sudo.state != 'done' and answer_sudo.survey_time_limit_reached: | ||
answer_sudo._mark_done() | ||
|
||
return request.render('survey.survey_page_fill', | ||
self._prepare_survey_data(access_data['survey_sudo'], answer_sudo, **post)) | ||
|
||
@http.route('/survey/begin/<string:survey_token>/<string:answer_token>', type='json', auth='public', website=True) | ||
def survey_begin(self, survey_token, answer_token, **post): | ||
""" Route used to start the survey user input and display the first survey page. """ | ||
|
||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) | ||
if access_data['validity_code'] == 'token_wrong': | ||
return {'error': access_data['validity_code']} | ||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] | ||
|
||
if answer_sudo.state != "new": | ||
return {'error': _("The survey has already started.")} | ||
|
||
answer_sudo._mark_in_progress() | ||
return self._prepare_question_html(survey_sudo, answer_sudo, **post) | ||
|
||
@http.route('/survey/next_question/<string:survey_token>/<string:answer_token>', type='json', auth='public', website=True) | ||
def survey_next_question(self, survey_token, answer_token, **post): | ||
""" Method used to display the next survey question in an ongoing session. | ||
Triggered on all attendees screens when the host goes to the next question. """ | ||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) | ||
if access_data['validity_code'] == 'token_wrong': | ||
return {'error': access_data['validity_code']} | ||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] | ||
|
||
if answer_sudo.state == 'new' and answer_sudo.is_session_answer: | ||
answer_sudo._mark_in_progress() | ||
|
||
return self._prepare_question_html(survey_sudo, answer_sudo, **post) | ||
|
||
@http.route('/survey/submit/<string:survey_token>/<string:answer_token>', type='json', auth='public', website=True) | ||
def survey_submit(self, survey_token, answer_token, **post): | ||
""" Submit a page from the survey. | ||
This will take into account the validation errors and store the answers to the questions. | ||
If the time limit is reached, errors will be skipped, answers will be ignored and | ||
survey state will be forced to 'done'""" | ||
# Survey Validation | ||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True) | ||
if access_data['validity_code'] == 'token_wrong': | ||
return {'error': access_data['validity_code']} | ||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo'] | ||
|
||
if answer_sudo.state == 'done': | ||
return {'error': 'unauthorized'} | ||
|
||
questions, page_or_question_id = survey_sudo._get_survey_questions(answer=answer_sudo, | ||
page_id=post.get('page_id'), | ||
question_id=post.get('question_id')) | ||
|
||
if not answer_sudo.test_entry and not survey_sudo._has_attempts_left(answer_sudo.partner_id, answer_sudo.email, answer_sudo.invite_token): | ||
# prevent cheating with users creating multiple 'user_input' before their last attempt | ||
return {'error': 'unauthorized'} | ||
|
||
if answer_sudo.survey_time_limit_reached or answer_sudo.question_time_limit_reached: | ||
if answer_sudo.question_time_limit_reached: | ||
time_limit = survey_sudo.session_question_start_time + relativedelta( | ||
seconds=survey_sudo.session_question_id.time_limit | ||
) | ||
time_limit += timedelta(seconds=3) | ||
else: | ||
time_limit = answer_sudo.start_datetime + timedelta(minutes=survey_sudo.time_limit) | ||
time_limit += timedelta(seconds=10) | ||
if fields.Datetime.now() > time_limit: | ||
# prevent cheating with users blocking the JS timer and taking all their time to answer | ||
return {'error': 'unauthorized'} | ||
|
||
errors = {} | ||
# Prepare answers / comment by question, validate and save answers | ||
for question in questions: | ||
inactive_questions = request.env['survey.question'] if answer_sudo.is_session_answer else answer_sudo._get_inactive_conditional_questions() | ||
if question in inactive_questions: # if question is inactive, skip validation and save | ||
continue | ||
answer, comment = self._extract_comment_from_answers(question, post.get(str(question.id))) | ||
errors.update(question.validate_question(answer, comment)) | ||
if not errors.get(question.id): | ||
answer_sudo.save_lines(question, answer, comment) | ||
|
||
if errors and not (answer_sudo.survey_time_limit_reached or answer_sudo.question_time_limit_reached): | ||
return {'error': 'validation', 'fields': errors} | ||
|
||
if not answer_sudo.is_session_answer: | ||
answer_sudo._clear_inactive_conditional_answers() | ||
|
||
if answer_sudo.survey_time_limit_reached or survey_sudo.questions_layout == 'one_page': | ||
answer_sudo._mark_done() | ||
elif 'previous_page_id' in post: | ||
# Go back to specific page using the breadcrumb. Lines are saved and survey continues | ||
return self._prepare_question_html(survey_sudo, answer_sudo, **post) | ||
else: | ||
vals = {'last_displayed_page_id': page_or_question_id} | ||
if not answer_sudo.is_session_answer: | ||
next_page = survey_sudo._get_next_page_or_question(answer_sudo, page_or_question_id) | ||
if not next_page: | ||
answer_sudo._mark_done() | ||
|
||
answer_sudo.write(vals) | ||
|
||
return self._prepare_question_html(survey_sudo, answer_sudo) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
# © 2019 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
2 changes: 1 addition & 1 deletion
2
survey_answer_for_partner/tests/test_answer_for_partner_wizard.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 15 additions & 47 deletions
62
survey_answer_for_partner/wizard/answer_survey_for_wizard.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,72 +1,40 @@ | ||
# © 2022 Numigi (tm) and all its contributors (https://bit.ly/numigiens) | ||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). | ||
import logging | ||
|
||
import uuid | ||
from odoo import api, fields, models | ||
from odoo.addons.base.models.res_partner import Partner | ||
from odoo.addons.survey.models.survey_user import SurveyUserInput | ||
from odoo.addons.survey.models.survey_survey import Survey | ||
from odoo.tools import pycompat | ||
import werkzeug | ||
from odoo import fields, models | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
""" | ||
The type of survey input should be `manually` in this module's use case. | ||
If the input is created, but the user quits before sending the answers, | ||
it will be garbage collected by a cron automatically. | ||
""" | ||
SURVEY_INPUT_TYPE = 'manually' | ||
|
||
|
||
def _generate_survey_input_token() -> str: | ||
"""Generate a token for a survey input. | ||
This function reproduces the behavior found in Odoo for survey invitations | ||
sent by email. | ||
See function create_token of odoo/addons/survey/wizard/survey_email_compose_message.py. | ||
""" | ||
return pycompat.to_text(uuid.uuid4()) | ||
|
||
|
||
def create_survey_input_for_partner(survey: Survey, partner: Partner) -> SurveyUserInput: | ||
"""Create a user input for the given survey and partner. | ||
:param survey: the survey to answer. | ||
:param partner: the partner for whom to answer for. | ||
:return: the user input | ||
""" | ||
return survey.env['survey.user_input'].create({ | ||
'survey_id': survey.id, | ||
# 'type': SURVEY_INPUT_TYPE, | ||
'state': 'new', | ||
'access_token': _generate_survey_input_token(), | ||
'partner_id': partner.id, | ||
}) | ||
|
||
|
||
class SurveyAnswerForPartnerWizard(models.TransientModel): | ||
|
||
_name = 'survey.answer.for.partner.wizard' | ||
_description = 'Survey Answer For Partner Wizard' | ||
|
||
survey_id = fields.Many2one('survey.survey', 'Survey') | ||
partner_id = fields.Many2one('res.partner', 'Partner') | ||
|
||
def action_validate(self): | ||
"""Open the website page with the survey answered for the partner. | ||
This method was inspired and adapted from the method action_test_survey | ||
of survey.survey defined in odoo/addons/survey/models/survey.py. | ||
""" | ||
user_input = create_survey_input_for_partner(self.survey_id, self.partner_id) | ||
user = self.env.user | ||
public_groups = self.env.ref("base.group_public", raise_if_not_found=False) | ||
if public_groups: | ||
public_users = public_groups.sudo().with_context(active_test=False).mapped("users") | ||
user = public_users[0] | ||
user_input_id = self.survey_id.sudo()._create_answer(user=user, partner=self.partner_id) | ||
url1 = '/survey/partner/%s' % self.survey_id.access_token | ||
url = '%s?%s' % ( | ||
url1, werkzeug.urls.url_encode({'answer_token': user_input_id and user_input_id.access_token or None})) | ||
return { | ||
'type': 'ir.actions.act_url', | ||
'name': "Test Survey", | ||
'name': "Start Survey", | ||
'target': 'self', | ||
'url': '/survey/test/%s' % user_input.access_token, | ||
'url': url, | ||
} | ||
|
||
|
||
|
||
|