diff --git a/resources/sql/autopatches/20161025.phortune.contact.1.sql b/resources/sql/autopatches/20161025.phortune.contact.1.sql new file mode 100644 index 0000000000..48bacd1a21 --- /dev/null +++ b/resources/sql/autopatches/20161025.phortune.contact.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phortune.phortune_merchant + ADD contactInfo LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL; 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/resources/sql/autopatches/20161027.calendar.01.externalinvitee.sql b/resources/sql/autopatches/20161027.calendar.01.externalinvitee.sql new file mode 100644 index 0000000000..0d3843c7d3 --- /dev/null +++ b/resources/sql/autopatches/20161027.calendar.01.externalinvitee.sql @@ -0,0 +1,12 @@ +CREATE TABLE {$NAMESPACE}_calendar.calendar_externalinvitee ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + nameIndex BINARY(12) NOT NULL, + uri LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + parameters LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + sourcePHID VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_name` (`nameIndex`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index eafb0acdd5..cb4a6c15df 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2098,6 +2098,9 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php', 'PhabricatorCalendarExportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php', 'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php', + 'PhabricatorCalendarExternalInvitee' => 'applications/calendar/storage/PhabricatorCalendarExternalInvitee.php', + 'PhabricatorCalendarExternalInviteePHIDType' => 'applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php', + 'PhabricatorCalendarExternalInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', 'PhabricatorCalendarICSFileImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php', @@ -2121,6 +2124,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', @@ -2137,10 +2141,14 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportOrphanLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php', 'PhabricatorCalendarImportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarImportPHIDType.php', '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', @@ -6934,6 +6942,12 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarExportTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarExternalInvitee' => array( + 'PhabricatorCalendarDAO', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorCalendarExternalInviteePHIDType' => 'PhabricatorPHIDType', + 'PhabricatorCalendarExternalInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', 'PhabricatorCalendarICSFileImportEngine' => 'PhabricatorCalendarICSImportEngine', @@ -6962,6 +6976,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportFetchLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType', + 'PhabricatorCalendarImportFrequencyTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportICSURITransaction' => 'PhabricatorCalendarImportTransactionType', @@ -6982,10 +6997,14 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportOrphanLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportPHIDType' => 'PhabricatorPHIDType', '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/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php index fa95f6951b..1e1a125cde 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -85,6 +85,8 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication { => 'PhabricatorCalendarImportDisableController', 'delete/(?P[1-9]\d*)/' => 'PhabricatorCalendarImportDeleteController', + 'reload/(?P[1-9]\d*)/' + => 'PhabricatorCalendarImportReloadController', 'drop/' => 'PhabricatorCalendarImportDropController', 'log/' => array( diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php index 711b2eeb3e..2436f2f96d 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php @@ -3,6 +3,10 @@ final class PhabricatorCalendarEventListController extends PhabricatorCalendarController { + private $viewYear; + private $viewMonth; + private $viewDay; + public function shouldAllowPublic() { return true; } @@ -16,6 +20,10 @@ final class PhabricatorCalendarEventListController $month = $request->getURIData('month'); $day = $request->getURIData('day'); + $this->viewYear = $year; + $this->viewMonth = $month; + $this->viewDay = $day; + $engine = new PhabricatorCalendarEventSearchEngine(); if ($month && $year) { @@ -33,9 +41,36 @@ final class PhabricatorCalendarEventListController protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); + $viewer = $this->getViewer(); + + $year = $this->viewYear; + $month = $this->viewMonth; + $day = $this->viewDay; + + $parameters = array(); + + // If the viewer clicks "Create Event" while on a particular day view, + // default the times to that day. + if ($year && $month && $day) { + $datetimes = PhabricatorCalendarEvent::newDefaultEventDateTimes( + $viewer, + PhabricatorTime::getNow()); + + foreach ($datetimes as $datetime) { + $datetime + ->setYear($year) + ->setMonth($month) + ->setDay($day); + } + + list($start, $end) = $datetimes; + $parameters['start'] = $start->getEpoch(); + $parameters['end'] = $end->getEpoch(); + } + id(new PhabricatorCalendarEventEditEngine()) ->setViewer($this->getViewer()) - ->addActionToCrumbs($crumbs); + ->addActionToCrumbs($crumbs, $parameters); return $crumbs; } 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/PhabricatorCalendarImportReloadController.php b/src/applications/calendar/controller/PhabricatorCalendarImportReloadController.php new file mode 100644 index 0000000000..df9e63b0ad --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarImportReloadController.php @@ -0,0 +1,52 @@ +getViewer(); + + $import = id(new PhabricatorCalendarImportQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$import) { + return new Aphront404Response(); + } + + $import_uri = $import->getURI(); + + if ($request->isFormPost()) { + $xactions = array(); + $xactions[] = id(new PhabricatorCalendarImportTransaction()) + ->setTransactionType( + PhabricatorCalendarImportReloadTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $editor = id(new PhabricatorCalendarImportEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($import, $xactions); + + return id(new AphrontRedirectResponse())->setURI($import_uri); + } + + return $this->newDialog() + ->setTitle(pht('Reload Events')) + ->appendParagraph( + pht( + 'Reload this source? Events imported from this source will '. + 'be updated.')) + ->addCancelButton($import_uri) + ->addSubmitButton(pht('Reload Events')); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index 59b0f86915..9c07d371d0 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -105,6 +105,17 @@ final class PhabricatorCalendarImportViewController ->setWorkflow(!$can_edit) ->setHref($edit_uri)); + $reload_uri = "import/reload/{$id}/"; + $reload_uri = $this->getApplicationURI($reload_uri); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Reload Import')) + ->setIcon('fa-refresh') + ->setDisabled(!$can_edit) + ->setWorkflow(true) + ->setHref($reload_uri)); + $disable_uri = "import/disable/{$id}/"; $disable_uri = $this->getApplicationURI($disable_uri); if ($import->getIsDisabled()) { @@ -123,7 +134,6 @@ final class PhabricatorCalendarImportViewController ->setWorkflow(true) ->setHref($disable_uri)); - if ($can_edit) { $can_delete = $engine->canDeleteAnyEvents($viewer, $import); } else { @@ -157,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/PhabricatorCalendarEventEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php index 17ea7552e9..fef14c13e1 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php @@ -164,6 +164,8 @@ final class PhabricatorCalendarEventEditEngine if ($this->getIsCreate() || $object->getIsRecurring()) { $fields[] = id(new PhabricatorEpochEditField()) + ->setIsLockable(false) + ->setIsDefaultable(false) ->setAllowNull(true) ->setKey('until') ->setLabel(pht('Repeat Until')) @@ -189,6 +191,8 @@ final class PhabricatorCalendarEventEditEngine $fields[] = id(new PhabricatorEpochEditField()) ->setKey('start') ->setLabel(pht('Start')) + ->setIsLockable(false) + ->setIsDefaultable(false) ->setTransactionType( PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE) ->setDescription(pht('Start time of the event.')) @@ -199,6 +203,8 @@ final class PhabricatorCalendarEventEditEngine $fields[] = id(new PhabricatorEpochEditField()) ->setKey('end') ->setLabel(pht('End')) + ->setIsLockable(false) + ->setIsDefaultable(false) ->setTransactionType( PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE) ->setDescription(pht('End time of the event.')) diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php index 655037c140..d22d34bee7 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -34,25 +34,22 @@ final class PhabricatorCalendarEventEditor } $actor = $this->getActor(); + + $invitees = $event->getInvitees(); + $event->copyFromParent($actor); $event->setIsStub(0); - $invitees = $event->getParentEvent()->getInvitees(); + $event->openTransaction(); + $event->save(); + foreach ($invitees as $invitee) { + $invitee + ->setEventPHID($event->getPHID()) + ->save(); + } + $event->saveTransaction(); - $new_invitees = array(); - foreach ($invitees as $invitee) { - $invitee = id(new PhabricatorCalendarEventInvitee()) - ->setEventPHID($event->getPHID()) - ->setInviteePHID($invitee->getInviteePHID()) - ->setInviterPHID($invitee->getInviterPHID()) - ->setStatus($invitee->getStatus()) - ->save(); - - $new_invitees[] = $invitee; - } - - $event->save(); - $event->attachInvitees($new_invitees); + $event->attachInvitees($invitees); } public function getTransactionTypes() { diff --git a/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php index 031a890d59..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') @@ -101,8 +105,44 @@ final class PhabricatorCalendarImportEditEngine ->setConduitDescription(pht('Disable or restore the import.')) ->setConduitTypeDescription(pht('True to cancel the import.')) ->setValue($object->getIsDisabled()), + id(new PhabricatorBoolEditField()) + ->setKey('delete') + ->setLabel(pht('Delete Imported Events')) + ->setDescription(pht('Delete all events from this source.')) + ->setTransactionType( + PhabricatorCalendarImportDisableTransaction::TRANSACTIONTYPE) + ->setIsConduitOnly(true) + ->setConduitDescription(pht('Disable or restore the import.')) + ->setConduitTypeDescription(pht('True to delete imported events.')) + ->setValue(false), + id(new PhabricatorBoolEditField()) + ->setKey('reload') + ->setLabel(pht('Reload Import')) + ->setDescription(pht('Reload events imported from this source.')) + ->setTransactionType( + PhabricatorCalendarImportDisableTransaction::TRANSACTIONTYPE) + ->setIsConduitOnly(true) + ->setConduitDescription(pht('Disable or restore the import.')) + ->setConduitTypeDescription(pht('True to reload the import.')) + ->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 fe22a9f8c9..2631dc1774 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php @@ -27,12 +27,110 @@ final class PhabricatorCalendarImportEditor protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { + $actor = $this->getActor(); - if ($this->getIsNewObject()) { - $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) { + $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) { $import_engine = $object->getEngine(); - $import_engine->didCreateImport($actor, $object); + $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 7284776da6..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, @@ -59,7 +63,7 @@ final class PhabricatorCalendarICSFileImportEngine } } - public function didCreateImport( + public function importEventsFromSource( PhabricatorUser $viewer, PhabricatorCalendarImport $import) { diff --git a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php index 7b1754a911..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, @@ -71,7 +75,7 @@ final class PhabricatorCalendarICSURIImportEngine return pht('ICS URI'); } - public function didCreateImport( + public function importEventsFromSource( PhabricatorUser $viewer, PhabricatorCalendarImport $import) { diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index 2fc9399fe3..1eae386869 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -25,7 +25,7 @@ abstract class PhabricatorCalendarImportEngine abstract public function getDisplayName(PhabricatorCalendarImport $import); - abstract public function didCreateImport( + abstract public function importEventsFromSource( PhabricatorUser $viewer, PhabricatorCalendarImport $import); @@ -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__) @@ -204,6 +207,8 @@ abstract class PhabricatorCalendarImportEngine $xactions = array(); $update_map = array(); + $invitee_map = array(); + $attendee_map = array(); foreach ($node_map as $full_uid => $node) { $event = idx($events, $full_uid); if (!$event) { @@ -219,6 +224,66 @@ abstract class PhabricatorCalendarImportEngine $this->updateEventFromNode($viewer, $event, $node); $xactions[$full_uid] = $this->newUpdateTransactions($event, $node); $update_map[$full_uid] = $event; + + $attendees = $node->getAttendees(); + $private_index = 1; + foreach ($attendees as $attendee) { + // Generate a "name" for this attendee which is not an email address. + // We avoid disclosing email addresses to be consistent with the rest + // of the product. + $name = $attendee->getName(); + if (preg_match('/@/', $name)) { + $name = new PhutilEmailAddress($name); + $name = $name->getDisplayName(); + } + + // If we don't have a name or the name still looks like it's an + // email address, give them a dummy placeholder name. + if (!strlen($name) || preg_match('/@/', $name)) { + $name = pht('Private User %d', $private_index); + $private_index++; + } + + $attendee_map[$full_uid][$name] = $attendee; + } + } + + $attendee_names = array(); + foreach ($attendee_map as $full_uid => $event_attendees) { + foreach ($event_attendees as $name => $attendee) { + $attendee_names[$name] = $attendee; + } + } + + if ($attendee_names) { + $external_invitees = id(new PhabricatorCalendarExternalInviteeQuery()) + ->setViewer($viewer) + ->withNames($attendee_names) + ->execute(); + $external_invitees = mpull($external_invitees, null, 'getName'); + + foreach ($attendee_names as $name => $attendee) { + if (isset($external_invitees[$name])) { + continue; + } + + $external_invitee = id(new PhabricatorCalendarExternalInvitee()) + ->setName($name) + ->setURI($attendee->getURI()) + ->setSourcePHID($import->getPHID()); + + try { + $external_invitee->save(); + } catch (AphrontDuplicateKeyQueryException $ex) { + $external_invitee = + id(new PhabricatorCalendarExternalInviteeQuery()) + ->setViewer($viewer) + ->withNames(array($name)) + ->executeOne(); + } + + $external_invitees[$name] = $external_invitee; + } } // Reorder events so we create parents first. This allows us to populate @@ -285,6 +350,51 @@ abstract class PhabricatorCalendarImportEngine $editor->applyTransactions($event, $event_xactions); + // We're just forcing attendees to the correct values here because + // transactions intentionally don't let you RSVP for other users. This + // might need to be turned into a special type of transaction eventually. + $attendees = $attendee_map[$full_uid]; + $old_map = $event->getInvitees(); + $old_map = mpull($old_map, null, 'getInviteePHID'); + + $new_map = array(); + foreach ($attendees as $name => $attendee) { + $phid = $external_invitees[$name]->getPHID(); + + $invitee = idx($old_map, $phid); + if (!$invitee) { + $invitee = id(new PhabricatorCalendarEventInvitee()) + ->setEventPHID($event->getPHID()) + ->setInviteePHID($phid) + ->setInviterPHID($import->getPHID()); + } + + switch ($attendee->getStatus()) { + case PhutilCalendarUserNode::STATUS_ACCEPTED: + $status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; + break; + case PhutilCalendarUserNode::STATUS_DECLINED: + $status = PhabricatorCalendarEventInvitee::STATUS_DECLINED; + break; + case PhutilCalendarUserNode::STATUS_INVITED: + default: + $status = PhabricatorCalendarEventInvitee::STATUS_INVITED; + break; + } + $invitee->setStatus($status); + $invitee->save(); + + $new_map[$phid] = $invitee; + } + + foreach ($old_map as $phid => $invitee) { + if (empty($new_map[$phid])) { + $invitee->delete(); + } + } + + $event->attachInvitees($new_map); + $import->newLogMessage( PhabricatorCalendarImportUpdateLogType::LOGTYPE, array( @@ -403,6 +513,9 @@ abstract class PhabricatorCalendarImportEngine $until_datetime->setViewerTimezone($timezone); $event->setUntilDateTime($until_datetime); } + + $count = $rrule->getCount(); + $event->setParameter('recurrenceCount', $count); } return $event; 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 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $invitee = $objects[$phid]; + + $name = $invitee->getName(); + $handle->setName($name); + } + } +} diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index 9949ee2f5a..5c6b34c9fb 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -633,6 +633,11 @@ final class PhabricatorCalendarEventQuery PhabricatorCalendarEvent $event, $raw_limit) { + $count = $event->getRecurrenceCount(); + if ($count && ($count <= $raw_limit)) { + return ($count - 1); + } + return $raw_limit; } diff --git a/src/applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php b/src/applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php new file mode 100644 index 0000000000..ea7200e614 --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php @@ -0,0 +1,68 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withNames(array $names) { + $this->names = $names; + return $this; + } + + public function newResultObject() { + return new PhabricatorCalendarExternalInvitee(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->names !== null) { + $name_indexes = array(); + foreach ($this->names as $name) { + $name_indexes[] = PhabricatorHash::digestForIndex($name); + } + $where[] = qsprintf( + $conn, + 'nameIndex IN (%Ls)', + $name_indexes); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorCalendarApplication'; + } + +} diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 64bdf07d46..c5d5c898f9 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -27,7 +27,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO protected $isRecurring = 0; - private $isGhostEvent = false; protected $instanceOfEventPHID; protected $sequenceIndex; @@ -60,6 +59,9 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO protected $recurrenceEndDate; protected $recurrenceFrequency = array(); + private $isGhostEvent = false; + private $stubInvitees; + public static function initializeNewCalendarEvent(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) @@ -75,10 +77,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO $default_icon = 'fa-calendar'; - $datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch( - $now, - $actor->getTimezoneIdentifier()); - $datetime_end = $datetime_start->newRelativeDateTime('PT1H'); + $datetime_defaults = self::newDefaultEventDateTimes( + $actor, + $now); + list($datetime_start, $datetime_end) = $datetime_defaults; return id(new PhabricatorCalendarEvent()) ->setDescription('') @@ -102,6 +104,31 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->applyViewerTimezone($actor); } + public static function newDefaultEventDateTimes( + PhabricatorUser $viewer, + $now) { + + $datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch( + $now, + $viewer->getTimezoneIdentifier()); + + // Advance the time by an hour, then round downwards to the nearest hour. + // For example, if it is currently 3:25 PM, we suggest a default start time + // of 4 PM. + $datetime_start = $datetime_start + ->newRelativeDateTime('PT1H') + ->newAbsoluteDateTime(); + $datetime_start->setMinute(0); + $datetime_start->setSecond(0); + + // Default the end time to an hour after the start time. + $datetime_end = $datetime_start + ->newRelativeDateTime('PT1H') + ->newAbsoluteDateTime(); + + return array($datetime_start, $datetime_end); + } + private function newChild( PhabricatorUser $actor, $sequence, @@ -226,10 +253,16 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return null; } + $limit = $sequence + 1; + $count = $this->getRecurrenceCount(); + if ($count && ($count < $limit)) { + return null; + } + $instances = $set->getEventsBetween( null, $this->newUntilDateTime(), - $sequence + 1); + $limit); return idx($instances, $sequence, null); } @@ -418,9 +451,34 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO } public function getInvitees() { + if ($this->getIsGhostEvent() || $this->getIsStub()) { + if ($this->stubInvitees === null) { + $this->stubInvitees = $this->newStubInvitees(); + } + return $this->stubInvitees; + } + return $this->assertAttached($this->invitees); } + private function newStubInvitees() { + $parent = $this->getParentEvent(); + + $parent_invitees = $parent->getInvitees(); + $stub_invitees = array(); + + foreach ($parent_invitees as $invitee) { + $stub_invitee = id(new PhabricatorCalendarEventInvitee()) + ->setInviteePHID($invitee->getInviteePHID()) + ->setInviterPHID($invitee->getInviterPHID()) + ->setStatus(PhabricatorCalendarEventInvitee::STATUS_INVITED); + + $stub_invitees[] = $stub_invitee; + } + + return $stub_invitees; + } + public function attachInvitees(array $invitees) { $this->invitees = $invitees; return $this; @@ -447,6 +505,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO if (!$invited) { return PhabricatorCalendarEventInvitee::STATUS_UNINVITED; } + $invited = $invited->getStatus(); return $invited; } @@ -907,9 +966,24 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO $rrule->setUntil($until); } + $count = $this->getRecurrenceCount(); + if ($count) { + $rrule->setCount($count); + } + return $rrule; } + public function getRecurrenceCount() { + $count = (int)$this->getParameter('recurrenceCount'); + + if (!$count) { + return null; + } + + return $count; + } + public function newRecurrenceSet() { if ($this->isChildEvent()) { return $this->getParentEvent()->newRecurrenceSet(); diff --git a/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php b/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php new file mode 100644 index 0000000000..b1a85cf520 --- /dev/null +++ b/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php @@ -0,0 +1,74 @@ +setInviterPHID($actor->getPHID()) + ->setStatus(self::STATUS_INVITED) + ->setEventPHID($event->getPHID()); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'parameters' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text', + 'nameIndex' => 'bytes12', + 'uri' => 'text', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_name' => array( + 'columns' => array('nameIndex'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorCalendarExternalInviteePHIDType::TYPECONST; + } + + public function save() { + $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); + return parent::save(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::getMostOpenPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } +} diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php index 93b0897ee5..7fdb3cd837 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarImport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php @@ -14,6 +14,12 @@ final class PhabricatorCalendarImport protected $engineType; protected $parameters = array(); protected $isDisabled = 0; + protected $triggerPHID; + protected $triggerFrequency; + + const FREQUENCY_ONCE = 'once'; + const FREQUENCY_HOURLY = 'hourly'; + const FREQUENCY_DAILY = 'daily'; private $engine = self::ATTACHABLE; @@ -27,7 +33,8 @@ final class PhabricatorCalendarImport ->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/PhabricatorCalendarEventInviteTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php index ea9355eb25..f1da2c1d77 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php @@ -30,10 +30,11 @@ final class PhabricatorCalendarEventInviteTransaction $map = array(); foreach ($add as $phid) { - $map[$phid] = $status_invited; + $map[$phid] = $status_invited; } + foreach ($rem as $phid) { - $map[$phid] = $status_uninvited; + $map[$phid] = $status_uninvited; } // If we're creating this event and the actor is inviting themselves, 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; + } + +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarImportReloadTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarImportReloadTransaction.php new file mode 100644 index 0000000000..ad9a5e1c29 --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarImportReloadTransaction.php @@ -0,0 +1,23 @@ +renderAuthor()); + } + +} diff --git a/src/applications/phortune/controller/PhortuneMerchantEditController.php b/src/applications/phortune/controller/PhortuneMerchantEditController.php index 3a6aad215f..4def6edebf 100644 --- a/src/applications/phortune/controller/PhortuneMerchantEditController.php +++ b/src/applications/phortune/controller/PhortuneMerchantEditController.php @@ -47,6 +47,7 @@ final class PhortuneMerchantEditController $e_name = true; $v_name = $merchant->getName(); $v_desc = $merchant->getDescription(); + $v_cont = $merchant->getContactInfo(); $v_members = $merchant->getMemberPHIDs(); $e_members = null; @@ -54,12 +55,14 @@ final class PhortuneMerchantEditController if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_desc = $request->getStr('desc'); + $v_cont = $request->getStr('cont'); $v_view = $request->getStr('viewPolicy'); $v_edit = $request->getStr('editPolicy'); $v_members = $request->getArr('memberPHIDs'); $type_name = PhortuneMerchantTransaction::TYPE_NAME; $type_desc = PhortuneMerchantTransaction::TYPE_DESCRIPTION; + $type_cont = PhortuneMerchantTransaction::TYPE_CONTACTINFO; $type_edge = PhabricatorTransactions::TYPE_EDGE; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; @@ -75,6 +78,10 @@ final class PhortuneMerchantEditController ->setTransactionType($type_desc) ->setNewValue($v_desc); + $xactions[] = id(new PhortuneMerchantTransaction()) + ->setTransactionType($type_cont) + ->setNewValue($v_cont); + $xactions[] = id(new PhortuneMerchantTransaction()) ->setTransactionType($type_view) ->setNewValue($v_view); @@ -127,6 +134,12 @@ final class PhortuneMerchantEditController ->setName('desc') ->setLabel(pht('Description')) ->setValue($v_desc)) + ->appendChild( + id(new PhabricatorRemarkupControl()) + ->setUser($viewer) + ->setName('cont') + ->setLabel(pht('Contact Info')) + ->setValue($v_cont)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) diff --git a/src/applications/phortune/controller/PhortuneMerchantViewController.php b/src/applications/phortune/controller/PhortuneMerchantViewController.php index a0e1100004..cc94da6f4f 100644 --- a/src/applications/phortune/controller/PhortuneMerchantViewController.php +++ b/src/applications/phortune/controller/PhortuneMerchantViewController.php @@ -36,7 +36,6 @@ final class PhortuneMerchantViewController ->execute(); $details = $this->buildDetailsView($merchant, $providers); - $description = $this->buildDescriptionView($merchant); $curtain = $this->buildCurtainView($merchant); $provider_list = $this->buildProviderList( @@ -53,7 +52,6 @@ final class PhortuneMerchantViewController ->setCurtain($curtain) ->setMainColumn(array( $details, - $description, $provider_list, $timeline, )); @@ -130,30 +128,30 @@ final class PhortuneMerchantViewController $view->addProperty(pht('Status'), $status_view); + $description = $merchant->getDescription(); + if (strlen($description)) { + $description = new PHUIRemarkupView($viewer, $description); + $view->addSectionHeader( + pht('Description'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($description); + } + + $contact_info = $merchant->getContactInfo(); + if (strlen($contact_info)) { + $contact_info = new PHUIRemarkupView($viewer, $contact_info); + $view->addSectionHeader( + pht('Contact Info'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($contact_info); + } + return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Details')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($view); } - private function buildDescriptionView(PhortuneMerchant $merchant) { - $viewer = $this->getViewer(); - $view = id(new PHUIPropertyListView()) - ->setUser($viewer); - - $description = $merchant->getDescription(); - if (strlen($description)) { - $description = new PHUIRemarkupView($viewer, $description); - $view->addTextContent($description); - return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Description')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($view); - } - - return null; - } - private function buildCurtainView(PhortuneMerchant $merchant) { $viewer = $this->getRequest()->getUser(); $id = $merchant->getID(); diff --git a/src/applications/phortune/editor/PhortuneMerchantEditor.php b/src/applications/phortune/editor/PhortuneMerchantEditor.php index 1c659c0d5f..20f329b202 100644 --- a/src/applications/phortune/editor/PhortuneMerchantEditor.php +++ b/src/applications/phortune/editor/PhortuneMerchantEditor.php @@ -16,6 +16,7 @@ final class PhortuneMerchantEditor $types[] = PhortuneMerchantTransaction::TYPE_NAME; $types[] = PhortuneMerchantTransaction::TYPE_DESCRIPTION; + $types[] = PhortuneMerchantTransaction::TYPE_CONTACTINFO; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDGE; @@ -30,6 +31,8 @@ final class PhortuneMerchantEditor return $object->getName(); case PhortuneMerchantTransaction::TYPE_DESCRIPTION: return $object->getDescription(); + case PhortuneMerchantTransaction::TYPE_CONTACTINFO: + return $object->getContactInfo(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -42,6 +45,7 @@ final class PhortuneMerchantEditor switch ($xaction->getTransactionType()) { case PhortuneMerchantTransaction::TYPE_NAME: case PhortuneMerchantTransaction::TYPE_DESCRIPTION: + case PhortuneMerchantTransaction::TYPE_CONTACTINFO: return $xaction->getNewValue(); } @@ -59,6 +63,9 @@ final class PhortuneMerchantEditor case PhortuneMerchantTransaction::TYPE_DESCRIPTION: $object->setDescription($xaction->getNewValue()); return; + case PhortuneMerchantTransaction::TYPE_CONTACTINFO: + $object->setContactInfo($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); @@ -71,6 +78,7 @@ final class PhortuneMerchantEditor switch ($xaction->getTransactionType()) { case PhortuneMerchantTransaction::TYPE_NAME: case PhortuneMerchantTransaction::TYPE_DESCRIPTION: + case PhortuneMerchantTransaction::TYPE_CONTACTINFO: return; } diff --git a/src/applications/phortune/storage/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php index 2f09abd6d7..fef1728014 100644 --- a/src/applications/phortune/storage/PhortuneMerchant.php +++ b/src/applications/phortune/storage/PhortuneMerchant.php @@ -8,6 +8,7 @@ final class PhortuneMerchant extends PhortuneDAO protected $name; protected $viewPolicy; protected $description; + protected $contactInfo; private $memberPHIDs = self::ATTACHABLE; @@ -23,6 +24,7 @@ final class PhortuneMerchant extends PhortuneDAO self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'description' => 'text', + 'contactInfo' => 'text', ), ) + parent::getConfiguration(); } diff --git a/src/applications/phortune/storage/PhortuneMerchantTransaction.php b/src/applications/phortune/storage/PhortuneMerchantTransaction.php index 32c32b0778..9c284ca7fd 100644 --- a/src/applications/phortune/storage/PhortuneMerchantTransaction.php +++ b/src/applications/phortune/storage/PhortuneMerchantTransaction.php @@ -5,6 +5,7 @@ final class PhortuneMerchantTransaction const TYPE_NAME = 'merchant:name'; const TYPE_DESCRIPTION = 'merchant:description'; + const TYPE_CONTACTINFO = 'merchant:contactinfo'; public function getApplicationName() { return 'phortune'; @@ -42,6 +43,10 @@ final class PhortuneMerchantTransaction return pht( '%s updated the description for this merchant.', $this->renderHandleLink($author_phid)); + case self::TYPE_CONTACTINFO: + return pht( + '%s updated the contact information for this merchant.', + $this->renderHandleLink($author_phid)); } return parent::getTitle(); @@ -51,6 +56,7 @@ final class PhortuneMerchantTransaction $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: + case self::TYPE_CONTACTINFO: return ($old === null); } return parent::shouldHide(); @@ -60,6 +66,8 @@ final class PhortuneMerchantTransaction switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return ($this->getOldValue() !== null); + case self::TYPE_CONTACTINFO: + return ($this->getOldValue() !== null); } return parent::hasChangeDetails(); diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php index 19399150e8..097fb54dd9 100644 --- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php +++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php @@ -36,17 +36,32 @@ final class PhortuneSubscriptionWorker extends PhabricatorWorker { ->setSubscription($subscription); // TODO: This isn't really ideal. It would be better to use an application - // actor than the original author of the subscription. In particular, if - // someone initiates a subscription, adds some other account managers, and - // later leaves the company, they'll continue "acting" here indefinitely. + // actor than a fairly arbitrary account member. + // However, for now, some of the stuff later in the pipeline requires a // valid actor with a real PHID. The subscription should eventually be // able to create these invoices "as" the application it is acting on // behalf of. - $actor = id(new PhabricatorPeopleQuery()) + + $members = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) - ->withPHIDs(array($subscription->getAuthorPHID())) - ->executeOne(); + ->withPHIDs($account->getMemberPHIDs()) + ->execute(); + $actor = null; + foreach ($members as $member) { + + // Don't act as a disabled user. If all of the users on the account are + // disabled this means we won't charge the subscription, but that's + // probably correct since it means no one can cancel or pay it anyway. + if ($member->getIsDisabled()) { + continue; + } + + // For now, just pick the first valid user we encounter as the actor. + $actor = $member; + break; + } + if (!$actor) { throw new Exception(pht('Failed to load actor to bill subscription!')); } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 8a71509c99..30f8aec320 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -1349,7 +1349,9 @@ abstract class PhabricatorEditEngine } - final public function addActionToCrumbs(PHUICrumbsView $crumbs) { + final public function addActionToCrumbs( + PHUICrumbsView $crumbs, + array $parameters = array()) { $viewer = $this->getViewer(); $can_create = $this->hasCreateCapability(); @@ -1385,6 +1387,11 @@ abstract class PhabricatorEditEngine $form_key = $config->getIdentifier(); $create_uri = $this->getEditURI(null, "form/{$form_key}/"); + if ($parameters) { + $create_uri = (string)id(new PhutilURI($create_uri)) + ->setQueryParams($parameters); + } + if (count($configs) > 1) { $menu_icon = 'fa-caret-square-o-down'; @@ -1395,6 +1402,11 @@ abstract class PhabricatorEditEngine $form_key = $config->getIdentifier(); $config_uri = $this->getEditURI(null, "form/{$form_key}/"); + if ($parameters) { + $config_uri = (string)id(new PhutilURI($config_uri)) + ->setQueryParams($parameters); + } + $item_icon = 'fa-plus'; $dropdown->addAction( diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php index 38a4fda0a6..e169171f98 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php @@ -128,6 +128,9 @@ final class PhabricatorEditEngineConfiguration $values = $this->getProperty('defaults', array()); foreach ($fields as $key => $field) { + if (!$field->getIsDefaultable()) { + continue; + } if ($is_new) { if (array_key_exists($key, $values)) { $field->readDefaultValueFromConfiguration($values[$key]); @@ -157,6 +160,11 @@ final class PhabricatorEditEngineConfiguration } } + // If the field isn't lockable, remove any lock we applied. + if (!$field->getIsLockable()) { + $field->setIsLocked(false); + } + $fields = $this->reorderFields($fields); $preamble = $this->getPreamble(); diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index be301ad7a0..aae75ac43d 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -202,6 +202,12 @@ final class PhabricatorEnv extends Phobject { phutil_load_library($library); } + // Drop any class map caches, since they will have generated without + // any classes from libraries. Without this, preflight setup checks can + // cause generation of a setup check cache that omits checks defined in + // libraries, for example. + PhutilClassMapQuery::deleteCaches(); + // If custom libraries specify config options, they won't get default // values as the Default source has already been loaded, so we get it to // pull in all options from non-phabricator libraries now they are loaded.