[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
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m45s
This commit is contained in:
@@ -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": [
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|
||||||
|
|||||||
20
survey_extra_fields/views/survey_question_views.xml
Normal file
20
survey_extra_fields/views/survey_question_views.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user