From b63b8ba8853071cbfe8740c932f98f00d76650eb Mon Sep 17 00:00:00 2001 From: Quentin Mondot Date: Wed, 18 Feb 2026 17:49:33 +0100 Subject: [PATCH] [IMP] survey_extra_fields: new params max_file_size and allowed_extensions for file question type --- survey_extra_fields/__manifest__.py | 1 + survey_extra_fields/controllers/main.py | 14 +++- survey_extra_fields/i18n/fr.po | 62 ++++++++++++++++- survey_extra_fields/models/survey_question.py | 9 +++ .../models/survey_user_input.py | 49 ++++++++++++-- .../static/src/js/survey_form.js | 25 +++++++ survey_extra_fields/tests/test_survey_file.py | 66 +++++++++++++++++++ .../views/survey_question_views.xml | 20 ++++++ .../views/survey_templates.xml | 3 + 9 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 survey_extra_fields/views/survey_question_views.xml diff --git a/survey_extra_fields/__manifest__.py b/survey_extra_fields/__manifest__.py index 9efe2a1..a2c6395 100644 --- a/survey_extra_fields/__manifest__.py +++ b/survey_extra_fields/__manifest__.py @@ -17,6 +17,7 @@ Add extra question types to surveys: "data": [ "views/survey_templates.xml", "views/survey_user_views.xml", + "views/survey_question_views.xml", ], "assets": { "survey.survey_assets": [ diff --git a/survey_extra_fields/controllers/main.py b/survey_extra_fields/controllers/main.py index 490bbeb..d417641 100644 --- a/survey_extra_fields/controllers/main.py +++ b/survey_extra_fields/controllers/main.py @@ -1,11 +1,17 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from __future__ import annotations + import base64 +from typing import TYPE_CHECKING, Any from odoo import http from odoo.http import request, content_disposition from odoo.addons.survey.controllers.main import Survey +if TYPE_CHECKING: + from werkzeug.wrappers import Response + class SurveyExtraFieldsController(Survey): @@ -14,7 +20,13 @@ class SurveyExtraFieldsController(Survey): type="http", auth="public", ) - def survey_file_download(self, survey_token, answer_token, line_id, **kwargs): + def survey_file_download( + self, + survey_token: str, + answer_token: str, + line_id: int, + **kwargs: Any + ) -> Response: survey = request.env["survey.survey"].sudo().search( [("access_token", "=", survey_token)], limit=1 ) diff --git a/survey_extra_fields/i18n/fr.po b/survey_extra_fields/i18n/fr.po index bdd35b1..8a6d53f 100644 --- a/survey_extra_fields/i18n/fr.po +++ b/survey_extra_fields/i18n/fr.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-17 14:34+0000\n" -"PO-Revision-Date: 2026-02-17 14:34+0000\n" +"POT-Creation-Date: 2026-02-18 16:38+0000\n" +"PO-Revision-Date: 2026-02-18 16:38+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -15,17 +15,47 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: survey_extra_fields +#: model_terms:ir.ui.view,arch_db:survey_extra_fields.survey_question_form_inh +msgid ".pdf,.docx,.xlsx" +msgstr "" + +#. module: survey_extra_fields +#: model:ir.model.fields,field_description:survey_extra_fields.field_survey_question__allowed_extensions +msgid "Allowed Extensions" +msgstr "Extensions autorisées" + #. module: survey_extra_fields #: model:ir.model.fields,field_description:survey_extra_fields.field_survey_user_input_line__answer_type msgid "Answer Type" msgstr "Type de réponse" +#. module: survey_extra_fields +#: model:ir.model.fields,help:survey_extra_fields.field_survey_question__allowed_extensions +msgid "" +"Comma-separated list of allowed extensions (e.g. .pdf,.docx). Leave empty to" +" allow all types." +msgstr "" +"Liste d'extensions autorisées séparées par une virgule (e.g. .pdf,.docx). Laisser vide pour" +" autoriser tous les types de fichier." + #. module: survey_extra_fields #: model:ir.model.fields.selection,name:survey_extra_fields.selection__survey_question__question_type__file #: model:ir.model.fields.selection,name:survey_extra_fields.selection__survey_user_input_line__answer_type__file msgid "File" msgstr "Fichier" +#. module: survey_extra_fields +#: model:ir.model.fields,field_description:survey_extra_fields.field_survey_question__max_file_size +#: model_terms:ir.ui.view,arch_db:survey_extra_fields.survey_question_form_inh +msgid "Max File Size (MB)" +msgstr "Taille maximale du fichier" + +#. module: survey_extra_fields +#: model:ir.model.fields,help:survey_extra_fields.field_survey_question__max_file_size +msgid "Maximum file size in MB. Leave 0 for no limit." +msgstr "Taille maximale du fichier en MB. Laisser à 0 pour ne pas restreindre la taille. La valeur par défaut est 10 MB." + #. module: survey_extra_fields #: model:ir.model.fields,field_description:survey_extra_fields.field_survey_question__question_type msgid "Question Type" @@ -58,6 +88,34 @@ msgstr "Entrée utilisateur du sondage" msgid "Survey User Input Line" msgstr "Ligne d'entrée pour l'utilisateur du sondage" +#. module: survey_extra_fields +#. odoo-python +#: code:addons/survey_extra_fields/models/survey_user_input.py:0 +#, python-format +msgid "The file '%(name)s' exceeds the maximum allowed size of %(size)s MB." +msgstr "Le fichier '%(name)s' dépasse la taille maximale autorisée de %(size)s MB." + +#. module: survey_extra_fields +#. odoo-python +#: code:addons/survey_extra_fields/models/survey_user_input.py:0 +#, python-format +msgid "The file '%(name)s' is not allowed. Accepted formats: %(exts)s." +msgstr "Le fichier '%(name)s' n'est pas autorisé. Les formats de fichier autorisés sont : %(exts)s." + +#. module: survey_extra_fields +#. odoo-javascript +#: code:addons/survey_extra_fields/static/src/js/survey_form.js:0 +#, python-format +msgid "The file exceeds the maximum allowed size of %s MB." +msgstr "Le fichier dépasse la taille maximale autorisée de %s MB." + +#. module: survey_extra_fields +#. odoo-javascript +#: code:addons/survey_extra_fields/static/src/js/survey_form.js:0 +#, python-format +msgid "This file type is not allowed. Accepted formats: %s." +msgstr "Le fichier n'est pas autorisé. Les formats de fichier autorisés sont : %s." + #. module: survey_extra_fields #. odoo-javascript #: code:addons/survey_extra_fields/static/src/js/survey_form.js:0 diff --git a/survey_extra_fields/models/survey_question.py b/survey_extra_fields/models/survey_question.py index 08e7e38..8bb622c 100644 --- a/survey_extra_fields/models/survey_question.py +++ b/survey_extra_fields/models/survey_question.py @@ -9,3 +9,12 @@ class SurveyQuestion(models.Model): question_type = fields.Selection( selection_add=[("file", "File")] ) + max_file_size = fields.Integer( + string="Max File Size (MB)", + default=10, + help="Maximum file size in MB. Leave 0 for no limit.", + ) + allowed_extensions = fields.Char( + string="Allowed Extensions", + help="Comma-separated list of allowed extensions (e.g. .pdf,.docx). Leave empty to allow all types.", + ) diff --git a/survey_extra_fields/models/survey_user_input.py b/survey_extra_fields/models/survey_user_input.py index c4631ee..5ea52d2 100644 --- a/survey_extra_fields/models/survey_user_input.py +++ b/survey_extra_fields/models/survey_user_input.py @@ -1,14 +1,23 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -import json +from __future__ import annotations -from odoo import models +import base64 +import json +import os +from typing import TYPE_CHECKING + +from odoo import _, models +from odoo.exceptions import ValidationError + +if TYPE_CHECKING: + from odoo.addons.survey_extra_fields.models.survey_question import SurveyQuestion class SurveyUserInput(models.Model): _inherit = "survey.user_input" - def save_lines(self, question, answer, comment=None): + def save_lines(self, question: SurveyQuestion, answer: str | None, comment: str | None = None) -> None: if question.question_type == "file": old_answers = self.env["survey.user_input.line"].search([ ("user_input_id", "=", self.id), @@ -22,8 +31,11 @@ class SurveyUserInput(models.Model): } if answer: file_data = json.loads(answer) - vals["value_file"] = file_data.get("data") - vals["value_file_fname"] = file_data.get("name") + file_b64 = file_data.get("data", "") + file_name = file_data.get("name", "") + self._check_file_constraints(question, file_b64, file_name) + vals["value_file"] = file_b64 + vals["value_file_fname"] = file_name else: vals.update(answer_type=None, skipped=True) if old_answers: @@ -32,3 +44,30 @@ class SurveyUserInput(models.Model): self.env["survey.user_input.line"].create(vals) else: return super().save_lines(question, answer, comment=comment) + + def _check_file_constraints( + self, + question: SurveyQuestion, + file_b64: str, + file_name: str + ) -> None: + if question.max_file_size: + file_size = len(base64.b64decode(file_b64)) + max_bytes = question.max_file_size * 1024 * 1024 + if file_size > max_bytes: + raise ValidationError( + _("The file '%(name)s' exceeds the maximum allowed size of %(size)s MB.", + name=file_name, size=question.max_file_size) + ) + if question.allowed_extensions: + allowed = [ + allowed_extension.strip().lower() + for allowed_extension in question.allowed_extensions.split(",") + if allowed_extension.strip() + ] + file_extension = os.path.splitext(file_name)[1].lower() + if file_extension not in allowed: + raise ValidationError( + _("The file '%(name)s' is not allowed. Accepted formats: %(exts)s.", + name=file_name, exts=question.allowed_extensions) + ) diff --git a/survey_extra_fields/static/src/js/survey_form.js b/survey_extra_fields/static/src/js/survey_form.js index d06a751..20844b8 100644 --- a/survey_extra_fields/static/src/js/survey_form.js +++ b/survey_extra_fields/static/src/js/survey_form.js @@ -108,6 +108,31 @@ odoo.define("survey_extra_fields.survey_form", function (require) { _t("This question requires an answer."); if (questionRequired && !(this.files && this.files.length > 0)) { errors[questionId] = constrErrorMsg; + return; + } + if (this.files && this.files.length > 0) { + var file = this.files[0]; + var maxSizeMB = parseInt($(this).data("maxFileSize")); + if (maxSizeMB && file.size > maxSizeMB * 1024 * 1024) { + errors[questionId] = _.str.sprintf( + _t("The file exceeds the maximum allowed size of %s MB."), + maxSizeMB + ); + return; + } + var allowedExtensions = $(this).data("allowedExtensions"); + if (allowedExtensions) { + var allowed = allowedExtensions.split(",").map(function (e) { + return e.trim().toLowerCase(); + }); + var ext = "." + file.name.split(".").pop().toLowerCase(); + if (!allowed.includes(ext)) { + errors[questionId] = _.str.sprintf( + _t("This file type is not allowed. Accepted formats: %s."), + allowedExtensions + ); + } + } } }); diff --git a/survey_extra_fields/tests/test_survey_file.py b/survey_extra_fields/tests/test_survey_file.py index 0961fd4..3529ac9 100644 --- a/survey_extra_fields/tests/test_survey_file.py +++ b/survey_extra_fields/tests/test_survey_file.py @@ -109,6 +109,72 @@ class TestSurveyFileSaveLines(TestSurveyFileCommon): self.assertTrue(line.skipped) +class TestSurveyFileConstraints(TestSurveyFileCommon): + """Test _check_file_constraints validation logic.""" + + def test_no_constraints(self): + """No max_file_size and no allowed_extensions: any file passes.""" + self.question_file.write({"max_file_size": 0, "allowed_extensions": False}) + answer = self._add_answer(self.survey, self.survey_manager.partner_id) + file_json = json.dumps({"data": self.file_b64, "name": "anything.exe"}) + answer.save_lines(self.question_file, file_json) + line = answer.user_input_line_ids.filtered( + lambda l: l.question_id == self.question_file + ) + self.assertFalse(line.skipped) + + def test_file_within_size_limit(self): + """File smaller than max_file_size passes.""" + self.question_file.write({"max_file_size": 10}) + answer = self._add_answer(self.survey, self.survey_manager.partner_id) + file_json = json.dumps({"data": self.file_b64, "name": self.file_name}) + answer.save_lines(self.question_file, file_json) + line = answer.user_input_line_ids.filtered( + lambda l: l.question_id == self.question_file + ) + self.assertFalse(line.skipped) + + def test_file_exceeds_size_limit(self): + """File larger than max_file_size raises ValidationError.""" + self.question_file.write({"max_file_size": 1}) + large_content = b"x" * (1 * 1024 * 1024 + 1) + large_b64 = base64.b64encode(large_content).decode() + answer = self._add_answer(self.survey, self.survey_manager.partner_id) + file_json = json.dumps({"data": large_b64, "name": self.file_name}) + with self.assertRaises(Exception): + answer.save_lines(self.question_file, file_json) + + def test_allowed_extension_passes(self): + """File with an allowed extension passes.""" + self.question_file.write({"allowed_extensions": ".pdf,.docx"}) + answer = self._add_answer(self.survey, self.survey_manager.partner_id) + file_json = json.dumps({"data": self.file_b64, "name": "report.docx"}) + answer.save_lines(self.question_file, file_json) + line = answer.user_input_line_ids.filtered( + lambda l: l.question_id == self.question_file + ) + self.assertFalse(line.skipped) + + def test_disallowed_extension_raises(self): + """File with a disallowed extension raises ValidationError.""" + self.question_file.write({"allowed_extensions": ".pdf,.docx"}) + answer = self._add_answer(self.survey, self.survey_manager.partner_id) + file_json = json.dumps({"data": self.file_b64, "name": "script.exe"}) + with self.assertRaises(Exception): + answer.save_lines(self.question_file, file_json) + + def test_both_constraints_valid(self): + """File respecting both size and extension constraints passes.""" + self.question_file.write({"max_file_size": 10, "allowed_extensions": ".pdf"}) + answer = self._add_answer(self.survey, self.survey_manager.partner_id) + file_json = json.dumps({"data": self.file_b64, "name": self.file_name}) + answer.save_lines(self.question_file, file_json) + line = answer.user_input_line_ids.filtered( + lambda l: l.question_id == self.question_file + ) + self.assertFalse(line.skipped) + + class TestSurveyFileDisplayName(TestSurveyFileCommon): """Test the display_name computation for file answer lines.""" diff --git a/survey_extra_fields/views/survey_question_views.xml b/survey_extra_fields/views/survey_question_views.xml new file mode 100644 index 0000000..8eaa08f --- /dev/null +++ b/survey_extra_fields/views/survey_question_views.xml @@ -0,0 +1,20 @@ + + + + + survey.question.form.inherit.extra_fields + survey.question + + + + + + + + + + diff --git a/survey_extra_fields/views/survey_templates.xml b/survey_extra_fields/views/survey_templates.xml index a68fcaf..26ac874 100644 --- a/survey_extra_fields/views/survey_templates.xml +++ b/survey_extra_fields/views/survey_templates.xml @@ -24,6 +24,9 @@ class="o_survey_question_file" t-att-name="question.id" t-att-data-question-type="question.question_type" + t-att-accept="question.allowed_extensions or None" + t-att-data-max-file-size="question.max_file_size or None" + t-att-data-allowed-extensions="question.allowed_extensions or None" />