first commit
This commit is contained in:
commit
9335de6fd4
42
README.rst
Executable file
42
README.rst
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
.. image:: https://img.shields.io/badge/license-LGPL--3-green.svg
|
||||||
|
:target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||||
|
:alt: License: LGPL-3
|
||||||
|
|
||||||
|
File Upload In Survey
|
||||||
|
=====================
|
||||||
|
This module is used for attachment of files in Survey Form
|
||||||
|
|
||||||
|
Company
|
||||||
|
-------
|
||||||
|
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
General Public License v3.0 (LGPL v3)
|
||||||
|
(https://www.gnu.org/licenses/lgpl-3.0-standalone.html)
|
||||||
|
|
||||||
|
Credits
|
||||||
|
-------
|
||||||
|
Developer: (V17) Mohammed Dilshad Tk@ Cybrosys, Contact: odoo@cybrosys.com
|
||||||
|
|
||||||
|
Contacts
|
||||||
|
--------
|
||||||
|
* Mail Contact : odoo@cybrosys.com
|
||||||
|
* Website : https://cybrosys.com
|
||||||
|
|
||||||
|
Bug Tracker
|
||||||
|
-----------
|
||||||
|
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported.
|
||||||
|
|
||||||
|
Maintainer
|
||||||
|
==========
|
||||||
|
.. image:: https://cybrosys.com/images/logo.png
|
||||||
|
:target: https://cybrosys.com
|
||||||
|
|
||||||
|
This module is maintained by Cybrosys Technologies.
|
||||||
|
|
||||||
|
For support and more information, please visit `Our Website <https://cybrosys.com/>`__
|
||||||
|
|
||||||
|
Further information
|
||||||
|
===================
|
||||||
|
HTML Description: `<static/description/index.html>`__
|
||||||
1
__init__.py
Executable file
1
__init__.py
Executable file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
25
__manifest__.py
Executable file
25
__manifest__.py
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
'name': "Image File Upload In Survey",
|
||||||
|
'version': "18.0.1.0.3",
|
||||||
|
'category': 'Extra Tools',
|
||||||
|
'summary': 'Attachment of Image Files in Survey Form',
|
||||||
|
'description': 'This module is used for attachments of image files in Survey Form, '
|
||||||
|
'You can also add multiple image file attachment to Survey Form .',
|
||||||
|
'author': 'Suherdy Yacob',
|
||||||
|
'depends': ['survey'],
|
||||||
|
'assets': {
|
||||||
|
'survey.survey_assets': [
|
||||||
|
'survey_upload_image/static/src/js/survey_form_attachment.js',
|
||||||
|
'survey_upload_image/static/src/js/SurveyFormWidget.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'data': [
|
||||||
|
'views/survey_question_views.xml',
|
||||||
|
'views/survey_user_views.xml',
|
||||||
|
'views/survey_templates.xml',
|
||||||
|
],
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
'application': False,
|
||||||
|
}
|
||||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
3
models/__init__.py
Executable file
3
models/__init__.py
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
from . import survey_question
|
||||||
|
from . import survey_user_input_line
|
||||||
|
from . import survey_user_input
|
||||||
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/survey_question.cpython-312.pyc
Normal file
BIN
models/__pycache__/survey_question.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/survey_user_input.cpython-312.pyc
Normal file
BIN
models/__pycache__/survey_user_input.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/survey_user_input_line.cpython-312.pyc
Normal file
BIN
models/__pycache__/survey_user_input_line.cpython-312.pyc
Normal file
Binary file not shown.
86
models/survey_question.py
Executable file
86
models/survey_question.py
Executable file
@ -0,0 +1,86 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Cybrosys Technologies Pvt. Ltd.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
|
||||||
|
# Author: Mohammed Dilshad Tk (odoo@cybrosys.com)
|
||||||
|
#
|
||||||
|
# You can modify it under the terms of the GNU LESSER
|
||||||
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
# (LGPL v3) along with this program.
|
||||||
|
# If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyQuestion(models.Model):
|
||||||
|
"""
|
||||||
|
This class extends the 'survey.question' model to add new functionality
|
||||||
|
for file uploads.
|
||||||
|
"""
|
||||||
|
_inherit = 'survey.question'
|
||||||
|
|
||||||
|
question_type = fields.Selection(
|
||||||
|
selection_add=[('upload_file', 'Upload File')],
|
||||||
|
help='Select the type of question to create.')
|
||||||
|
upload_multiple_file = fields.Boolean(string='Upload Multiple File',
|
||||||
|
help='Check this box if you want to '
|
||||||
|
'allow users to upload '
|
||||||
|
'multiple files')
|
||||||
|
|
||||||
|
def validate_question(self, answer, comment=None):
|
||||||
|
"""Validate question answer."""
|
||||||
|
self.ensure_one()
|
||||||
|
if self.question_type == 'upload_file':
|
||||||
|
if self.constr_mandatory:
|
||||||
|
# Answer comes as a JSON string '[dataURLs, fileNames]' or raw list from previous steps?
|
||||||
|
# The controller passes what it received.
|
||||||
|
# If we parsed it in `_save_lines`, that's too late. Validation happens before.
|
||||||
|
|
||||||
|
# We need to handle the string if it's not parsed yet, or parsed if Odoo does something.
|
||||||
|
# Standard Odoo validation receives raw inputs usually.
|
||||||
|
import logging
|
||||||
|
from odoo.http import request
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Fallback: Check for prefixed param if answer is empty
|
||||||
|
# because standard controller looks for str(id)
|
||||||
|
if not answer or answer == '[]':
|
||||||
|
# Try multiple key variants including empty string (common issue with t-att-name)
|
||||||
|
keys_to_check = [f'upload_{self.id}', str(self.id), '']
|
||||||
|
for key in keys_to_check:
|
||||||
|
val = request.params.get(key)
|
||||||
|
if val and val != '[]':
|
||||||
|
answer = val
|
||||||
|
break
|
||||||
|
|
||||||
|
is_answered = False
|
||||||
|
if answer and answer != '[]':
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
# If answer is a string, try parsing it
|
||||||
|
if isinstance(answer, str):
|
||||||
|
answer_data = json.loads(answer)
|
||||||
|
else:
|
||||||
|
answer_data = answer
|
||||||
|
|
||||||
|
# Check if we have dataURLs (first element of list)
|
||||||
|
if isinstance(answer_data, list) and len(answer_data) > 0 and answer_data[0]:
|
||||||
|
is_answered = True
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"VALIDATE QUESTION {self.id}: Error parsing answer: {e}")
|
||||||
|
is_answered = False
|
||||||
|
|
||||||
|
if not is_answered:
|
||||||
|
return {self.id: "CUSTOM TAG: This question requires an answer."}
|
||||||
|
return {}
|
||||||
|
return super(SurveyQuestion, self).validate_question(answer, comment)
|
||||||
166
models/survey_user_input.py
Executable file
166
models/survey_user_input.py
Executable file
@ -0,0 +1,166 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Cybrosys Technologies Pvt. Ltd.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
|
||||||
|
# Author: Mohammed Dilshad Tk (odoo@cybrosys.com)
|
||||||
|
#
|
||||||
|
# You can modify it under the terms of the GNU LESSER
|
||||||
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
# (LGPL v3) along with this program.
|
||||||
|
# If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyUserInput(models.Model):
|
||||||
|
"""
|
||||||
|
This class extends the 'survey.user_input' model to add custom
|
||||||
|
functionality for saving user answers.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
_save_lines: Save the user's answer for the given question
|
||||||
|
_save_line_file:Save the user's file upload answer for the given
|
||||||
|
question
|
||||||
|
_get_line_answer_file_upload_values:
|
||||||
|
Get the values to use when creating or updating a user input line
|
||||||
|
for a file upload answer
|
||||||
|
"""
|
||||||
|
_inherit = "survey.user_input"
|
||||||
|
|
||||||
|
def _save_lines(self, question, answer, comment=None,
|
||||||
|
overwrite_existing=False):
|
||||||
|
"""Save the user's answer for the given question."""
|
||||||
|
old_answers = self.env['survey.user_input.line'].search([
|
||||||
|
('user_input_id', '=', self.id),
|
||||||
|
('question_id', '=', question.id), ])
|
||||||
|
if question.question_type == 'upload_file':
|
||||||
|
res = self._save_line_simple_answers(question, old_answers, answer)
|
||||||
|
else:
|
||||||
|
res = super(SurveyUserInput, self)._save_lines(question, answer, comment,
|
||||||
|
overwrite_existing)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _save_line_simple_answers(self, question, old_answers, answer):
|
||||||
|
""" Save the user's file upload answer for the given question."""
|
||||||
|
vals = self._get_line_answer_file_upload_values(question,
|
||||||
|
'upload_file', answer)
|
||||||
|
if old_answers:
|
||||||
|
old_answers.write(vals)
|
||||||
|
return old_answers
|
||||||
|
else:
|
||||||
|
return self.env['survey.user_input.line'].create(vals)
|
||||||
|
|
||||||
|
def _get_line_answer_file_upload_values(self, question, answer_type,
|
||||||
|
answer):
|
||||||
|
"""Get the values to use when creating or updating a user input line
|
||||||
|
for a file upload answer.
|
||||||
|
Auto-compress images if they are uploaded."""
|
||||||
|
vals = {
|
||||||
|
'user_input_id': self.id,
|
||||||
|
'question_id': question.id,
|
||||||
|
'skipped': False,
|
||||||
|
'answer_type': answer_type,
|
||||||
|
}
|
||||||
|
if answer_type == 'upload_file':
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# RETRIEVAL LOGIC: Same as in validate_question
|
||||||
|
# If standard controller failed to pass the answer (because key mismatch),
|
||||||
|
# we try to fetch it from request.params ourselves.
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
import logging
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if not answer or answer == '[]':
|
||||||
|
from odoo.http import request
|
||||||
|
if request:
|
||||||
|
keys_to_check = [f'upload_{question.id}', str(question.id), '']
|
||||||
|
for key in keys_to_check:
|
||||||
|
val = request.params.get(key)
|
||||||
|
if val and val != '[]':
|
||||||
|
answer = val
|
||||||
|
break
|
||||||
|
|
||||||
|
if isinstance(answer, str):
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
answer = json.loads(answer)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"SAVE QUESTION {question.id}: JSON Decode Error: {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not answer or not isinstance(answer, list) or len(answer) < 2:
|
||||||
|
return vals # Skipped=False, but no files.
|
||||||
|
|
||||||
|
file_data = answer[0]
|
||||||
|
file_name = answer[1]
|
||||||
|
attachment_ids = []
|
||||||
|
|
||||||
|
for i in range(len(answer[1])):
|
||||||
|
data = file_data[i]
|
||||||
|
fname = file_name[i]
|
||||||
|
|
||||||
|
# Validation: Restrict to PNG, JPG, JPEG
|
||||||
|
if not fname.lower().endswith(('.png', '.jpg', '.jpeg')):
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
raise UserError(f"Only image files (PNG, JPG, JPEG) are allowed. Invalid file: {fname}")
|
||||||
|
|
||||||
|
# Auto-compression logic
|
||||||
|
try:
|
||||||
|
# Check if file is an image based on extension or simple check
|
||||||
|
if fname.lower().endswith(('.png', '.jpg', '.jpeg')):
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
# Decode base64
|
||||||
|
image_stream = io.BytesIO(base64.b64decode(data))
|
||||||
|
img = Image.open(image_stream)
|
||||||
|
|
||||||
|
# Convert to RGB if RGBA/P to avoid issues saving as JPEG
|
||||||
|
if img.mode in ('RGBA', 'P'):
|
||||||
|
img = img.convert('RGB')
|
||||||
|
|
||||||
|
# Resize if too large (max 1024x1024)
|
||||||
|
max_size = (1024, 1024)
|
||||||
|
img.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# Compress with lower quality
|
||||||
|
output_stream = io.BytesIO()
|
||||||
|
img.save(output_stream, format='JPEG', quality=50, optimize=True)
|
||||||
|
data = base64.b64encode(output_stream.getvalue())
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"SAVE QUESTION {question.id}: Compression Error: {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
attachment = self.env['ir.attachment'].create({
|
||||||
|
'name': fname,
|
||||||
|
'type': 'binary',
|
||||||
|
'datas': data,
|
||||||
|
})
|
||||||
|
attachment_ids.append(attachment.id)
|
||||||
|
|
||||||
|
# Use Command tuples for Many2many relationship
|
||||||
|
# (6, 0, ids) replaces all existing records with the new list
|
||||||
|
vals['value_file_data_ids'] = [(6, 0, attachment_ids)]
|
||||||
|
return vals
|
||||||
|
|
||||||
|
def action_delete_uploaded_files(self):
|
||||||
|
"""Action to delete uploaded files for selected surveys."""
|
||||||
|
for record in self:
|
||||||
|
file_answers = record.user_input_line_ids.filtered(
|
||||||
|
lambda l: l.answer_type == 'upload_file' and l.value_file_data_ids
|
||||||
|
)
|
||||||
|
for line in file_answers:
|
||||||
|
# Unlink attachments
|
||||||
|
line.value_file_data_ids.unlink()
|
||||||
|
|
||||||
61
models/survey_user_input_line.py
Executable file
61
models/survey_user_input_line.py
Executable file
@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Cybrosys Technologies Pvt. Ltd.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
|
||||||
|
# Author: Mohammed Dilshad Tk (odoo@cybrosys.com)
|
||||||
|
#
|
||||||
|
# You can modify it under the terms of the GNU LESSER
|
||||||
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
# (LGPL v3) along with this program.
|
||||||
|
# If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyUserInputLine(models.Model):
|
||||||
|
"""
|
||||||
|
This class extends the 'survey.user_input.line' model to add additional
|
||||||
|
fields and constraints for file uploads.
|
||||||
|
Methods:
|
||||||
|
_check_answer_type_skipped:Check that a line's answer type is
|
||||||
|
not set to 'upload_file' if the line is skipped
|
||||||
|
"""
|
||||||
|
_inherit = "survey.user_input.line"
|
||||||
|
|
||||||
|
answer_type = fields.Selection(
|
||||||
|
selection_add=[('upload_file', 'Upload File')],
|
||||||
|
help="The type of answer for this question (upload_file if the user "
|
||||||
|
"is uploading a file).")
|
||||||
|
value_file_data_ids = fields.Many2many('ir.attachment',
|
||||||
|
help="The attachments "
|
||||||
|
"corresponding to the user's "
|
||||||
|
"file upload answer, if any.")
|
||||||
|
|
||||||
|
@api.constrains('skipped', 'answer_type')
|
||||||
|
def _check_answer_type_skipped(self):
|
||||||
|
""" Check that a line's answer type is not set to 'upload_file' if
|
||||||
|
the line is skipped."""
|
||||||
|
for line in self:
|
||||||
|
if line.answer_type != 'upload_file':
|
||||||
|
super(SurveyUserInputLine, line)._check_answer_type_skipped()
|
||||||
|
|
||||||
|
@api.depends('answer_type', 'value_file_data_ids')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
"""Override to include file names in the display name."""
|
||||||
|
super()._compute_display_name()
|
||||||
|
for line in self:
|
||||||
|
if line.answer_type == 'upload_file':
|
||||||
|
if line.value_file_data_ids:
|
||||||
|
line.display_name = ', '.join(line.value_file_data_ids.mapped('name'))
|
||||||
|
else:
|
||||||
|
line.display_name = 'No file uploaded'
|
||||||
13
static/src/js/SurveyFormWidget.js
Executable file
13
static/src/js/SurveyFormWidget.js
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
/** @odoo-module */
|
||||||
|
import SurveyFormWidget from '@survey/js/survey_form';
|
||||||
|
SurveyFormWidget.include({
|
||||||
|
/** Get all question answers by question type */
|
||||||
|
_prepareSubmitValues(formData, params) {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.$('[data-question-type]').each(function () {
|
||||||
|
if ($(this).data('questionType') === 'upload_file'){
|
||||||
|
params[this.name] = [$(this).data('oe-data'), $(this).data('oe-file_name')];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
134
static/src/js/survey_form_attachment.js
Executable file
134
static/src/js/survey_form_attachment.js
Executable file
@ -0,0 +1,134 @@
|
|||||||
|
/** @odoo-module */
|
||||||
|
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||||
|
import SurveyFormWidget from '@survey/js/survey_form';
|
||||||
|
import SurveyPreloadImageMixin from "@survey/js/survey_preload_image_mixin";
|
||||||
|
/** Extends publicWidget to create "SurveyFormUpload" */
|
||||||
|
publicWidget.registry.SurveyFormUpload = publicWidget.Widget.extend(SurveyPreloadImageMixin, {
|
||||||
|
selector: '.o_survey_form',
|
||||||
|
events: {
|
||||||
|
'change .o_survey_upload_file': '_onFileChange',
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
// this.rpc = this.bindService("rpc");
|
||||||
|
},
|
||||||
|
/** On adding file function */
|
||||||
|
_onFileChange: function (event) {
|
||||||
|
var self = this;
|
||||||
|
var files = event.target.files;
|
||||||
|
var fileNames = [];
|
||||||
|
var dataURLs = [];
|
||||||
|
|
||||||
|
var $target = $(event.target);
|
||||||
|
// Find the container for this specific question
|
||||||
|
var $container = $target.closest('.o_survey_upload_container');
|
||||||
|
|
||||||
|
// Find elements scoped to this container
|
||||||
|
var $fileList = $container.find('.o_survey_upload_list');
|
||||||
|
var fileListEl = $fileList[0];
|
||||||
|
|
||||||
|
var $hiddenInput = $container.find('input.o_survey_upload_file_value');
|
||||||
|
|
||||||
|
// Clear existing file list and delete button
|
||||||
|
if (fileListEl) {
|
||||||
|
fileListEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
$target.attr('data-oe-data', '');
|
||||||
|
$target.attr('data-oe-file_name', '');
|
||||||
|
$hiddenInput.val('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadedFiles = 0;
|
||||||
|
var totalFiles = files.length;
|
||||||
|
|
||||||
|
// Create container for previews
|
||||||
|
var previewContainer = document.createElement('div');
|
||||||
|
previewContainer.className = 'o_survey_file_previews d-flex flex-wrap gap-2 mb-2';
|
||||||
|
if (fileListEl) fileListEl.appendChild(previewContainer);
|
||||||
|
|
||||||
|
// Function to finish processing when all files are read
|
||||||
|
var checkAllFilesLoaded = function () {
|
||||||
|
if (loadedFiles === totalFiles) {
|
||||||
|
// Get question ID from data attribute - checking both input and hidden for flexibility
|
||||||
|
var questionId = $target.attr('data-question-id') || $hiddenInput.attr('data-question-id');
|
||||||
|
|
||||||
|
var finalPayload = JSON.stringify([dataURLs, fileNames]);
|
||||||
|
$target.attr('data-oe-data', JSON.stringify(dataURLs));
|
||||||
|
$target.attr('data-oe-file_name', JSON.stringify(fileNames));
|
||||||
|
|
||||||
|
// Set name dynamically to ensure backend finds it with the correct key
|
||||||
|
if (questionId) {
|
||||||
|
$hiddenInput.attr('name', 'upload_' + questionId);
|
||||||
|
}
|
||||||
|
$hiddenInput.val(finalPayload);
|
||||||
|
|
||||||
|
// Create delete button only once
|
||||||
|
var deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'btn btn-danger btn-sm mt-2';
|
||||||
|
deleteBtn.textContent = 'Delete All';
|
||||||
|
deleteBtn.type = 'button'; // Prevent form submission
|
||||||
|
deleteBtn.addEventListener('click', function () {
|
||||||
|
// Clear file list
|
||||||
|
if (fileListEl) fileListEl.innerHTML = '';
|
||||||
|
// Clear input field attributes
|
||||||
|
$target.attr('data-oe-data', '');
|
||||||
|
$target.attr('data-oe-file_name', '');
|
||||||
|
$hiddenInput.val('');
|
||||||
|
// Reset file input
|
||||||
|
$target.val('');
|
||||||
|
});
|
||||||
|
if (fileListEl) fileListEl.appendChild(deleteBtn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.readAsDataURL(files[i]);
|
||||||
|
reader.onload = function (e) {
|
||||||
|
var file = files[i];
|
||||||
|
var filename = file.name;
|
||||||
|
var dataURL = e.target.result.split(',')[1]; /** split base64 data */
|
||||||
|
|
||||||
|
// Ensure order is preserved might be tricky with async, but simple push is ok for now
|
||||||
|
// or use index if strict order needed.
|
||||||
|
// Detailed handling often requires mapping index.
|
||||||
|
fileNames.push(filename);
|
||||||
|
dataURLs.push(dataURL);
|
||||||
|
|
||||||
|
// Create preview element
|
||||||
|
var previewItem = document.createElement('div');
|
||||||
|
previewItem.className = 'card p-1';
|
||||||
|
previewItem.style.width = '100px';
|
||||||
|
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = e.target.result; // Use full data URL for preview
|
||||||
|
img.className = 'card-img-top';
|
||||||
|
img.style.height = '80px';
|
||||||
|
img.style.objectFit = 'cover';
|
||||||
|
img.title = filename;
|
||||||
|
previewItem.appendChild(img);
|
||||||
|
} else {
|
||||||
|
var icon = document.createElement('div');
|
||||||
|
icon.className = 'text-center p-3';
|
||||||
|
icon.innerHTML = '<i class="fa fa-file fa-2x"></i>';
|
||||||
|
previewItem.appendChild(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameDiv = document.createElement('div');
|
||||||
|
nameDiv.className = 'card-body p-1 text-truncate small';
|
||||||
|
nameDiv.textContent = filename;
|
||||||
|
previewItem.appendChild(nameDiv);
|
||||||
|
|
||||||
|
previewContainer.appendChild(previewItem);
|
||||||
|
|
||||||
|
loadedFiles++;
|
||||||
|
checkAllFilesLoaded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export default publicWidget.registry.SurveyFormUpload;
|
||||||
22
views/survey_question_views.xml
Executable file
22
views/survey_question_views.xml
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Inherited form view if survey.question-->
|
||||||
|
<record id="survey_question_form" model="ir.ui.view">
|
||||||
|
<field name="name">survey.question.view.form.inherit.survey.upload.file</field>
|
||||||
|
<field name="model">survey.question</field>
|
||||||
|
<field name="inherit_id" ref="survey.survey_question_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[hasclass('o_preview_questions')]"
|
||||||
|
position="inside">
|
||||||
|
<div invisible="question_type != 'upload_file'">
|
||||||
|
<p class="o_upload_file">Upload Files
|
||||||
|
<i class="fa fa-upload"/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='constr_mandatory']" position="after">
|
||||||
|
<field name="upload_multiple_file" invisible="question_type != 'upload_file'"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
69
views/survey_templates.xml
Executable file
69
views/survey_templates.xml
Executable file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<!-- If question_type= upload file then t-call template-->
|
||||||
|
<template id="question_page_upload_answer"
|
||||||
|
inherit_id="survey.question_container">
|
||||||
|
<xpath expr="//div[@role='alert']" position="before">
|
||||||
|
<t t-if="question.question_type == 'upload_file'">
|
||||||
|
<t t-call="survey_upload_image.multi_upload_file"/>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
<!-- Answer View-->
|
||||||
|
<template id="multi_upload_file">
|
||||||
|
<div class="o_survey_upload_container">
|
||||||
|
<div class="o_survey_upload_box" id="survey_upload_box">
|
||||||
|
<div class="o_survey_upload_box_header">Upload Files</div>
|
||||||
|
<br/>
|
||||||
|
<div class="o_survey_upload_box_body">
|
||||||
|
<input type="file" class="o_survey_upload_file"
|
||||||
|
data-oe-data=""
|
||||||
|
data-oe-file_name=""
|
||||||
|
accept="image/png, image/jpeg, image/jpg, .png, .jpg, .jpeg"
|
||||||
|
t-att-data-question-type="question.question_type"
|
||||||
|
t-att-multiple="question.upload_multiple_file"
|
||||||
|
t-att-data-question-id="question.id"
|
||||||
|
/>
|
||||||
|
<input type="hidden" class="o_survey_upload_file_value" name="temp_upload_name" t-att-data-question-id="question.id"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<div class="o_survey_upload_list"/>
|
||||||
|
<t t-if="question.upload_multiple_file == False">
|
||||||
|
<div class="o_survey_upload_note">Note: You can only upload one
|
||||||
|
file.
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-if="question.upload_multiple_file == True">
|
||||||
|
<div class="o_survey_upload_note">Note: You can upload
|
||||||
|
Multiple files.
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!--Show the answer in print page if question type = upload file then t-call template-->
|
||||||
|
<template id="survey_page_print_upload_answer"
|
||||||
|
inherit_id="survey.survey_page_print">
|
||||||
|
<xpath expr="//div[hasclass('o_survey_question_error')]"
|
||||||
|
position="before">
|
||||||
|
<t t-if="question.question_type == 'upload_file'">
|
||||||
|
<t t-call="survey_upload_image.multi_upload_answer"/>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
<!--Answer Value attachments-->
|
||||||
|
<template id="multi_upload_answer">
|
||||||
|
<t t-if="answer_lines.value_file_data_ids">
|
||||||
|
<div>
|
||||||
|
<t t-foreach="answer_lines.value_file_data_ids"
|
||||||
|
t-as="attachment">
|
||||||
|
<a t-attf-href="/web/content/{{ attachment.id }}?download=true">
|
||||||
|
<i class="fa fa-download"/>
|
||||||
|
<t t-esc="attachment.name"/>
|
||||||
|
</a>
|
||||||
|
<br/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
49
views/survey_user_views.xml
Executable file
49
views/survey_user_views.xml
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Inherited the survey.user_input.line to add fields -->
|
||||||
|
<record id="survey_user_input_line_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">survey.user_input.line.view.form.inherit.survey.upload.image</field>
|
||||||
|
<field name="model">survey.user_input.line</field>
|
||||||
|
<field name="inherit_id" ref="survey.survey_user_input_line_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='suggested_answer_id']" position="after">
|
||||||
|
<field name="value_file_data_ids" context="{'form_view_ref': 'survey_upload_image.view_attachment_form_survey_upload'}">
|
||||||
|
<list>
|
||||||
|
<field name="datas" widget="image" options="{'size': [50, 50]}" string="Preview"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file_size"/>
|
||||||
|
<field name="type"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_attachment_form_survey_upload" model="ir.ui.view">
|
||||||
|
<field name="name">ir.attachment.form.survey.upload</field>
|
||||||
|
<field name="model">ir.attachment</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Attachment">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="datas" widget="image" options="{'size': [400, 400]}"/>
|
||||||
|
<field name="file_size"/>
|
||||||
|
<field name="mimetype"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_survey_user_input_delete_files" model="ir.actions.server">
|
||||||
|
<field name="name">Delete Uploaded Files</field>
|
||||||
|
<field name="model_id" ref="survey.model_survey_user_input"/>
|
||||||
|
<field name="binding_model_id" ref="survey.model_survey_user_input"/>
|
||||||
|
<field name="binding_view_types">list,form</field>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
action = records.action_delete_uploaded_files()
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user