[IMP] survey_extra_fields: new params max_file_size and allowed_extensions for file question type
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m45s

This commit is contained in:
2026-02-18 17:49:33 +01:00
parent e45bccc3e3
commit b63b8ba885
9 changed files with 241 additions and 8 deletions

View File

@@ -17,6 +17,7 @@ Add extra question types to surveys:
"data": [ "data": [
"views/survey_templates.xml", "views/survey_templates.xml",
"views/survey_user_views.xml", "views/survey_user_views.xml",
"views/survey_question_views.xml",
], ],
"assets": { "assets": {
"survey.survey_assets": [ "survey.survey_assets": [

View File

@@ -1,11 +1,17 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from __future__ import annotations
import base64 import base64
from typing import TYPE_CHECKING, Any
from odoo import http from odoo import http
from odoo.http import request, content_disposition from odoo.http import request, content_disposition
from odoo.addons.survey.controllers.main import Survey from odoo.addons.survey.controllers.main import Survey
if TYPE_CHECKING:
from werkzeug.wrappers import Response
class SurveyExtraFieldsController(Survey): class SurveyExtraFieldsController(Survey):
@@ -14,7 +20,13 @@ class SurveyExtraFieldsController(Survey):
type="http", type="http",
auth="public", 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( survey = request.env["survey.survey"].sudo().search(
[("access_token", "=", survey_token)], limit=1 [("access_token", "=", survey_token)], limit=1
) )

View File

@@ -6,8 +6,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 16.0\n" "Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-17 14:34+0000\n" "POT-Creation-Date: 2026-02-18 16:38+0000\n"
"PO-Revision-Date: 2026-02-17 14:34+0000\n" "PO-Revision-Date: 2026-02-18 16:38+0000\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -15,17 +15,47 @@ msgstr ""
"Content-Transfer-Encoding: \n" "Content-Transfer-Encoding: \n"
"Plural-Forms: \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 #. module: survey_extra_fields
#: model:ir.model.fields,field_description:survey_extra_fields.field_survey_user_input_line__answer_type #: model:ir.model.fields,field_description:survey_extra_fields.field_survey_user_input_line__answer_type
msgid "Answer Type" msgid "Answer Type"
msgstr "Type de réponse" 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 #. 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_question__question_type__file
#: model:ir.model.fields.selection,name:survey_extra_fields.selection__survey_user_input_line__answer_type__file #: model:ir.model.fields.selection,name:survey_extra_fields.selection__survey_user_input_line__answer_type__file
msgid "File" msgid "File"
msgstr "Fichier" 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 #. module: survey_extra_fields
#: model:ir.model.fields,field_description:survey_extra_fields.field_survey_question__question_type #: model:ir.model.fields,field_description:survey_extra_fields.field_survey_question__question_type
msgid "Question Type" msgid "Question Type"
@@ -58,6 +88,34 @@ msgstr "Entrée utilisateur du sondage"
msgid "Survey User Input Line" msgid "Survey User Input Line"
msgstr "Ligne d'entrée pour l'utilisateur du sondage" 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 #. module: survey_extra_fields
#. odoo-javascript #. odoo-javascript
#: code:addons/survey_extra_fields/static/src/js/survey_form.js:0 #: code:addons/survey_extra_fields/static/src/js/survey_form.js:0

View File

@@ -9,3 +9,12 @@ class SurveyQuestion(models.Model):
question_type = fields.Selection( question_type = fields.Selection(
selection_add=[("file", "File")] 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.",
)

View File

@@ -1,14 +1,23 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). # 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): class SurveyUserInput(models.Model):
_inherit = "survey.user_input" _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": if question.question_type == "file":
old_answers = self.env["survey.user_input.line"].search([ old_answers = self.env["survey.user_input.line"].search([
("user_input_id", "=", self.id), ("user_input_id", "=", self.id),
@@ -22,8 +31,11 @@ class SurveyUserInput(models.Model):
} }
if answer: if answer:
file_data = json.loads(answer) file_data = json.loads(answer)
vals["value_file"] = file_data.get("data") file_b64 = file_data.get("data", "")
vals["value_file_fname"] = file_data.get("name") 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: else:
vals.update(answer_type=None, skipped=True) vals.update(answer_type=None, skipped=True)
if old_answers: if old_answers:
@@ -32,3 +44,30 @@ class SurveyUserInput(models.Model):
self.env["survey.user_input.line"].create(vals) self.env["survey.user_input.line"].create(vals)
else: else:
return super().save_lines(question, answer, comment=comment) 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)
)

View File

@@ -108,6 +108,31 @@ odoo.define("survey_extra_fields.survey_form", function (require) {
_t("This question requires an answer."); _t("This question requires an answer.");
if (questionRequired && !(this.files && this.files.length > 0)) { if (questionRequired && !(this.files && this.files.length > 0)) {
errors[questionId] = constrErrorMsg; 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
);
}
}
} }
}); });

View File

@@ -109,6 +109,72 @@ class TestSurveyFileSaveLines(TestSurveyFileCommon):
self.assertTrue(line.skipped) 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): class TestSurveyFileDisplayName(TestSurveyFileCommon):
"""Test the display_name computation for file answer lines.""" """Test the display_name computation for file answer lines."""

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="survey_question_form_inh" model="ir.ui.view">
<field name="name">survey.question.form.inherit.extra_fields</field>
<field name="model">survey.question</field>
<field name="inherit_id" ref="survey.survey_question_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='options']//field[@name='matrix_subtype']" position="before">
<field name="max_file_size"
attrs="{'invisible': [('question_type', '!=', 'file')]}"
string="Max File Size (MB)"/>
<field name="allowed_extensions"
attrs="{'invisible': [('question_type', '!=', 'file')]}"
placeholder=".pdf,.docx,.xlsx"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -24,6 +24,9 @@
class="o_survey_question_file" class="o_survey_question_file"
t-att-name="question.id" t-att-name="question.id"
t-att-data-question-type="question.question_type" 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"
/> />
</t> </t>
</div> </div>