diff --git a/muk_converter/__init__.py b/muk_converter/__init__.py index 091b8c9..d1bca94 100644 --- a/muk_converter/__init__.py +++ b/muk_converter/__init__.py @@ -18,6 +18,5 @@ ################################################################################### from . import service -from . import tools from . import models from . import wizards \ No newline at end of file diff --git a/muk_converter/__manifest__.py b/muk_converter/__manifest__.py index 94a8e8d..86d5639 100644 --- a/muk_converter/__manifest__.py +++ b/muk_converter/__manifest__.py @@ -20,7 +20,7 @@ { "name": "MuK Converter", "summary": """Universal Converter""", - "version": '11.0.1.1.6', + "version": '11.0.1.2.2', "category": 'Extra Tools', "license": "AGPL-3", "website": "https://www.mukit.at", @@ -30,15 +30,17 @@ "Mathias Markl ", ], "depends": [ - "muk_utils", + "iap", + "base_setup", "muk_autovacuum", "muk_fields_lobject", ], "data": [ "security/ir.model.access.csv", - "views/convert.xml", "data/params.xml", "data/autovacuum.xml", + "views/convert.xml", + "views/res_config_settings_view.xml", ], "qweb": [ "static/src/xml/*.xml", @@ -48,9 +50,7 @@ ], "external_dependencies": { "python": [], - "bin": [ - "unoconv", - ], + "bin": [], }, "application": False, "installable": True, diff --git a/muk_converter/data/params.xml b/muk_converter/data/params.xml index c17f55c..e30d8dd 100644 --- a/muk_converter/data/params.xml +++ b/muk_converter/data/params.xml @@ -19,6 +19,12 @@ + + muk_converter.service + unoconv + + + muk_converter.max_store 20 diff --git a/muk_converter/doc/changelog.rst b/muk_converter/doc/changelog.rst index 4222f97..fb52946 100644 --- a/muk_converter/doc/changelog.rst +++ b/muk_converter/doc/changelog.rst @@ -1,3 +1,8 @@ +`1.2.0` +------- + +- Added In-App Purchases option + `1.1.0` ------- diff --git a/muk_converter/doc/index.rst b/muk_converter/doc/index.rst index 89b15a2..ad200a9 100644 --- a/muk_converter/doc/index.rst +++ b/muk_converter/doc/index.rst @@ -121,6 +121,13 @@ Contributors * Mathias Markl +Images +------------ + +Some pictures are based on or inspired by the icon set of Font Awesome: + +* `Font Awesome `_ + Author & Maintainer ------------------- diff --git a/muk_converter/models/__init__.py b/muk_converter/models/__init__.py index 7b5786b..8a01742 100644 --- a/muk_converter/models/__init__.py +++ b/muk_converter/models/__init__.py @@ -17,5 +17,6 @@ # ################################################################################### +from . import store from . import converter -from . import store \ No newline at end of file +from . import res_config_settings \ No newline at end of file diff --git a/muk_converter/models/converter.py b/muk_converter/models/converter.py index a5642d5..3b0339f 100644 --- a/muk_converter/models/converter.py +++ b/muk_converter/models/converter.py @@ -17,12 +17,14 @@ # ################################################################################### +import base64 import hashlib import logging -from odoo import api, models, fields +from odoo import api, models, fields, SUPERUSER_ID -from odoo.addons.muk_converter.tools import converter +from odoo.addons.muk_converter.service.unoconv import UnoconvConverter +from odoo.addons.muk_converter.service.provider import RemoteConverter _logger = logging.getLogger(__name__) @@ -30,28 +32,62 @@ class Converter(models.AbstractModel): _name = 'muk_converter.converter' _description = 'Converter' + + #---------------------------------------------------------- + # Functions + #---------------------------------------------------------- + + @api.model + def formats(self): + return self._provider().formats @api.model - def convert(self, filename, content, format="pdf", recompute=False): - def parse(filename, content, format): - return converter.convert(filename, content, format) - def store(checksum, filename, content, format, stored): - if not stored.exists(): - self.env['muk_converter.store'].sudo().create({ - 'checksum': checksum, - 'format': format, - 'content_fname': filename, - 'content': content}) - else: - stored.write({'used_date': fields.Datetime.now}) - checksum = hashlib.sha1(content).hexdigest() - stored = self.env['muk_converter.store'].sudo().search( - [["checksum", "=", checksum], ["format", "=", format]], limit=1) + def imports(self): + return self._provider().imports + + @api.model + def convert(self, filename, content, format="pdf", recompute=False, store=True): + binary_content = base64.b64decode(content) + checksum = hashlib.sha1(binary_content).hexdigest() + stored = self._retrieve(checksum, format) if not recompute and stored.exists(): - return stored.content - else: - output = parse(filename, content, format) + return base64.b64encode(stored.content) + else: name = "%s.%s" % (filename, format) - store(checksum, name, output, format, stored) - return output - \ No newline at end of file + output = self._parse(filename, binary_content, format) + if store: + self._store(checksum, name, output, format, stored) + return base64.b64encode(output) + + #---------------------------------------------------------- + # Helper + #---------------------------------------------------------- + + @api.model + def _provider(self): + params = self.env['ir.config_parameter'].sudo() + service = params.get_param('muk_converter.service') + if service == 'unoconv': + return UnoconvConverter() + else: + return RemoteConverter(env=self.env) + + @api.model + def _parse(self, filename, content, format): + return self._provider().convert(content, filename=filename, format=format) + + @api.model + def _retrieve(self, checksum, format): + domain = [["checksum", "=", checksum], ["format", "=", format]] + return self.env['muk_converter.store'].sudo().search(domain, limit=1) + + @api.model + def _store(self, checksum, filename, content, format, stored): + if stored and stored.exists(): + stored.write({'used_date': fields.Datetime.now}) + else: + self.env['muk_converter.store'].sudo().create({ + 'checksum': checksum, + 'format': format, + 'content_fname': filename, + 'content': content}) \ No newline at end of file diff --git a/muk_converter/models/res_config_settings.py b/muk_converter/models/res_config_settings.py new file mode 100644 index 0000000..829f25f --- /dev/null +++ b/muk_converter/models/res_config_settings.py @@ -0,0 +1,83 @@ +################################################################################### +# +# Copyright (C) 2017 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import logging +import textwrap + +from odoo import api, fields, models + +class ResConfigSettings(models.TransientModel): + + _inherit = 'res.config.settings' + + converter_service = fields.Selection( + selection=[ + ("unoconv", "Local"), + ("provider", "Service")], + string="Converter", + default="provider", + help=textwrap.dedent("""\ + Converter engine, which is used for the conversion: + - Local: Use a locally installed unoconv installation + - Service: Use a service to do the conversion + """)) + + converter_max_store = fields.Integer( + string="Storage Size", + help=textwrap.dedent("""\ + To certify the conversion, converted files can be saved + and loaded from memory if necessary. You can set a maximum + size of the storage to prevent massive memory requirements. + """)) + + converter_credit = fields.Boolean( + compute='_compute_converter_credit', + string="Converter insufficient credit") + + @api.multi + def set_values(self): + res = super(ResConfigSettings, self).set_values() + param = self.env['ir.config_parameter'].sudo() + param.set_param("muk_converter.service", self.converter_service) + param.set_param("muk_converter.max_store", self.converter_max_store) + return res + + @api.model + def get_values(self): + res = super(ResConfigSettings, self).get_values() + params = self.env['ir.config_parameter'].sudo() + res.update( + converter_service=params.get_param("muk_converter.service", default="provider"), + converter_max_store=int(params.get_param("muk_converter.max_store", default=20)) + ) + return res + + @api.multi + def _compute_converter_credit(self): + credits = self.env['iap.account'].get_credits('muk_converter') + for record in self: + record.converter_credit = credits <= 0 + @api.multi + def redirect_to_buy_converter_credit(self): + url = self.env['iap.account'].get_credits_url('muk_converter') + return { + 'type': 'ir.actions.act_url', + 'url': url, + 'target': '_new', + } diff --git a/muk_converter/models/store.py b/muk_converter/models/store.py index ebece78..1e994f0 100644 --- a/muk_converter/models/store.py +++ b/muk_converter/models/store.py @@ -29,13 +29,17 @@ class Store(models.Model): _name = 'muk_converter.store' _description = 'Converter Store' + + #---------------------------------------------------------- + # Database + #---------------------------------------------------------- name = fields.Char( compute="_compute_name", string="Name", store=True) - used_date = fields.Date( + used_date = fields.Datetime( string="Used on", default=fields.Datetime.now) @@ -55,6 +59,10 @@ class Store(models.Model): string="Data", required=True) + #---------------------------------------------------------- + # Read + #---------------------------------------------------------- + @api.depends('checksum', 'content_fname') def _compute_name(self): for record in self: diff --git a/muk_converter/service/__init__.py b/muk_converter/service/__init__.py index c91e4b8..32bd9ac 100644 --- a/muk_converter/service/__init__.py +++ b/muk_converter/service/__init__.py @@ -18,3 +18,4 @@ ################################################################################### from . import unoconv +from . import provider diff --git a/muk_converter/service/provider.py b/muk_converter/service/provider.py new file mode 100644 index 0000000..f0bf3d1 --- /dev/null +++ b/muk_converter/service/provider.py @@ -0,0 +1,89 @@ +################################################################################### +# +# Copyright (C) 2018 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import base64 +import logging + +from odoo.addons.iap import jsonrpc + +from odoo.addons.muk_utils.tools.cache import memoize +from odoo.addons.muk_utils.tools.file import guess_extension + +_logger = logging.getLogger(__name__) + +CONVERTER_DEFAULT_ENDPOINT = 'https://iap-converter.mukit.at' +CONVERTER_ENDPOINT_FORMATS = '/iap/converter/1/formats' +CONVERTER_ENDPOINT_IMPORTS = '/iap/converter/1/imports' +CONVERTER_ENDPOINT_CONVERT = '/iap/converter/1/convert' + +class RemoteConverter(object): + + def __init__(self, env): + self.params = env['ir.config_parameter'].sudo() + self.account = env['iap.account'].get('muk_converter') + + def endpoint(self, route): + return "%s%s" % (self.params.get_param('muk_converter.endpoint', CONVERTER_DEFAULT_ENDPOINT), route) + + def payload(self, params={}): + params.update({ + 'account_token': self.account.account_token, + 'database_uuid': self.params.get_param('database.uuid'), + }) + return params + + @property + @memoize(timeout=3600) + def formats(self): + return jsonrpc(self.endpoint(CONVERTER_ENDPOINT_FORMATS), params=self.payload()) + + @property + @memoize(timeout=3600) + def imports(self): + return jsonrpc(self.endpoint(CONVERTER_ENDPOINT_IMPORTS), params=self.payload()) + + def convert(self, binary, mimetype=None, filename=None, export="binary", doctype="document", format="pdf"): + """ Converts a binary value to the given format. + + :param binary: The binary value. + :param mimetype: The mimetype of the binary value. + :param filename: The filename of the binary value. + :param export: The output format (binary, file, base64). + :param doctype: Specify the document type (document, graphics, presentation, spreadsheet). + :param format: Specify the output format for the document. + :return: Returns the output depending on the given format. + :raises ValueError: The file extension could not be determined or the format is invalid. + """ + params = { + 'format': format, + 'doctype': doctype, + 'mimetype': mimetype, + 'filename': filename, + 'content': base64.b64encode(binary), + } + result = jsonrpc(self.endpoint(CONVERTER_ENDPOINT_CONVERT), params=self.payload(params)) + if export == 'base64': + return result + if export == 'file': + output = io.BytesIO() + output.write(base64.b64decode(result)) + output.close() + return output + else: + return base64.b64decode(result) \ No newline at end of file diff --git a/muk_converter/service/unoconv.py b/muk_converter/service/unoconv.py index d452f08..e367562 100644 --- a/muk_converter/service/unoconv.py +++ b/muk_converter/service/unoconv.py @@ -1,156 +1,147 @@ -################################################################################### -# -# Copyright (C) 2018 MuK IT GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -################################################################################### - -import os -import io -import base64 -import shutil -import urllib -import logging -import tempfile -import mimetypes - -from subprocess import Popen -from subprocess import PIPE -from subprocess import CalledProcessError - -from contextlib import closing - -from odoo.tools import config -from odoo.tools.mimetypes import guess_mimetype - -from odoo.addons.muk_utils.tools import utils_os - -_logger = logging.getLogger(__name__) - -FORMATS = [ - "bib", "bmp", "csv", "dbf", "dif", "doc", "doc6", "doc95", "docbook", "docx", "docx7", "emf", - "eps", "fodg", "fodp", "fods", "fodt", "gif", "html", "jpg", "latex", "mediawiki", "met", "odd", - "odg", "odp", "ods", "odt", "ooxml", "otg", "otp", "ots", "ott", "pbm", "pct", "pdb", "pdf", "pgm", - "png", "pot", "potm", "ppm", "pps", "ppt", "pptx", "psw", "pwp", "pxl", "ras", "rtf", "sda", "sdc", - "sdc3", "sdc4", "sdd", "sdd3", "sdd4", "sdw", "sdw3", "sdw4", "slk", "stc", "std", "sti", "stw", - "svg", "svm", "swf", "sxc", "sxd", "sxd3", "sxd5", "sxi", "sxw", "text", "tiff", "txt", "uop", "uos", - "uot", "vor", "vor3", "vor4", "vor5", "wmf", "wps", "xhtml", "xls", "xls5", "xls95", "xlsx", "xlt", - "xlt5", "xlt95", "xpm""bib", "bmp", "csv", "dbf", "dif", "doc", "doc6", "doc95", "docbook", "docx", - "docx7", "emf", "eps", "fodg", "fodp", "fods", "fodt", "gif", "html", "jpg", "latex", "mediawiki", - "met", "odd", "odg", "odp", "ods", "odt", "ooxml", "otg", "otp", "ots", "ott", "pbm", "pct", "pdb", - "pdf", "pgm", "png", "pot", "potm", "ppm", "pps", "ppt", "pptx", "psw", "pwp", "pxl", "ras", "rtf", - "sda", "sdc", "sdc3", "sdc4", "sdd", "sdd3", "sdd4", "sdw", "sdw3", "sdw4", "slk", "stc", "std", - "sti", "stw", "svg", "svm", "swf", "sxc", "sxd", "sxd3", "sxd5", "sxi", "sxw", "text", "tiff", - "txt", "uop", "uos", "uot", "vor", "vor3", "vor4", "vor5", "wmf", "wps", "xhtml", "xls", "xls5", - "xls95", "xlsx", "xlt", "xlt5", "xlt95", "xpm" -] - -IMPORTS = [ - "bmp", "csv", "dbf", "dif", "doc", "docx", "dot", "emf", "eps", "epub", "fodg", "fodp", "fods", - "fodt", "gif", "gnm", "gnumeric", "htm", "html", "jpeg", "jpg", "met", "mml", "odb", "odf", "odg", - "odp", "ods", "odt", "pbm", "pct", "pdb", "pdf", "pgm", "png", "pot", "ppm", "pps", "ppt", "pptx", - "psw", "pxl", "ras", "rtf", "sda", "sdc", "sdd", "sdp", "sdw", "sgl", "slk", "stc", "std", "sti", - "stw", "svg", "svm", "swf", "sxc", "sxd", "sxi", "sxm", "sxw", "tif", "tiff", "txt", "uof", "uop", - "uos", "uot", "vor", "wmf", "wri", "xls", "xlsx", "xlt", "xlw", "xml", "xpm""bmp", "csv", "dbf", - "dif", "doc", "docx", "dot", "emf", "eps", "epub", "fodg", "fodp", "fods", "fodt", "gif", "gnm", - "gnumeric", "htm", "html", "jpeg", "jpg", "met", "mml", "odb", "odf", "odg", "odp", "ods", "odt", - "pbm", "pct", "pdb", "pdf", "pgm", "png", "pot", "ppm", "pps", "ppt", "pptx", "psw", "pxl", "ras", - "rtf", "sda", "sdc", "sdd", "sdp", "sdw", "sgl", "slk", "stc", "std", "sti", "stw", "svg", "svm", - "swf", "sxc", "sxd", "sxi", "sxm", "sxw", "tif", "tiff", "text", "uof", "uop", "uos", "uot", "vor", - "wmf", "wri", "xls", "xlsx", "xlt", "xlw", "xml", "xpm" -] - -def formats(): - return FORMATS - -def imports(): - return IMPORTS - -def unoconv_environ(): - env = os.environ.copy() - uno_path = config.get('uno_path', False) - if uno_path: - env['UNO_PATH'] = config['uno_path'] - return env - -def convert(input_path, output_path, doctype="document", format="pdf"): - """ - Convert a file to the given format. - - :param input_path: The path of the file to convert. - :param output_path: The path of the output where the converted file is to be saved. - :param doctype: Specify the document type (document, graphics, presentation, spreadsheet). - :param format: Specify the output format for the document. - :raises CalledProcessError: The command returned non-zero exit status 1. - :raises OSError: This exception is raised when a system function returns a system-related error. - """ - try: - env = unoconv_environ() - shell = True if os.name in ('nt', 'os2') else False - args = ['unoconv', '--format=%s' % format, '--output=%s' % output_path, input_path] - process = Popen(args, stdout=PIPE, env=env, shell=shell) - outs, errs = process.communicate() - return_code = process.wait() - if return_code: - raise CalledProcessError(return_code, args, outs, errs) - except CalledProcessError: - _logger.exception("Error while running unoconv.") - raise - except OSError: - _logger.exception("Error while running unoconv.") - raise - -def convert_binary(binary, mimetype=None, filename=None, export="binary", doctype="document", format="pdf"): - """ - Converts a binary value to the given format. - - :param binary: The binary value. - :param mimetype: The mimetype of the binary value. - :param filename: The filename of the binary value. - :param export: The output format (binary, file, base64). - :param doctype: Specify the document type (document, graphics, presentation, spreadsheet). - :param format: Specify the output format for the document. - :return: Returns the output depending on the given format. - :raises ValueError: The file extension could not be determined or the format is invalid. - """ - extension = utils_os.get_extension(binary, filename, mimetype) - if not extension: - raise ValueError("The file extension could not be determined.") - if format not in FORMATS: - raise ValueError("Invalid export format.") - if extension not in IMPORTS: - raise ValueError("Invalid import format.") - tmp_dir = tempfile.mkdtemp() - try: - tmp_wpath = os.path.join(tmp_dir, "tmpfile." + extension) - tmp_ppath = os.path.join(tmp_dir, "tmpfile." + format) - if os.name == 'nt': - tmp_wpath = tmp_wpath.replace("\\", "/") - tmp_ppath = tmp_ppath.replace("\\", "/") - with closing(open(tmp_wpath, 'wb')) as file: - file.write(binary) - convert(tmp_wpath, tmp_ppath, doctype, format) - with closing(open(tmp_ppath, 'rb')) as file: - if export == 'file': - output = io.BytesIO() - output.write(file.read()) - output.close() - return output - elif export == 'base64': - return base64.b64encode(file.read()) - else: - return file.read() - finally: - shutil.rmtree(tmp_dir) \ No newline at end of file +################################################################################### +# +# Copyright (C) 2018 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import os +import io +import base64 +import shutil +import urllib +import logging +import tempfile +import mimetypes + +from subprocess import Popen +from subprocess import PIPE +from subprocess import CalledProcessError + +from contextlib import closing + +from odoo.tools import config +from odoo.tools.mimetypes import guess_mimetype + +from odoo.addons.muk_utils.tools.file import guess_extension + +_logger = logging.getLogger(__name__) + +UNOCONV_FORMATS = [ + "bib", "bmp", "csv", "dbf", "dif", "doc", "doc6", "doc95", "docbook", "docx", "docx7", "emf", + "eps", "fodg", "fodp", "fods", "fodt", "gif", "html", "jpg", "latex", "mediawiki", "met", "odd", + "odg", "odp", "ods", "odt", "ooxml", "otg", "otp", "ots", "ott", "pbm", "pct", "pdb", "pdf", "pgm", + "png", "pot", "potm", "ppm", "pps", "ppt", "pptx", "psw", "pwp", "pxl", "ras", "rtf", "sda", "sdc", + "sdc3", "sdc4", "sdd", "sdd3", "sdd4", "sdw", "sdw3", "sdw4", "slk", "stc", "std", "sti", "stw", + "svg", "svm", "swf", "sxc", "sxd", "sxd3", "sxd5", "sxi", "sxw", "text", "tiff", "txt", "uop", "uos", + "uot", "vor", "vor3", "vor4", "vor5", "wmf", "wps", "xhtml", "xls", "xls5", "xls95", "xlsx", "xlt", + "xlt5", "xlt95", "xpm""bib", "bmp", "csv", "dbf", "dif", "doc", "doc6", "doc95", "docbook", "docx", + "docx7", "emf", "eps", "fodg", "fodp", "fods", "fodt", "gif", "html", "jpg", "latex", "mediawiki", + "met", "odd", "odg", "odp", "ods", "odt", "ooxml", "otg", "otp", "ots", "ott", "pbm", "pct", "pdb", + "pdf", "pgm", "png", "pot", "potm", "ppm", "pps", "ppt", "pptx", "psw", "pwp", "pxl", "ras", "rtf", + "sda", "sdc", "sdc3", "sdc4", "sdd", "sdd3", "sdd4", "sdw", "sdw3", "sdw4", "slk", "stc", "std", + "sti", "stw", "svg", "svm", "swf", "sxc", "sxd", "sxd3", "sxd5", "sxi", "sxw", "text", "tiff", + "txt", "uop", "uos", "uot", "vor", "vor3", "vor4", "vor5", "wmf", "wps", "xhtml", "xls", "xls5", + "xls95", "xlsx", "xlt", "xlt5", "xlt95", "xpm" +] + +UNOCONV_IMPORTS = [ + "bmp", "csv", "dbf", "dif", "doc", "docx", "dot", "emf", "eps", "epub", "fodg", "fodp", "fods", + "fodt", "gif", "gnm", "gnumeric", "htm", "html", "jpeg", "jpg", "met", "mml", "odb", "odf", "odg", + "odp", "ods", "odt", "pbm", "pct", "pdb", "pdf", "pgm", "png", "pot", "ppm", "pps", "ppt", "pptx", + "psw", "pxl", "ras", "rtf", "sda", "sdc", "sdd", "sdp", "sdw", "sgl", "slk", "stc", "std", "sti", + "stw", "svg", "svm", "swf", "sxc", "sxd", "sxi", "sxm", "sxw", "tif", "tiff", "txt", "uof", "uop", + "uos", "uot", "vor", "wmf", "wri", "xls", "xlsx", "xlt", "xlw", "xml", "xpm""bmp", "csv", "dbf", + "dif", "doc", "docx", "dot", "emf", "eps", "epub", "fodg", "fodp", "fods", "fodt", "gif", "gnm", + "gnumeric", "htm", "html", "jpeg", "jpg", "met", "mml", "odb", "odf", "odg", "odp", "ods", "odt", + "pbm", "pct", "pdb", "pdf", "pgm", "png", "pot", "ppm", "pps", "ppt", "pptx", "psw", "pxl", "ras", + "rtf", "sda", "sdc", "sdd", "sdp", "sdw", "sgl", "slk", "stc", "std", "sti", "stw", "svg", "svm", + "swf", "sxc", "sxd", "sxi", "sxm", "sxw", "tif", "tiff", "text", "uof", "uop", "uos", "uot", "vor", + "wmf", "wri", "xls", "xlsx", "xlt", "xlw", "xml", "xpm" +] + +class UnoconvConverter(object): + + @property + def formats(self): + return UNOCONV_FORMATS + + @property + def imports(self): + return UNOCONV_IMPORTS + + def environ(self): + env = os.environ.copy() + uno_path = config.get('uno_path', False) + if uno_path: + env['UNO_PATH'] = config['uno_path'] + return env + + def convert(self, binary, mimetype=None, filename=None, export="binary", doctype="document", format="pdf"): + """ Converts a binary value to the given format. + + :param binary: The binary value. + :param mimetype: The mimetype of the binary value. + :param filename: The filename of the binary value. + :param export: The output format (binary, file, base64). + :param doctype: Specify the document type (document, graphics, presentation, spreadsheet). + :param format: Specify the output format for the document. + :return: Returns the output depending on the given format. + :raises ValueError: The file extension could not be determined or the format is invalid. + """ + extension = guess_extension(filename=filename, mimetype=mimetype, binary=binary) + if not extension: + raise ValueError("The file extension could not be determined.") + if format not in self.formats: + raise ValueError("Invalid export format.") + if extension not in self.imports: + raise ValueError("Invalid import format.") + tmp_dir = tempfile.mkdtemp() + try: + tmp_wpath = os.path.join(tmp_dir, "tmpfile." + extension) + tmp_ppath = os.path.join(tmp_dir, "tmpfile." + format) + if os.name == 'nt': + tmp_wpath = tmp_wpath.replace("\\", "/") + tmp_ppath = tmp_ppath.replace("\\", "/") + with closing(open(tmp_wpath, 'wb')) as file: + file.write(binary) + shell = True if os.name in ('nt', 'os2') else False + args = ['unoconv', '--format=%s' % format, '--output=%s' % tmp_ppath, tmp_wpath] + process = Popen(args, stdout=PIPE, env=self.environ(), shell=shell) + outs, errs = process.communicate() + return_code = process.wait() + if return_code: + raise CalledProcessError(return_code, args, outs, errs) + with closing(open(tmp_ppath, 'rb')) as file: + if export == 'file': + output = io.BytesIO() + output.write(file.read()) + output.close() + return output + elif export == 'base64': + return base64.b64encode(file.read()) + else: + return file.read() + except CalledProcessError: + _logger.exception("Error while running unoconv.") + raise + except OSError: + _logger.exception("Error while running unoconv.") + raise + finally: + shutil.rmtree(tmp_dir) + +unoconv = UnoconvConverter() + diff --git a/muk_converter/static/description/icon.png b/muk_converter/static/description/icon.png index 51e2549..1b124a9 100644 Binary files a/muk_converter/static/description/icon.png and b/muk_converter/static/description/icon.png differ diff --git a/muk_converter/static/description/icon.svg b/muk_converter/static/description/icon.svg new file mode 100644 index 0000000..c06d89d --- /dev/null +++ b/muk_converter/static/description/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/muk_converter/static/description/index.html b/muk_converter/static/description/index.html index b8b7d63..4ebe6fc 100644 --- a/muk_converter/static/description/index.html +++ b/muk_converter/static/description/index.html @@ -20,30 +20,29 @@ -
-

Demo

-
-
-
User:
-
-
-
apps
-
-
-
Password:
-
-
-
demo
-
-
+

Demo

+
+
+
User:
+
+
+
apps
+
+
+
Password:
+
+
+
demo
+
+
- Live Preview + style="position: relative; overflow: hidden;"> + Live Preview +
@@ -55,13 +54,13 @@ - + \ No newline at end of file diff --git a/muk_converter/tests/test_converter.py b/muk_converter/tests/test_converter.py index f1fd914..4c84bbf 100644 --- a/muk_converter/tests/test_converter.py +++ b/muk_converter/tests/test_converter.py @@ -18,13 +18,12 @@ ################################################################################### import os +import base64 import logging import unittest from odoo.tests import common -from odoo.addons.muk_converter.tools import converter - _path = os.path.dirname(os.path.dirname(__file__)) _logger = logging.getLogger(__name__) @@ -32,24 +31,41 @@ class ConverterTestCase(common.TransactionCase): def setUp(self): super(ConverterTestCase, self).setUp() + self.params = self.env['ir.config_parameter'] + self.store = self.env['muk_converter.store'].sudo() + self.converter = self.env['muk_converter.converter'] + self.store_count = self.store.search([], count=True) + self.params.set_param('muk_converter.service', 'unoconv') def tearDown(self): super(ConverterTestCase, self).tearDown() def test_formats(self): - self.assertTrue(converter.formats()) + self.assertTrue(self.converter.formats()) + + def test_imports(self): + self.assertTrue(self.converter.imports()) @unittest.skipIf(os.environ.get('TRAVIS', False), "Skipped for Travis CI") - def test_convert(self): + def test_convert_basic(self): + with open(os.path.join(_path, 'tests/data', 'sample.png'), 'rb') as file: + self.assertTrue(self.converter.convert('sample.png', base64.b64encode(file.read()))) + + @unittest.skipIf(os.environ.get('TRAVIS', False), "Skipped for Travis CI") + def test_convert_format(self): with open(os.path.join(_path, 'tests/data', 'sample.png'), 'rb') as file: - self.assertTrue(converter.convert('sample.png', file.read(), "pdf")) + self.assertTrue(self.converter.convert('sample.png', base64.b64encode(file.read()), format="html")) @unittest.skipIf(os.environ.get('TRAVIS', False), "Skipped for Travis CI") - def test_convert2pdf(self): + def test_convert_stored(self): with open(os.path.join(_path, 'tests/data', 'sample.png'), 'rb') as file: - self.assertTrue(converter.convert2pdf('sample.png', file.read())) + self.assertTrue(self.converter.convert('sample.png', base64.b64encode(file.read()))) + self.assertTrue(self.store.search([], count=True) >= self.store_count) + self.assertTrue(self.converter.convert('sample.png', base64.b64encode(file.read()))) @unittest.skipIf(os.environ.get('TRAVIS', False), "Skipped for Travis CI") - def test_convert2html(self): + def test_convert_recompute(self): with open(os.path.join(_path, 'tests/data', 'sample.png'), 'rb') as file: - self.assertTrue(converter.convert2html('sample.png', file.read())) \ No newline at end of file + self.assertTrue(self.converter.convert('sample.png', base64.b64encode(file.read()), recompute=True, store=False)) + self.assertTrue(self.store.search([], count=True) == self.store_count) + self.assertTrue(self.converter.convert('sample.png', base64.b64encode(file.read()))) \ No newline at end of file diff --git a/muk_converter/tests/test_unoconv.py b/muk_converter/tests/test_unoconv.py index 26e0ab1..9fa73b5 100644 --- a/muk_converter/tests/test_unoconv.py +++ b/muk_converter/tests/test_unoconv.py @@ -23,7 +23,7 @@ import unittest from odoo.tests import common -from odoo.addons.muk_converter.service import unoconv +from odoo.addons.muk_converter.service.unoconv import UnoconvConverter _path = os.path.dirname(os.path.dirname(__file__)) _logger = logging.getLogger(__name__) @@ -32,12 +32,13 @@ class UnoconvTestCase(common.TransactionCase): def setUp(self): super(UnoconvTestCase, self).setUp() + self.unoconv = UnoconvConverter() def tearDown(self): super(UnoconvTestCase, self).tearDown() @unittest.skipIf(os.environ.get('TRAVIS', False), "Skipped for Travis CI") - def test_convert_binary(self): + def test_convert(self): with open(os.path.join(_path, 'tests/data', 'sample.png'), 'rb') as file: - self.assertTrue(unoconv.convert_binary(file.read())) + self.assertTrue(self.unoconv.convert(file.read(), filename='sample.png')) \ No newline at end of file diff --git a/muk_converter/tools/converter.py b/muk_converter/tools/converter.py index eed7da3..aa64f4e 100644 --- a/muk_converter/tools/converter.py +++ b/muk_converter/tools/converter.py @@ -1,45 +1,45 @@ -################################################################################### -# -# Copyright (C) 2018 MuK IT GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -################################################################################### - -import logging - -from odoo import tools - -from odoo.addons.muk_converter.service import unoconv - -_logger = logging.getLogger(__name__) - -def formats(): - return unoconv.formats() - -def selection_formats(): - return list(map(lambda format: (format, format.upper()), unoconv.formats())) - -def imports(): - return unoconv.imports() - -def convert(filename, content, format): - return unoconv.convert_binary(binary=content, filename=filename, format=format) - -def convert2pdf(filename, content): - return unoconv.convert_binary(binary=content, filename=filename) - -def convert2html(filename, content): - output = unoconv.convert_binary(binary=content, filename=filename, format="html") +################################################################################### +# +# Copyright (C) 2018 MuK IT GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +################################################################################### + +import logging + +from odoo import tools + +from odoo.addons.muk_converter.service.unoconv import unoconv + +_logger = logging.getLogger(__name__) + +def formats(): + return unoconv.formats + +def selection_formats(): + return list(map(lambda format: (format, format.upper()), unoconv.formats) + +def imports(): + return unoconv.imports + +def convert(filename, content, format): + return unoconv.convert(content, filename=filename, format=format) + +def convert2pdf(filename, content): + return unoconv.convert(content, filename=filename, format="pdf") + +def convert2html(filename, content): + output = unoconv.convert(content, filename=filename, format="html") return tools.html_sanitize(output) \ No newline at end of file diff --git a/muk_converter/views/convert.xml b/muk_converter/views/convert.xml index 918b1c4..4b8c2e4 100644 --- a/muk_converter/views/convert.xml +++ b/muk_converter/views/convert.xml @@ -27,12 +27,8 @@ - - - + diff --git a/muk_converter/views/res_config_settings_view.xml b/muk_converter/views/res_config_settings_view.xml new file mode 100644 index 0000000..902bc28 --- /dev/null +++ b/muk_converter/views/res_config_settings_view.xml @@ -0,0 +1,64 @@ + + + + + + + + res.config.settings.view.form + res.config.settings + + +
+

File Converter

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/muk_converter/wizards/convert.py b/muk_converter/wizards/convert.py index 209524f..d766a8f 100644 --- a/muk_converter/wizards/convert.py +++ b/muk_converter/wizards/convert.py @@ -25,8 +25,8 @@ import mimetypes from odoo import _, api, fields, models -from odoo.addons.muk_utils.tools.http import get_response -from odoo.addons.muk_converter.tools import converter +# from odoo.addons.muk_utils.tools.http import get_response TODO +#from odoo.addons.muk_converter.tools import converter _logger = logging.getLogger(__name__) @@ -34,35 +34,40 @@ class ConverterWizard(models.TransientModel): _name = "muk_converter.convert" + #---------------------------------------------------------- + # Selections + #---------------------------------------------------------- + + def _format_selection(self): + formats = self.env['muk_converter.converter'].formats() + return list(map(lambda format: (format, format.upper()), formats)) + + #---------------------------------------------------------- + # Database + #---------------------------------------------------------- + state = fields.Selection( - selection=[("export", "Export"), ("download", "Download")], + selection=[ + ("export", "Export"), + ("download", "Download")], string="State", required=True, default="export") - - type = fields.Selection( - selection=[("url", "URL"), ("binary", "File")], - string="Type", - default="binary", - change_default=True, - states={'export': [('required', True)]}, - help="Either a binary file or an url can be converted") - - input_url = fields.Char( - string="Url") input_name = fields.Char( - string="Filename") + string="Filename", + states={'export': [('required', True)]}) input_binary = fields.Binary( - string="File") + string="File", + states={'export': [('required', True)]}) format = fields.Selection( - selection=converter.selection_formats(), + selection=_format_selection, string="Format", default="pdf", states={'export': [('required', True)]}) - + output_name = fields.Char( string="Filename", readonly=True, @@ -73,37 +78,26 @@ class ConverterWizard(models.TransientModel): readonly=True, states={'download': [('required', True)]}) + #---------------------------------------------------------- + # Functions + #---------------------------------------------------------- + @api.multi def convert(self): - def export(record, content, filename): - name = "%s.%s" % (os.path.splitext(filename)[0], record.format) - output = record.env['muk_converter.converter'].convert(filename, content) - record.write({ - 'state': 'download', - 'output_name': name, - 'output_binary': base64.b64encode(output)}) - return { - "name": _("Convert File"), - 'type': 'ir.actions.act_window', - 'res_model': 'muk_converter.convert', - 'view_mode': 'form', - 'view_type': 'form', - 'res_id': record.id, - 'views': [(False, 'form')], - 'target': 'new', - } - record = self[0] - if record.input_url: - status, headers, content = get_response(record.input_url) - if status != 200: - raise ValueError("Failed to retrieve the file from the url.") - else: - extension = mimetypes.guess_extension(headers['content-type'])[1:].strip().lower() - if extension not in converter.imports(): - raise ValueError("Invalid import format.") - else: - return export(record, content, record.input_name or "%s.%s" % (uuid.uuid4(), extension)) - elif record.input_name and record.input_binary: - return export(record, base64.b64decode(record.input_binary), record.input_name) - else: - raise ValueError("The conversion requires either a valid url or a filename and a file.") \ No newline at end of file + self.ensure_one() + name = "%s.%s" % (os.path.splitext(self.input_name)[0], self.format) + output = self.env['muk_converter.converter'].convert(self.input_name, self.input_binary) + self.write({ + 'state': 'download', + 'output_name': name, + 'output_binary': output}) + return { + "name": _("Convert File"), + 'type': 'ir.actions.act_window', + 'res_model': 'muk_converter.convert', + 'view_mode': 'form', + 'view_type': 'form', + 'res_id': self.id, + 'views': [(False, 'form')], + 'target': 'new', + } \ No newline at end of file