From 892e3d7d25c298d2c1d2f98464e3feb160bad4cd Mon Sep 17 00:00:00 2001 From: Nidhi Patel Date: Tue, 5 Jan 2021 06:53:54 +0000 Subject: [PATCH] [IMP] web: Clean the export file names The purpose of this task is to clean up and provide clear export file names. Currently, When exporting data: - From a pivot view, the file name is 'table' - From a list view, the file name is technical name of model so in this commit, Change the export file name as below: - For list view quick export and standard record export, the filename will be 'model_description(model_technical_name)' - For pivot view quick export, the file name will be 'Pivot(string set on view)(model_technical_name)' and if string is not set then 'PivotUntitled(model_technical_name)' In all the cases space and forward slash will be removed. closes odoo/odoo#64123 Taskid: 2237840 Related: odoo/enterprise#15579 Signed-off-by: Xavier Morel (xmo) --- addons/web/controllers/main.py | 32 +++++++++---- addons/web/controllers/pivot.py | 9 ++-- .../src/js/views/pivot/pivot_controller.js | 1 + odoo/tools/__init__.py | 2 +- odoo/tools/osutil.py | 46 ++++++++++++++++++- 5 files changed, 73 insertions(+), 17 deletions(-) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 559ba04698f..014b4a66d07 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -34,7 +34,7 @@ import odoo import odoo.modules.registry from odoo.api import call_kw, Environment from odoo.modules import get_module_path, get_resource_path -from odoo.tools import image_process, topological_sort, html_escape, pycompat, ustr, apply_inheritance_specs, lazy_property, float_repr +from odoo.tools import image_process, topological_sort, html_escape, pycompat, ustr, apply_inheritance_specs, lazy_property, float_repr, osutil from odoo.tools.mimetypes import guess_mimetype from odoo.tools.translate import _ from odoo.tools.misc import str2bool, xlsxwriter, file_open @@ -1884,12 +1884,20 @@ class ExportFormat(object): """ Provides the format's content type """ raise NotImplementedError() - def filename(self, base): - """ Creates a valid filename for the format (with extension) from the - provided base name (exension-less) - """ + @property + def extension(self): raise NotImplementedError() + def filename(self, base): + """ Creates a filename *without extension* for the item / format of + model ``base``. + """ + if base not in request.env: + return base + + model_description = request.env['ir.model']._get(base).name + return f"{model_description} ({base})" + def from_data(self, fields, rows): """ Conversion method from Odoo's export data to whatever the current export class outputs @@ -1939,9 +1947,11 @@ class ExportFormat(object): export_data = records.export_data(field_names).get('datas',[]) response_data = self.from_data(columns_headers, export_data) + # TODO: call `clean_filename` directly in `content_disposition`? return request.make_response(response_data, headers=[('Content-Disposition', - content_disposition(self.filename(model))), + content_disposition( + osutil.clean_filename(self.filename(model) + self.extension))), ('Content-Type', self.content_type)], cookies={'fileToken': token}) @@ -1956,8 +1966,9 @@ class CSVExport(ExportFormat, http.Controller): def content_type(self): return 'text/csv;charset=utf8' - def filename(self, base): - return base + '.csv' + @property + def extension(self): + return '.csv' def from_group_data(self, fields, groups): raise UserError(_("Exporting grouped data to csv is not supported.")) @@ -1991,8 +2002,9 @@ class ExcelExport(ExportFormat, http.Controller): def content_type(self): return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - def filename(self, base): - return base + '.xlsx' + @property + def extension(self): + return '.xlsx' def from_group_data(self, fields, groups): with GroupExportXlsxWriter(fields, groups.count) as xlsx_writer: diff --git a/addons/web/controllers/pivot.py b/addons/web/controllers/pivot.py index addb8f2bd61..e51a2560478 100644 --- a/addons/web/controllers/pivot.py +++ b/addons/web/controllers/pivot.py @@ -5,9 +5,9 @@ from collections import deque import io import json -from odoo import http -from odoo.http import request -from odoo.tools import ustr +from odoo import http, _ +from odoo.http import content_disposition, request +from odoo.tools import ustr, osutil from odoo.tools.misc import xlsxwriter @@ -102,9 +102,10 @@ class TableExporter(http.Controller): workbook.close() xlsx_data = output.getvalue() + filename = osutil.clean_filename(_("Pivot %(title)s (%(model_name)s)", title=jdata['title'], model_name=jdata['model'])) response = request.make_response(xlsx_data, headers=[('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'), - ('Content-Disposition', 'attachment; filename=table.xlsx')], + ('Content-Disposition', content_disposition(filename + '.xlsx'))], cookies={'fileToken': token}) return response diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js index 1138778156a..35c345322cd 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -140,6 +140,7 @@ odoo.define('web.PivotController', function (require) { } const table = this.model.exportData(); table.title = this.title; + table.model = this.modelName; session.get_file({ url: '/web/pivot/export_xlsx', data: { data: JSON.stringify(table) }, diff --git a/odoo/tools/__init__.py b/odoo/tools/__init__.py index e3404b9b0f1..546ec7f0431 100644 --- a/odoo/tools/__init__.py +++ b/odoo/tools/__init__.py @@ -5,7 +5,6 @@ from . import _monkeypatches from . import pycompat from . import win32 from . import appdirs -from . import osutil from . import pdf from . import cloc from .config import config @@ -21,3 +20,4 @@ from .xml_utils import * from .date_utils import * from .convert import * from .template_inheritance import * +from . import osutil diff --git a/odoo/tools/osutil.py b/odoo/tools/osutil.py index 7ab7a8ced57..422b915e5fa 100644 --- a/odoo/tools/osutil.py +++ b/odoo/tools/osutil.py @@ -4,15 +4,57 @@ """ Some functions related to the os and os.path module """ -from contextlib import contextmanager import logging import os -from os.path import join as opj +import re import tempfile import zipfile +from contextlib import contextmanager +from os.path import join as opj + +from .translate import _ + _logger = logging.getLogger(__name__) +WINDOWS_RESERVED = re.compile(r''' + ^ + # forbidden stems: reserved keywords + (:?CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]) + # even with an extension this is recommended against + (:?\..*)? + $ +''', flags=re.IGNORECASE | re.VERBOSE) +def clean_filename(name, replacement=''): + """ Strips or replaces possibly problematic or annoying characters our of + the input string, in order to make it a valid filename in most operating + systems (including dropping reserved Windows filenames). + + If this results in an empty string, results in "Untitled" (localized). + + Allows: + + * any alphanumeric character (unicode) + * underscore (_) as that's innocuous + * dot (.) except in leading position to avoid creating dotfiles + * dash (-) except in leading position to avoid annoyance / confusion with + command options + * brackets ([ and ]), while they correspond to shell *character class* + they're a common way to mark / tag files especially on windows + * parenthesis ("(" and ")"), a more natural though less common version of + the former + * space (" ") + + :param str name: file name to clean up + :param str replacement: + replacement string to use for sequences of problematic input, by default + an empty string to remove them entirely, each contiguous sequence of + problems is replaced by a single replacement + :rtype: str + """ + if WINDOWS_RESERVED.match(name): + return _("Untitled") + return re.sub(r'[^\w_.()\[\] -]+', replacement, name).lstrip('.-') or _("Untitled") def listdir(dir, recursive=False): """Allow to recursively get the file listing following symlinks, returns