diff --git a/application/Espo/Core/Mail/Sender.php b/application/Espo/Core/Mail/Sender.php index 081d1e1252..0e74514533 100644 --- a/application/Espo/Core/Mail/Sender.php +++ b/application/Espo/Core/Mail/Sender.php @@ -284,6 +284,7 @@ class Sender $contents = $a->get('contents'); } else { $fileName = $this->getEntityManager()->getRepository('Attachment')->getFilePath($a); + if (!is_file($fileName)) continue; $contents = file_get_contents($fileName); } $attachment = new MimePart($contents); @@ -303,6 +304,7 @@ class Sender $contents = $a->get('contents'); } else { $fileName = $this->getEntityManager()->getRepository('Attachment')->getFilePath($a); + if (!is_file($fileName)) continue; $contents = file_get_contents($fileName); } $attachment = new MimePart($contents); diff --git a/application/Espo/Jobs/Cleanup.php b/application/Espo/Jobs/Cleanup.php index 4e0f2627fa..30d7489c57 100644 --- a/application/Espo/Jobs/Cleanup.php +++ b/application/Espo/Jobs/Cleanup.php @@ -433,6 +433,8 @@ class Cleanup extends \Espo\Core\Jobs\Base $period = '-' . $this->getConfig()->get('cleanupDeletedRecordsPeriod', $this->cleanupDeletedRecordsPeriod); $datetime = new \DateTime($period); + $serviceFactory = $this->getServiceFactory(); + $scopeList = array_keys($this->getMetadata()->get(['scopes'])); foreach ($scopeList as $scope) { if (!$this->getMetadata()->get(['scopes', $scope, 'entity'])) continue; @@ -446,6 +448,15 @@ class Cleanup extends \Espo\Core\Jobs\Base if (!method_exists($repository, 'select')) continue; if (!method_exists($repository, 'deleteFromDb')) continue; + $hasCleanupMethod = false; + $service = null; + if ($serviceFactory->checkExists($scope)) { + $service = $serviceFactory->create($scope); + if (method_exists($service, 'cleanup')) { + $hasCleanupMethod = true; + } + } + $whereClause = [ 'deleted' => 1, ]; @@ -458,6 +469,13 @@ class Cleanup extends \Espo\Core\Jobs\Base $deletedEntityList = $repository->select(['id', 'deleted'])->where($whereClause)->find(['withDeleted' => true]); foreach ($deletedEntityList as $e) { + if ($hasCleanupMethod) { + try { + $service->cleanup($e->id); + } catch (\Throwable $e) { + $GLOBALS['log']->error("Cleanup job: Cleanup scope {$scope}: " . $e->getMessage()); + } + } $this->cleanupDeletedEntity($e); } } diff --git a/application/Espo/ORM/DB/Query/Base.php b/application/Espo/ORM/DB/Query/Base.php index f76fb752c0..907fe9c9a7 100644 --- a/application/Espo/ORM/DB/Query/Base.php +++ b/application/Espo/ORM/DB/Query/Base.php @@ -1670,6 +1670,10 @@ abstract class Base $sql = implode(' ' .$logicalOperator . ' ', $sqlList); + if (count($sqlList) > 1) { + $sql = '(' . $sql . ')'; + } + return $sql; } diff --git a/application/Espo/Resources/layouts/User/massUpdatePortal.json b/application/Espo/Resources/layouts/User/massUpdatePortal.json new file mode 100644 index 0000000000..49eb5a3cac --- /dev/null +++ b/application/Espo/Resources/layouts/User/massUpdatePortal.json @@ -0,0 +1,4 @@ +[ + "portalRoles", + "isActive" +] \ No newline at end of file diff --git a/application/Espo/Resources/layouts/User/massUpdatePortalApi.json b/application/Espo/Resources/layouts/User/massUpdatePortalApi.json new file mode 100644 index 0000000000..0746fbc849 --- /dev/null +++ b/application/Espo/Resources/layouts/User/massUpdatePortalApi.json @@ -0,0 +1,3 @@ +[ + "isActive" +] \ No newline at end of file diff --git a/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json b/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json index e490ea6097..8a10c1eeeb 100644 --- a/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json +++ b/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json @@ -118,6 +118,10 @@ "view": "views/admin/dynamic-logic/conditions/field-types/base", "typeList": ["isEmpty", "isNotEmpty", "equals", "notEquals", "greaterThan", "lessThan", "greaterThanOrEquals", "lessThanOrEquals"] }, + "currency": { + "view": "views/admin/dynamic-logic/conditions/field-types/base", + "typeList": ["isEmpty", "isNotEmpty", "equals", "notEquals", "greaterThan", "lessThan", "greaterThanOrEquals", "lessThanOrEquals"] + }, "date": { "view": "views/admin/dynamic-logic/conditions/field-types/date", "typeList": ["isEmpty", "isNotEmpty", "isToday", "inFuture", "inPast", "equals", "notEquals"] diff --git a/application/Espo/Resources/metadata/clientDefs/User.json b/application/Espo/Resources/metadata/clientDefs/User.json index ba2cfc51e6..1be3f0eef4 100644 --- a/application/Espo/Resources/metadata/clientDefs/User.json +++ b/application/Espo/Resources/metadata/clientDefs/User.json @@ -14,7 +14,8 @@ "list":"views/user/record/list" }, "modalViews": { - "detail": "views/user/modals/detail" + "detail": "views/user/modals/detail", + "massUpdate": "views/user/modals/mass-update" }, "defaultSidePanel": { "detail": { diff --git a/application/Espo/SelectManagers/Email.php b/application/Espo/SelectManagers/Email.php index 0776101b55..4eebe04449 100644 --- a/application/Espo/SelectManagers/Email.php +++ b/application/Espo/SelectManagers/Email.php @@ -49,6 +49,12 @@ class Email extends \Espo\Core\SelectManagers\Base foreach ($params['where'] as $item) { if ($item['type'] === 'textFilter') { $skipIndex = true; + break; + } else { + if (isset($item['attribute']) && $this->getSeed()->getAttributeParam($item['attribute'], 'type') === 'foreignId') { + $skipIndex = true; + break; + } } } } diff --git a/client/res/templates/modals/mass-update.tpl b/client/res/templates/modals/mass-update.tpl index 969d19f311..fcd787b7c3 100644 --- a/client/res/templates/modals/mass-update.tpl +++ b/client/res/templates/modals/mass-update.tpl @@ -1,4 +1,4 @@ -{{#unless fields}} +{{#unless fieldList}} {{translate 'No fields available for Mass Update'}} {{else}}
@@ -6,8 +6,8 @@
diff --git a/client/src/views/admin/field-manager/fields/options-with-style.js b/client/src/views/admin/field-manager/fields/options-with-style.js index 55c38a2a83..0c470bec18 100644 --- a/client/src/views/admin/field-manager/fields/options-with-style.js +++ b/client/src/views/admin/field-manager/fields/options-with-style.js @@ -95,7 +95,7 @@ Espo.define('views/admin/field-manager/fields/options-with-style', 'views/admin/ } } var translated = this.getLanguage().translateOption(item, 'style', 'LayoutManager'); - var innerHtml = '
'+translated+'
'; + var innerHtml = '
'+translated+'
'; itemListHtml += '
  • '+innerHtml+'
  • ' }, this); diff --git a/client/src/views/fields/link-multiple.js b/client/src/views/fields/link-multiple.js index 21e168628e..a9c962fb56 100644 --- a/client/src/views/fields/link-multiple.js +++ b/client/src/views/fields/link-multiple.js @@ -220,9 +220,9 @@ Espo.define('views/fields/link-multiple', 'views/fields/base', function (Dep) { response.list.forEach(function(item) { list.push({ id: item.id, - name: item.name, + name: item.name || item.id, data: item.id, - value: item.name + value: item.name || item.id, }); }, this); return { diff --git a/client/src/views/fields/link-parent.js b/client/src/views/fields/link-parent.js index 70a0f065a7..c1884ded1c 100644 --- a/client/src/views/fields/link-parent.js +++ b/client/src/views/fields/link-parent.js @@ -277,9 +277,9 @@ Espo.define('views/fields/link-parent', 'views/fields/base', function (Dep) { response.list.forEach(function(item) { list.push({ id: item.id, - name: item.name, + name: item.name || item.id, data: item.id, - value: item.name, + value: item.name || item.id, attributes: item }); }, this); diff --git a/client/src/views/fields/link.js b/client/src/views/fields/link.js index 434ad7cae4..df33b3c353 100644 --- a/client/src/views/fields/link.js +++ b/client/src/views/fields/link.js @@ -287,9 +287,9 @@ Espo.define('views/fields/link', 'views/fields/base', function (Dep) { response.list.forEach(function(item) { list.push({ id: item.id, - name: item.name, + name: item.name || item.id, data: item.id, - value: item.name, + value: item.name || item.id, attributes: item }); }, this); @@ -333,9 +333,9 @@ Espo.define('views/fields/link', 'views/fields/base', function (Dep) { response.list.forEach(function(item) { list.push({ id: item.id, - name: item.name, + name: item.name || item.id, data: item.id, - value: item.name + value: item.name || item.id, }); }, this); return { diff --git a/client/src/views/modals/mass-update.js b/client/src/views/modals/mass-update.js index c81a1bcfd0..206dd26b17 100644 --- a/client/src/views/modals/mass-update.js +++ b/client/src/views/modals/mass-update.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { +define('views/modals/mass-update', 'views/modal', function (Dep) { return Dep.extend({ @@ -34,10 +34,13 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { template: 'modals/mass-update', + layoutName: 'massUpdate', + data: function () { return { scope: this.scope, - fields: this.fields + fieldList: this.fieldList, + entityType: this.entityType, }; }, @@ -68,7 +71,9 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { } ]; - this.scope = this.options.scope; + this.entityType = this.options.entityType || this.options.scope; + this.scope = this.options.scope || this.entityType; + this.ids = this.options.ids; this.where = this.options.where; this.selectData = this.options.selectData; @@ -76,15 +81,18 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { this.headerHtml = this.translate(this.scope, 'scopeNamesPlural') + ' » ' + this.translate('Mass Update'); + var fobiddenList = this.getAcl().getScopeForbiddenFieldList(this.entityType, 'edit') || []; + this.wait(true); - this.getModelFactory().create(this.scope, function (model) { + this.getModelFactory().create(this.entityType, function (model) { this.model = model; - this.getHelper().layoutManager.get(this.scope, 'massUpdate', function (layout) { + this.getHelper().layoutManager.get(this.entityType, this.layoutName, function (layout) { layout = layout || []; - this.fields = []; + this.fieldList = []; layout.forEach(function (field) { + if (~fobiddenList.indexOf(field)) return; if (model.hasField(field)) { - this.fields.push(field); + this.fieldList.push(field); } }, this); @@ -92,7 +100,7 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { }.bind(this)); }.bind(this)); - this.fieldList = []; + this.addedFieldList = []; }, addField: function (name) { @@ -107,7 +115,7 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { } this.notify('Loading...'); - var label = this.translate(name, 'fields', this.scope); + var label = this.translate(name, 'fields', this.entityType); var html = '
    '; this.$el.find('.fields-container').append(html); @@ -123,7 +131,7 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { }, mode: 'edit' }, function (view) { - this.fieldList.push(name); + this.addedFieldList.push(name); view.render(); view.notify(false); }.bind(this)); @@ -135,7 +143,7 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { var self = this; var attributes = {}; - this.fieldList.forEach(function (field) { + this.addedFieldList.forEach(function (field) { var view = self.getView(field); _.extend(attributes, view.fetch()); }); @@ -143,7 +151,7 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { this.model.set(attributes); var notValid = false; - this.fieldList.forEach(function (field) { + this.addedFieldList.forEach(function (field) { var view = self.getView(field); notValid = view.validate() || notValid; }); @@ -151,7 +159,7 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { if (!notValid) { self.notify('Saving...'); $.ajax({ - url: this.scope + '/action/massUpdate', + url: this.entityType + '/action/massUpdate', type: 'PUT', data: JSON.stringify({ attributes: attributes, @@ -178,12 +186,12 @@ Espo.define('views/modals/mass-update', 'views/modal', function (Dep) { }, reset: function () { - this.fieldList.forEach(function (field) { + this.addedFieldList.forEach(function (field) { this.clearView(field); this.$el.find('.cell[data-name="'+field+'"]').remove(); }, this); - this.fieldList = []; + this.addedFieldList = []; this.model.clear(); diff --git a/client/src/views/record/list.js b/client/src/views/record/list.js index 6e64d66222..359bdd7ff3 100644 --- a/client/src/views/record/list.js +++ b/client/src/views/record/list.js @@ -795,12 +795,16 @@ define('views/record/list', 'view', function (Dep) { ids = this.checkedList; } - this.createView('massUpdate', 'views/modals/mass-update', { - scope: this.entityType, + var viewName = this.getMetadata().get(['clientDefs', this.entityType, 'modalViews', 'massUpdate']) || + 'views/modals/mass-update'; + + this.createView('massUpdate', viewName, { + scope: this.scope, + entityType: this.entityType, ids: ids, where: this.collection.getWhere(), selectData: this.collection.data, - byWhere: this.allResultIsChecked + byWhere: this.allResultIsChecked, }, function (view) { view.render(); view.notify(false); diff --git a/client/src/views/user/modals/mass-update.js b/client/src/views/user/modals/mass-update.js new file mode 100644 index 0000000000..4bb0268350 --- /dev/null +++ b/client/src/views/user/modals/mass-update.js @@ -0,0 +1,44 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2019 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: https://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +define('views/user/modals/mass-update', 'views/modals/mass-update', function (Dep) { + + return Dep.extend({ + + setup: function () { + + if (this.options.scope === 'ApiUser') { + this.layoutName = 'massUpdateApi'; + } else if (this.options.scope === 'PortalUser') { + this.layoutName = 'massUpdatePortal'; + } + Dep.prototype.setup.call(this); + }, + + }); +}); diff --git a/tests/unit/Espo/ORM/DB/QueryTest.php b/tests/unit/Espo/ORM/DB/QueryTest.php index 7430dc3b77..00b15b6f25 100644 --- a/tests/unit/Espo/ORM/DB/QueryTest.php +++ b/tests/unit/Espo/ORM/DB/QueryTest.php @@ -269,7 +269,26 @@ class QueryTest extends \PHPUnit\Framework\TestCase 'withDeleted' => true, ]); - $expectedSql = "SELECT note.id AS `id` FROM `note` LEFT JOIN `post` AS `post` ON post.name = 'test' OR post.name IS NULL"; + $expectedSql = "SELECT note.id AS `id` FROM `note` LEFT JOIN `post` AS `post` ON (post.name = 'test' OR post.name IS NULL)"; + + $this->assertEquals($expectedSql, $sql); + } + + public function testJoinConditions4() + { + $sql = $this->query->createSelectQuery('Note', [ + 'select' => ['id'], + 'leftJoins' => [['post', 'post', [ + 'name' => null, + 'OR' => [ + ['name' => 'test'], + ['post.name' => null], + ] + ]]], + 'withDeleted' => true, + ]); + + $expectedSql = "SELECT note.id AS `id` FROM `note` LEFT JOIN `post` AS `post` ON post.name IS NULL AND (post.name = 'test' OR post.name IS NULL)"; $this->assertEquals($expectedSql, $sql); }