Browse Source

publish muk_converter - 11.0

pull/16/head
MuK IT GmbH 5 years ago
parent
commit
298a385846
  1. 1
      muk_converter/__init__.py
  2. 12
      muk_converter/__manifest__.py
  3. 6
      muk_converter/data/params.xml
  4. 5
      muk_converter/doc/changelog.rst
  5. 7
      muk_converter/doc/index.rst
  6. 3
      muk_converter/models/__init__.py
  7. 82
      muk_converter/models/converter.py
  8. 83
      muk_converter/models/res_config_settings.py
  9. 10
      muk_converter/models/store.py
  10. 1
      muk_converter/service/__init__.py
  11. 89
      muk_converter/service/provider.py
  12. 303
      muk_converter/service/unoconv.py
  13. BIN
      muk_converter/static/description/icon.png
  14. 1
      muk_converter/static/description/icon.svg
  15. 51
      muk_converter/static/description/index.html
  16. 34
      muk_converter/tests/test_converter.py
  17. 7
      muk_converter/tests/test_unoconv.py
  18. 88
      muk_converter/tools/converter.py
  19. 6
      muk_converter/views/convert.xml
  20. 64
      muk_converter/views/res_config_settings_view.xml
  21. 94
      muk_converter/wizards/convert.py

1
muk_converter/__init__.py

@ -18,6 +18,5 @@
###################################################################################
from . import service
from . import tools
from . import models
from . import wizards

12
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 <mathias.markl@mukit.at>",
],
"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,

6
muk_converter/data/params.xml

@ -19,6 +19,12 @@
<odoo noupdate="1">
<record id="muk_converter_service" model="ir.config_parameter">
<field name="key">muk_converter.service</field>
<field name="value">unoconv</field>
<field name="group_ids" eval="[(4, ref('base.group_system'))]"/>
</record>
<record id="muk_converter_max_store" model="ir.config_parameter">
<field name="key">muk_converter.max_store</field>
<field name="value">20</field>

5
muk_converter/doc/changelog.rst

@ -1,3 +1,8 @@
`1.2.0`
-------
- Added In-App Purchases option
`1.1.0`
-------

7
muk_converter/doc/index.rst

@ -121,6 +121,13 @@ Contributors
* Mathias Markl <mathias.markl@mukit.at>
Images
------------
Some pictures are based on or inspired by the icon set of Font Awesome:
* `Font Awesome <https://fontawesome.com>`_
Author & Maintainer
-------------------

3
muk_converter/models/__init__.py

@ -17,5 +17,6 @@
#
###################################################################################
from . import store
from . import converter
from . import store
from . import res_config_settings

82
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
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})

83
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 <http://www.gnu.org/licenses/>.
#
###################################################################################
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',
}

10
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:

1
muk_converter/service/__init__.py

@ -18,3 +18,4 @@
###################################################################################
from . import unoconv
from . import provider

89
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 <http://www.gnu.org/licenses/>.
#
###################################################################################
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)

303
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 <http://www.gnu.org/licenses/>.
#
###################################################################################
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)
###################################################################################
#
# 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 <http://www.gnu.org/licenses/>.
#
###################################################################################
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()

BIN
muk_converter/static/description/icon.png

Before

Width: 250  |  Height: 250  |  Size: 19 KiB

After

Width: 250  |  Height: 250  |  Size: 20 KiB

1
muk_converter/static/description/icon.svg
File diff suppressed because it is too large
View File

51
muk_converter/static/description/index.html

@ -20,30 +20,29 @@
</div>
</section>
<section class="oe_container oe_dark"
<section class="oe_container oe_dark"
style="margin-bottom: 20px; border-top: 5px solid #797979; border-bottom: 5px solid #797979;">
<h3 class="oe_slogan" style="margin-bottom: 10px;">Demo</h3>
<div class="row" style="margin: auto; max-width: 200px;">
<div class="col-xs-6">
<h5 class="oe_slogan" style="font-size: 20px; margin: 2px;">User:</h5>
</div>
<div class="col-xs-6">
<h5 class="oe_slogan" style="font-size: 20px; margin: 2px;">apps</h5>
</div>
<div class="col-xs-6">
<h5 class="oe_slogan" style="font-size: 20px; margin: 2px;">Password:</h5>
</div>
<div class="col-xs-6">
<h5 class="oe_slogan" style="font-size: 20px; margin: 2px;">demo</h5>
</div>
</div>
<h3 class="oe_slogan" style="margin-bottom: 10px;">Demo</h3>
<div class="row" style="margin: auto; max-width: 250px;">
<div class="col-6 col-xs-6">
<h5 class="oe_slogan" style="font-size: 20px; margin: 2px;">User:</h5>
</div>
<div class="col-6 col-xs-6">
<h5 class="oe_slogan" style="font-size: 20px; margin: 2px;">apps</h5>
</div>
<div class="col-6 col-xs-6">
<h5 class="oe_slogan" style="font-size: 20px; margin: 2px;">Password:</h5>
</div>
<div class="col-6 col-xs-6">
<h5 class="oe_slogan" style="font-size: 20px; margin: 2px;">demo</h5>
</div>
</div>
<div class="oe_slogan" style="margin-top: 5px;">
<a class="btn btn-primary btn-lg mt8"
href="https://demo.mukit.at/web/login"
style="position: relative; overflow: hidden;"><span
class="o_ripple"
style="height: 138px; width: 138px; top: -35.2969px; left: -8.17188px;"></span>
<i class="fa fa-video-camera"></i> Live Preview </a>
style="position: relative; overflow: hidden;">
<i class="fa fa-video-camera"></i> Live Preview
</a>
</div>
</section>
@ -55,13 +54,13 @@
<div class="oe_slogan">
<a class="btn btn-primary btn-lg mt8" href="mailto:sale@mukit.at">
<i class="fa fa-envelope"></i> Email
</a> <a class="btn btn-primary btn-lg mt8"
href="https://mukit.at/page/contactus"> <i class="fa fa-phone"></i>
Contact
</a> <a class="btn btn-primary btn-lg mt8" href="mailto:support@mukit.at">
</a>
<a class="btn btn-primary btn-lg mt8" href="https://mukit.at/page/contactus">
<i class="fa fa-phone"></i> Contact
</a>
<a class="btn btn-primary btn-lg mt8" href="mailto:support@mukit.at">
<i class="fa fa-life-ring"></i> Support
</a>
</div>
<img src="logo.png" style="width: 200px; margin-bottom: 20px;"
class="center-block">
<img src="logo.png" style="width: 200px; margin-bottom: 20px; display: block;" class="mx-auto center-block">
</section>

34
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()))
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())))

7
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'))

88
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 <http://www.gnu.org/licenses/>.
#
###################################################################################
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 <http://www.gnu.org/licenses/>.
#
###################################################################################
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)

6
muk_converter/views/convert.xml

@ -27,12 +27,8 @@
<group states="export">
<field invisible="1" name="state" />
<group>
<field name="type" />
<field name="input_url" widget="url"
attrs="{'required':[('type','=','url')], 'invisible':[('type','=','binary')]}" />
<field name="input_name" invisible="1" />
<field name="input_binary" filename="input_name"
attrs="{'required':[('type','=','binary')], 'invisible':[('type','=','url')]}" />
<field name="input_binary" filename="input_name" />
</group>
<group>
<field name="format" />

64
muk_converter/views/res_config_settings_view.xml

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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 <http://www.gnu.org/licenses/>.
-->
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div name="multi_company" position="after">
<h2>File Converter</h2>
<div class="row mt16 o_settings_container" name="web_client">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane"></div>
<div class="o_setting_right_pane">
<label for="converter_service"/>
<div class="text-muted">
Converter engine, which is used for the conversion
</div>
<div class="mt8">
<field name="converter_service" class="o_light_label" widget="radio" required="True"/>
</div>
<div class="content-group" attrs="{'invisible': [('converter_service','!=','provider')]}">
<div id="partner_autocomplete_settings" position="inside">
<widget name="iap_credit_checker" service_name="muk_converter"/>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane"></div>
<div class="o_setting_right_pane">
<label for="converter_max_store"/>
<div class="text-muted">
Maximum storage size of the converter store
</div>
<div class="mt8">
<field name="converter_max_store" class="o_light_label" />
</div>
</div>
</div>
</div>
</div>
</field>
</record>
</odoo>

94
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.")
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',
}
Loading…
Cancel
Save