Files

143 lines
5.9 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import io
import zipfile
from os.path import join as opj
from werkzeug.utils import secure_filename
from odoo import http
from odoo.http import Stream, request
from odoo.tools import consteq
class WhatsappSign(http.Controller):
def _split_filename(self, filename):
""" Manually split a filename into (base_name, extension).
:param str filename: The file name string
:return: The base name and extension (extension includes the leading dot)
:rtype: tuple
"""
dot_index = filename.rfind('.')
# No dot or dot is first character (e.g. ".bashrc")
if dot_index <= 0:
return filename, ''
base_name = filename[:dot_index]
ext = filename[dot_index:] # includes the dot
return base_name, ext
def _generate_sign_request_zip_name(self, sign_request):
""" Generates a sanitized base name for the ZIP file and its internal folder
based on the sign request's subject or a default fallback.
This method removes any file extension (e.g., ".pdf") from the subject,
strips unsafe characters, and replaces spaces with underscores to create
a safe filename.
:param sign_request: The sign request record
:return: A sanitized base name to use for the ZIP file and its internal folder
:rtype: str
"""
subject = sign_request.subject
if subject:
# Remove any file extension (e.g., ".pdf")
name, _ = self._split_filename(subject)
else:
name = request.env._("Attachments of Request %s", str(sign_request.reference))
return secure_filename(name)
def _create_zip_response(self, sign_request):
""" Creates a ZIP file containing multiple attachments for sign requests.
:params Model.model sign_request: The sign request record
:return: A streamed HTTP response containing the ZIP file
"""
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_DEFLATED) as zipfile_obj:
existing_document_names = set()
for attachment in sign_request.attachment_ids:
download_name = self._format_attachments_name(
sign_request, attachment, existing_document_names
)
zipfile_obj.writestr(download_name, attachment.raw)
content = buffer.getvalue()
filename = f'{self._generate_sign_request_zip_name(sign_request)}.zip'
return Stream(
type='data',
data=content,
mimetype='application/zip',
download_name=filename,
as_attachment=True,
size=len(content),
).get_response()
def _format_attachments_name(self, sign_request, attachment, existing_document_names):
""" Generates a unique, sanitized attachment name prefixed with the sign request folder.
Ensures the attachment name does not conflict with existing names by appending a counter
if necessary, preserving the file extension.
:param sign_request: The sign request record
:param attachment: The attachment to be named
:param set existing_document_names: Set of existing attachment names to check for duplicates
:return: A unique attachment name with folder prefix
:rtype: str
"""
zip_name = self._generate_sign_request_zip_name(sign_request)
base_name, ext = self._split_filename(attachment.name)
download_name = attachment.name
counter = 1
while download_name in existing_document_names:
download_name = f"{base_name} ({counter}){ext}"
counter += 1
existing_document_names.add(download_name)
# Prepend the zip folder name after resolving duplicates
download_name = opj(zip_name, download_name)
return download_name
@http.route([
'/sign/download/attachments/<int:request_id>/<token>',
'/sign/download/attachments/<int:request_id>/<token>/<int:sign_attachment_id>'
], type='http', auth='public')
def download_attachment(self, request_id, token, attachment_id=None, **post):
""" Handles downloading attachments related to a sign request.
This method processes requests to download one or all attachments associated with
a sign request. It authenticates the request using the provided token and returns
either the specified attachment (if an attachment ID is provided and valid), or
all attachments related to the sign request.
:param int request_id: ID of the sign request
:param str token: Access token used to authorize the request
:param int attachment_id: ID of a specific attachment to download
:param post: Additional POST parameters (currently unused)
:return: A response containing the specified attachment or all attachments,
or an appropriate redirect or error response if no valid attachments are found
or access is unauthorized
"""
sign_request = request.env['sign.request'].sudo().browse(request_id).exists()
if not sign_request or not consteq(sign_request.access_token, token):
return request.not_found()
if attachment_id:
attachment = request.env['ir.attachment'].sudo().browse(attachment_id)
if attachment in sign_request.attachment_ids:
return attachment._to_http_stream().get_response(as_attachment=True)
else:
return request.not_found()
attachment_ids = sign_request.attachment_ids
if len(attachment_ids) == 1:
return attachment_ids._to_http_stream().get_response(as_attachment=True)
elif len(attachment_ids) > 1: # If there are multiple attachments and no specific ID was provided, create a ZIP
return self._create_zip_response(sign_request)
return request.not_found()