Skip to content

Commit

Permalink
Ta#50025 (#9)
Browse files Browse the repository at this point in the history
* TA#50025 [MIG]: survey_answer_for_partner

* TA#50025 [MIG]: survey_answer_for_partner
  • Loading branch information
houdaBENTALEB authored Feb 8, 2023
1 parent ef93cb9 commit 21ebd39
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 52 deletions.
3 changes: 2 additions & 1 deletion survey_answer_for_partner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from . import models
from . import wizard
from . import wizard
from . import controllers
3 changes: 3 additions & 0 deletions survey_answer_for_partner/controllers/__init__.py
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
174 changes: 174 additions & 0 deletions survey_answer_for_partner/controllers/main.py
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)

2 changes: 1 addition & 1 deletion survey_answer_for_partner/models/partner_smart_button.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# © 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).

from odoo import fields, models
Expand Down
2 changes: 1 addition & 1 deletion survey_answer_for_partner/tests/__init__.py
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).
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# © 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).

from odoo.tests import common
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# © 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).

from odoo.tests import common
Expand Down
62 changes: 15 additions & 47 deletions survey_answer_for_partner/wizard/answer_survey_for_wizard.py
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,
}




0 comments on commit 21ebd39

Please sign in to comment.