From 8bc11444a2ee191c1f93f6854c8e25b84c838517 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 25 Feb 2021 14:18:42 +0100 Subject: [PATCH 01/60] cmis document widget --- cmis_field/fields/__init__.py | 1 + cmis_field/fields/cmis_document.py | 33 ++++++++++++++++++++++++++++ cmis_field/models/ir_model_fields.py | 4 ++-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 cmis_field/fields/cmis_document.py diff --git a/cmis_field/fields/__init__.py b/cmis_field/fields/__init__.py index d93e7350..2890bf66 100644 --- a/cmis_field/fields/__init__.py +++ b/cmis_field/fields/__init__.py @@ -1 +1,2 @@ from .cmis_folder import CmisFolder +from .cmis_document import CmisDocument diff --git a/cmis_field/fields/cmis_document.py b/cmis_field/fields/cmis_document.py new file mode 100644 index 00000000..54fff21f --- /dev/null +++ b/cmis_field/fields/cmis_document.py @@ -0,0 +1,33 @@ +# Copyright 2020 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, _ +from odoo.tools.sql import pg_varchar + + +class CmisDocument(fields.Field): + type = 'cmis_document' + column_type = ('varchar', pg_varchar()) + _slots = { + 'backend_name': None, + 'copy': False, # noderef are not copied by default + } + + def __init__(self, backend_name=None, string=None, **kwargs): + super(CmisDocument, self).__init__( + backend_name=backend_name, string=string, **kwargs) + + def get_backend(self, env, raise_if_not_found=True): + return env['cmis.backend'].get_by_name( + self.backend_name, raise_if_not_found) + + def _description_backend(self, env): + backend = self.get_backend(env, raise_if_not_found=False) + if not backend: + if self.backend_name: + msg = (_('Backend named %s not found. ' + 'Please check your configuration.') % + self.backend_name) + else: + msg = _('No backend found. Please check your configuration.') + return {'backend_error': msg} + return backend.get_web_description()[backend.id] diff --git a/cmis_field/models/ir_model_fields.py b/cmis_field/models/ir_model_fields.py index e68f25e1..0e928165 100644 --- a/cmis_field/models/ir_model_fields.py +++ b/cmis_field/models/ir_model_fields.py @@ -9,6 +9,6 @@ class IrModelFields(models.Model): _inherit = "ir.model.fields" ttype = fields.Selection( - selection_add=[("cmis_folder", "CMIS Folder")], - ondelete={"cmis_folder": "cascade"}, + selection_add=[('cmis_folder', 'CMIS Folder'), ('cmis_document', 'CMIS Document')], + ondelete = {"cmis_folder": "cascade"}, ) From bc940534d5e7daac4e1a42538dea3f2d9aa28e82 Mon Sep 17 00:00:00 2001 From: Pierre Halleux Date: Fri, 8 Apr 2022 11:42:23 +0200 Subject: [PATCH 02/60] cmis_field: add method to get cmis object to cmis_document field --- cmis_field/fields/cmis_document.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cmis_field/fields/cmis_document.py b/cmis_field/fields/cmis_document.py index 54fff21f..8409ceab 100644 --- a/cmis_field/fields/cmis_document.py +++ b/cmis_field/fields/cmis_document.py @@ -31,3 +31,17 @@ def _description_backend(self, env): msg = _('No backend found. Please check your configuration.') return {'backend_error': msg} return backend.get_web_description()[backend.id] + + def get_cmis_object(self, record): + """Returns an instance of + :class:`cmislib.browser.binding.BrowserDocument` + This instance is a proxy object that can be used to perform action on + the document into the cmis container + :param record: + """ + val = self.__get__(record, record) + if not val: + return None + backend = self.get_backend(record.env) + repo = backend.get_cmis_repository() + return repo.getObject(val) From ce948b1029d7e1a1db28b24d60319a959dd142a5 Mon Sep 17 00:00:00 2001 From: Pierre Halleux Date: Mon, 30 May 2022 14:10:56 +0200 Subject: [PATCH 03/60] cmis_web: allow adding document when no value When there is no value on the cmis_document field, allows the user to add a document to the GED and sets that document's objectId as value to the cmis_document field. --- cmis_field/controllers/main.py | 10 +++ cmis_field/fields/cmis_document.py | 138 ++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/cmis_field/controllers/main.py b/cmis_field/controllers/main.py index 528c3942..e4b1e73b 100644 --- a/cmis_field/controllers/main.py +++ b/cmis_field/controllers/main.py @@ -12,3 +12,13 @@ def create_field_value(self, model_name, res_id, field_name): model_inst._fields[field_name].create_value(model_inst) value = getattr(model_inst, field_name) return {"value": value} + + @http.route('/web/cmis/field/create_document_value', type='json', methods=['POST'], + auth="user") + def create_document_field_value(self, model_name, res_id, field_name, documents): + if documents: + model_inst = http.request.env[model_name].browse(int(res_id)) + model_inst._fields[field_name].create_value(model_inst, {res_id: documents[0]}) + value = getattr(model_inst, field_name) + return {'value': value} + return {'value': None} diff --git a/cmis_field/fields/cmis_document.py b/cmis_field/fields/cmis_document.py index 8409ceab..e5ce4256 100644 --- a/cmis_field/fields/cmis_document.py +++ b/cmis_field/fields/cmis_document.py @@ -1,7 +1,15 @@ # Copyright 2020 ACSONE SA/NV () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import fields, _ + +import base64 +from io import BytesIO +import threading +import time +from functools import partial +from odoo import api, fields, registry, SUPERUSER_ID, _ +from odoo.exceptions import UserError from odoo.tools.sql import pg_varchar +from cmislib.exceptions import ObjectNotFoundException class CmisDocument(fields.Field): @@ -10,6 +18,9 @@ class CmisDocument(fields.Field): _slots = { 'backend_name': None, 'copy': False, # noderef are not copied by default + 'create_parent_get': None, + 'create_properties_get': None, + 'create_method': None } def __init__(self, backend_name=None, string=None, **kwargs): @@ -19,7 +30,7 @@ def __init__(self, backend_name=None, string=None, **kwargs): def get_backend(self, env, raise_if_not_found=True): return env['cmis.backend'].get_by_name( self.backend_name, raise_if_not_found) - + def _description_backend(self, env): backend = self.get_backend(env, raise_if_not_found=False) if not backend: @@ -45,3 +56,126 @@ def get_cmis_object(self, record): backend = self.get_backend(record.env) repo = backend.get_cmis_repository() return repo.getObject(val) + + def create_value(self, records, documents): + """Create a new folder for each record into the cmis container and + store the value as field value + """ + for record in records: + self._check_null(record) + self._create_value(records, documents) + + def _create_value(self, records, documents): + backend = self.get_backend(records.env) + if self.create_method: + fct = self.create_method + if not callable(fct): + fct = getattr(records, fct) + fct(self, backend) + return + self._create_in_cmis(records, backend, documents) + + def _create_in_cmis(self, records, backend, documents): + parents = self.get_create_parents(records, backend) + properties = self.get_create_properties(records, backend) + for record in records: + document = documents[record.id] + name = document.get("name") + backend.is_valid_cmis_name(name, raise_if_invalid=True) + parent = parents[record.id] + props = properties[record.id] or {} + content = self._decode_file(document) + value = parent.createDocument( + name=name, + properties=props, + contentFile=content, + contentType=document.get("mimetype"), + ) + + def clean_up_document(cmis_object_id, backend_id, dbname): + db_registry = registry(dbname) + with api.Environment.manage(), db_registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + backend = env["cmis.backend"].browse(backend_id) + _repo = backend.get_cmis_repository() + # The rollback is delayed by an arbitrary length of time to give + # the GED time to create the folder. If the folder is not properly + # created at the time the rollback executes, it cannot be deleted. + time.sleep(0.5) + try: + _repo.getObject(cmis_object_id).delete() + except ObjectNotFoundException: + pass + + # remove created resource in case of rollback + test_mode = getattr(threading.currentThread(), 'testing', False) + if not test_mode: + record.env.cr.after( + 'rollback', + partial( + clean_up_document, + value.getObjectId(), + backend.id, + record.env.cr.dbname + ) + ) + + self.__set__(record, value.getObjectId()) + + @staticmethod + def _decode_file(document): + file = document.get("data") + _, content = file.split(",") + return BytesIO(base64.b64decode(content)) + + def get_create_parents(self, records, backend): + """return the cmis:objectId of the cmis folder to use as parent of the + new folder. + :rtype: dict + :return: a dictionay with an entry for each record with the following + structure :: + + {record.id: 'cmis:objectId'} + + """ + if self.create_parent_get: + fct = self.create_parent_get + if not callable(fct): + fct = getattr(records, fct) + return fct(self, backend) + path_parts = self.get_default_parent_path_parts(records, backend) + parent_cmis_object = backend.get_folder_by_path_parts( + path_parts, create_if_not_found=True) + return dict.fromkeys(records.ids, parent_cmis_object) + + def get_default_parent_path_parts(self, records, backend): + """Return the default path parts into the cmis container to use as + parent on folder create. By default: + backend.initial_directory_write / record._name + """ + path_parts = backend.initial_directory_write.split('/') + path_parts.append(records[0]._name.replace('.', '_')) + return path_parts + + def get_create_properties(self, records, backend): + """Return the properties to use to created the folder into the CMIS + container. + :rtype: dict + :return: a dictionay with an entry for each record with the following + structure :: + + {record.id: {'cmis:xxx': 'val1', ...}} + + """ + if self.create_properties_get: + fct = self.create_properties_get + if not callable(fct): + fct = getattr(records, fct) + return fct(self, backend) + return dict.fromkeys(records.ids, None) + + def _check_null(self, record, raise_exception=True): + val = self.__get__(record, record) + if val and raise_exception: + raise UserError(_('A value is already assigned to %s') % self) + return val From 1442ee86ed6cb8c65e3edad6685387fdeacdb8d7 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 26 Mar 2021 09:59:08 +0100 Subject: [PATCH 04/60] document field: adapt fix to handle multiple backends see original PR: https://github.com/acsone/alfodoo/pull/154/files --- cmis_field/fields/cmis_document.py | 31 ++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/cmis_field/fields/cmis_document.py b/cmis_field/fields/cmis_document.py index e5ce4256..74628ef4 100644 --- a/cmis_field/fields/cmis_document.py +++ b/cmis_field/fields/cmis_document.py @@ -23,16 +23,39 @@ class CmisDocument(fields.Field): 'create_method': None } - def __init__(self, backend_name=None, string=None, **kwargs): - super(CmisDocument, self).__init__( - backend_name=backend_name, string=string, **kwargs) + def __init__(self, string=None, **kwargs): + super(CmisDocument, self).__init__(string=string, **kwargs) + + def _is_registry_loading_mode(self, env): + """ + Check if we are in the installation process. + """ + return env.context.get("install_mode") def get_backend(self, env, raise_if_not_found=True): return env['cmis.backend'].get_by_name( self.backend_name, raise_if_not_found) def _description_backend(self, env): - backend = self.get_backend(env, raise_if_not_found=False) + if self.inherited: + # In the case of a cmis field inherited from another module + # the attribute backend_name is not inherited so we have to + # get it on the original fiel + backend = self.inherited_field.get_backend(env, raise_if_not_found=False) + else: + backend = self.get_backend(env, raise_if_not_found=False) + if len(backend) > 1: + if self._is_registry_loading_mode(env): + # While the registry is loading, specific attributes are not available + # on the field (such as `backend_name`). At this stage, the fields + # are accessed to validate the xml views of the module being + # loaded/updated. We can therefore safely takes the first backend + # into the list. + backend = backend[:1] + else: + msg = (_('Too many backend found. ' + 'Please check your configuration.')) + return {'backend_error': msg} if not backend: if self.backend_name: msg = (_('Backend named %s not found. ' From 6f0f53afb190f9363b91f336623e314e705bf38b Mon Sep 17 00:00:00 2001 From: Pierre Halleux Date: Thu, 15 Sep 2022 17:21:43 +0200 Subject: [PATCH 05/60] cmis_field: remove file decoding from document creation --- cmis_field/fields/cmis_document.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/cmis_field/fields/cmis_document.py b/cmis_field/fields/cmis_document.py index 74628ef4..1ed77968 100644 --- a/cmis_field/fields/cmis_document.py +++ b/cmis_field/fields/cmis_document.py @@ -1,8 +1,6 @@ # Copyright 2020 ACSONE SA/NV () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import base64 -from io import BytesIO import threading import time from functools import partial @@ -81,8 +79,16 @@ def get_cmis_object(self, record): return repo.getObject(val) def create_value(self, records, documents): - """Create a new folder for each record into the cmis container and + """Create a new document for each record into the cmis container and store the value as field value + + :param records: An odoo recordset + :param documents: A mapping of files to their corresponding record as a dict of + dicts. The key of the parent dict should be the id of the corresponding record, + while each child dict should contain a 'name' key for the name of the file, + a 'mimetype' key for its mimetype and a 'data' key for its content (bytes). + :type documents: dict[:int, dict['name': str, 'mimetype': str, 'file': bytes]] + """ for record in records: self._check_null(record) @@ -107,11 +113,10 @@ def _create_in_cmis(self, records, backend, documents): backend.is_valid_cmis_name(name, raise_if_invalid=True) parent = parents[record.id] props = properties[record.id] or {} - content = self._decode_file(document) - value = parent.createDocument( - name=name, + object_id = parent.createDocument( + name, properties=props, - contentFile=content, + contentFile=document.get("data"), contentType=document.get("mimetype"), ) @@ -137,19 +142,13 @@ def clean_up_document(cmis_object_id, backend_id, dbname): 'rollback', partial( clean_up_document, - value.getObjectId(), + object_id, backend.id, record.env.cr.dbname ) ) - self.__set__(record, value.getObjectId()) - - @staticmethod - def _decode_file(document): - file = document.get("data") - _, content = file.split(",") - return BytesIO(base64.b64decode(content)) + self.__set__(record, object_id) def get_create_parents(self, records, backend): """return the cmis:objectId of the cmis folder to use as parent of the From 3b8484cabd1d8f87db6e7cc50f81ec8ad40a075a Mon Sep 17 00:00:00 2001 From: Pierre Halleux Date: Tue, 27 Sep 2022 17:13:49 +0200 Subject: [PATCH 06/60] cmis_field: properly decode file before adding new document --- cmis_field/controllers/main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmis_field/controllers/main.py b/cmis_field/controllers/main.py index e4b1e73b..14495c44 100644 --- a/cmis_field/controllers/main.py +++ b/cmis_field/controllers/main.py @@ -1,5 +1,8 @@ # Copyright 2016 ACSONE SA/NV () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +import base64 +from io import BytesIO + from odoo import http @@ -17,8 +20,15 @@ def create_field_value(self, model_name, res_id, field_name): auth="user") def create_document_field_value(self, model_name, res_id, field_name, documents): if documents: + self._decode_files(documents) model_inst = http.request.env[model_name].browse(int(res_id)) model_inst._fields[field_name].create_value(model_inst, {res_id: documents[0]}) value = getattr(model_inst, field_name) return {'value': value} return {'value': None} + + def _decode_files(self, documents): + for doc in documents: + file_ = doc.get("data") + _, content = file_.split(",") + doc["data"] = BytesIO(base64.b64decode(content)) From 3e836e8dbe34f6004eeabd320c21d04f267fb1ed Mon Sep 17 00:00:00 2001 From: Benjamin Willig Date: Wed, 13 Dec 2023 11:25:42 +0100 Subject: [PATCH 07/60] [MIG] field cmis_document: migration to 16.0 --- cmis_field/fields/__init__.py | 2 +- cmis_field/fields/cmis_document.py | 23 +++++++++++------------ cmis_field/models/ir_model_fields.py | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/cmis_field/fields/__init__.py b/cmis_field/fields/__init__.py index 2890bf66..55884b62 100644 --- a/cmis_field/fields/__init__.py +++ b/cmis_field/fields/__init__.py @@ -1,2 +1,2 @@ -from .cmis_folder import CmisFolder from .cmis_document import CmisDocument +from .cmis_folder import CmisFolder diff --git a/cmis_field/fields/cmis_document.py b/cmis_field/fields/cmis_document.py index 1ed77968..1555d10c 100644 --- a/cmis_field/fields/cmis_document.py +++ b/cmis_field/fields/cmis_document.py @@ -11,18 +11,17 @@ class CmisDocument(fields.Field): - type = 'cmis_document' - column_type = ('varchar', pg_varchar()) - _slots = { - 'backend_name': None, - 'copy': False, # noderef are not copied by default - 'create_parent_get': None, - 'create_properties_get': None, - 'create_method': None - } - - def __init__(self, string=None, **kwargs): - super(CmisDocument, self).__init__(string=string, **kwargs) + type = "cmis_document" + column_type = ("varchar", pg_varchar()) + backend_name = None + create_method = None + create_parent_get = None + create_properties_get = None + copy = False # noderef are not copied by default + + def __init__(self, **kwargs): + self.backend_name = kwargs.get("backend_name") + super().__init__(**kwargs) def _is_registry_loading_mode(self, env): """ diff --git a/cmis_field/models/ir_model_fields.py b/cmis_field/models/ir_model_fields.py index 0e928165..5503037f 100644 --- a/cmis_field/models/ir_model_fields.py +++ b/cmis_field/models/ir_model_fields.py @@ -10,5 +10,5 @@ class IrModelFields(models.Model): ttype = fields.Selection( selection_add=[('cmis_folder', 'CMIS Folder'), ('cmis_document', 'CMIS Document')], - ondelete = {"cmis_folder": "cascade"}, + ondelete = {"cmis_folder": "cascade", "cmis_document": "cascade"}, ) From dfcec84dc1c20c3ec8c94a7fac0a470d92312a74 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 26 Mar 2021 09:59:08 +0100 Subject: [PATCH 08/60] add a created column Add an "Created" column that allows ordering objects in a folder by `cmis:creationDate`. --- cmis_web/static/src/js/form_widgets.js | 1816 ++++++++++++++++++++++ cmis_web/static/src/xml/form_widgets.xml | 372 +++++ 2 files changed, 2188 insertions(+) create mode 100644 cmis_web/static/src/js/form_widgets.js create mode 100644 cmis_web/static/src/xml/form_widgets.xml diff --git a/cmis_web/static/src/js/form_widgets.js b/cmis_web/static/src/js/form_widgets.js new file mode 100644 index 00000000..5b69667e --- /dev/null +++ b/cmis_web/static/src/js/form_widgets.js @@ -0,0 +1,1816 @@ +/*--------------------------------------------------------- + + * Odoo cmis_web + + * Author Laurent Mignon 2016 Acsone SA/NV + + * License in __openerp__.py at root level of the module + + *--------------------------------------------------------- + +*/ + +odoo.define('cmis_web.form_widgets', function (require) { + "use strict"; + + + var core = require('web.core'); + var registry = require('web.field_registry'); + var basicFields = require('web.basic_fields'); + var time = require('web.time'); + var Dialog = require('web.Dialog'); + var framework = require('web.framework'); + var DocumentViewer = require('cmis_web.DocumentViewer') + var crash_manager = require('web.CrashManager'); + + var _t = core._t; + var QWeb = core.qweb; + + Dialog.include({ + check_validity: function () { + if (this.el.checkValidity()) { + return true; + } + else { + // Use pseudo HMLT5 submit to display validation errors + $('').hide().appendTo(this.$el).click().remove(); + } + }, + }); + + var CmisRenameContentDialog = Dialog.extend({ + template: 'CmisRenameContentDialog', + init: function (parent, cmisObject) { + var self = this; + var options = { + buttons: [ + { + text: _t("Rename"), + classes: "btn-primary", + click: function (e) { + e.stopPropagation(); + if (self.check_validity()) { + self.on_click_process(); + } + } + }, + { + text: _t("Cancel"), + click: function (e) { + e.stopPropagation(); + self.$el.parents('.modal').modal('hide'); + } + }, + + ], + close: function () { + self.close(); + } + }; + this._super(parent, options); + this.cmisObject = cmisObject; + this.cmisSession = parent.cmis_session; + }, + + start: function () { + this.$newName = this.$el.find('#new-name'); + this.$newName.val(this.cmisObject.name); + this.$newName.select(); + }, + on_click_process: function () { + var self = this; + var newName = this.$newName.val() + if (newName !== this.cmisObject.name && this.check_validity()) { + this.cmisSession + .updateProperties(this.cmisObject.objectId, {'cmis:name': newName}) + .ok(function (cmisObject) { + self.getParent().trigger('cmis_node_updated', [cmisObject]); + self.$el.parents('.modal').modal('hide'); + }); + } + } + }); + + var CmisDuplicateDocumentResolver = Dialog.extend({ + template: 'CmisDuplicateDocumentResolver', + init: function (parent, parent_cmisobject, file) { + var self = this; + var options = { + buttons: [ + { + text: _t("Process"), + classes: "btn-primary", + click: function (e) { + e.stopPropagation(); + if (self.check_validity()) { + self.on_click_process(); + } + } + }, + { + text: _t("Cancel"), + click: function (e) { + e.stopPropagation(); + self.$el.parents('.modal').modal('hide'); + } + }, + + ], + close: function () { + self.close(); + }, + title: file.name + _t(" already exists") + }; + this._super(parent, options); + this.parent_cmisobject = parent_cmisobject; + this.cmis_session = parent.cmis_session; + this.file = file; + this.new_filename = ''; + this.original_objectId = ''; + }, + + renderElement: function () { + this._super(); + this.$new_filename = this.$el.find('#new-filename'); + this.$new_filename.val(this.new_filename); + }, + + escape_query_param: function(param){ + param = param.replace(new RegExp("'", "g"), "\\'"); + return param; + }, + + /** + * Method called between @see init and @see start. Performs asynchronous + * calls required by the rendering and the start method. + */ + willStart: function () { + var self = this; + var re = /(?:\.([^.]+))?$/; + var parts = re.exec(this.file.name); + var name_without_ext = this.file.name.slice(0, -parts[1].length - 1); + var ext = parts[1]; + // looks for an alternate filename + var dfd1 = $.Deferred(); + this.cmis_session.query('' + + "SELECT cmis:name FROM cmis:document WHERE " + + "IN_FOLDER('" + this.parent_cmisobject.objectId + + "') AND cmis:name like '" + self.escape_query_param(name_without_ext) + "-%." + ext + "'") + .ok(function (data) { + var cpt = data.results.length; + var filenames = _.map( + data.results, + function (item) { + return item.succinctProperties['cmis:name'][0]; + }); + while (true) { + self.new_filename = name_without_ext + '-' + + cpt + '.' + ext; + if (_.contains(filenames, self.new_filename)) { + cpt += 1; + } else { + break; + } + } + dfd1.resolve(); + }) + .notOk(function (error) { + self.getParent().on_cmis_error(error); + dfd1.reject(error); + }); + // get original document + var dfd2 = $.Deferred(); + this.cmis_session.query('' + + "SELECT cmis:objectId FROM cmis:document WHERE " + + "IN_FOLDER('" + this.parent_cmisobject.objectId + + "') AND cmis:name = '" + self.escape_query_param(this.file.name) + "'") + .ok(function (data) { + self.original_objectId = data.results[0].succinctProperties['cmis:objectId']; + dfd2.resolve(); + }) + .notOk(function (error) { + self.getParent().on_cmis_error(error); + dfd2.reject(error); + }); + return $.when(this._super.apply(this, arguments), dfd1.promise(), dfd2.promise()); + }, + + on_click_process: function () { + var self = this; + var rename = this.$el.find("input:radio[name='duplicate-radios']:checked").val() === "rename"; + if (rename) { + this.cmis_session + .createDocument(this.parent_cmisobject.objectId, this.file, {'cmis:name': this.$new_filename.val()}, this.file.mimeType) + .ok(function (new_cmisobject) { + self.getParent().trigger('cmis_node_created', [new_cmisobject]); + self.$el.parents('.modal').modal('hide'); + }); + } else { + var major = this.$el.find("#new-version-type").val() === "major"; + var comment = this.$el.find('#comment').val(); + self.cmis_session.checkOut(self.original_objectId) + .ok(function (checkedOutNode) { + self.cmis_session + .checkIn(checkedOutNode.succinctProperties['cmis:objectId'], major, {}, self.file, comment) + .ok(function (data) { + // after checkin the working copy must be deleted (self.data) + // the date received into the response is the new version + // created + self.getParent().trigger('cmis_node_deleted', [self.original_objectId]); + self.$el.parents('.modal').modal('hide'); + }); + }); + } + } + }); + + var CmisCreateFolderDialog = Dialog.extend({ + template: 'CmisCreateFolderDialog', + init: function (parent, parent_cmisobject) { + var self = this; + var options = { + buttons: [ + { + text: _t("Create"), + classes: "btn-primary", + click: function () { + if (self.check_validity()) { + self.on_click_create(); + } + } + }, + { + text: _t("Close"), + click: function () { + self.$el.parents('.modal').modal('hide'); + } + }, + ], + close: function () { + self.close(); + }, + title: (_t("Create Folder ")) + }; + this._super(parent, options); + this.parent_cmisobject = parent_cmisobject; + }, + + on_click_create: function () { + var self = this; + var input = this.$el.find("input[type='text']")[0]; + framework.blockUI(); + var cmis_session = this.getParent().cmis_session; + cmis_session + .createFolder(this.parent_cmisobject.objectId, input.value) + .ok(function (new_cmisobject) { + framework.unblockUI(); + self.getParent().trigger('cmis_node_created', [new_cmisobject]); + self.$el.parents('.modal').modal('hide'); + }); + }, + + close: function () { + this._super(); + } + }); + + var CmisCreateDocumentDialog = Dialog.extend({ + template: 'CmisCreateDocumentDialog', + events: { + 'change .btn-file :file': 'on_file_change' + }, + + init: function (parent, parent_cmisobject) { + var self = this; + var options = { + buttons: [ + { + text: _t("Add"), + classes: "btn-primary", + click: function (e) { + e.stopPropagation(); + if (self.check_validity()) { + self.on_click_create(); + } + } + }, + { + text: _t("Close"), + click: function (e) { + e.stopPropagation(); + self.$el.parents('.modal').modal('hide'); + } + }, + + ], + close: function () { + self.close(); + }, + title: _t("Create Documents "), + }; + this._super(parent, options); + this.parent_cmisobject = parent_cmisobject; + }, + + on_file_change: function (e) { + var input = $(e.target), + numFiles = input.get(0).files ? input.get(0).files.length : 1, + label = input.val().replace(/\\/g, '/').replace(/.*\//, ''), + log = numFiles > 1 ? numFiles + ' files selected' : label; + var input_text = input.closest('.input-group').find(':text'); + input_text.val(log); + }, + + on_click_create: function () { + var self = this, + input = this.$el.find("input[type='file']")[0], + numFiles = input.files ? input.files.length : 1; + var processedFiles = []; + if (numFiles > 0) { + framework.blockUI(); + } + var parent = this.getParent(); + var cmis_session = parent.cmis_session; + _.each(input.files, function (file, index, list) { + cmis_session + .createDocument(this.parent_cmisobject.objectId, file, {'cmis:name': file.name}, file.mimeType) + .ok(function (data) { + processedFiles.push(data); + if (processedFiles.length == numFiles) { + framework.unblockUI(); + parent.trigger('cmis_node_created', [processedFiles]); + } + }); + }, self); + self.$el.parents('.modal').modal('hide'); + }, + + close: function () { + this._super(); + } + }); + + var SingleFileUpload = Dialog.extend({ + events: { + 'change .btn-file :file': 'on_file_change' + }, + + init: function (parent, cmisObjectWrapped, options) { + var self = this; + var btnOkTitle = _t('OK'); + if (!_.isUndefined(options) && _.has(options, 'btnOkTitle')) { + btnOkTitle = options.btnOkTitle; + } + options = _.defaults(options || {}, { + buttons: [ + { + text: btnOkTitle, + classes: "btn-primary", + click: function (e) { + e.stopPropagation(); + if (self.check_validity()) { + self.on_click_ok(); + } + } + }, + { + text: _t("Close"), + click: function (e) { + e.stopPropagation(); + self.$el.parents('.modal').modal('hide'); + } + }, + ], + close: function () { + self.close(); + } + }); + this._super(parent, options); + this.data = cmisObjectWrapped; + }, + + on_file_change: function (e) { + var input = $(e.target), + label = input.val().replace(/\\/g, '/').replace(/.*\//, ''), + input_text = input.closest('.input-group').find(':text'); + input_text.val(label); + }, + + on_click_ok: function () { + var self = this; + var input = this.$el.find("input[type='file']")[0] + var numFiles = input.files ? input.files.length : 1; + if (numFiles == 0) { + this.close(); + } + var file = input.files[0]; + var fileName = file.name; + framework.blockUI(); + this._do_upload(file, fileName).then(function (data) { + framework.unblockUI(); + if (!_.isUndefined(data)) { + self.getParent().trigger('cmis_node_content_updated', [data]); + } + self.$el.parents('.modal').modal('hide'); + }); + }, + + /** + * This method must be implemented into concrete dialog an return a promise + * The promise must be resolved with updated cmisObject + */ + _do_upload: function (file, filename) { + }, + + close: function () { + this._super(); + } + }); + + var CmisUpdateContentStreamDialog = SingleFileUpload.extend({ + template: 'CmisUpdateContentStreamView', + + init: function (parent, cmisObjectWrapped) { + var self = this; + var options = { + btnOkTitle: _t("Update content"), + title: _t("Update content of ") + cmisObjectWrapped.name, + }; + this._super(parent, cmisObjectWrapped, options); + }, + + _do_upload: function (file, fileName) { + var dfd = $.Deferred(); + this.data.cmis_session + .setContentStream(this.data.objectId, file, true, fileName) + .ok(function (data) { + dfd.resolve(data); + }); + return dfd.promise(); + }, + }); + + var CmisCheckinDialog = SingleFileUpload.extend({ + template: 'CmisCheckinView', + + init: function (parent, cmisObjectWrapped) { + var self = this; + var options = { + btnOkTitle: _t("Import new version"), + title: _t("Import new version of ") + cmisObjectWrapped.name, + }; + this._super(parent, cmisObjectWrapped, options); + }, + + _do_upload: function (file, fileName) { + var self = this; + var dfd = $.Deferred(); + var major = this.$el.find("input:radio[name='version-radios']:checked").val() === "major"; + var comment = this.$el.find('#comment').val(); + this.data.cmis_session + .checkIn(this.data.objectId, major, {}, file, comment) + .ok(function (data) { + // after checkin the working copy must be deleted (self.data) + // the date received into the response is the new version + // created + self.getParent().trigger('cmis_node_deleted', [self.data.cmis_object]); + dfd.resolve(data); + }); + return dfd.promise(); + }, + }); + + var CmisObjectWrapper = core.Class.extend({ + + init: function (parent, cmis_object, cmis_session) { + this.parent = parent; + this.cmis_object = cmis_object; + this.cmis_session = cmis_session; + this.parse_object(cmis_object); + }, + + _clone: function () { + return new CmisObjectWrapper(this.parent, this.cmis_object, this.cmis_session); + }, + + parse_object: function (cmis_object) { + this.name = this.getSuccinctProperty('cmis:name', cmis_object); + this.mimetype = this.getSuccinctProperty('cmis:contentStreamMimeType', cmis_object); + this.baseTypeId = this.getSuccinctProperty('cmis:baseTypeId', cmis_object); + this.title = this.getSuccinctProperty('cm:title', cmis_object) || ''; + this.description = this.getSuccinctProperty('cmis:description', cmis_object); + this.lastModificationDate = this.getSuccinctProperty('cmis:lastModificationDate', cmis_object); + this.creationDate = this.getSuccinctProperty('cmis:creationDate', cmis_object); + this.lastModifiedBy = this.getSuccinctProperty('cmis:lastModifiedBy', cmis_object); + this.objectId = this.getSuccinctProperty('cmis:objectId', cmis_object); + this.versionSeriesId = this.getSuccinctProperty('cmis:versionSeriesId', cmis_object); + this.versionLabel = this.getSuccinctProperty('cmis:versionLabel'); + this.url = this.cmis_session.getContentStreamURL(this.objectId, 'attachment'); + this.allowableActions = cmis_object.allowableActions; + this.renditions = cmis_object.renditions; + }, + + getSuccinctProperty: function (property, cmis_object) { + cmis_object = cmis_object || this.cmis_object; + return cmis_object.succinctProperties[property]; + }, + + _get_css_class: function () { + if (this.baseTypeId === 'cmis:folder') { + return 'fa fa-folder cmis-folder'; + } + + if (this.mimetype) { + switch (this.mimetype) { + case 'application/pdf': + return 'fa fa-file-pdf-o'; + case 'text/plain': + return 'fa fa-file-text-o'; + case 'text/html': + return 'fa fa-file-code-o'; + case 'application/json': + return 'fa fa-file-code-o'; + case 'application/gzip': + return 'fa fa-file-archive-o'; + case 'application/zip': + return 'fa fa-file-archive-o'; + case 'application/octet-stream': + return 'fa fa-file-o'; + } + switch (this.mimetype.split('/')[0]) { + case 'image': + return 'fa fa-file-image-o'; + case 'audio': + return 'fa fa-file-audio-o'; + case 'video': + return 'fa fa-file-video-o'; + } + } + if (this.baseTypeId === 'cmis:document') { + return 'fa fa-file-o'; + } + return 'fa fa-fw'; + }, + + /** fName + * return the cmis:name formatted to be rendered in ta datatable cell + * + **/ + fName: function () { + var cls = this._get_css_class(); + var val = "
" + this.name; + val = val + "
"; + if (this.getSuccinctProperty('cmis:isVersionSeriesCheckedOut')) { + val = val + "
" + _t('By:') + ' ' + this.getSuccinctProperty('cmis:versionSeriesCheckedOutBy') + '
'; + } + return val; + }, + + /** fLastModificationDate + * return the cmis:mastModificationDate formatted to be rendered in ta datatable cell + * + **/ + fLastModificationDate: function () { + return this.format_cmis_timestamp(this.lastModificationDate); + }, + + /** + * Format cmis object creation date + * @returns the cmis:creationDate formatted to be rendered in a datatable cell + * + **/ + fCreationDate: function() { + return this.format_cmis_timestamp(this.creationDate); + }, + + + fDetails: function () { + return '
'; + }, + + format_cmis_timestamp: function (cmis_timestamp) { + if (cmis_timestamp) { + var d = new Date(cmis_timestamp); + var l10n = _t.database.parameters; + var date_format = time.strftime_to_moment_format(l10n.date_format); + var time_format = time.strftime_to_moment_format(l10n.time_format); + var value = moment(d); + return value.format(date_format + ' ' + time_format); + } + return ''; + }, + + /** + * Content actions + * + * render the list of available actions + */ + fContentActions: function () { + var ctx = {object: this}; + _.map(this.cmis_object.allowableActions, function (value, actionName) { + ctx[actionName] = value; + }); + ctx['canPreview'] = ctx['canGetContentStream']; // && this.mimetype === 'application/pdf'; + ctx['isFolder'] = this.baseTypeId == 'cmis:folder'; + return QWeb.render("CmisContentActions", ctx); + }, + + get_content_url: function () { + return this.cmis_session.getContentStreamURL(this.objectId, 'inline'); + }, + + get_preview_url: function () { + var rendition = _.findWhere(this.renditions, {mimeType: 'application/pdf'}); + if (this.mimetype === 'application/pdf') { + return this.get_content_url(); + } else if (rendition) { + return this.cmis_session.getContentStreamURL(rendition['streamId']); + } + return null; + }, + + get_preview_type: function () { + if (this.baseTypeId === 'cmis:folder') { + return undefined; + } + if (this.mimetype.match("(image)")) { + return 'image'; + } + if (this.mimetype.match("(video)")) { + return 'video'; + } + // here we hope that alfresco is able to render the document as pdf + return "pdf"; + }, + + + /** + * Refresh the information by reloading data from the server + * The method return a deferred called once the information are up to date + */ + refresh: function () { + var self = this; + var dfd = $.Deferred() + var options = DEFAULT_CMIS_OPTIONS; + var oldValue = this._clone(); + this.cmis_session.getObject( + this.objectId, + 'latest', options).ok(function (data) { + self.parse_object(data); + self.parent.trigger('wrapped_cmis_node_reloaded', oldValue, self); + dfd.resolve(self); + }); + return dfd.promise(); + }, + + }); + + var DEFAULT_CMIS_OPTIONS = { + includeAllowableActions: true, + renditionFilter: 'application/pdf', + } + + /** + * A Mixin class defining common methods used by Cmis widgets + */ + var CmisMixin = { + + init: function () { + this.cmis_session_initialized = $.Deferred(); + this.cmis_config_loaded = $.Deferred(); + this.cmis_location = null; + this.cmis_backend_id = null; + this.cmis_backend_fields = ['id', 'location']; + }, + + /** + * Load CMIS settings from Odoo server + */ + load_cmis_config: function () { + this.bind_cmis_config(this.backend); + this.on_cmis_config_loaded(this.backend); + }, + + /** + * Parse the result of the call to the server to retrieve the CMIS settings + */ + bind_cmis_config: function (result) { + if (result.backend_error) { + this.do_warn( + _t("CMIS Backend Config Error"), + result.backend_error, + true); + return; + } + this.cmis_location = result.location; + this.cmis_backend_id = result.id; + }, + + on_cmis_config_loaded: function (result) { + this.cmis_config_loaded.resolve(); + }, + + /** + * Initialize the CmisJS session and register handlers for warnings and errors + * occuring when calling the CMIS DMS + */ + init_cmis_session: function () { + var self = this; + $.when(this.cmis_config_loaded).done(function () { + self.cmis_session = cmis.createSession(self.cmis_location); + self.cmis_session.setGlobalHandlers(self.on_cmis_error, self.on_cmis_error); + self.cmis_session_initialized.resolve(); + self.cmis_session.setCharacterSet(document.characterSet); + }); + }, + + /** + * Load the default repository if required. + * token or credentils must already be set. + * At this stage the widget doesn't support multi repositories but + * if we want to get a chance to put a token based on the data from + * the odoo model, this method can only be called once the values + * are provided by the form controller ant we load the root folder for + * exemple (set_root_folder_id). + * Loading the repositories is required before calling to others cmis + * methods + */ + load_cmis_repositories: function () { + var dfd = $.Deferred(); + var self = this; + if (this.cmis_session.repositories) { + return dfd.resolve(); + } else { + self.cmis_session + .loadRepositories() + .ok(function (data) { + dfd.resolve(); + }) + .notOk(function (error) { + self.on_cmis_error(error); + dfd.reject(error); + }); + } + return dfd.promise(); + }, + + /** + * Method called by the cmis session in case of error or warning + */ + on_cmis_error: function (error) { + framework.unblockUI(); + if (error) { + if (error.type == 'application/json') { + error = JSON.parse(error.text); + new Dialog(this, { + size: 'medium', + title: _t("CMIS Error "), + $content: $('
').html(QWeb.render('CMISSession.warning', {error: error})) + }).open(); + } else { + new Dialog(this, { + size: 'medium', + title: _t("CMIS Error"), + subtitle: error.statusText, + $content: $('
').html(error.text) + }).open(); + } + } + }, + + /** + * Wrap a + */ + wrap_cmis_object: function (cmisObject) { + if (_.has(cmisObject, 'object')) { + cmisObject = cmisObject.object; + } + return new CmisObjectWrapper(this, cmisObject, this.cmis_session); + }, + + wrap_cmis_objects: function (cmisObjects) { + var self = this; + return _.chain(cmisObjects) + .map(function (item) { + return self.wrap_cmis_object(item) + }) + .uniq(function (wrapped) { + return wrapped.objectId + }) + .value() + }, + }; + + var FieldCmisFolder = basicFields.FieldChar.extend(CmisMixin, { + template: "FieldCmisFolder", + + widget_class: 'field_cmis_folder', + datatable: null, + displayed_folder_id: null, + + events: { + 'change input': 'store_dom_value', + 'click td.details-control': 'on_click_details_control', + 'click button.cmis-create-root': 'on_click_create_root', + }, + + /* + * Override base methods + */ + + init: function () { + this._super.apply(this, arguments); + CmisMixin.init.call(this); + this.id_for_table = _.uniqueId('field_cmis_folder_widgets_table'); + this.table_rendered = $.Deferred(); + this.on('cmis_node_created', this, this.on_cmis_node_created); + this.on('cmis_node_deleted', this, this.on_cmis_node_deleted); + this.on('cmis_node_updated', this, this.on_cmis_node_updated); + this.on('cmis_node_content_updated', this, this.on_cmis_node_content_updated); + this.on('wrapped_cmis_node_reloaded', this, this.on_wrapped_cmis_node_reloaded); + this.backend = this.field.backend; + this.formatType = 'char'; + this.clipboardAction = undefined; + this.clipboardObject = undefined; + }, + + reset_widget: function () { + if (this.datatable) { + this.table_rendered = $.Deferred(); + this.datatable.destroy(); + this.datatable = null; + this.root_folder_id = null; + this.displayed_folder_id = null; + } + }, + + destroy: function () { + this.reset_widget(); + this._super.apply(this, arguments); + }, + + _render: function () { + this._super.apply(this, arguments); + this.states = []; + this.load_cmis_config(); + this.init_cmis_session(); + var value = this.value; + if (this.$input) { + this.$input.val(value); + } + if (!this.res_id) { + // hide the widget if the record is not yet created + this.$el.hide(); + } + this.$el.find('button.cmis-create-root').addClass('o_hidden'); + + if (this.$el.is(':visible')) { + // if the element is visible, we render it. If it's in a tab + // the rendition will be don on tab activation + this.render_datatable(); + } + + this.set_root_folder_id(value); + if (!value && this.field.allow_create) { + var self = this; + this.$el.find('button.cmis-create-root').removeClass('o_hidden'); + } + var self = this; + self.add_tab_listener(); + }, + + _renderReadonly: function () { + // in edit mode we need the in + this._prepareInput(this.$el); + }, + /** + * @override + */ + isSet: function () { + return true; + }, + + /* + * Cmis content events + */ + on_cmis_node_created: function (cmisobjects) { + this.refresh_datatable(); + }, + + on_cmis_node_deleted: function (cmisobjects) { + this.refresh_datatable(); + }, + + on_cmis_node_updated: function (cmisobjects) { + this.refresh_datatable(); + }, + + on_wrapped_cmis_node_reloaded: function (oldValue, newValue) { + this.refresh_datatable(); + }, + + on_cmis_node_content_updated: function (cmisobjects) { + this.on_cmis_node_updated(cmisobjects); + }, + + /* + * Specific methods + */ + + /** + * Create a node for the current model into the DMS + */ + on_click_create_root: function () { + if (!this.res_id) { + Dialog.alert(this, _t('Create your object first')); + return; + } + var self = this; + $.when(this.cmis_config_loaded).done(function () { + self._rpc({route:'/web/cmis/field/create_value', params:{ + 'model_name': self.model, + 'res_id': self.res_id, + 'field_name': self.name + }}).then(function (vals) { + var cmis_objectid = vals[self.res_id]; + var changes = {}; + changes[self.name] = cmis_objectid; + self.trigger_up('field_changed', { + dataPointID: self.dataPointID, + changes: changes, + }); + }); + }); + }, + + /** + * Add tab listener to render the table only when the tabe is active + * if the control is displayed in an inactive tab + */ + add_tab_listener: function () { + var self = this; + $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) { + var tab_id = self.id_for_table; + var active_tab = $(e.target.hash); + if (active_tab.find('#' + tab_id).length == 1) { + self.render_datatable(); + return; + } + }); + }, + + get_datatable_config: function () { + var l10n = _t.database.parameters; + var self = this; + var config = { + searching: false, + scrollY: '40vh', + scrollCollapse: true, + pageLength: 25, + deferRender: true, + serverSide: true, + autoWidth: false, + responsive: true, + colReorder: { + realtime: false, + }, + stateSave: true, + ajax: $.proxy(self, 'datatable_query_cmis_data'), + buttons: [{ + extend: 'collection', + text: _t('Columns') + '', + buttons: ['columnsToggle'], + }], + columns: [ + { + className: 'details-control', + orderable: false, + data: 'fDetails()', + defaultContent: '', + width: '12px' + }, + {data: 'fName()'}, + { + data: 'title', + visible: false + }, + {data: 'description'}, + { + data: 'fLastModificationDate()', + width: '120px' + }, + { + data:'fCreationDate()', + width:'120px', + visible: false, + }, + { + data: 'lastModifiedBy', + width: '60px', + visible: false, + }, + { + data: 'fContentActions()', + defaultContent: '', + orderable: false, + width: "80px", + }, + ], + select: false, + rowId: 'objectId', + language: { + "decimal": l10n.decimal_point, + "emptyTable": _t("No data available in table"), + "info": _t("Showing _START_ to _END_ of _TOTAL_ entries"), + "infoEmpty": _t("Showing 0 to 0 of 0 entries"), + "infoFiltered": _t("(filtered from _MAX_ total entries)"), + "infoPostFix": _t(""), + "thousands": l10n.thousands_sep, + "lengthMenu": _t("Show _MENU_ entries"), + "loadingRecords": _t("Loading..."), + "processing": _t("Processing..."), + "search": _t("Search:"), + "zeroRecords": _t("No matching records found"), + "paginate": { + "first": _t("First"), + "last": _t("Last"), + "next": _t("Next"), + "previous": _t("Previous") + }, + "aria": { + "sortAscending": _t(": activate to sort column ascending"), + "sortDescending": _t(": activate to sort column descending") + } + }, + dom: "<'row'<'col-sm-6 cmis-root-content-buttons'><'col-sm-6'Blf>>" + + "<'row'<'col-sm-12'<'cmis-breadcrumb-container'>>>" + + "<'row'<'col-sm-12'tr>>" + + "<'row'<'col-sm-5'i><'col-sm-7'p>>", + "order": [[1, 'asc']] + }; + return config; + }, + + render_datatable: function () { + if (_.isNull(this.datatable)) { + var self = this; + this.$datatable = this.$el.find('#' + this.id_for_table); + this.$datatable.on('preInit.dt', $.proxy(self, 'on_datatable_preinit')); + this.$datatable.on('draw.dt', $.proxy(self, 'on_datatable_draw')); + this.$datatable.on('column-reorder.dt', $.proxy(self, 'on_datatable_column_reorder')); + var config = this.get_datatable_config(); + this.datatable = this.$datatable.DataTable(config); + this.table_rendered.resolve(); + } else { + this.datatable.draw(); + } + }, + + /** + * This method is called by DataTables when a table is being initialised + * and is about to request data. At the point of being called the table will + * have its columns and features initialised, but no data will have been + * loaded (either by Ajax, or reading from the DOM). + */ + on_datatable_preinit: function (e, settings) { + this.$breadcrumb = $('