preserve duration
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii 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.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\RecordHooks\Event;
|
||||
|
||||
use Espo\Core\Record\Hook\UpdateHook;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Field\Date;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Defs as OrmDefs;
|
||||
|
||||
/**
|
||||
* @implements UpdateHook<CoreEntity>
|
||||
*/
|
||||
class BeforeUpdatePreserveDuration implements UpdateHook
|
||||
{
|
||||
private OrmDefs $ormDefs;
|
||||
|
||||
private const DATE_START = 'dateStart';
|
||||
|
||||
private const DATE_END = 'dateEnd';
|
||||
|
||||
public function __construct(OrmDefs $ormDefs)
|
||||
{
|
||||
$this->ormDefs = $ormDefs;
|
||||
}
|
||||
|
||||
public function process(Entity $entity, UpdateParams $params): void
|
||||
{
|
||||
/** @var CoreEntity $entity */
|
||||
|
||||
if (!$entity->isAttributeChanged('dateStart')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity->isAttributeWritten('dateEnd')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$preserveDurationDisabled = $this->ormDefs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField('dateEnd')
|
||||
->getParam('preserveDurationDisabled');
|
||||
|
||||
if ($preserveDurationDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processDateTime($entity);
|
||||
$this->processDate($entity);
|
||||
}
|
||||
|
||||
private function processDateTime(Entity $entity): void
|
||||
{
|
||||
$dateStartFetchedString = $entity->getFetched('dateStart');
|
||||
$dateStartString = $entity->get('dateStart');
|
||||
$dateEndString = $entity->get('dateEnd');
|
||||
|
||||
if (!$dateStartFetchedString || !$dateStartString || !$dateEndString) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateStartFetched = DateTime::fromString($dateStartFetchedString);
|
||||
$dateStart = DateTime::fromString($dateStartString);
|
||||
$dateEnd = DateTime::fromString($dateEndString);
|
||||
|
||||
$diff = $dateStartFetched->diff($dateEnd);
|
||||
|
||||
$dateEndModified = $dateStart->add($diff);
|
||||
|
||||
$entity->set('dateEnd', $dateEndModified->getString());
|
||||
}
|
||||
|
||||
private function processDate(Entity $entity): void
|
||||
{
|
||||
$dateStartFetchedString = $entity->getFetched('dateStartDate');
|
||||
$dateStartString = $entity->get('dateStartDate');
|
||||
$dateEndString = $entity->get('dateEndDate');
|
||||
|
||||
if (!$dateStartFetchedString || !$dateStartString || !$dateEndString) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateStartFetched = Date::fromString($dateStartFetchedString);
|
||||
$dateStart = Date::fromString($dateStartString);
|
||||
$dateEnd = Date::fromString($dateEndString);
|
||||
|
||||
$diff = $dateStartFetched->diff($dateEnd);
|
||||
|
||||
$dateEndModified = $dateStart->add($diff);
|
||||
|
||||
$entity->set('dateEndDate', $dateEndModified->getString());
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ use Espo\Core\{
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @extends Database<\Espo\Core\ORM\Entity>
|
||||
@@ -58,11 +57,6 @@ class Event extends Database implements
|
||||
*/
|
||||
protected $reminderSkippingStatusList = ['Held', 'Not Held'];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $preserveDuration = true;
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $options
|
||||
* @return void
|
||||
@@ -109,31 +103,6 @@ class Event extends Database implements
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!$entity->isNew() &&
|
||||
$this->preserveDuration &&
|
||||
$entity->isAttributeChanged('dateStart') &&
|
||||
$entity->get('dateStart') &&
|
||||
!$entity->isAttributeChanged('dateEnd')
|
||||
) {
|
||||
$dateEndPrevious = $entity->getFetched('dateEnd');
|
||||
$dateStartPrevious = $entity->getFetched('dateStart');
|
||||
|
||||
if ($dateStartPrevious && $dateEndPrevious) {
|
||||
$dtStart = new DateTime($dateStartPrevious);
|
||||
$dtEnd = new DateTime($dateEndPrevious);
|
||||
$dt = new DateTime($entity->get('dateStart'));
|
||||
|
||||
$duration = ($dtEnd->getTimestamp() - $dtStart->getTimestamp());
|
||||
|
||||
$dt->modify('+' . $duration . ' seconds');
|
||||
|
||||
$dateEnd = $dt->format('Y-m-d H:i:s');
|
||||
|
||||
$entity->set('dateEnd', $dateEnd);
|
||||
}
|
||||
}
|
||||
|
||||
parent::beforeSave($entity, $options);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
"name":"todays"
|
||||
}
|
||||
],
|
||||
"forcePatchAttributeDependencyMap": {
|
||||
"dateEnd": ["dateStart"],
|
||||
"dateEndDate": ["dateStartDate"]
|
||||
},
|
||||
"dynamicLogic":{
|
||||
"fields":{
|
||||
"duration": {
|
||||
|
||||
@@ -4,5 +4,9 @@
|
||||
],
|
||||
"saverClassNameList": [
|
||||
"Espo\\Core\\FieldProcessing\\Reminder\\Saver"
|
||||
],
|
||||
"beforeUpdateHookClassNameList": [
|
||||
"__APPEND__",
|
||||
"Espo\\Classes\\RecordHooks\\Event\\BeforeUpdatePreserveDuration"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@ class Task extends EventRepository
|
||||
{
|
||||
protected $reminderSkippingStatusList = ['Completed', 'Canceled'];
|
||||
|
||||
protected $preserveDuration = false;
|
||||
|
||||
protected function beforeSave(Entity $entity, array $options = [])
|
||||
{
|
||||
if ($entity->isAttributeChanged('status')) {
|
||||
|
||||
@@ -91,6 +91,10 @@
|
||||
"activitiesCreate": true,
|
||||
"historyCreate": true
|
||||
},
|
||||
"forcePatchAttributeDependencyMap": {
|
||||
"dateEnd": ["dateStart"],
|
||||
"dateEndDate": ["dateStartDate"]
|
||||
},
|
||||
"dynamicLogic":{
|
||||
"fields":{
|
||||
"reminders": {
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
"activitiesCreate": true,
|
||||
"historyCreate": true
|
||||
},
|
||||
"forcePatchAttributeDependencyMap": {
|
||||
"dateEnd": ["dateStart"],
|
||||
"dateEndDate": ["dateStartDate"]
|
||||
},
|
||||
"dynamicLogic":{
|
||||
"fields":{
|
||||
"reminders": {
|
||||
|
||||
@@ -5,5 +5,9 @@
|
||||
],
|
||||
"saverClassNameList": [
|
||||
"Espo\\Core\\FieldProcessing\\Reminder\\Saver"
|
||||
],
|
||||
"beforeUpdateHookClassNameList": [
|
||||
"__APPEND__",
|
||||
"Espo\\Classes\\RecordHooks\\Event\\BeforeUpdatePreserveDuration"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,9 @@
|
||||
"saverClassNameList": [
|
||||
"Espo\\Core\\FieldProcessing\\Reminder\\Saver",
|
||||
"Espo\\Modules\\Crm\\Classes\\FieldProcessing\\Meeting\\SourceEmailSaver"
|
||||
],
|
||||
"beforeUpdateHookClassNameList": [
|
||||
"__APPEND__",
|
||||
"Espo\\Classes\\RecordHooks\\Event\\BeforeUpdatePreserveDuration"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -100,6 +100,11 @@ class BaseEntity implements Entity
|
||||
|
||||
private ?ValueAccessor $valueAccessor = null;
|
||||
|
||||
/**
|
||||
* @var array<string,bool>
|
||||
*/
|
||||
private array $writtenMap = [];
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* attributes?: array<string,array<string,mixed>>,
|
||||
@@ -490,6 +495,8 @@ class BaseEntity implements Entity
|
||||
}
|
||||
|
||||
$this->setInContainer($attribute, $preparedValue);
|
||||
|
||||
$this->writtenMap[$attribute] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -897,6 +904,14 @@ class BaseEntity implements Entity
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an attribute was written (since syncing with DB) regardless being changed.
|
||||
*/
|
||||
public function isAttributeWritten(string $name): bool
|
||||
{
|
||||
return $this->writtenMap[$name] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $v1
|
||||
* @param mixed $v2
|
||||
@@ -1028,6 +1043,8 @@ class BaseEntity implements Entity
|
||||
foreach ($this->fetchedValuesContainer as $attribute => $value) {
|
||||
$this->setFetched($attribute, $value);
|
||||
}
|
||||
|
||||
$this->writtenMap = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -658,36 +658,58 @@ define(
|
||||
|
||||
var headers = options.headers || {};
|
||||
|
||||
var model = this.model;
|
||||
|
||||
this.lastSaveCancelReason = null;
|
||||
|
||||
this.beforeBeforeSave();
|
||||
|
||||
var data = this.fetch();
|
||||
|
||||
var model = this.model;
|
||||
|
||||
let fetchedAttributes = this.fetch();
|
||||
var initialAttributes = this.attributes;
|
||||
|
||||
var beforeSaveAttributes = this.model.getClonedAttributes();
|
||||
|
||||
data = _.extend(Espo.Utils.cloneDeep(beforeSaveAttributes), data);
|
||||
let attributes = _.extend(
|
||||
Espo.Utils.cloneDeep(beforeSaveAttributes),
|
||||
fetchedAttributes
|
||||
);
|
||||
|
||||
var setAttributes = false;
|
||||
let setAttributes = {};
|
||||
|
||||
if (model.isNew()) {
|
||||
setAttributes = data;
|
||||
setAttributes = attributes;
|
||||
}
|
||||
else {
|
||||
for (var name in data) {
|
||||
if (_.isEqual(initialAttributes[name], data[name])) {
|
||||
|
||||
if (!model.isNew()) {
|
||||
for (let attr in attributes) {
|
||||
if (_.isEqual(initialAttributes[attr], attributes[attr])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(setAttributes || (setAttributes = {}))[name] = data[name];
|
||||
setAttributes[attr] = attributes[attr];
|
||||
}
|
||||
|
||||
let forcePatchAttributeDependencyMap = this.forcePatchAttributeDependencyMap || {};
|
||||
|
||||
for (let attr in forcePatchAttributeDependencyMap) {
|
||||
if (attr in setAttributes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(attr in fetchedAttributes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let depAttributeList = forcePatchAttributeDependencyMap[attr];
|
||||
|
||||
let treatAsChanged = !! depAttributeList.find(attr => attr in setAttributes);
|
||||
|
||||
if (treatAsChanged) {
|
||||
setAttributes[attr] = attributes[attr];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!setAttributes) {
|
||||
if (Object.keys(setAttributes).length === 0) {
|
||||
if (!options.skipNotModifiedWarning) {
|
||||
this.afterNotModified();
|
||||
}
|
||||
|
||||
@@ -1236,6 +1236,9 @@ define('views/record/detail', ['views/record/base', 'view-record-helper'], funct
|
||||
this.getHelper().processSetupHandlers(this, this.setupHandlerType);
|
||||
|
||||
this.initInlideEditDynamicWithLogicInteroperability();
|
||||
|
||||
this.forcePatchAttributeDependencyMap = this.getMetadata()
|
||||
.get(['clientDefs', this.scope, 'forcePatchAttributeDependencyMap']) || {};
|
||||
},
|
||||
|
||||
setupBeforeFinal: function () {
|
||||
|
||||
@@ -75,7 +75,7 @@ class EntityTest extends \PHPUnit\Framework\TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Espo\ORM\Entity
|
||||
* @return \Espo\ORM\BaseEntity
|
||||
*/
|
||||
protected function createEntity(string $entityType, ?string $className = null)
|
||||
{
|
||||
@@ -380,4 +380,23 @@ class EntityTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
$entity->getId();
|
||||
}
|
||||
|
||||
public function testIsAttributeWritten1(): void
|
||||
{
|
||||
$entity = $this->createEntity('Test');
|
||||
|
||||
$entity->set([
|
||||
'int' => '1',
|
||||
'object' => (object) [],
|
||||
]);
|
||||
|
||||
$entity->setAsFetched();
|
||||
|
||||
$this->assertEquals(false, $entity->isAttributeWritten('int'));
|
||||
|
||||
$entity->set('int', '1');
|
||||
|
||||
$this->assertEquals(true, $entity->isAttributeWritten('int'));
|
||||
$this->assertEquals(false, $entity->isAttributeWritten('object'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"manifest": {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii 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.
|
||||
************************************************************************/
|
||||
|
||||
use Espo\Core\Container;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
|
||||
class AfterUpgrade
|
||||
{
|
||||
public function run(Container $container): void
|
||||
{
|
||||
$this->updateEventMetadata($container->get('metadata'), $container->get('fileManager'));
|
||||
}
|
||||
|
||||
private function updateEventMetadata(Metadata $metadata, FileManager $fileManager): void
|
||||
{
|
||||
$defs = $metadata->get(['scopes']);
|
||||
|
||||
$toSave = false;
|
||||
|
||||
foreach ($defs as $entityType => $item) {
|
||||
$isCustom = $item['isCustom'] ?? false;
|
||||
$type = $item['type'] ?? false;
|
||||
|
||||
if (!$isCustom || $type !== 'Event') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$toSave = true;
|
||||
|
||||
$metadata->set('recordDefs', $entityType, [
|
||||
'beforeUpdateHookClassNameList' => [
|
||||
"__APPEND__",
|
||||
"Espo\\Classes\\RecordHooks\\Event\\BeforeUpdatePreserveDuration"
|
||||
]
|
||||
]);
|
||||
|
||||
$metadata->set('clientDefs', $entityType, [
|
||||
'forcePatchAttributeDependencyMap' => [
|
||||
"dateEnd" => ["dateStart"],
|
||||
"dateEndDate" => ["dateStartDate"]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($toSave) {
|
||||
$metadata->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user