diff --git a/resources/sql/autopatches/20161026.calendar.01.importtriggers.sql b/resources/sql/autopatches/20161026.calendar.01.importtriggers.sql new file mode 100644 index 0000000000..f584aea578 --- /dev/null +++ b/resources/sql/autopatches/20161026.calendar.01.importtriggers.sql @@ -0,0 +1,8 @@ +ALTER TABLE {$NAMESPACE}_calendar.calendar_import + ADD triggerPHID VARBINARY(64); + +ALTER TABLE {$NAMESPACE}_calendar.calendar_import + ADD triggerFrequency VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}; + +UPDATE {$NAMESPACE}_calendar.calendar_import + SET triggerFrequency = 'once' WHERE triggerFrequency = ''; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 043ed75b47..14acf65f1c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2121,6 +2121,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportEpochLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php', 'PhabricatorCalendarImportFetchLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php', 'PhabricatorCalendarImportFrequencyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php', + 'PhabricatorCalendarImportFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportFrequencyTransaction.php', 'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php', 'PhabricatorCalendarImportICSLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php', 'PhabricatorCalendarImportICSURITransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php', @@ -2139,10 +2140,12 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportQuery' => 'applications/calendar/query/PhabricatorCalendarImportQuery.php', 'PhabricatorCalendarImportReloadController' => 'applications/calendar/controller/PhabricatorCalendarImportReloadController.php', 'PhabricatorCalendarImportReloadTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportReloadTransaction.php', + 'PhabricatorCalendarImportReloadWorker' => 'applications/calendar/worker/PhabricatorCalendarImportReloadWorker.php', 'PhabricatorCalendarImportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportSearchEngine.php', 'PhabricatorCalendarImportTransaction' => 'applications/calendar/storage/PhabricatorCalendarImportTransaction.php', 'PhabricatorCalendarImportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php', 'PhabricatorCalendarImportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php', + 'PhabricatorCalendarImportTriggerLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php', 'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php', 'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php', 'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php', @@ -6964,6 +6967,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportFetchLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType', + 'PhabricatorCalendarImportFrequencyTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportICSURITransaction' => 'PhabricatorCalendarImportTransactionType', @@ -6986,10 +6990,12 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarImportReloadController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportReloadTransaction' => 'PhabricatorCalendarImportTransactionType', + 'PhabricatorCalendarImportReloadWorker' => 'PhabricatorWorker', 'PhabricatorCalendarImportSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarImportTransaction' => 'PhabricatorModularTransaction', 'PhabricatorCalendarImportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarImportTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorCalendarImportTriggerLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule', diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php b/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php index 03f515087b..858d4f440a 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php @@ -33,7 +33,7 @@ final class PhabricatorCalendarImportDropController ->addCancelButton($cancel_uri, pht('Done')); } - $engine = new PhabricatorCalendarICSImportEngine(); + $engine = new PhabricatorCalendarICSFileImportEngine(); $imports = array(); foreach ($files as $file) { $import = PhabricatorCalendarImport::initializeNewCalendarImport( diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index d8de0b3ea3..9c07d371d0 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -167,6 +167,50 @@ final class PhabricatorCalendarImportViewController pht('Source Type'), $engine->getImportEngineTypeName()); + if ($import->getIsDisabled()) { + $auto_updates = phutil_tag('em', array(), pht('Import Disabled')); + $has_trigger = false; + } else { + $frequency = $import->getTriggerFrequency(); + $frequency_map = PhabricatorCalendarImport::getTriggerFrequencyMap(); + $frequency_names = ipull($frequency_map, 'name'); + $auto_updates = idx($frequency_names, $frequency, $frequency); + + if ($frequency == PhabricatorCalendarImport::FREQUENCY_ONCE) { + $has_trigger = false; + $auto_updates = phutil_tag('em', array(), $auto_updates); + } else { + $has_trigger = true; + } + } + + $properties->addProperty( + pht('Automatic Updates'), + $auto_updates); + + if ($has_trigger) { + $trigger = id(new PhabricatorWorkerTriggerQuery()) + ->setViewer($viewer) + ->withPHIDs(array($import->getTriggerPHID())) + ->needEvents(true) + ->executeOne(); + + if (!$trigger) { + $next_trigger = phutil_tag('em', array(), pht('Invalid Trigger')); + } else { + $now = PhabricatorTime::getNow(); + $next_epoch = $trigger->getNextEventPrediction(); + $next_trigger = pht( + '%s (%s)', + phabricator_datetime($next_epoch, $viewer), + phutil_format_relative_time($next_epoch - $now)); + } + + $properties->addProperty( + pht('Next Update'), + $next_trigger); + } + $engine->appendImportProperties( $viewer, $import, diff --git a/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php index 44e3619514..c3dc29bb51 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php @@ -80,6 +80,9 @@ final class PhabricatorCalendarImportEditEngine protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); + $engine = $object->getEngine(); + $can_trigger = $engine->supportsTriggers($object); + $fields = array( id(new PhabricatorTextEditField()) ->setKey('name') @@ -89,6 +92,7 @@ final class PhabricatorCalendarImportEditEngine PhabricatorCalendarImportNameTransaction::TRANSACTIONTYPE) ->setConduitDescription(pht('Rename the import.')) ->setConduitTypeDescription(pht('New import name.')) + ->setPlaceholder($object->getDisplayName()) ->setValue($object->getName()), id(new PhabricatorBoolEditField()) ->setKey('disabled') @@ -123,6 +127,22 @@ final class PhabricatorCalendarImportEditEngine ->setValue(false), ); + if ($can_trigger) { + $frequency_map = PhabricatorCalendarImport::getTriggerFrequencyMap(); + $frequency_options = ipull($frequency_map, 'name'); + + $fields[] = id(new PhabricatorSelectEditField()) + ->setKey('frequency') + ->setLabel(pht('Update Automatically')) + ->setDescription(pht('Configure an automatic update frequncy.')) + ->setTransactionType( + PhabricatorCalendarImportFrequencyTransaction::TRANSACTIONTYPE) + ->setConduitDescription(pht('Set the automatic update frequency.')) + ->setConduitTypeDescription(pht('Update frequency constant.')) + ->setValue($object->getTriggerFrequency()) + ->setOptions($frequency_options); + } + $import_engine = $object->getEngine(); foreach ($import_engine->newEditEngineFields($this, $object) as $field) { $fields[] = $field; diff --git a/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php b/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php index 6db68b9525..2631dc1774 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php @@ -27,26 +27,112 @@ final class PhabricatorCalendarImportEditor protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { - - $type_reload = PhabricatorCalendarImportReloadTransaction::TRANSACTIONTYPE; + $actor = $this->getActor(); // We import events when you create a source, or if you later reload it // explicitly. $should_reload = $this->getIsNewObject(); + + // We adjust the import trigger if you change the import frequency or + // disable the import. + $should_trigger = false; + foreach ($xactions as $xaction) { - if ($xaction->getTransactionType() == $type_reload) { - $should_reload = true; - break; + $xaction_type = $xaction->getTransactionType(); + switch ($xaction_type) { + case PhabricatorCalendarImportReloadTransaction::TRANSACTIONTYPE: + $should_reload = true; + break; + case PhabricatorCalendarImportFrequencyTransaction::TRANSACTIONTYPE: + $should_trigger = true; + break; + case PhabricatorCalendarImportDisableTransaction::TRANSACTIONTYPE: + $should_trigger = true; + break; } } if ($should_reload) { - $actor = $this->getActor(); - $import_engine = $object->getEngine(); $import_engine->importEventsFromSource($actor, $object); } + if ($should_trigger) { + $trigger_phid = $object->getTriggerPHID(); + if ($trigger_phid) { + $trigger = id(new PhabricatorWorkerTriggerQuery()) + ->setViewer($actor) + ->withPHIDs(array($trigger_phid)) + ->executeOne(); + + if ($trigger) { + $engine = new PhabricatorDestructionEngine(); + $engine->destroyObject($trigger); + } + } + + $frequency = $object->getTriggerFrequency(); + $now = PhabricatorTime::getNow(); + switch ($frequency) { + case PhabricatorCalendarImport::FREQUENCY_ONCE: + $clock = null; + break; + case PhabricatorCalendarImport::FREQUENCY_HOURLY: + $clock = new PhabricatorMetronomicTriggerClock( + array( + 'period' => phutil_units('1 hour in seconds'), + )); + break; + case PhabricatorCalendarImport::FREQUENCY_DAILY: + $clock = new PhabricatorDailyRoutineTriggerClock( + array( + 'start' => $now, + )); + break; + default: + throw new Exception( + pht( + 'Unknown import trigger frequency "%s".', + $frequency)); + } + + // If the object has been disabled, don't write a new trigger. + if ($object->getIsDisabled()) { + $clock = null; + } + + if ($clock) { + $trigger_action = new PhabricatorScheduleTaskTriggerAction( + array( + 'class' => 'PhabricatorCalendarImportReloadWorker', + 'data' => array( + 'importPHID' => $object->getPHID(), + ), + 'options' => array( + 'objectPHID' => $object->getPHID(), + 'priority' => PhabricatorWorker::PRIORITY_BULK, + ), + )); + + $trigger_phid = PhabricatorPHID::generateNewPHID( + PhabricatorWorkerTriggerPHIDType::TYPECONST); + + $object + ->setTriggerPHID($trigger_phid) + ->save(); + + $trigger = id(new PhabricatorWorkerTrigger()) + ->setClock($clock) + ->setAction($trigger_action) + ->setPHID($trigger_phid) + ->save(); + } else { + $object + ->setTriggerPHID(null) + ->save(); + } + } + return $xactions; } diff --git a/src/applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php index 0b159b62c6..2bc4ac37ff 100644 --- a/src/applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php @@ -17,6 +17,10 @@ final class PhabricatorCalendarICSFileImportEngine return pht('Import an event in ".ics" (iCalendar) format.'); } + public function supportsTriggers(PhabricatorCalendarImport $import) { + return false; + } + public function appendImportProperties( PhabricatorUser $viewer, PhabricatorCalendarImport $import, diff --git a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php index 06693fd40d..82f6191c24 100644 --- a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php @@ -17,6 +17,10 @@ final class PhabricatorCalendarICSURIImportEngine return pht('Import or subscribe to a calendar in .ics format by URI.'); } + public function supportsTriggers(PhabricatorCalendarImport $import) { + return true; + } + public function appendImportProperties( PhabricatorUser $viewer, PhabricatorCalendarImport $import, diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index 0b91a6244c..7f51b5f74e 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -39,6 +39,9 @@ abstract class PhabricatorCalendarImportEngine throw new PhutilMethodNotImplementedException(); } + abstract public function supportsTriggers( + PhabricatorCalendarImport $import); + final public static function getAllImportEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php new file mode 100644 index 0000000000..b41a893850 --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php @@ -0,0 +1,32 @@ +setEditPolicy($actor->getPHID()) ->setIsDisabled(0) ->setEngineType($engine->getImportEngineType()) - ->attachEngine($engine); + ->attachEngine($engine) + ->setTriggerFrequency(self::FREQUENCY_ONCE); } protected function getConfiguration() { @@ -40,6 +47,8 @@ final class PhabricatorCalendarImport 'name' => 'text', 'engineType' => 'text64', 'isDisabled' => 'bool', + 'triggerPHID' => 'phid?', + 'triggerFrequency' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_author' => array( @@ -85,6 +94,21 @@ final class PhabricatorCalendarImport return $this->getEngine()->getDisplayName($this); } + public static function getTriggerFrequencyMap() { + return array( + self::FREQUENCY_ONCE => array( + 'name' => pht('No Automatic Updates'), + ), + self::FREQUENCY_HOURLY => array( + 'name' => pht('Update Hourly'), + ), + self::FREQUENCY_DAILY => array( + 'name' => pht('Update Daily'), + ), + ); + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -155,6 +179,17 @@ final class PhabricatorCalendarImport $this->openTransaction(); + $trigger_phid = $this->getTriggerPHID(); + if ($trigger_phid) { + $trigger = id(new PhabricatorWorkerTriggerQuery()) + ->setViewer($viewer) + ->withPHIDs(array($trigger_phid)) + ->executeOne(); + if ($trigger) { + $engine->destroyObject($trigger); + } + } + $events = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withImportSourcePHIDs(array($this->getPHID())) diff --git a/src/applications/calendar/worker/PhabricatorCalendarImportReloadWorker.php b/src/applications/calendar/worker/PhabricatorCalendarImportReloadWorker.php new file mode 100644 index 0000000000..3abc9479c6 --- /dev/null +++ b/src/applications/calendar/worker/PhabricatorCalendarImportReloadWorker.php @@ -0,0 +1,48 @@ +loadImport(); + $viewer = PhabricatorUser::getOmnipotentUser(); + + if ($import->getIsDisabled()) { + return; + } + + $author = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($import->getAuthorPHID())) + ->needUserSettings(true) + ->executeOne(); + + $import_engine = $import->getEngine(); + + $import->newLogMessage( + PhabricatorCalendarImportTriggerLogType::LOGTYPE, + array()); + + $import_engine->importEventsFromSource($author, $import); + } + + private function loadImport() { + $viewer = PhabricatorUser::getOmnipotentUser(); + + $data = $this->getTaskData(); + $import_phid = idx($data, 'importPHID'); + + $import = id(new PhabricatorCalendarImportQuery()) + ->setViewer($viewer) + ->withPHIDs(array($import_phid)) + ->executeOne(); + if (!$import) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Failed to load import with PHID "%s".', + $import_phid)); + } + + return $import; + } + +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarImportFrequencyTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarImportFrequencyTransaction.php new file mode 100644 index 0000000000..177adbbf0b --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarImportFrequencyTransaction.php @@ -0,0 +1,45 @@ +getTriggerFrequency(); + } + + public function applyInternalEffects($object, $value) { + $object->setTriggerFrequency($value); + } + + public function getTitle() { + return pht( + '%s changed the automatic update frequency for this import.', + $this->renderAuthor()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $frequency_map = PhabricatorCalendarImport::getTriggerFrequencyMap(); + $valid = array_keys($frequency_map); + $valid = array_fuse($valid); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + + if (!isset($valid[$value])) { + $errors[] = $this->newInvalidError( + pht( + 'Import frequency "%s" is not valid. Valid frequences are: %s.', + $value, + implode(', ', $valid)), + $xaction); + } + } + + return $errors; + } + +}