diff --git a/survey_answer_for_partner/__init__.py b/survey_answer_for_partner/__init__.py index 823e52b..ac93395 100644 --- a/survey_answer_for_partner/__init__.py +++ b/survey_answer_for_partner/__init__.py @@ -2,4 +2,5 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from . import models -from . import wizard \ No newline at end of file +from . import wizard +from . import controllers \ No newline at end of file diff --git a/survey_answer_for_partner/controllers/__init__.py b/survey_answer_for_partner/controllers/__init__.py new file mode 100644 index 0000000..1d380ac --- /dev/null +++ b/survey_answer_for_partner/controllers/__init__.py @@ -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 \ No newline at end of file diff --git a/survey_answer_for_partner/controllers/main.py b/survey_answer_for_partner/controllers/main.py new file mode 100644 index 0000000..ef35e7a --- /dev/null +++ b/survey_answer_for_partner/controllers/main.py @@ -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/', 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/', 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//', 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//', 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//', 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//', 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) + diff --git a/survey_answer_for_partner/models/partner_smart_button.py b/survey_answer_for_partner/models/partner_smart_button.py index d993df5..2a40fa1 100644 --- a/survey_answer_for_partner/models/partner_smart_button.py +++ b/survey_answer_for_partner/models/partner_smart_button.py @@ -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 diff --git a/survey_answer_for_partner/tests/__init__.py b/survey_answer_for_partner/tests/__init__.py index 6ef2df9..0ca2c59 100644 --- a/survey_answer_for_partner/tests/__init__.py +++ b/survey_answer_for_partner/tests/__init__.py @@ -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). diff --git a/survey_answer_for_partner/tests/test_answer_for_partner_wizard.py b/survey_answer_for_partner/tests/test_answer_for_partner_wizard.py index bcff346..8c59955 100644 --- a/survey_answer_for_partner/tests/test_answer_for_partner_wizard.py +++ b/survey_answer_for_partner/tests/test_answer_for_partner_wizard.py @@ -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 diff --git a/survey_answer_for_partner/tests/test_partner_smart_button.py b/survey_answer_for_partner/tests/test_partner_smart_button.py index 1b1e658..eeb8da7 100644 --- a/survey_answer_for_partner/tests/test_partner_smart_button.py +++ b/survey_answer_for_partner/tests/test_partner_smart_button.py @@ -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 diff --git a/survey_answer_for_partner/wizard/answer_survey_for_wizard.py b/survey_answer_for_partner/wizard/answer_survey_for_wizard.py index ac0d6e5..1093d74 100644 --- a/survey_answer_for_partner/wizard/answer_survey_for_wizard.py +++ b/survey_answer_for_partner/wizard/answer_survey_for_wizard.py @@ -1,13 +1,11 @@ # © 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. @@ -15,38 +13,9 @@ 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' @@ -54,19 +23,18 @@ class SurveyAnswerForPartnerWizard(models.TransientModel): 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, } - - - -