1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-30 18:52:42 +01:00

(stable) Promote 2016 Week 45

This commit is contained in:
epriestley 2016-11-05 08:24:43 -07:00
commit 111c639551
87 changed files with 3242 additions and 839 deletions

1
bin/calendar Symbolic link
View file

@ -0,0 +1 @@
../scripts/setup/manage_calendar.php

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

@ -89,6 +89,7 @@ return array(
'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49',
'rsrc/css/application/pholio/pholio.css' => 'ca89d380',
'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02',
'rsrc/css/application/phortune/phortune-invoice.css' => '476055e2',
'rsrc/css/application/phortune/phortune.css' => '5b99dae0',
'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad',
'rsrc/css/application/phriction/phriction-document-css.css' => '4282e4ad',
@ -381,9 +382,8 @@ return array(
'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => 'edd1ba66',
'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18',
'rsrc/js/application/calendar/behavior-day-view.js' => '4b3c4443',
'rsrc/js/application/calendar/behavior-event-all-day.js' => '937bb700',
'rsrc/js/application/calendar/behavior-event-all-day.js' => 'b41537c9',
'rsrc/js/application/calendar/behavior-month-view.js' => 'fe33e256',
'rsrc/js/application/calendar/behavior-recurring-edit.js' => '5f1c4d5f',
'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408',
'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '358c717b',
'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '9bbf3762',
@ -650,7 +650,7 @@ return array(
'javelin-behavior-editengine-reorder-configs' => 'd7a74243',
'javelin-behavior-editengine-reorder-fields' => 'b59e1e96',
'javelin-behavior-error-log' => '6882e80a',
'javelin-behavior-event-all-day' => '937bb700',
'javelin-behavior-event-all-day' => 'b41537c9',
'javelin-behavior-fancy-datepicker' => '568931f3',
'javelin-behavior-global-drag-and-drop' => '960f6a39',
'javelin-behavior-herald-rule-editor' => '7ebaeed3',
@ -703,7 +703,6 @@ return array(
'javelin-behavior-project-create' => '065227cc',
'javelin-behavior-quicksand-blacklist' => '7927a7d3',
'javelin-behavior-read-only-warning' => 'ba158207',
'javelin-behavior-recurring-edit' => '5f1c4d5f',
'javelin-behavior-refresh-csrf' => 'ab2f381b',
'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf',
'javelin-behavior-releeph-request-state-change' => 'a0b57eb8',
@ -840,6 +839,7 @@ return array(
'phortune-credit-card-form' => '2290aeef',
'phortune-credit-card-form-css' => '8391eb02',
'phortune-css' => '5b99dae0',
'phortune-invoice-css' => '476055e2',
'phrequent-css' => 'ffc185ad',
'phriction-document-css' => '4282e4ad',
'phui-action-panel-css' => '91c7b835',

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_phortune.phortune_merchant
ADD profileImagePHID VARBINARY(64);

View file

@ -0,0 +1,5 @@
ALTER TABLE {$NAMESPACE}_phortune.phortune_merchant
ADD invoiceEmail VARCHAR(255) COLLATE {$COLLATE_TEXT} NOT NULL;
ALTER TABLE {$NAMESPACE}_phortune.phortune_merchant
ADD invoiceFooter LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL;

View file

@ -0,0 +1,5 @@
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
ADD seriesParentPHID VARBINARY(64);
UPDATE {$NAMESPACE}_calendar.calendar_event
SET seriesParentPHID = instanceOfEventPHID;

View file

@ -0,0 +1,8 @@
CREATE TABLE {$NAMESPACE}_calendar.calendar_notification (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
eventPHID VARBINARY(64) NOT NULL,
utcInitialEpoch INT UNSIGNED NOT NULL,
targetPHID VARBINARY(64) NOT NULL,
didNotifyEpoch INT UNSIGNED NOT NULL,
UNIQUE KEY `key_notify` (eventPHID, utcInitialEpoch, targetPHID)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1 @@
DROP TABLE {$NAMESPACE}_calendar.calendar_holiday;

View file

@ -0,0 +1,17 @@
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
DROP allDayDateFrom;
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
DROP allDayDateTo;
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
DROP dateFrom;
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
DROP dateTo;
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
DROP recurrenceEndDate;
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
DROP recurrenceFrequency;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_calendar.calendar_eventinvitee
ADD availability VARCHAR(64) NOT NULL;

View file

@ -0,0 +1,3 @@
UPDATE {$NAMESPACE}_calendar.calendar_eventinvitee
SET availability = 'default'
WHERE availability = '';

View file

@ -1,62 +0,0 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
// http://www.opm.gov/operating_status_schedules/fedhol/
$holidays = array(
'2014-01-01' => "New Year's Day",
'2014-01-20' => 'Birthday of Martin Luther King, Jr.',
'2014-02-17' => "Washington's Birthday",
'2014-05-26' => 'Memorial Day',
'2014-07-04' => 'Independence Day',
'2014-09-01' => 'Labor Day',
'2014-10-13' => 'Columbus Day',
'2014-11-11' => 'Veterans Day',
'2014-11-27' => 'Thanksgiving Day',
'2014-12-25' => 'Christmas Day',
'2015-01-01' => "New Year's Day",
'2015-01-19' => 'Birthday of Martin Luther King, Jr.',
'2015-02-16' => "Washington's Birthday",
'2015-05-25' => 'Memorial Day',
'2015-07-03' => 'Independence Day',
'2015-09-07' => 'Labor Day',
'2015-10-12' => 'Columbus Day',
'2015-11-11' => 'Veterans Day',
'2015-11-26' => 'Thanksgiving Day',
'2015-12-25' => 'Christmas Day',
'2016-01-01' => "New Year's Day",
'2016-01-18' => 'Birthday of Martin Luther King, Jr.',
'2016-02-15' => "Washington's Birthday",
'2016-05-30' => 'Memorial Day',
'2016-07-04' => 'Independence Day',
'2016-09-05' => 'Labor Day',
'2016-10-10' => 'Columbus Day',
'2016-11-11' => 'Veterans Day',
'2016-11-24' => 'Thanksgiving Day',
'2016-12-26' => 'Christmas Day',
'2017-01-02' => "New Year's Day",
'2017-01-16' => 'Birthday of Martin Luther King, Jr.',
'2017-02-10' => "Washington's Birthday",
'2017-05-29' => 'Memorial Day',
'2017-07-04' => 'Independence Day',
'2017-09-04' => 'Labor Day',
'2017-10-09' => 'Columbus Day',
'2017-11-10' => 'Veterans Day',
'2017-11-23' => 'Thanksgiving Day',
'2017-12-25' => 'Christmas Day',
);
$table = new PhabricatorCalendarHoliday();
$conn_w = $table->establishConnection('w');
$table_name = $table->getTableName();
foreach ($holidays as $day => $name) {
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (day, name) VALUES (%s, %s)',
$table_name,
$day,
$name);
}

View file

@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline(pht('manage Calendar'));
$args->setSynopsis(<<<EOSYNOPSIS
**calendar** __command__ [__options__]
Manage Calendar.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorCalendarManagementWorkflow')
->execute();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);

View file

@ -1687,6 +1687,7 @@ phutil_register_library_map(array(
'PHUITimelineView' => 'view/phui/PHUITimelineView.php',
'PHUITwoColumnView' => 'view/phui/PHUITwoColumnView.php',
'PHUITypeaheadExample' => 'applications/uiexample/examples/PHUITypeaheadExample.php',
'PHUIUserAvailabilityView' => 'applications/calendar/view/PHUIUserAvailabilityView.php',
'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php',
'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.php',
'PassphraseAbstractKey' => 'applications/passphrase/keys/PassphraseAbstractKey.php',
@ -2034,6 +2035,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php',
'PhabricatorCalendarEventAcceptTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAcceptTransaction.php',
'PhabricatorCalendarEventAllDayTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php',
'PhabricatorCalendarEventAvailabilityController' => 'applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php',
'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php',
'PhabricatorCalendarEventCancelTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventCancelTransaction.php',
'PhabricatorCalendarEventDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDateTransaction.php',
@ -2049,6 +2051,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php',
'PhabricatorCalendarEventEndDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php',
'PhabricatorCalendarEventExportController' => 'applications/calendar/controller/PhabricatorCalendarEventExportController.php',
'PhabricatorCalendarEventForkTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventForkTransaction.php',
'PhabricatorCalendarEventFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php',
'PhabricatorCalendarEventFulltextEngine' => 'applications/calendar/search/PhabricatorCalendarEventFulltextEngine.php',
'PhabricatorCalendarEventHeraldAdapter' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php',
@ -2066,6 +2069,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php',
'PhabricatorCalendarEventNameHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventNameHeraldField.php',
'PhabricatorCalendarEventNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventNameTransaction.php',
'PhabricatorCalendarEventNotificationView' => 'applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php',
'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php',
'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php',
'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php',
@ -2101,8 +2105,6 @@ phutil_register_library_map(array(
'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',
'PhabricatorCalendarICSImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSImportEngine.php',
'PhabricatorCalendarICSURIImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php',
@ -2111,6 +2113,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php',
'PhabricatorCalendarImportDefaultLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php',
'PhabricatorCalendarImportDeleteController' => 'applications/calendar/controller/PhabricatorCalendarImportDeleteController.php',
'PhabricatorCalendarImportDeleteLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDeleteLogType.php',
'PhabricatorCalendarImportDeleteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php',
'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php',
'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php',
@ -2128,6 +2131,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php',
'PhabricatorCalendarImportICSLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php',
'PhabricatorCalendarImportICSURITransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php',
'PhabricatorCalendarImportICSWarningLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSWarningLogType.php',
'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php',
'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php',
'PhabricatorCalendarImportLog' => 'applications/calendar/storage/PhabricatorCalendarImportLog.php',
@ -2151,6 +2155,10 @@ phutil_register_library_map(array(
'PhabricatorCalendarImportTriggerLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php',
'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php',
'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php',
'PhabricatorCalendarManagementNotifyWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php',
'PhabricatorCalendarManagementWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementWorkflow.php',
'PhabricatorCalendarNotification' => 'applications/calendar/storage/PhabricatorCalendarNotification.php',
'PhabricatorCalendarNotificationEngine' => 'applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php',
'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php',
'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php',
@ -4173,6 +4181,7 @@ phutil_register_library_map(array(
'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php',
'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php',
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php',
'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php',
@ -4180,11 +4189,13 @@ phutil_register_library_map(array(
'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php',
'PhortuneMerchantController' => 'applications/phortune/controller/PhortuneMerchantController.php',
'PhortuneMerchantEditController' => 'applications/phortune/controller/PhortuneMerchantEditController.php',
'PhortuneMerchantEditEngine' => 'applications/phortune/editor/PhortuneMerchantEditEngine.php',
'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php',
'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php',
'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/PhortuneMerchantInvoiceCreateController.php',
'PhortuneMerchantListController' => 'applications/phortune/controller/PhortuneMerchantListController.php',
'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php',
'PhortuneMerchantPictureController' => 'applications/phortune/controller/PhortuneMerchantPictureController.php',
'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php',
'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php',
'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php',
@ -6453,6 +6464,7 @@ phutil_register_library_map(array(
'PHUITimelineView' => 'AphrontView',
'PHUITwoColumnView' => 'AphrontTagView',
'PHUITypeaheadExample' => 'PhabricatorUIExample',
'PHUIUserAvailabilityView' => 'AphrontTagView',
'PHUIWorkboardView' => 'AphrontTagView',
'PHUIWorkpanelView' => 'AphrontTagView',
'PassphraseAbstractKey' => 'Phobject',
@ -6870,6 +6882,7 @@ phutil_register_library_map(array(
),
'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction',
'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventAvailabilityController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType',
@ -6885,6 +6898,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorCalendarEventEndDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventExportController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventForkTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFrequencyTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorCalendarEventHeraldAdapter' => 'HeraldAdapter',
@ -6905,6 +6919,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorCalendarEventNameHeraldField' => 'PhabricatorCalendarEventHeraldField',
'PhabricatorCalendarEventNameTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventNotificationView' => 'Phobject',
'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand',
@ -6948,8 +6963,6 @@ phutil_register_library_map(array(
),
'PhabricatorCalendarExternalInviteePHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarExternalInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO',
'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase',
'PhabricatorCalendarICSFileImportEngine' => 'PhabricatorCalendarICSImportEngine',
'PhabricatorCalendarICSImportEngine' => 'PhabricatorCalendarImportEngine',
'PhabricatorCalendarICSURIImportEngine' => 'PhabricatorCalendarICSImportEngine',
@ -6963,6 +6976,7 @@ phutil_register_library_map(array(
),
'PhabricatorCalendarImportDefaultLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportDeleteController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDeleteLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportDeleteTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType',
@ -6980,6 +6994,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportICSURITransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSWarningLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportIgnoredNodeLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportLog' => array(
@ -7007,6 +7022,10 @@ phutil_register_library_map(array(
'PhabricatorCalendarImportTriggerLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarManagementNotifyWorkflow' => 'PhabricatorCalendarManagementWorkflow',
'PhabricatorCalendarManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCalendarNotification' => 'PhabricatorCalendarDAO',
'PhabricatorCalendarNotificationEngine' => 'Phobject',
'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec',
@ -9418,6 +9437,7 @@ phutil_register_library_map(array(
'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
'PhortuneDAO' => 'PhabricatorLiskDAO',
'PhortuneErrCode' => 'PhortuneConstants',
'PhortuneInvoiceView' => 'AphrontTagView',
'PhortuneLandingController' => 'PhortuneController',
'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType',
'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType',
@ -9429,11 +9449,13 @@ phutil_register_library_map(array(
'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability',
'PhortuneMerchantController' => 'PhortuneController',
'PhortuneMerchantEditController' => 'PhortuneMerchantController',
'PhortuneMerchantEditEngine' => 'PhabricatorEditEngine',
'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantController',
'PhortuneMerchantListController' => 'PhortuneMerchantController',
'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType',
'PhortuneMerchantPictureController' => 'PhortuneMerchantController',
'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneMerchantTransaction' => 'PhabricatorApplicationTransaction',

View file

@ -3,8 +3,21 @@
final class AphrontBoolHTTPParameterType
extends AphrontHTTPParameterType {
protected function getParameterExists(AphrontRequest $request, $key) {
if ($request->getExists($key)) {
return true;
}
$checkbox_key = $this->getCheckboxKey($key);
if ($request->getExists($checkbox_key)) {
return true;
}
return false;
}
protected function getParameterValue(AphrontRequest $request, $key) {
return $request->getBool($key);
return (bool)$request->getBool($key);
}
protected function getParameterTypeName() {
@ -26,4 +39,8 @@ final class AphrontBoolHTTPParameterType
);
}
public function getCheckboxKey($key) {
return "{$key}.exists";
}
}

View file

@ -61,6 +61,8 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication {
=> 'PhabricatorCalendarEventJoinController',
'export/(?P<id>[1-9]\d*)/(?P<filename>[^/]*)'
=> 'PhabricatorCalendarEventExportController',
'availability/(?P<id>[1-9]\d*)/(?P<availability>[^/]+)/'
=> 'PhabricatorCalendarEventAvailabilityController',
),
'export/' => array(
$this->getQueryRoutePattern()
@ -104,6 +106,16 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication {
'name' => pht('Calendar User Guide'),
'href' => PhabricatorEnv::getDoclink('Calendar User Guide'),
),
array(
'name' => pht('Importing Events'),
'href' => PhabricatorEnv::getDoclink(
'Calendar User Guide: Importing Events'),
),
array(
'name' => pht('Exporting Events'),
'href' => PhabricatorEnv::getDoclink(
'Calendar User Guide: Exporting Events'),
),
);
}

View file

@ -0,0 +1,56 @@
<?php
final class PhabricatorCalendarEventAvailabilityController
extends PhabricatorCalendarController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$event = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$event) {
return new Aphront404Response();
}
$response = $this->newImportedEventResponse($event);
if ($response) {
return $response;
}
$cancel_uri = $event->getURI();
if (!$event->getIsUserAttending($viewer->getPHID())) {
return $this->newDialog()
->setTitle(pht('Not Attending Event'))
->appendParagraph(
pht(
'You can not change your display availability for events you '.
'are not attending.'))
->addCancelButton($cancel_uri);
}
// TODO: This endpoint currently only works via AJAX. It would be vaguely
// nice to provide a plain HTML version of the workflow where we return
// a dialog with a vanilla <select /> in it for cases where all the JS
// breaks.
$request->validateCSRF();
$invitee = $event->getInviteeForPHID($viewer->getPHID());
$map = PhabricatorCalendarEventInvitee::getAvailabilityMap();
$new_availability = $request->getURIData('availability');
if (isset($map[$new_availability])) {
$invitee
->setAvailability($new_availability)
->save();
// Invalidate the availability cache.
$viewer->writeAvailabilityCache(array(), null);
}
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
}

View file

@ -32,16 +32,51 @@ final class PhabricatorCalendarEventCancelController
$is_parent = $event->isParentEvent();
$is_child = $event->isChildEvent();
$is_cancelled = $event->getIsCancelled();
if ($is_child) {
$is_parent_cancelled = $event->getParentEvent()->getIsCancelled();
} else {
$is_parent_cancelled = false;
}
$is_cancelled = $event->getIsCancelled();
$is_recurring = $event->getIsRecurring();
$validation_exception = null;
if ($request->isFormPost()) {
$targets = array($event);
if ($is_recurring) {
$mode = $request->getStr('mode');
$is_future = ($mode == 'future');
// We need to fork the event if we're cancelling just the parent, or
// are cancelling a child and all future events.
$must_fork = ($is_child && $is_future) ||
($is_parent && !$is_future);
if ($must_fork) {
$fork_target = $event->loadForkTarget($viewer);
if ($fork_target) {
$xactions = array();
$xaction = id(new PhabricatorCalendarEventTransaction())
->setTransactionType(
PhabricatorCalendarEventForkTransaction::TRANSACTIONTYPE)
->setNewValue(true);
$editor = id(new PhabricatorCalendarEventEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$editor->applyTransactions($fork_target, array($xaction));
}
}
if ($is_future) {
$future = $event->loadFutureEvents($viewer);
foreach ($future as $future_event) {
$targets[] = $future_event;
}
}
}
foreach ($targets as $target) {
$xactions = array();
$xaction = id(new PhabricatorCalendarEventTransaction())
@ -56,64 +91,67 @@ final class PhabricatorCalendarEventCancelController
->setContinueOnMissingFields(true);
try {
$editor->applyTransactions($event, array($xaction));
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
$editor->applyTransactions($target, array($xaction));
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
break;
}
}
if (!$validation_exception) {
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
}
if ($is_cancelled) {
if ($is_parent_cancelled) {
$title = pht('Cannot Reinstate Instance');
$paragraph = pht(
'You cannot reinstate an instance of a cancelled recurring event.');
$cancel = pht('Back');
$submit = null;
} else if ($is_child) {
$title = pht('Reinstate Instance');
$paragraph = pht(
'Reinstate this instance of this recurring event?');
$cancel = pht('Back');
$submit = pht('Reinstate Instance');
} else if ($is_parent) {
$title = pht('Reinstate Recurring Event');
$paragraph = pht(
'Reinstate all instances of this recurring event which have not '.
'been individually cancelled?');
$cancel = pht('Back');
$submit = pht('Reinstate Recurring Event');
} else {
$title = pht('Reinstate Event');
$paragraph = pht('Reinstate this event?');
$cancel = pht('Back');
$submit = pht('Reinstate Event');
}
if ($is_recurring) {
$body = pht(
'This event is part of a series. Which events do you want to '.
'reinstate?');
$show_control = true;
} else {
if ($is_child) {
$title = pht('Cancel Instance');
$paragraph = pht('Cancel this instance of this recurring event?');
$cancel = pht('Back');
$submit = pht('Cancel Instance');
} else if ($is_parent) {
$title = pht('Cancel Recurrin Event');
$paragraph = pht('Cancel this entire series of recurring events?');
$cancel = pht('Back');
$submit = pht('Cancel Recurring Event');
$body = pht('Reinstate this event?');
$show_control = false;
}
$submit = pht('Reinstate Event');
} else {
$title = pht('Cancel Event');
$paragraph = pht(
'Cancel this event? You can always reinstate the event later.');
$cancel = pht('Back');
if ($is_recurring) {
$body = pht(
'This event is part of a series. Which events do you want to '.
'cancel?');
$show_control = true;
} else {
$body = pht('Cancel this event?');
$show_control = false;
}
$submit = pht('Cancel Event');
}
}
return $this->newDialog()
$dialog = $this->newDialog()
->setTitle($title)
->setValidationException($validation_exception)
->appendParagraph($paragraph)
->addCancelButton($cancel_uri, $cancel)
->appendParagraph($body)
->addCancelButton($cancel_uri, pht('Back'))
->addSubmitButton($submit);
if ($show_control) {
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Cancel Events'))
->setName('mode')
->setOptions(
array(
'this' => pht('Only This Event'),
'future' => pht('All Future Events'),
)));
$dialog->appendForm($form);
}
return $dialog;
}
}

View file

@ -6,6 +6,9 @@ final class PhabricatorCalendarEventEditController
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$engine = id(new PhabricatorCalendarEventEditEngine())
->setController($this);
$id = $request->getURIData('id');
if ($id) {
$event = id(new PhabricatorCalendarEventQuery())
@ -16,11 +19,66 @@ final class PhabricatorCalendarEventEditController
if ($response) {
return $response;
}
$cancel_uri = $event->getURI();
$page = $request->getURIData('pageKey');
if ($page == 'recurring') {
if ($event->isChildEvent()) {
return $this->newDialog()
->setTitle(pht('Series Event'))
->appendParagraph(
pht(
'This event is an instance in an event series. To change '.
'the behavior for the series, edit the parent event.'))
->addCancelButton($cancel_uri);
}
} else if ($event->getIsRecurring()) {
// If the user submits a comment or makes an edit via comment actions,
// always target only the current event. It doesn't make sense to add
// comments to every instance of an event, and the other actions don't
// make much sense to apply to all instances either.
if ($engine->isCommentAction()) {
$mode = PhabricatorCalendarEventEditEngine::MODE_THIS;
} else {
$mode = $request->getStr('mode');
}
return id(new PhabricatorCalendarEventEditEngine())
->setController($this)
->buildResponse();
if (!$mode) {
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Edit Events'))
->setName('mode')
->setOptions(
array(
PhabricatorCalendarEventEditEngine::MODE_THIS
=> pht('Edit Only This Event'),
PhabricatorCalendarEventEditEngine::MODE_FUTURE
=> pht('Edit All Future Events'),
)));
return $this->newDialog()
->setTitle(pht('Edit Event'))
->appendParagraph(
pht(
'This event is part of a series. Which events do you '.
'want to edit?'))
->appendForm($form)
->addSubmitButton(pht('Continue'))
->addCancelButton($cancel_uri)
->setDisableWorkflowOnSubmit(true);
}
$engine
->addContextParameter('mode', $mode)
->setSeriesEditMode($mode);
}
}
return $engine->buildResponse();
}
}

View file

@ -101,7 +101,7 @@ final class PhabricatorCalendarEventViewController
$viewer = $this->getViewer();
$id = $event->getID();
if ($event->isCancelledEvent()) {
if ($event->getIsCancelled()) {
$icon = 'fa-ban';
$color = 'red';
$status = pht('Cancelled');
@ -132,6 +132,43 @@ final class PhabricatorCalendarEventViewController
$header->addActionLink($action);
}
$options = PhabricatorCalendarEventInvitee::getAvailabilityMap();
$is_attending = $event->getIsUserAttending($viewer->getPHID());
if ($is_attending) {
$invitee = $event->getInviteeForPHID($viewer->getPHID());
$selected = $invitee->getDisplayAvailability($event);
if (!$selected) {
$selected = PhabricatorCalendarEventInvitee::AVAILABILITY_AVAILABLE;
}
$selected_option = idx($options, $selected);
$availability_select = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-circle '.$selected_option['color'])
->setText(pht('Availability: %s', $selected_option['name']));
$dropdown = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($options as $key => $option) {
$uri = "event/availability/{$id}/{$key}/";
$uri = $this->getApplicationURI($uri);
$dropdown->addAction(
id(new PhabricatorActionView())
->setName($option['name'])
->setIcon('fa-circle '.$option['color'])
->setHref($uri)
->setWorkflow(true));
}
$availability_select->setDropdownMenu($dropdown);
$header->addActionLink($availability_select);
}
return $header;
}
@ -146,11 +183,9 @@ final class PhabricatorCalendarEventViewController
PhabricatorPolicyCapability::CAN_EDIT);
$edit_uri = "event/edit/{$id}/";
if ($event->isChildEvent()) {
$edit_label = pht('Edit This Instance');
} else {
$edit_uri = $this->getApplicationURI($edit_uri);
$is_recurring = $event->getIsRecurring();
$edit_label = pht('Edit Event');
}
$curtain = $this->newCurtainView($event);
@ -159,11 +194,28 @@ final class PhabricatorCalendarEventViewController
id(new PhabricatorActionView())
->setName($edit_label)
->setIcon('fa-pencil')
->setHref($this->getApplicationURI($edit_uri))
->setHref($edit_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
->setWorkflow(!$can_edit || $is_recurring));
}
$recurring_uri = "{$edit_uri}page/recurring/";
$can_recurring = $can_edit && !$event->isChildEvent();
if ($event->getIsRecurring()) {
$recurring_label = pht('Edit Recurrence');
} else {
$recurring_label = pht('Make Recurring');
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName($recurring_label)
->setIcon('fa-repeat')
->setHref($recurring_uri)
->setDisabled(!$can_recurring)
->setWorkflow(true));
$can_attend = !$event->isImportedEvent();
if ($is_attending) {
@ -187,22 +239,10 @@ final class PhabricatorCalendarEventViewController
$cancel_uri = $this->getApplicationURI("event/cancel/{$id}/");
$cancel_disabled = !$can_edit;
if ($event->isChildEvent()) {
$cancel_label = pht('Cancel This Instance');
$reinstate_label = pht('Reinstate This Instance');
if ($event->getParentEvent()->getIsCancelled()) {
$cancel_disabled = true;
}
} else if ($event->isParentEvent()) {
$cancel_label = pht('Cancel All');
$reinstate_label = pht('Reinstate All');
} else {
$cancel_label = pht('Cancel Event');
$reinstate_label = pht('Reinstate Event');
}
if ($event->isCancelledEvent()) {
if ($event->getIsCancelled()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName($reinstate_label)

View file

@ -5,6 +5,21 @@ final class PhabricatorCalendarEventEditEngine
const ENGINECONST = 'calendar.event';
private $rawTransactions;
private $seriesEditMode = self::MODE_THIS;
const MODE_THIS = 'this';
const MODE_FUTURE = 'future';
public function setSeriesEditMode($series_edit_mode) {
$this->seriesEditMode = $series_edit_mode;
return $this;
}
public function getSeriesEditMode() {
return $this->seriesEditMode;
}
public function getEngineName() {
return pht('Calendar Events');
}
@ -67,12 +82,19 @@ final class PhabricatorCalendarEventEditEngine
$invitee_phids = $object->getInviteePHIDsForEdit();
}
$frequency_options = array(
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => pht('Daily'),
PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => pht('Weekly'),
PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => pht('Monthly'),
PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => pht('Yearly'),
);
$frequency_map = PhabricatorCalendarEvent::getFrequencyMap();
$frequency_options = ipull($frequency_map, 'label');
$rrule = $object->newRecurrenceRule();
if ($rrule) {
$frequency = $rrule->getFrequency();
} else {
$frequency = null;
}
// At least for now, just hide "Invitees" when editing all future events.
// This may eventually deserve a more nuanced approach.
$is_future = ($this->getSeriesEditMode() == self::MODE_FUTURE);
$fields = array(
id(new PhabricatorTextEditField())
@ -85,15 +107,38 @@ final class PhabricatorCalendarEventEditEngine
->setConduitDescription(pht('Rename the event.'))
->setConduitTypeDescription(pht('New event name.'))
->setValue($object->getName()),
id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setDescription(pht('Description of the event.'))
id(new PhabricatorBoolEditField())
->setKey('isAllDay')
->setOptions(pht('Normal Event'), pht('All Day Event'))
->setAsCheckbox(true)
->setTransactionType(
PhabricatorCalendarEventDescriptionTransaction::TRANSACTIONTYPE)
->setConduitDescription(pht('Update the event description.'))
->setConduitTypeDescription(pht('New event description.'))
->setValue($object->getDescription()),
PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE)
->setDescription(pht('Marks this as an all day event.'))
->setConduitDescription(pht('Make the event an all day event.'))
->setConduitTypeDescription(pht('Mark the event as an all day event.'))
->setValue($object->getIsAllDay()),
id(new PhabricatorEpochEditField())
->setKey('start')
->setLabel(pht('Start'))
->setIsLockable(false)
->setIsDefaultable(false)
->setTransactionType(
PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE)
->setDescription(pht('Start time of the event.'))
->setConduitDescription(pht('Change the start time of the event.'))
->setConduitTypeDescription(pht('New event start time.'))
->setValue($object->getStartDateTimeEpoch()),
id(new PhabricatorEpochEditField())
->setKey('end')
->setLabel(pht('End'))
->setIsLockable(false)
->setIsDefaultable(false)
->setTransactionType(
PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE)
->setDescription(pht('End time of the event.'))
->setConduitDescription(pht('Change the end time of the event.'))
->setConduitTypeDescription(pht('New event end time.'))
->setValue($object->newEndDateTimeForEdit()->getEpoch()),
id(new PhabricatorBoolEditField())
->setKey('cancelled')
->setOptions(pht('Active'), pht('Cancelled'))
@ -117,6 +162,7 @@ final class PhabricatorCalendarEventEditEngine
->setConduitTypeDescription(pht('New event host.'))
->setSingleValue($object->getHostPHID()),
id(new PhabricatorDatasourceEditField())
->setIsHidden($is_future)
->setKey('inviteePHIDs')
->setAliases(array('invite', 'invitee', 'invitees', 'inviteePHID'))
->setLabel(pht('Invitees'))
@ -128,91 +174,16 @@ final class PhabricatorCalendarEventEditEngine
->setConduitTypeDescription(pht('New event invitees.'))
->setValue($invitee_phids)
->setCommentActionLabel(pht('Change Invitees')),
);
if ($this->getIsCreate()) {
$fields[] = id(new PhabricatorBoolEditField())
->setKey('isRecurring')
->setLabel(pht('Recurring'))
->setOptions(pht('One-Time Event'), pht('Recurring Event'))
id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setDescription(pht('Description of the event.'))
->setTransactionType(
PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE)
->setDescription(pht('One time or recurring event.'))
->setConduitDescription(pht('Make the event recurring.'))
->setConduitTypeDescription(pht('Mark the event as a recurring event.'))
->setValue($object->getIsRecurring());
$rrule = $object->newRecurrenceRule();
if ($rrule) {
$frequency = $rrule->getFrequency();
} else {
$frequency = null;
}
$fields[] = id(new PhabricatorSelectEditField())
->setKey('frequency')
->setLabel(pht('Frequency'))
->setOptions($frequency_options)
->setTransactionType(
PhabricatorCalendarEventFrequencyTransaction::TRANSACTIONTYPE)
->setDescription(pht('Recurring event frequency.'))
->setConduitDescription(pht('Change the event frequency.'))
->setConduitTypeDescription(pht('New event frequency.'))
->setValue($frequency);
}
if ($this->getIsCreate() || $object->getIsRecurring()) {
$fields[] = id(new PhabricatorEpochEditField())
->setIsLockable(false)
->setIsDefaultable(false)
->setAllowNull(true)
->setKey('until')
->setLabel(pht('Repeat Until'))
->setTransactionType(
PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE)
->setDescription(pht('Last instance of the event.'))
->setConduitDescription(pht('Change when the event repeats until.'))
->setConduitTypeDescription(pht('New final event time.'))
->setValue($object->getUntilDateTimeEpoch());
}
$fields[] = id(new PhabricatorBoolEditField())
->setKey('isAllDay')
->setLabel(pht('All Day'))
->setOptions(pht('Normal Event'), pht('All Day Event'))
->setTransactionType(
PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE)
->setDescription(pht('Marks this as an all day event.'))
->setConduitDescription(pht('Make the event an all day event.'))
->setConduitTypeDescription(pht('Mark the event as an all day event.'))
->setValue($object->getIsAllDay());
$fields[] = id(new PhabricatorEpochEditField())
->setKey('start')
->setLabel(pht('Start'))
->setIsLockable(false)
->setIsDefaultable(false)
->setTransactionType(
PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE)
->setDescription(pht('Start time of the event.'))
->setConduitDescription(pht('Change the start time of the event.'))
->setConduitTypeDescription(pht('New event start time.'))
->setValue($object->getStartDateTimeEpoch());
$fields[] = id(new PhabricatorEpochEditField())
->setKey('end')
->setLabel(pht('End'))
->setIsLockable(false)
->setIsDefaultable(false)
->setTransactionType(
PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE)
->setDescription(pht('End time of the event.'))
->setConduitDescription(pht('Change the end time of the event.'))
->setConduitTypeDescription(pht('New event end time.'))
->setValue($object->getEndDateTimeEpoch());
$fields[] = id(new PhabricatorIconSetEditField())
PhabricatorCalendarEventDescriptionTransaction::TRANSACTIONTYPE)
->setConduitDescription(pht('Update the event description.'))
->setConduitTypeDescription(pht('New event description.'))
->setValue($object->getDescription()),
id(new PhabricatorIconSetEditField())
->setKey('icon')
->setLabel(pht('Icon'))
->setIconSet(new PhabricatorCalendarIconSet())
@ -221,7 +192,50 @@ final class PhabricatorCalendarEventEditEngine
->setDescription(pht('Event icon.'))
->setConduitDescription(pht('Change the event icon.'))
->setConduitTypeDescription(pht('New event icon.'))
->setValue($object->getIcon());
->setValue($object->getIcon()),
// NOTE: We're being a little sneaky here. This field is hidden and
// always has the value "true", so it makes the event recurring when you
// submit a form which contains the field. Then we put the the field on
// the "recurring" page in the "Make Recurring" dialog to simplify the
// workflow. This is still normal, explicit field from the perspective
// of the API.
id(new PhabricatorBoolEditField())
->setIsHidden(true)
->setKey('isRecurring')
->setLabel(pht('Recurring'))
->setOptions(pht('One-Time Event'), pht('Recurring Event'))
->setTransactionType(
PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE)
->setDescription(pht('One time or recurring event.'))
->setConduitDescription(pht('Make the event recurring.'))
->setConduitTypeDescription(pht('Mark the event as a recurring event.'))
->setValue(true),
id(new PhabricatorSelectEditField())
->setKey('frequency')
->setLabel(pht('Frequency'))
->setOptions($frequency_options)
->setTransactionType(
PhabricatorCalendarEventFrequencyTransaction::TRANSACTIONTYPE)
->setDescription(pht('Recurring event frequency.'))
->setConduitDescription(pht('Change the event frequency.'))
->setConduitTypeDescription(pht('New event frequency.'))
->setValue($frequency),
id(new PhabricatorEpochEditField())
->setIsLockable(false)
->setIsDefaultable(false)
->setAllowNull(true)
->setHideTime($object->getIsAllDay())
->setKey('until')
->setLabel(pht('Repeat Until'))
->setTransactionType(
PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE)
->setDescription(pht('Last instance of the event.'))
->setConduitDescription(pht('Change when the event repeats until.'))
->setConduitTypeDescription(pht('New final event time.'))
->setValue($object->getUntilDateTimeEpoch()),
);
return $fields;
}
@ -263,8 +277,133 @@ final class PhabricatorCalendarEventEditEngine
}
}
return $fields;
}
protected function newPages($object) {
// Controls for event recurrence behavior go on a separate page which we
// put in a dialog. This simplifies event creation in the common case.
return array(
id(new PhabricatorEditPage())
->setKey('core')
->setLabel(pht('Core'))
->setIsDefault(true),
id(new PhabricatorEditPage())
->setKey('recurring')
->setLabel(pht('Recurrence'))
->setFieldKeys(
array(
'isRecurring',
'frequency',
'until',
)),
);
}
protected function willApplyTransactions($object, array $xactions) {
$viewer = $this->getViewer();
$is_parent = $object->isParentEvent();
$is_child = $object->isChildEvent();
$is_future = ($this->getSeriesEditMode() === self::MODE_FUTURE);
// Figure out which transactions we can apply to the whole series of events.
// Some transactions (like comments) can never be bulk applied.
$inherited_xactions = array();
foreach ($xactions as $xaction) {
$modular_type = $xaction->getModularType();
if (!($modular_type instanceof PhabricatorCalendarEventTransactionType)) {
continue;
}
$inherited_edit = $modular_type->isInheritedEdit();
if ($inherited_edit) {
$inherited_xactions[] = $xaction;
}
}
$this->rawTransactions = $this->cloneTransactions($inherited_xactions);
$must_fork = ($is_child && $is_future) ||
($is_parent && !$is_future);
// We don't need to fork when editing a parent event if none of the edits
// can transfer to child events. For example, commenting on a parent is
// fine.
if ($is_parent && !$is_future) {
if (!$inherited_xactions) {
$must_fork = false;
}
}
if ($must_fork) {
$fork_target = $object->loadForkTarget($viewer);
if ($fork_target) {
$fork_xaction = id(new PhabricatorCalendarEventTransaction())
->setTransactionType(
PhabricatorCalendarEventForkTransaction::TRANSACTIONTYPE)
->setNewValue(true);
if ($fork_target->getPHID() == $object->getPHID()) {
// We're forking the object itself, so just slip it into the
// transactions we're going to apply.
array_unshift($xactions, $fork_xaction);
} else {
// Otherwise, we're forking a different object, so we have to
// apply that separately.
$this->applyTransactions($fork_target, array($fork_xaction));
}
}
}
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
$viewer = $this->getViewer();
if ($this->getSeriesEditMode() !== self::MODE_FUTURE) {
return;
}
$targets = $object->loadFutureEvents($viewer);
if (!$targets) {
return;
}
foreach ($targets as $target) {
$apply = $this->cloneTransactions($this->rawTransactions);
$this->applyTransactions($target, $apply);
}
}
private function applyTransactions($target, array $xactions) {
$viewer = $this->getViewer();
// TODO: This isn't the most accurate source we could use, but this mode
// is web-only for now.
$content_source = PhabricatorContentSource::newForSource(
PhabricatorWebContentSource::SOURCECONST);
$editor = id(new PhabricatorCalendarEventEditor())
->setActor($viewer)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
try {
$editor->applyTransactions($target, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
// Just ignore any issues we run into.
}
}
private function cloneTransactions(array $xactions) {
$result = array();
foreach ($xactions as $xaction) {
$result[] = clone $xaction;
}
return $result;
}
}

View file

@ -3,6 +3,9 @@
final class PhabricatorCalendarEventEditor
extends PhabricatorApplicationTransactionEditor {
private $oldIsAllDay;
private $newIsAllDay;
public function getEditorApplicationClass() {
return 'PhabricatorCalendarApplication';
}
@ -11,12 +14,28 @@ final class PhabricatorCalendarEventEditor
return pht('Calendar');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this event.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function getOldIsAllDay() {
return $this->oldIsAllDay;
}
public function getNewIsAllDay() {
return $this->newIsAllDay;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
@ -25,6 +44,22 @@ final class PhabricatorCalendarEventEditor
if ($object->getIsStub()) {
$this->materializeStub($object);
}
// Before doing anything, figure out if the event will be an all day event
// or not after the edit. This affects how we store datetime values, and
// whether we render times or not.
$old_allday = $object->getIsAllDay();
$new_allday = $old_allday;
$type_allday = PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() != $type_allday) {
continue;
}
$target_alllday = (bool)$xaction->getNewValue();
}
$this->oldIsAllDay = $old_allday;
$this->newIsAllDay = $new_allday;
}
private function materializeStub(PhabricatorCalendarEvent $event) {
@ -269,28 +304,31 @@ final class PhabricatorCalendarEventEditor
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
$monogram = $object->getMonogram();
return id(new PhabricatorMetaMTAMail())
->setSubject("E{$id}: {$name}")
->addHeader('Thread-Topic', "E{$id}: ".$object->getName());
->setSubject("{$monogram}: {$name}")
->addHeader('Thread-Topic', $monogram);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$description = $object->getDescription();
$body = parent::buildMailBody($object, $xactions);
$description = $object->getDescription();
if ($this->getIsNewObject()) {
if (strlen($description)) {
$body->addRemarkupSection(
pht('EVENT DESCRIPTION'),
$description);
}
}
$body->addLinkSection(
pht('EVENT DETAIL'),
PhabricatorEnv::getProductionURI('/E'.$object->getID()));
PhabricatorEnv::getProductionURI($object->getURI()));
$ics_attachment = $this->newICSAttachment($object);
$body->addAttachment($ics_attachment);

View file

@ -28,6 +28,17 @@ abstract class PhabricatorCalendarICSImportEngine
$document = null;
}
foreach ($parser->getWarnings() as $warning) {
$import->newLogMessage(
PhabricatorCalendarImportICSWarningLogType::LOGTYPE,
array(
'ics.warning.code' => $warning['code'],
'ics.warning.line' => $warning['line'],
'ics.warning.text' => $warning['text'],
'ics.warning.message' => $warning['message'],
));
}
return $this->importEventDocument($viewer, $import, $document);
}

View file

@ -225,6 +225,7 @@ abstract class PhabricatorCalendarImportEngine
$xactions[$full_uid] = $this->newUpdateTransactions($event, $node);
$update_map[$full_uid] = $event;
$attendee_map[$full_uid] = array();
$attendees = $node->getAttendees();
$private_index = 1;
foreach ($attendees as $attendee) {
@ -330,7 +331,7 @@ abstract class PhabricatorCalendarImportEngine
foreach ($update_map as $full_uid => $event) {
$parent_uid = $this->getParentNodeUID($node_map[$full_uid]);
if ($parent_uid) {
$parent_phid = $update_map[$full_uid]->getPHID();
$parent_phid = $update_map[$parent_uid]->getPHID();
} else {
$parent_phid = null;
}
@ -409,10 +410,28 @@ abstract class PhabricatorCalendarImportEngine
array());
}
// TODO: When the source is a subscription-based ICS file or some other
// similar source, we should load all events from the source here and
// destroy the ones we didn't update. These are events that have been
// deleted.
// Delete any events which are no longer present in the source.
$updated_events = mpull($update_map, null, 'getPHID');
$source_events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withImportSourcePHIDs(array($import->getPHID()))
->execute();
$engine = new PhabricatorDestructionEngine();
foreach ($source_events as $source_event) {
if (isset($updated_events[$source_event->getPHID()])) {
// We imported and updated this event, so keep it around.
continue;
}
$import->newLogMessage(
PhabricatorCalendarImportDeleteLogType::LOGTYPE,
array(
'name' => $source_event->getName(),
));
$engine->destroyObject($source_event);
}
}
private function getFullNodeUID(PhutilCalendarEventNode $node) {
@ -453,6 +472,12 @@ abstract class PhabricatorCalendarImportEngine
$xactions = array();
$uid = $node->getUID();
if (!$event->getID()) {
$xactions[] = id(new PhabricatorCalendarEventTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_CREATE)
->setNewValue(true);
}
$name = $node->getName();
if (!strlen($name)) {
if (strlen($uid)) {
@ -502,6 +527,8 @@ abstract class PhabricatorCalendarImportEngine
->setStartDateTime($start_datetime)
->setEndDateTime($end_datetime);
$event->setIsAllDay((int)$start_datetime->getIsAllDay());
// TODO: This should be transactional, but the transaction only accepts
// simple frequency rules right now.
$rrule = $node->getRecurrenceRule();
@ -525,11 +552,18 @@ abstract class PhabricatorCalendarImportEngine
PhabricatorUser $viewer,
PhabricatorCalendarImport $import) {
$any_event = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withImportSourcePHIDs(array($import->getPHID()))
->setLimit(1)
->execute();
$table = new PhabricatorCalendarEvent();
$conn = $table->establishConnection('r');
// Using a CalendarEventQuery here was failing oddly in a way that was
// difficult to reproduce locally (see T11808). Just check the table
// directly; this is significantly more efficient anyway.
$any_event = queryfx_all(
$conn,
'SELECT phid FROM %T WHERE importSourcePHID = %s LIMIT 1',
$table->getTableName(),
$import->getPHID());
return (bool)$any_event;
}

View file

@ -0,0 +1,34 @@
<?php
final class PhabricatorCalendarImportDeleteLogType
extends PhabricatorCalendarImportLogType {
const LOGTYPE = 'delete';
public function getDisplayType(
PhabricatorUser $viewer,
PhabricatorCalendarImportLog $log) {
return pht('Deleted Event');
}
public function getDisplayDescription(
PhabricatorUser $viewer,
PhabricatorCalendarImportLog $log) {
return pht(
'Deleted event "%s" which is no longer present in the source.',
$log->getParameter('name'));
}
public function getDisplayIcon(
PhabricatorUser $viewer,
PhabricatorCalendarImportLog $log) {
return 'fa-times';
}
public function getDisplayColor(
PhabricatorUser $viewer,
PhabricatorCalendarImportLog $log) {
return 'grey';
}
}

View file

@ -0,0 +1,37 @@
<?php
final class PhabricatorCalendarImportICSWarningLogType
extends PhabricatorCalendarImportLogType {
const LOGTYPE = 'ics.warning';
public function getDisplayType(
PhabricatorUser $viewer,
PhabricatorCalendarImportLog $log) {
return pht('ICS Parser Warning');
}
public function getDisplayDescription(
PhabricatorUser $viewer,
PhabricatorCalendarImportLog $log) {
return pht(
'Warning ("%s") while parsing ICS data (near line %s): %s',
$log->getParameter('ics.warning.code'),
$log->getParameter('ics.warning.line'),
$log->getParameter('ics.warning.message'));
}
public function getDisplayIcon(
PhabricatorUser $viewer,
PhabricatorCalendarImportLog $log) {
return 'fa-exclamation-triangle';
}
public function getDisplayColor(
PhabricatorUser $viewer,
PhabricatorCalendarImportLog $log) {
return 'yellow';
}
}

View file

@ -0,0 +1,40 @@
<?php
final class PhabricatorCalendarManagementNotifyWorkflow
extends PhabricatorCalendarManagementWorkflow {
protected function didConstruct() {
$this
->setName('notify')
->setExamples('**notify** [options]')
->setSynopsis(
pht(
'Test and debug notifications about upcoming events.'))
->setArguments(
array(
array(
'name' => 'minutes',
'param' => 'N',
'help' => pht(
'Notify about events in the next __N__ minutes (default: 15). '.
'Setting this to a larger value makes testing easier.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$engine = new PhabricatorCalendarNotificationEngine();
$minutes = $args->getArg('minutes');
if ($minutes) {
$engine->setNotifyWindow(phutil_units("{$minutes} minutes in seconds"));
}
$engine->publishNotifications();
return 0;
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class PhabricatorCalendarManagementWorkflow
extends PhabricatorManagementWorkflow {}

View file

@ -0,0 +1,61 @@
<?php
final class PhabricatorCalendarEventNotificationView
extends Phobject {
private $viewer;
private $event;
private $epoch;
private $dateTime;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setEvent(PhabricatorCalendarEvent $event) {
$this->event = $event;
return $this;
}
public function getEvent() {
return $this->event;
}
public function setEpoch($epoch) {
$this->epoch = $epoch;
return $this;
}
public function getEpoch() {
return $this->epoch;
}
public function setDateTime(PhutilCalendarDateTime $date_time) {
$this->dateTime = $date_time;
return $this;
}
public function getDateTime() {
return $this->dateTime;
}
public function getDisplayMinutes() {
$epoch = $this->getEpoch();
$now = PhabricatorTime::getNow();
$minutes = (int)ceil(($epoch - $now) / 60);
return new PhutilNumber($minutes);
}
public function getDisplayTime() {
$viewer = $this->getViewer();
$epoch = $this->getEpoch();
return phabricator_datetime($epoch, $viewer);
}
}

View file

@ -0,0 +1,304 @@
<?php
final class PhabricatorCalendarNotificationEngine
extends Phobject {
private $cursor;
private $notifyWindow;
public function getCursor() {
if (!$this->cursor) {
$now = PhabricatorTime::getNow();
$this->cursor = $now - phutil_units('10 minutes in seconds');
}
return $this->cursor;
}
public function setCursor($cursor) {
$this->cursor = $cursor;
return $this;
}
public function setNotifyWindow($notify_window) {
$this->notifyWindow = $notify_window;
return $this;
}
public function getNotifyWindow() {
if (!$this->notifyWindow) {
return phutil_units('15 minutes in seconds');
}
return $this->notifyWindow;
}
public function publishNotifications() {
$cursor = $this->getCursor();
$now = PhabricatorTime::getNow();
if ($cursor > $now) {
return;
}
$calendar_class = 'PhabricatorCalendarApplication';
if (!PhabricatorApplication::isClassInstalled($calendar_class)) {
return;
}
try {
$lock = PhabricatorGlobalLock::newLock('calendar.notify')
->lock(5);
} catch (PhutilLockException $ex) {
return;
}
$caught = null;
try {
$this->sendNotifications();
} catch (Exception $ex) {
$caught = $ex;
}
$lock->unlock();
// Wait a little while before checking for new notifications to send.
$this->setCursor($cursor + phutil_units('1 minute in seconds'));
if ($caught) {
throw $caught;
}
}
private function sendNotifications() {
$cursor = $this->getCursor();
$window_min = $cursor - phutil_units('16 hours in seconds');
$window_max = $cursor + phutil_units('16 hours in seconds');
$viewer = PhabricatorUser::getOmnipotentUser();
$events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withDateRange($window_min, $window_max)
->withIsCancelled(false)
->withIsImported(false)
->setGenerateGhosts(true)
->execute();
if (!$events) {
// No events are starting soon in any timezone, so there is nothing
// left to be done.
return;
}
$attendee_map = array();
foreach ($events as $key => $event) {
$notifiable_phids = array();
foreach ($event->getInvitees() as $invitee) {
if (!$invitee->isAttending()) {
continue;
}
$notifiable_phids[] = $invitee->getInviteePHID();
}
if (!$notifiable_phids) {
unset($events[$key]);
}
$attendee_map[$key] = array_fuse($notifiable_phids);
}
if (!$attendee_map) {
// None of the events have any notifiable attendees, so there is no
// one to notify of anything.
return;
}
$all_attendees = array();
foreach ($attendee_map as $key => $attendee_phids) {
foreach ($attendee_phids as $attendee_phid) {
$all_attendees[$attendee_phid] = $attendee_phid;
}
}
$user_map = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs($all_attendees)
->withIsDisabled(false)
->needUserSettings(true)
->execute();
$user_map = mpull($user_map, null, 'getPHID');
if (!$user_map) {
// None of the attendees are valid users: they're all imported users
// or projects or invalid or some other kind of unnotifiable entity.
return;
}
$all_event_phids = array();
foreach ($events as $key => $event) {
foreach ($event->getNotificationPHIDs() as $phid) {
$all_event_phids[$phid] = $phid;
}
}
$table = new PhabricatorCalendarNotification();
$conn = $table->establishConnection('w');
$rows = queryfx_all(
$conn,
'SELECT * FROM %T WHERE eventPHID IN (%Ls) AND targetPHID IN (%Ls)',
$table->getTableName(),
$all_event_phids,
$all_attendees);
$sent_map = array();
foreach ($rows as $row) {
$event_phid = $row['eventPHID'];
$target_phid = $row['targetPHID'];
$initial_epoch = $row['utcInitialEpoch'];
$sent_map[$event_phid][$target_phid][$initial_epoch] = $row;
}
$now = PhabricatorTime::getNow();
$notify_min = $now;
$notify_max = $now + $this->getNotifyWindow();
$notify_map = array();
foreach ($events as $key => $event) {
$initial_epoch = $event->getUTCInitialEpoch();
$event_phids = $event->getNotificationPHIDs();
// Select attendees who actually exist, and who we have not sent any
// notifications to yet.
$attendee_phids = $attendee_map[$key];
$users = array_select_keys($user_map, $attendee_phids);
foreach ($users as $user_phid => $user) {
foreach ($event_phids as $event_phid) {
if (isset($sent_map[$event_phid][$user_phid][$initial_epoch])) {
unset($users[$user_phid]);
continue 2;
}
}
}
if (!$users) {
continue;
}
// Discard attendees for whom the event start time isn't soon. Events
// may start at different times for different users, so we need to
// check every user's start time.
foreach ($users as $user_phid => $user) {
$user_datetime = $event->newStartDateTime()
->setViewerTimezone($user->getTimezoneIdentifier());
$user_epoch = $user_datetime->getEpoch();
if ($user_epoch < $notify_min || $user_epoch > $notify_max) {
unset($users[$user_phid]);
continue;
}
$view = id(new PhabricatorCalendarEventNotificationView())
->setViewer($user)
->setEvent($event)
->setDateTime($user_datetime)
->setEpoch($user_epoch);
$notify_map[$user_phid][] = $view;
}
}
$mail_list = array();
$mark_list = array();
$now = PhabricatorTime::getNow();
foreach ($notify_map as $user_phid => $events) {
$user = $user_map[$user_phid];
$locale = PhabricatorEnv::beginScopedLocale($user->getTranslation());
$caught = null;
try {
$mail_list[] = $this->newMailMessage($user, $events);
} catch (Exception $ex) {
$caught = $ex;
}
unset($locale);
if ($caught) {
throw $ex;
}
foreach ($events as $view) {
$event = $view->getEvent();
foreach ($event->getNotificationPHIDs() as $phid) {
$mark_list[] = qsprintf(
$conn,
'(%s, %s, %d, %d)',
$phid,
$user_phid,
$event->getUTCInitialEpoch(),
$now);
}
}
}
// Mark all the notifications we're about to send as delivered so we
// do not double-notify.
foreach (PhabricatorLiskDAO::chunkSQL($mark_list) as $chunk) {
queryfx(
$conn,
'INSERT IGNORE INTO %T
(eventPHID, targetPHID, utcInitialEpoch, didNotifyEpoch)
VALUES %Q',
$table->getTableName(),
$chunk);
}
foreach ($mail_list as $mail) {
$mail->saveAndSend();
}
}
private function newMailMessage(PhabricatorUser $viewer, array $events) {
$events = msort($events, 'getEpoch');
$next_event = head($events);
$body = new PhabricatorMetaMTAMailBody();
foreach ($events as $event) {
$body->addTextSection(
null,
pht(
'%s is starting in %s minute(s), at %s.',
$event->getEvent()->getName(),
$event->getDisplayMinutes(),
$event->getDisplayTime()));
$body->addLinkSection(
pht('EVENT DETAIL'),
PhabricatorEnv::getProductionURI($event->getEvent()->getURI()));
}
$next_event = head($events)->getEvent();
$subject = $next_event->getName();
if (count($events) > 1) {
$more = pht(
'(+%s more...)',
new PhutilNumber(count($events) - 1));
$subject = "{$subject} {$more}";
}
$calendar_phid = id(new PhabricatorCalendarApplication())
->getPHID();
return id(new PhabricatorMetaMTAMail())
->setSubject($subject)
->addTos(array($viewer->getPHID()))
->setSensitiveContent(false)
->setFrom($calendar_phid)
->setIsBulk(true)
->setSubjectPrefix(pht('[Calendar]'))
->setVarySubjectPrefix(pht('[Reminder]'))
->setThreadID($next_event->getPHID(), false)
->setRelatedPHID($next_event->getPHID())
->setBody($body->render())
->setHTMLBody($body->renderHTML());
}
}

View file

@ -41,7 +41,7 @@ final class PhabricatorCalendarEventPHIDType extends PhabricatorPHIDType {
->setFullName(pht('%s: %s', $monogram, $name))
->setURI($uri);
if ($event->isCancelledEvent()) {
if ($event->getIsCancelled()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
}
}

View file

@ -17,6 +17,9 @@ final class PhabricatorCalendarEventQuery
private $importSourcePHIDs;
private $importAuthorPHIDs;
private $importUIDs;
private $utcInitialEpochMin;
private $utcInitialEpochMax;
private $isImported;
private $generateGhosts = false;
@ -45,6 +48,12 @@ final class PhabricatorCalendarEventQuery
return $this;
}
public function withUTCInitialEpochBetween($min, $max) {
$this->utcInitialEpochMin = $min;
$this->utcInitialEpochMax = $max;
return $this;
}
public function withInvitedPHIDs(array $phids) {
$this->inviteePHIDs = $phids;
return $this;
@ -95,6 +104,11 @@ final class PhabricatorCalendarEventQuery
return $this;
}
public function withIsImported($is_imported) {
$this->isImported = $is_imported;
return $this;
}
protected function getDefaultOrderVector() {
return array('start', 'id');
}
@ -112,7 +126,7 @@ final class PhabricatorCalendarEventQuery
return array(
'start' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'dateFrom',
'column' => 'utcInitialEpoch',
'reverse' => true,
'type' => 'int',
'unique' => false,
@ -371,6 +385,20 @@ final class PhabricatorCalendarEventQuery
$this->rangeEnd + phutil_units('16 hours in seconds'));
}
if ($this->utcInitialEpochMin !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch >= %d',
$this->utcInitialEpochMin);
}
if ($this->utcInitialEpochMax !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch <= %d',
$this->utcInitialEpochMax);
}
if ($this->inviteePHIDs !== null) {
$where[] = qsprintf(
$conn,
@ -450,6 +478,18 @@ final class PhabricatorCalendarEventQuery
$this->importUIDs);
}
if ($this->isImported !== null) {
if ($this->isImported) {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IS NULL');
}
}
return $where;
}

View file

@ -303,7 +303,7 @@ final class PhabricatorCalendarEventSearchEngine
$item->addAttribute($event->renderEventDate($viewer, false));
if ($event->isCancelledEvent()) {
if ($event->getIsCancelled()) {
$item->setDisabled(true);
}
@ -368,7 +368,7 @@ final class PhabricatorCalendarEventSearchEngine
$event_view = id(new AphrontCalendarEventView())
->setHostPHID($event->getHostPHID())
->setEpochRange($epoch_min, $epoch_max)
->setIsCancelled($event->isCancelledEvent())
->setIsCancelled($event->getIsCancelled())
->setName($event->getName())
->setURI($event->getURI())
->setIsAllDay($event->getIsAllDay())
@ -441,7 +441,7 @@ final class PhabricatorCalendarEventSearchEngine
->setIconColor($status_color)
->setName($event->getName())
->setURI($event->getURI())
->setIsCancelled($event->isCancelledEvent());
->setIsCancelled($event->getIsCancelled());
$day_view->addEvent($event_view);
}

View file

@ -27,6 +27,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
protected $isRecurring = 0;
protected $seriesParentPHID;
protected $instanceOfEventPHID;
protected $sequenceIndex;
@ -51,14 +52,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
private $viewerTimezone;
// TODO: DEPRECATED. Remove once we're sure the migrations worked.
protected $allDayDateFrom;
protected $allDayDateTo;
protected $dateFrom;
protected $dateTo;
protected $recurrenceEndDate;
protected $recurrenceFrequency = array();
private $isGhostEvent = false;
private $stubInvitees;
@ -94,10 +87,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID())
->attachInvitees(array())
->setDateFrom(0)
->setDateTo(0)
->setAllDayDateFrom(0)
->setAllDayDateTo(0)
->setStartDateTime($datetime_start)
->setEndDateTime($datetime_end)
->attachImportSource(null)
@ -140,17 +129,20 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
'a recurring parent event!'));
}
$series_phid = $this->getSeriesParentPHID();
if (!$series_phid) {
$series_phid = $this->getPHID();
}
$child = id(new self())
->setIsCancelled(0)
->setIsStub(0)
->setInstanceOfEventPHID($this->getPHID())
->setSeriesParentPHID($series_phid)
->setSequenceIndex($sequence)
->setIsRecurring(true)
->attachParentEvent($this)
->setAllDayDateFrom(0)
->setAllDayDateTo(0)
->setDateFrom(0)
->setDateTo(0);
->attachImportSource(null);
return $child->copyFromParent($actor, $start);
}
@ -165,6 +157,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
'editPolicy' => true,
'name' => true,
'description' => true,
'isCancelled' => true,
);
// Read these fields from the parent event instead of this event. For
@ -204,7 +197,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
->setViewPolicy($parent->getViewPolicy())
->setEditPolicy($parent->getEditPolicy())
->setName($parent->getName())
->setDescription($parent->getDescription());
->setDescription($parent->getDescription())
->setIsCancelled($parent->getIsCancelled());
if ($start) {
$start_datetime = $start;
@ -399,6 +393,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
'icon' => 'text32',
'mailKey' => 'bytes20',
'isRecurring' => 'bool',
'seriesParentPHID' => 'phid?',
'instanceOfEventPHID' => 'phid?',
'sequenceIndex' => 'uint32?',
'isStub' => 'bool',
@ -410,18 +405,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
'importSourcePHID' => 'phid?',
'importUIDIndex' => 'bytes12?',
'importUID' => 'text?',
// TODO: DEPRECATED.
'allDayDateFrom' => 'epoch',
'allDayDateTo' => 'epoch',
'dateFrom' => 'epoch',
'dateTo' => 'epoch',
'recurrenceEndDate' => 'epoch?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_date' => array(
'columns' => array('dateFrom', 'dateTo'),
),
'key_instance' => array(
'columns' => array('instanceOfEventPHID', 'sequenceIndex'),
'unique' => true,
@ -433,9 +418,11 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'),
'unique' => true,
),
'key_series' => array(
'columns' => array('seriesParentPHID', 'utcInitialEpoch'),
),
),
self::CONFIG_SERIALIZATION => array(
'recurrenceFrequency' => self::SERIALIZATION_JSON,
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
@ -461,6 +448,29 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $this->assertAttached($this->invitees);
}
public function getInviteeForPHID($phid) {
$invitees = $this->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
return idx($invitees, $phid);
}
public static function getFrequencyMap() {
return array(
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array(
'label' => pht('Daily'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => array(
'label' => pht('Weekly'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => array(
'label' => pht('Monthly'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => array(
'label' => pht('Yearly'),
),
);
}
private function newStubInvitees() {
$parent = $this->getParentEvent();
@ -552,7 +562,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $this->assertAttached($this->parentEvent);
}
public function attachParentEvent($event) {
public function attachParentEvent(PhabricatorCalendarEvent $event = null) {
$this->parentEvent = $event;
return $this;
}
@ -565,20 +575,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return ($this->instanceOfEventPHID !== null);
}
public function isCancelledEvent() {
if ($this->getIsCancelled()) {
return true;
}
if ($this->isChildEvent()) {
if ($this->getParentEvent()->getIsCancelled()) {
return true;
}
}
return false;
}
public function renderEventDate(
PhabricatorUser $viewer,
$show_end) {
@ -631,7 +627,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
public function getDisplayIcon(PhabricatorUser $viewer) {
if ($this->isCancelledEvent()) {
if ($this->getIsCancelled()) {
return 'fa-times';
}
@ -655,7 +651,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
}
public function getDisplayIconColor(PhabricatorUser $viewer) {
if ($this->isCancelledEvent()) {
if ($this->getIsCancelled()) {
return 'red';
}
@ -679,7 +675,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
}
public function getDisplayIconLabel(PhabricatorUser $viewer) {
if ($this->isCancelledEvent()) {
if ($this->getIsCancelled()) {
return pht('Cancelled');
}
@ -758,8 +754,18 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
$host_handle = $handles[$host_phid];
$host_name = $host_handle->getFullName();
$host_uri = $host_handle->getURI();
$host_uri = PhabricatorEnv::getURI($host_uri);
// NOTE: Gmail shows "Who: Unknown Organizer*" if the organizer URI does
// not look like an email address. Use a synthetic address so it shows
// the host name instead.
$install_uri = PhabricatorEnv::getProductionURI('/');
$install_uri = new PhutilURI($install_uri);
// This should possibly use "metamta.reply-handler-domain" instead, but
// we do not currently accept mail for users anyway, and that option may
// not be configured.
$mail_domain = $install_uri->getDomain();
$host_uri = "mailto:{$host_phid}@{$mail_domain}";
$organizer = id(new PhutilCalendarUserNode())
->setName($host_name)
@ -818,26 +824,35 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
public function newStartDateTime() {
$datetime = $this->getParameter('startDateTime');
if ($datetime) {
return $this->newDateTimeFromDictionary($datetime);
}
$epoch = $this->getDateFrom();
return $this->newDateTimeFromEpoch($epoch);
}
public function getStartDateTimeEpoch() {
return $this->newStartDateTime()->getEpoch();
}
public function newEndDateTime() {
public function newEndDateTimeForEdit() {
$datetime = $this->getParameter('endDateTime');
if ($datetime) {
return $this->newDateTimeFromDictionary($datetime);
}
$epoch = $this->getDateTo();
return $this->newDateTimeFromEpoch($epoch);
public function newEndDateTime() {
$datetime = $this->newEndDateTimeForEdit();
// If this is an all day event, we move the end date time forward to the
// first second of the following day. This is consistent with what users
// expect: an all day event from "Nov 1" to "Nov 1" lasts the entire day.
if ($this->getIsAllDay()) {
$datetime = $datetime
->newAbsoluteDateTime()
->setHour(0)
->setMinute(0)
->setSecond(0)
->newRelativeDateTime('P1D')
->newAbsoluteDateTime();
}
return $datetime;
}
public function getEndDateTimeEpoch() {
@ -850,12 +865,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $this->newDateTimeFromDictionary($datetime);
}
$epoch = $this->getRecurrenceEndDate();
if (!$epoch) {
return null;
}
return $this->newDateTimeFromEpoch($epoch);
}
public function getUntilDateTimeEpoch() {
$datetime = $this->newUntilDateTime();
@ -930,10 +941,14 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
$datetime->newAbsoluteDateTime()->toDictionary());
}
public function setUntilDateTime(PhutilCalendarDateTime $datetime) {
return $this->setParameter(
'untilDateTime',
$datetime->newAbsoluteDateTime()->toDictionary());
public function setUntilDateTime(PhutilCalendarDateTime $datetime = null) {
if ($datetime) {
$value = $datetime->newAbsoluteDateTime()->toDictionary();
} else {
$value = null;
}
return $this->setParameter('untilDateTime', $value);
}
public function setRecurrenceRule(PhutilCalendarRecurrenceRule $rrule) {
@ -1015,6 +1030,82 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $this;
}
public function loadForkTarget(PhabricatorUser $viewer) {
if (!$this->getIsRecurring()) {
// Can't fork an event which isn't recurring.
return null;
}
if ($this->isChildEvent()) {
// If this is a child event, this is the fork target.
return $this;
}
if (!$this->isValidSequenceIndex($viewer, 1)) {
// This appears to be a "recurring" event with no valid instances: for
// example, its "until" date is before the second instance would occur.
// This can happen if we already forked the event or if users entered
// silly stuff. Just edit the event directly without forking anything.
return null;
}
$next_event = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withInstanceSequencePairs(
array(
array($this->getPHID(), 1),
))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$next_event) {
$next_event = $this->newStub($viewer, 1);
}
return $next_event;
}
public function loadFutureEvents(PhabricatorUser $viewer) {
// NOTE: If you can't edit some of the future events, we just
// don't try to update them. This seems like it's probably what
// users are likely to expect.
// NOTE: This only affects events that are currently in the same
// series, not all events that were ever in the original series.
// We could use series PHIDs instead of parent PHIDs to affect more
// events if this turns out to be counterintuitive. Other
// applications differ in their behavior.
return id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withParentEventPHIDs(array($this->getPHID()))
->withUTCInitialEpochBetween($this->getUTCInitialEpoch(), null)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
}
public function getNotificationPHIDs() {
$phids = array();
if ($this->getPHID()) {
$phids[] = $this->getPHID();
}
if ($this->getSeriesParentPHID()) {
$phids[] = $this->getSeriesParentPHID();
}
return $phids;
}
/* -( Markup Interface )--------------------------------------------------- */
@ -1078,7 +1169,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getImportSource()) {
if ($this->isImportedEvent()) {
return PhabricatorPolicies::POLICY_NOONE;
} else {
return $this->getEditPolicy();
@ -1087,7 +1178,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->getImportSource()) {
if ($this->isImportedEvent()) {
return false;
}
@ -1113,7 +1204,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
}
public function describeAutomaticCapability($capability) {
if ($this->getImportSource()) {
if ($this->isImportedEvent()) {
return pht(
'Events imported from external sources can not be edited in '.
'Phabricator.');

View file

@ -7,12 +7,18 @@ final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO
protected $inviteePHID;
protected $inviterPHID;
protected $status;
protected $availability = self::AVAILABILITY_DEFAULT;
const STATUS_INVITED = 'invited';
const STATUS_ATTENDING = 'attending';
const STATUS_DECLINED = 'declined';
const STATUS_UNINVITED = 'uninvited';
const AVAILABILITY_DEFAULT = 'default';
const AVAILABILITY_AVAILABLE = 'available';
const AVAILABILITY_BUSY = 'busy';
const AVAILABILITY_AWAY = 'away';
public static function initializeNewCalendarEventInvitee(
PhabricatorUser $actor, $event) {
return id(new PhabricatorCalendarEventInvitee())
@ -25,6 +31,7 @@ final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text64',
'availability' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_event' => array(
@ -50,6 +57,50 @@ final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO
}
}
public function getDisplayAvailability(PhabricatorCalendarEvent $event) {
switch ($this->getAvailability()) {
case self::AVAILABILITY_DEFAULT:
case self::AVAILABILITY_BUSY:
return self::AVAILABILITY_BUSY;
case self::AVAILABILITY_AWAY:
return self::AVAILABILITY_AWAY;
default:
return null;
}
}
public static function getAvailabilityMap() {
return array(
self::AVAILABILITY_AVAILABLE => array(
'color' => 'green',
'name' => pht('Available'),
),
self::AVAILABILITY_BUSY => array(
'color' => 'yellow',
'name' => pht('Busy'),
),
self::AVAILABILITY_AWAY => array(
'color' => 'red',
'name' => pht('Away'),
),
);
}
public static function getAvailabilitySpec($const) {
return idx(self::getAvailabilityMap(), $const, array());
}
public static function getAvailabilityName($const) {
$spec = self::getAvailabilitySpec($const);
return idx($spec, 'name', $const);
}
public static function getAvailabilityColor($const) {
$spec = self::getAvailabilitySpec($const);
return idx($spec, 'color', 'indigo');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -1,24 +0,0 @@
<?php
final class PhabricatorCalendarHoliday extends PhabricatorCalendarDAO {
protected $day;
protected $name;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'day' => 'date',
'name' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'day' => array(
'columns' => array('day'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
}

View file

@ -0,0 +1,27 @@
<?php
final class PhabricatorCalendarNotification
extends PhabricatorCalendarDAO {
protected $eventPHID;
protected $utcInitialEpoch;
protected $targetPHID;
protected $didNotifyEpoch;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'utcInitialEpoch' => 'epoch',
'didNotifyEpoch' => 'epoch',
),
self::CONFIG_KEY_SCHEMA => array(
'key_notify' => array(
'columns' => array('eventPHID', 'utcInitialEpoch', 'targetPHID'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
}

View file

@ -1,19 +0,0 @@
<?php
final class PhabricatorCalendarHolidayTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
protected function willRunTests() {
parent::willRunTests();
id(new PhabricatorCalendarHoliday())
->setDay('2012-01-02')
->setName(pht('International Testing Day'))
->save();
}
}

View file

@ -0,0 +1,85 @@
<?php
final class PHUIUserAvailabilityView
extends AphrontTagView {
private $user;
public function setAvailableUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getAvailableUser() {
return $this->user;
}
protected function getTagContent() {
$viewer = $this->getViewer();
$user = $this->getAvailableUser();
$until = $user->getAwayUntil();
if (!$until) {
return pht('Available');
}
$const = $user->getDisplayAvailability();
$name = PhabricatorCalendarEventInvitee::getAvailabilityName($const);
$color = PhabricatorCalendarEventInvitee::getAvailabilityColor($const);
$away_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setShade($color)
->setName($name)
->setDotColor($color);
$now = PhabricatorTime::getNow();
// Try to load the event handle. If it's invalid or the user can't see it,
// we'll just render a generic message.
$object_phid = $user->getAvailabilityEventPHID();
$handle = null;
if ($object_phid) {
$handles = $viewer->loadHandles(array($object_phid));
$handle = $handles[$object_phid];
if (!$handle->isComplete() || $handle->getPolicyFiltered()) {
$handle = null;
}
}
switch ($const) {
case PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY:
if ($handle) {
$description = pht(
'Away at %s until %s.',
$handle->renderLink(),
$viewer->formatShortDateTime($until, $now));
} else {
$description = pht(
'Away until %s.',
$viewer->formatShortDateTime($until, $now));
}
break;
case PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY:
default:
if ($handle) {
$description = pht(
'Busy at %s until %s.',
$handle->renderLink(),
$viewer->formatShortDateTime($until, $now));
} else {
$description = pht(
'Busy until %s.',
$viewer->formatShortDateTime($until, $now));
}
break;
}
return array(
$away_tag,
' ',
$description,
);
}
}

View file

@ -39,7 +39,7 @@ final class PhabricatorCalendarEventAllDayTransaction
public function getTitle() {
if ($this->getNewValue()) {
return pht(
'%s changed this as an all day event.',
'%s changed this to an all day event.',
$this->renderAuthor());
} else {
return pht(

View file

@ -5,8 +5,21 @@ abstract class PhabricatorCalendarEventDateTransaction
abstract protected function getInvalidDateMessage();
public function isInheritedEdit() {
return false;
}
public function generateNewValue($object, $value) {
return $value->getEpoch();
$editor = $this->getEditor();
if ($value->isDisabled()) {
return null;
}
return $value->newPhutilDateTime()
->setIsAllDay($editor->getNewIsAllDay())
->newAbsoluteDateTime()
->toDictionary();
}
public function validateTransactions($object, array $xactions) {

View file

@ -6,20 +6,32 @@ final class PhabricatorCalendarEventEndDateTransaction
const TRANSACTIONTYPE = 'calendar.enddate';
public function generateOldValue($object) {
// TODO: Upgrade this.
return $object->getEndDateTimeEpoch();
$editor = $this->getEditor();
return $object->newEndDateTimeForEdit()
->newAbsoluteDateTime()
->setIsAllDay($editor->getOldIsAllDay())
->toDictionary();
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
$editor = $this->getEditor();
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($value);
$datetime->setIsAllDay($editor->getNewIsAllDay());
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$value,
$actor->getTimezoneIdentifier());
$datetime->setIsAllDay($object->getIsAllDay());
$object->setEndDateTime($datetime);
}
public function shouldHide() {
if ($this->isCreateTransaction()) {
return true;
}
return false;
}
public function getTitle() {
return pht(
'%s changed the end date for this event from %s to %s.',

View file

@ -0,0 +1,66 @@
<?php
final class PhabricatorCalendarEventForkTransaction
extends PhabricatorCalendarEventTransactionType {
const TRANSACTIONTYPE = 'calendar.fork';
public function generateOldValue($object) {
return false;
}
public function shouldHide() {
// This transaction is purely an internal implementation detail which
// supports editing groups of events like "All Future Events".
return true;
}
public function applyInternalEffects($object, $value) {
$parent = $object->getParentEvent();
$object->setInstanceOfEventPHID(null);
$object->attachParentEvent(null);
$rrule = $parent->newRecurrenceRule();
$object->setRecurrenceRule($rrule);
$until = $parent->newUntilDateTime();
if ($until) {
$object->setUntilDateTime($until);
}
$old_sequence_index = $object->getSequenceIndex();
$object->setSequenceIndex(0);
// Stop the parent event from recurring after the start date of this event.
// Since the "until" time is inclusive, rewind it by one second. We could
// figure out the previous instance's time instead or use a COUNT, but this
// seems simpler as long as it doesn't cause any issues.
$until_cutoff = $object->newStartDateTime()
->newRelativeDateTime('-PT1S')
->newAbsoluteDateTime();
$parent->setUntilDateTime($until_cutoff);
$parent->save();
// NOTE: If we implement "COUNT" on editable events, we need to adjust
// the "COUNT" here and divide it up between the parent and the fork.
// Make all following children of the old parent children of this node
// instead.
$conn = $object->establishConnection('w');
queryfx(
$conn,
'UPDATE %T SET
instanceOfEventPHID = %s,
sequenceIndex = (sequenceIndex - %d)
WHERE instanceOfEventPHID = %s
AND utcInstanceEpoch > %d',
$object->getTableName(),
$object->getPHID(),
$old_sequence_index,
$parent->getPHID(),
$object->getUTCInstanceEpoch());
}
}

View file

@ -19,6 +19,33 @@ final class PhabricatorCalendarEventFrequencyTransaction
$rrule = id(new PhutilCalendarRecurrenceRule())
->setFrequency($value);
// If the user creates a monthly event on the 29th, 30th or 31st of a
// month, it means "the 30th of every month" as far as the RRULE is
// concerned. Such an event will not occur on months with fewer days.
// This is surprising, and proably not what the user wants. Instead,
// schedule these events relative to the end of the month: on the "-1st",
// "-2nd" or "-3rd" day of the month. For example, a monthly event on
// the 31st of a 31-day month translates to "every month, on the last
// day of the month".
if ($value == PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY) {
$start_datetime = $object->newStartDateTime();
$y = $start_datetime->getYear();
$m = $start_datetime->getMonth();
$d = $start_datetime->getDay();
if ($d >= 29) {
$year_map = PhutilCalendarRecurrenceRule::getYearMap(
$y,
PhutilCalendarRecurrenceRule::WEEKDAY_MONDAY);
$month_days = $year_map['monthDays'][$m];
$schedule_on = -(($month_days + 1) - $d);
$rrule->setByMonthDay(array($schedule_on));
}
}
$object->setRecurrenceRule($rrule);
}

View file

@ -13,6 +13,14 @@ final class PhabricatorCalendarEventIconTransaction
$object->setIcon($value);
}
public function shouldHide() {
if ($this->isCreateTransaction()) {
return true;
}
return false;
}
public function getTitle() {
$old = $this->getIconLabel($this->getOldValue());
$new = $this->getIconLabel($this->getNewValue());

View file

@ -13,6 +13,17 @@ final class PhabricatorCalendarEventRecurringTransaction
return (int)$value;
}
public function isInheritedEdit() {
return false;
}
public function shouldHide() {
// This event isn't interesting on its own, and is accompanied by an
// "alice set this event to repeat weekly." event in normal circumstances
// anyway.
return true;
}
public function applyInternalEffects($object, $value) {
$object->setIsRecurring($value);
}
@ -30,11 +41,14 @@ final class PhabricatorCalendarEventRecurringTransaction
continue;
}
if ($xaction->getNewValue()) {
continue;
}
$errors[] = $this->newInvalidError(
pht(
'An event can only be made recurring when it is created. '.
'You can not convert an existing event into a recurring '.
'event or vice versa.'),
'An event can not be stopped from recurring once it has been '.
'made recurring. You can cancel the event.'),
$xaction);
}

View file

@ -8,6 +8,10 @@ abstract class PhabricatorCalendarEventReplyTransaction
return $object->getUserInviteStatus($actor_phid);
}
public function isInheritedEdit() {
return false;
}
public function applyExternalEffects($object, $value) {
$acting_phid = $this->getActingAsPHID();

View file

@ -6,20 +6,32 @@ final class PhabricatorCalendarEventStartDateTransaction
const TRANSACTIONTYPE = 'calendar.startdate';
public function generateOldValue($object) {
// TODO: Upgrade this.
return $object->getStartDateTimeEpoch();
$editor = $this->getEditor();
return $object->newStartDateTime()
->newAbsoluteDateTime()
->setIsAllDay($editor->getOldIsAllDay())
->toDictionary();
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
$editor = $this->getEditor();
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($value);
$datetime->setIsAllDay($editor->getNewIsAllDay());
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$value,
$actor->getTimezoneIdentifier());
$datetime->setIsAllDay($object->getIsAllDay());
$object->setStartDateTime($datetime);
}
public function shouldHide() {
if ($this->isCreateTransaction()) {
return true;
}
return false;
}
public function getTitle() {
return pht(
'%s changed the start date for this event from %s to %s.',

View file

@ -1,4 +1,10 @@
<?php
abstract class PhabricatorCalendarEventTransactionType
extends PhabricatorModularTransactionType {}
extends PhabricatorModularTransactionType {
public function isInheritedEdit() {
return true;
}
}

View file

@ -6,36 +6,58 @@ final class PhabricatorCalendarEventUntilDateTransaction
const TRANSACTIONTYPE = 'calendar.recurrenceenddate';
public function generateOldValue($object) {
// TODO: Upgrade this.
return $object->getUntilDateTimeEpoch();
$editor = $this->getEditor();
$until = $object->newUntilDateTime();
if (!$until) {
return null;
}
return $until
->newAbsoluteDateTime()
->setIsAllDay($editor->getOldIsAllDay())
->toDictionary();
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
$editor = $this->getEditor();
// TODO: DEPRECATED.
$object->setRecurrenceEndDate($value);
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$value,
$actor->getTimezoneIdentifier());
$datetime->setIsAllDay($object->getIsAllDay());
if ($value) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($value);
$datetime->setIsAllDay($editor->getNewIsAllDay());
$object->setUntilDateTime($datetime);
} else {
$object->setUntilDateTime(null);
}
}
public function getTitle() {
if ($this->getNewValue()) {
return pht(
'%s changed this event to repeat until %s.',
$this->renderAuthor(),
$this->renderNewDate());
} else {
return pht(
'%s changed this event to repeat forever.',
$this->renderAuthor());
}
}
public function getTitleForFeed() {
if ($this->getNewValue()) {
return pht(
'%s changed %s to repeat until %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderNewDate());
} else {
return pht(
'%s changed %s to repeat forever.',
$this->renderAuthor(),
$this->renderObject());
}
}
protected function getInvalidDateMessage() {

View file

@ -36,7 +36,7 @@ final class PhabricatorHelpMainMenuBarExtension
$help_name = pht('%s Help', $application->getName());
$help_item = id(new PHUIListItemView())
->setIcon('fa-life-ring')
->setIcon('fa-book')
->addClass('core-menu-item')
->setID($help_id)
->setName($help_name)

View file

@ -405,11 +405,11 @@ final class ManiphestTaskSearchEngine
}
protected function getNewUserBody() {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create a Task'))
->setHref('/maniphest/task/edit/')
->setColor(PHUIButtonView::GREEN);
$viewer = $this->requireViewer();
$create_button = id(new ManiphestEditEngine())
->setViewer($viewer)
->newNUXBUtton(pht('Create a Task'));
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();

View file

@ -18,11 +18,35 @@ final class PhabricatorOAuthServerTokenController
$grant_type = $request->getStr('grant_type');
$code = $request->getStr('code');
$redirect_uri = $request->getStr('redirect_uri');
$client_phid = $request->getStr('client_id');
$client_secret = $request->getStr('client_secret');
$response = new PhabricatorOAuthResponse();
$server = new PhabricatorOAuthServer();
$client_id_parameter = $request->getStr('client_id');
$client_id_header = idx($_SERVER, 'PHP_AUTH_USER');
if (strlen($client_id_parameter) && strlen($client_id_header)) {
if ($client_id_parameter !== $client_id_header) {
throw new Exception(
pht(
'Request included a client_id parameter and an "Authorization" '.
'header with a username, but the values "%s" and "%s") disagree. '.
'The values must match.',
$client_id_parameter,
$client_id_header));
}
}
$client_secret_parameter = $request->getStr('client_secret');
$client_secret_header = idx($_SERVER, 'PHP_AUTH_PW');
if (strlen($client_secret_parameter)) {
// If the `client_secret` parameter is present, prefer parameters.
$client_phid = $client_id_parameter;
$client_secret = $client_secret_parameter;
} else {
// Otherwise, read values from the "Authorization" header.
$client_phid = $client_id_header;
$client_secret = $client_secret_header;
}
if ($grant_type != 'authorization_code') {
$response->setError('unsupported_grant_type');
$response->setErrorDescription(

View file

@ -204,11 +204,11 @@ final class PhabricatorPasteSearchEngine
}
protected function getNewUserBody() {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create a Paste'))
->setHref('/paste/create/')
->setColor(PHUIButtonView::GREEN);
$viewer = $this->requireViewer();
$create_button = id(new PhabricatorPasteEditEngine())
->setViewer($viewer)
->newNUXButton(pht('Create a Paste'));
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();

View file

@ -58,8 +58,8 @@ final class PhabricatorPeopleProfileViewController
->appendChild($feed);
$projects = $this->buildProjectsView($user);
$badges = $this->buildBadgesView($user);
$calendar = $this->buildCalendarDayView($user);
$badges = $this->buildBadgesView($user);
require_celerity_resource('project-view-css');
$home = id(new PHUITwoColumnView())
@ -73,8 +73,8 @@ final class PhabricatorPeopleProfileViewController
->setSideColumn(
array(
$projects,
$badges,
$calendar,
$badges,
));
$nav = $this->getProfileMenu();

View file

@ -10,7 +10,7 @@ final class PhabricatorUserStatusField
}
public function getFieldName() {
return pht('Status');
return pht('Availability');
}
public function getFieldDescription() {
@ -29,7 +29,10 @@ final class PhabricatorUserStatusField
public function renderPropertyViewValue(array $handles) {
$user = $this->getObject();
$viewer = $this->requireViewer();
return $user->getAvailabilityDescription($viewer);
return id(new PHUIUserAvailabilityView())
->setViewer($viewer)
->setAvailableUser($user);
}
}

View file

@ -66,7 +66,12 @@ final class PhabricatorPeopleUserPHIDType extends PhabricatorPHIDType {
} else {
$until = $user->getAwayUntil();
if ($until) {
$away = PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY;
if ($user->getDisplayAvailability() == $away) {
$availability = PhabricatorObjectHandle::AVAILABILITY_NONE;
} else {
$availability = PhabricatorObjectHandle::AVAILABILITY_PARTIAL;
}
}
}

View file

@ -395,18 +395,28 @@ final class PhabricatorPeopleQuery
// Group all the events by invited user. Only examine events that users
// are actually attending.
$map = array();
$invitee_map = array();
foreach ($events as $event) {
foreach ($event->getInvitees() as $invitee) {
if (!$invitee->isAttending()) {
continue;
}
// If the user is set to "Available" for this event, don't consider it
// when computin their away status.
if (!$invitee->getDisplayAvailability($event)) {
continue;
}
$invitee_phid = $invitee->getInviteePHID();
if (!isset($rebuild[$invitee_phid])) {
continue;
}
$map[$invitee_phid][] = $event;
$event_phid = $event->getPHID();
$invitee_map[$invitee_phid][$event_phid] = $invitee;
}
}
@ -426,6 +436,7 @@ final class PhabricatorPeopleQuery
}
$cursor = $min_range;
$next_event = null;
if ($events) {
// Find the next time when the user has no meetings. If we move forward
// because of an event, we check again for events after that one ends.
@ -435,6 +446,9 @@ final class PhabricatorPeopleQuery
$to = $event->getEndDateTimeEpoch();
if (($from <= $cursor) && ($to > $cursor)) {
$cursor = $to;
if (!$next_event) {
$next_event = $event;
}
continue 2;
}
}
@ -443,13 +457,29 @@ final class PhabricatorPeopleQuery
}
if ($cursor > $min_range) {
$invitee = $invitee_map[$phid][$next_event->getPHID()];
$availability_type = $invitee->getDisplayAvailability($next_event);
$availability = array(
'until' => $cursor,
'eventPHID' => $event->getPHID(),
'availability' => $availability_type,
);
$availability_ttl = $cursor;
// We only cache this availability until the end of the current event,
// since the event PHID (and possibly the availability type) are only
// valid for that long.
// NOTE: This doesn't handle overlapping events with the greatest
// possible care. In theory, if you're attenting multiple events
// simultaneously we should accommodate that. However, it's complex
// to compute, rare, and probably not confusing most of the time.
$availability_ttl = $next_event->getStartDateTimeEpochForCache();
} else {
$availability = array(
'until' => null,
'eventPHID' => null,
'availability' => null,
);
$availability_ttl = $max_range;
}

View file

@ -960,20 +960,29 @@ final class PhabricatorUser
}
/**
* Describe the user's availability.
*
* @param PhabricatorUser Viewing user.
* @return string Human-readable description of away status.
* @task availability
*/
public function getAvailabilityDescription(PhabricatorUser $viewer) {
$until = $this->getAwayUntil();
if ($until) {
return pht('Away until %s', phabricator_datetime($until, $viewer));
} else {
return pht('Available');
public function getDisplayAvailability() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
$busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY;
return idx($availability, 'availability', $busy);
}
public function getAvailabilityEventPHID() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'eventPHID');
}

View file

@ -75,8 +75,11 @@ final class PhabricatorUserCardView extends AphrontTagView {
if (PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorCalendarApplication',
$viewer)) {
$availability = $user->getAvailabilityDescription($viewer);
$body[] = $this->addItem(pht('Status'), $availability);
$body[] = $this->addItem(
pht('Availability'),
id(new PHUIUserAvailabilityView())
->setViewer($viewer)
->setAvailableUser($user));
}
$badges = $this->buildBadges($user, $viewer);

View file

@ -61,6 +61,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
'cart/(?P<id>\d+)/' => array(
'' => 'PhortuneCartViewController',
'checkout/' => 'PhortuneCartCheckoutController',
'(?P<action>print)/' => 'PhortuneCartViewController',
'(?P<action>cancel|refund)/' => 'PhortuneCartCancelController',
'update/' => 'PhortuneCartUpdateController',
),
@ -81,7 +82,9 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
),
'merchant/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PhortuneMerchantListController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneMerchantEditController',
'picture/(?:(?P<id>\d+)/)?' => 'PhortuneMerchantPictureController',
$this->getEditRoutePattern('edit/')
=> 'PhortuneMerchantEditController',
'orders/(?P<merchantID>\d+)/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneCartListController',
'(?P<merchantID>\d+)/' => array(

View file

@ -3,9 +3,12 @@
final class PhortuneCartViewController
extends PhortuneCartController {
private $action = null;
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$this->action = $request->getURIData('action');
$authority = $this->loadMerchantAuthority();
require_celerity_resource('phortune-css');
@ -129,7 +132,7 @@ final class PhortuneCartViewController
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader(pht('Order Detail'))
->setHeader($cart->getName())
->setHeaderIcon('fa-shopping-cart');
if ($cart->getStatus() == PhortuneCart::STATUS_PURCHASED) {
@ -188,6 +191,8 @@ final class PhortuneCartViewController
$crumbs->addTextCrumb(pht('Cart %d', $cart->getID()));
$crumbs->setBorder(true);
if (!$this->action) {
$class = 'phortune-cart-page';
$timeline = $this->buildTransactionTimeline(
$cart,
new PhortuneCartTransactionQuery());
@ -206,18 +211,55 @@ final class PhortuneCartViewController
$timeline,
));
return $this->newPage()
} else {
$class = 'phortune-invoice-view';
$crumbs = null;
$merchant_phid = $cart->getMerchantPHID();
$buyer_phid = $cart->getAuthorPHID();
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withPHIDs(array($merchant_phid))
->needProfileImage(true)
->executeOne();
$buyer = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($buyer_phid))
->needProfileImage(true)
->executeOne();
// TODO: Add account "Contact" info
$merchant_contact = new PHUIRemarkupView(
$viewer, $merchant->getContactInfo());
$description = null;
$view = id(new PhortuneInvoiceView())
->setMerchantName($merchant->getName())
->setMerchantLogo($merchant->getProfileImageURI())
->setMerchantContact($merchant_contact)
->setMerchantFooter($merchant->getInvoiceFooter())
->setAccountName($buyer->getRealName())
->setStatus($error_view)
->setContent(array(
$description,
$details,
$cart_box,
$charges,
));
}
$page = $this->newPage()
->setTitle(pht('Cart %d', $cart->getID()))
->setCrumbs($crumbs)
->addClass('phortune-cart-page')
->addClass($class)
->appendChild($view);
if ($crumbs) {
$page->setCrumbs($crumbs);
}
return $page;
}
private function buildDetailsView(PhortuneCart $cart) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($cart);
@ -229,9 +271,10 @@ final class PhortuneCartViewController
$cart->getMerchantPHID(),
));
$view->addProperty(
pht('Order Name'),
$cart->getName());
if ($this->action == 'print') {
$view->addProperty(pht('Order Name'), $cart->getName());
}
$view->addProperty(
pht('Account'),
$handles[$cart->getAccountPHID()]->renderLink());
@ -276,7 +319,7 @@ final class PhortuneCartViewController
$refund_uri = $this->getApplicationURI("{$prefix}cart/{$id}/refund/");
$update_uri = $this->getApplicationURI("{$prefix}cart/{$id}/update/");
$accept_uri = $this->getApplicationURI("{$prefix}cart/{$id}/accept/");
$print_uri = $this->getApplicationURI("{$prefix}cart/{$id}/?__print__=1");
$print_uri = $this->getApplicationURI("{$prefix}cart/{$id}/print/");
$curtain->addAction(
id(new PhabricatorActionView())

View file

@ -4,193 +4,8 @@ final class PhortuneMerchantEditController
extends PhortuneMerchantController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if ($id) {
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$merchant) {
return new Aphront404Response();
return id(new PhortuneMerchantEditEngine())
->setController($this)
->buildResponse();
}
$is_new = false;
} else {
$this->requireApplicationCapability(
PhortuneMerchantCapability::CAPABILITY);
$merchant = PhortuneMerchant::initializeNewMerchant($viewer);
$merchant->attachMemberPHIDs(array($viewer->getPHID()));
$is_new = true;
}
if ($is_new) {
$title = pht('Create Merchant');
$button_text = pht('Create Merchant');
$cancel_uri = $this->getApplicationURI('merchant/');
} else {
$title = pht(
'Edit Merchant %d %s',
$merchant->getID(),
$merchant->getName());
$button_text = pht('Save Changes');
$cancel_uri = $this->getApplicationURI(
'/merchant/'.$merchant->getID().'/');
}
$e_name = true;
$v_name = $merchant->getName();
$v_desc = $merchant->getDescription();
$v_cont = $merchant->getContactInfo();
$v_members = $merchant->getMemberPHIDs();
$e_members = null;
$validation_exception = null;
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;
$edge_members = PhortuneMerchantHasMemberEdgeType::EDGECONST;
$xactions = array();
$xactions[] = id(new PhortuneMerchantTransaction())
->setTransactionType($type_name)
->setNewValue($v_name);
$xactions[] = id(new PhortuneMerchantTransaction())
->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);
$xactions[] = id(new PhortuneMerchantTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_members)
->setNewValue(
array(
'=' => array_fuse($v_members),
));
$editor = id(new PhortuneMerchantEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($merchant, $xactions);
$id = $merchant->getID();
$merchant_uri = $this->getApplicationURI("merchant/{$id}/");
return id(new AphrontRedirectResponse())->setURI($merchant_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_name = $ex->getShortMessage($type_name);
$e_mbmers = $ex->getShortMessage($type_edge);
$merchant->setViewPolicy($v_view);
}
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($merchant)
->execute();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setValue($v_name)
->setError($e_name))
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($viewer)
->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())
->setLabel(pht('Members'))
->setName('memberPHIDs')
->setValue($v_members)
->setError($e_members))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('viewPolicy')
->setPolicyObject($merchant)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicies($policies))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($button_text)
->addCancelButton($cancel_uri));
$header = id(new PHUIHeaderView())
->setHeader($title);
$crumbs = $this->buildApplicationCrumbs();
if ($is_new) {
$crumbs->addTextCrumb(pht('Create Merchant'));
$header->setHeaderIcon('fa-plus-square');
} else {
$crumbs->addTextCrumb(
pht('Merchant %d', $merchant->getID()),
$this->getApplicationURI('/merchant/'.$merchant->getID().'/'));
$crumbs->addTextCrumb(pht('Edit'));
$header->setHeaderIcon('fa-pencil');
}
$crumbs->setBorder(true);
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Merchant'))
->setValidationException($validation_exception)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}

View file

@ -0,0 +1,234 @@
<?php
final class PhortuneMerchantPictureController
extends PhortuneMerchantController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($id))
->needProfileImage(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$merchant) {
return new Aphront404Response();
}
$uri = $merchant->getViewURI();
$supported_formats = PhabricatorFile::getTransformableImageFormats();
$e_file = true;
$errors = array();
if ($request->isFormPost()) {
$phid = $request->getStr('phid');
$is_default = false;
if ($phid == PhabricatorPHIDConstants::PHID_VOID) {
$phid = null;
$is_default = true;
} else if ($phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
} else {
if ($request->getFileExists('picture')) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['picture'],
array(
'authorPHID' => $viewer->getPHID(),
'canCDN' => true,
));
} else {
$e_file = pht('Required');
$errors[] = pht(
'You must choose a file when uploading a merchant logo.');
}
}
if (!$errors && !$is_default) {
if (!$file->isTransformableImage()) {
$e_file = pht('Not Supported');
$errors[] = pht(
'This server only supports these image formats: %s.',
implode(', ', $supported_formats));
} else {
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
$xformed = $xform->executeTransform($file);
}
}
if (!$errors) {
if ($is_default) {
$new_value = null;
} else {
$xformed->attachToObject($merchant->getPHID());
$new_value = $xformed->getPHID();
}
$xactions = array();
$xactions[] = id(new PhortuneMerchantTransaction())
->setTransactionType(PhortuneMerchantTransaction::TYPE_PICTURE)
->setNewValue($new_value);
$editor = id(new PhortuneMerchantEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($merchant, $xactions);
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
$title = pht('Edit Merchant Picture');
$form = id(new PHUIFormLayoutView())
->setUser($viewer);
$default_image = PhabricatorFile::loadBuiltin($viewer, 'merchant.png');
$images = array();
$current = $merchant->getProfileImagePHID();
$has_current = false;
if ($current) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($current))
->executeOne();
if ($file) {
if ($file->isTransformableImage()) {
$has_current = true;
$images[$current] = array(
'uri' => $file->getBestURI(),
'tip' => pht('Current Picture'),
);
}
}
}
$images[PhabricatorPHIDConstants::PHID_VOID] = array(
'uri' => $default_image->getBestURI(),
'tip' => pht('Default Picture'),
);
require_celerity_resource('people-profile-css');
Javelin::initBehavior('phabricator-tooltips', array());
$buttons = array();
foreach ($images as $phid => $spec) {
$button = javelin_tag(
'button',
array(
'class' => 'grey profile-image-button',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $spec['tip'],
'size' => 300,
),
),
phutil_tag(
'img',
array(
'height' => 50,
'width' => 50,
'src' => $spec['uri'],
)));
$button = array(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'phid',
'value' => $phid,
)),
$button,
);
$button = phabricator_form(
$viewer,
array(
'class' => 'profile-image-form',
'method' => 'POST',
),
$button);
$buttons[] = $button;
}
if ($has_current) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Current Logo'))
->setValue(array_shift($buttons)));
}
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Use Logo'))
->setValue($buttons));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
$upload_form = id(new AphrontFormView())
->setUser($viewer)
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormFileControl())
->setName('picture')
->setLabel(pht('Upload Logo'))
->setError($e_file)
->setCaption(
pht('Supported formats: %s', implode(', ', $supported_formats))))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($uri)
->setValue(pht('Upload Logo')));
$upload_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Upload New Logo'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($upload_form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($merchant->getName(), $uri);
$crumbs->addTextCrumb(pht('Merchant Logo'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Merchant Logo'))
->setHeaderIcon('fa-camera');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$form_box,
$upload_box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
}

View file

@ -10,6 +10,7 @@ final class PhortuneMerchantViewController
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($id))
->needProfileImage(true)
->executeOne();
if (!$merchant) {
return new Aphront404Response();
@ -28,7 +29,7 @@ final class PhortuneMerchantViewController
->setHeader($merchant->getName())
->setUser($viewer)
->setPolicyObject($merchant)
->setHeaderIcon('fa-bank');
->setImage($merchant->getProfileImageURI());
$providers = id(new PhortunePaymentProviderConfigQuery())
->setViewer($viewer)
@ -128,6 +129,13 @@ final class PhortuneMerchantViewController
$view->addProperty(pht('Status'), $status_view);
$invoice_from = $merchant->getInvoiceEmail();
if (!$invoice_from) {
$invoice_from = pht('No email address set');
$invoice_from = phutil_tag('em', array(), $invoice_from);
}
$view->addProperty(pht('Invoice From'), $invoice_from);
$description = $merchant->getDescription();
if (strlen($description)) {
$description = new PHUIRemarkupView($viewer, $description);
@ -146,6 +154,15 @@ final class PhortuneMerchantViewController
$view->addTextContent($contact_info);
}
$footer_info = $merchant->getInvoiceFooter();
if (strlen($footer_info)) {
$footer_info = new PHUIRemarkupView($viewer, $footer_info);
$view->addSectionHeader(
pht('Invoice Footer'),
PHUIPropertyListView::ICON_SUMMARY);
$view->addTextContent($footer_info);
}
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
@ -171,6 +188,14 @@ final class PhortuneMerchantViewController
->setWorkflow(!$can_edit)
->setHref($this->getApplicationURI("merchant/edit/{$id}/")));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Logo'))
->setIcon('fa-camera')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($this->getApplicationURI("merchant/picture/{$id}/")));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View Orders'))

View file

@ -0,0 +1,138 @@
<?php
final class PhortuneMerchantEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'phortune.merchant';
public function getEngineName() {
return pht('Phortune');
}
public function getEngineApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
public function getSummaryHeader() {
return pht('Configure Phortune Merchant Forms');
}
public function getSummaryText() {
return pht('Configure creation and editing forms for Phortune Merchants.');
}
protected function newEditableObject() {
return PhortuneMerchant::initializeNewMerchant($this->getViewer());
}
protected function newObjectQuery() {
return new PhortuneMerchantQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Merchant');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Merchant: %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return $object->getName();
}
protected function getObjectCreateShortText() {
return pht('Create Merchant');
}
protected function getObjectName() {
return pht('Merchant');
}
protected function getObjectCreateCancelURI($object) {
return $this->getApplication()->getApplicationURI('/');
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('edit/');
}
protected function getObjectViewURI($object) {
return $object->getViewURI();
}
public function isEngineConfigurable() {
return false;
}
protected function buildCustomEditFields($object) {
$viewer = $this->getViewer();
if ($this->getIsCreate()) {
$member_phids = array($viewer->getPHID());
} else {
$member_phids = $object->getMemberPHIDs();
}
return array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setDescription(pht('Merchant name.'))
->setConduitTypeDescription(pht('New Merchant name.'))
->setIsRequired(true)
->setTransactionType(PhortuneMerchantTransaction::TYPE_NAME)
->setValue($object->getName()),
id(new PhabricatorUsersEditField())
->setKey('members')
->setAliases(array('memberPHIDs'))
->setLabel(pht('Members'))
->setUseEdgeTransactions(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhortuneMerchantHasMemberEdgeType::EDGECONST)
->setDescription(pht('Initial merchant members.'))
->setConduitDescription(pht('Set merchant members.'))
->setConduitTypeDescription(pht('New list of members.'))
->setInitialValue($object->getMemberPHIDs())
->setValue($member_phids),
id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setDescription(pht('Merchant description.'))
->setConduitTypeDescription(pht('New merchant description.'))
->setTransactionType(PhortuneMerchantTransaction::TYPE_DESCRIPTION)
->setValue($object->getDescription()),
id(new PhabricatorRemarkupEditField())
->setKey('contactInfo')
->setLabel(pht('Contact Info'))
->setDescription(pht('Merchant contact information.'))
->setConduitTypeDescription(pht('Merchant contact information.'))
->setTransactionType(PhortuneMerchantTransaction::TYPE_CONTACTINFO)
->setValue($object->getContactInfo()),
id(new PhabricatorTextEditField())
->setKey('invoiceEmail')
->setLabel(pht('Invoice From Email'))
->setDescription(pht('Email address invoices are sent from.'))
->setConduitTypeDescription(
pht('Email address invoices are sent from.'))
->setTransactionType(PhortuneMerchantTransaction::TYPE_INVOICEEMAIL)
->setValue($object->getInvoiceEmail()),
id(new PhabricatorRemarkupEditField())
->setKey('invoiceFooter')
->setLabel(pht('Invoice Footer'))
->setDescription(pht('Footer on invoice forms.'))
->setConduitTypeDescription(pht('Footer on invoice forms.'))
->setTransactionType(PhortuneMerchantTransaction::TYPE_INVOICEFOOTER)
->setValue($object->getInvoiceFooter()),
);
}
}

View file

@ -17,6 +17,9 @@ final class PhortuneMerchantEditor
$types[] = PhortuneMerchantTransaction::TYPE_NAME;
$types[] = PhortuneMerchantTransaction::TYPE_DESCRIPTION;
$types[] = PhortuneMerchantTransaction::TYPE_CONTACTINFO;
$types[] = PhortuneMerchantTransaction::TYPE_PICTURE;
$types[] = PhortuneMerchantTransaction::TYPE_INVOICEEMAIL;
$types[] = PhortuneMerchantTransaction::TYPE_INVOICEFOOTER;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDGE;
@ -33,6 +36,12 @@ final class PhortuneMerchantEditor
return $object->getDescription();
case PhortuneMerchantTransaction::TYPE_CONTACTINFO:
return $object->getContactInfo();
case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
return $object->getInvoiceEmail();
case PhortuneMerchantTransaction::TYPE_INVOICEFOOTER:
return $object->getInvoiceFooter();
case PhortuneMerchantTransaction::TYPE_PICTURE:
return $object->getProfileImagePHID();
}
return parent::getCustomTransactionOldValue($object, $xaction);
@ -46,6 +55,9 @@ final class PhortuneMerchantEditor
case PhortuneMerchantTransaction::TYPE_NAME:
case PhortuneMerchantTransaction::TYPE_DESCRIPTION:
case PhortuneMerchantTransaction::TYPE_CONTACTINFO:
case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
case PhortuneMerchantTransaction::TYPE_INVOICEFOOTER:
case PhortuneMerchantTransaction::TYPE_PICTURE:
return $xaction->getNewValue();
}
@ -66,6 +78,15 @@ final class PhortuneMerchantEditor
case PhortuneMerchantTransaction::TYPE_CONTACTINFO:
$object->setContactInfo($xaction->getNewValue());
return;
case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
$object->setInvoiceEmail($xaction->getNewValue());
return;
case PhortuneMerchantTransaction::TYPE_INVOICEFOOTER:
$object->setInvoiceFooter($xaction->getNewValue());
return;
case PhortuneMerchantTransaction::TYPE_PICTURE:
$object->setProfileImagePHID($xaction->getNewValue());
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
@ -79,6 +100,9 @@ final class PhortuneMerchantEditor
case PhortuneMerchantTransaction::TYPE_NAME:
case PhortuneMerchantTransaction::TYPE_DESCRIPTION:
case PhortuneMerchantTransaction::TYPE_CONTACTINFO:
case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
case PhortuneMerchantTransaction::TYPE_INVOICEFOOTER:
case PhortuneMerchantTransaction::TYPE_PICTURE:
return;
}
@ -109,6 +133,30 @@ final class PhortuneMerchantEditor
$errors[] = $error;
}
break;
case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
$new_email = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhortuneMerchantTransaction::TYPE_INVOICEEMAIL:
$new_email = $xaction->getNewValue();
break;
}
}
if (strlen($new_email)) {
$email = new PhutilEmailAddress($new_email);
$domain = $email->getDomainName();
if (!$domain) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('%s is not a valid email.', $new_email),
nonempty(last($xactions), null));
$errors[] = $error;
}
}
break;
}
return $errors;

View file

@ -6,6 +6,7 @@ final class PhortuneMerchantQuery
private $ids;
private $phids;
private $memberPHIDs;
private $needProfileImage;
public function withIDs(array $ids) {
$this->ids = $ids;
@ -22,6 +23,11 @@ final class PhortuneMerchantQuery
return $this;
}
public function needProfileImage($need) {
$this->needProfileImage = $need;
return $this;
}
protected function loadPage() {
$table = new PhortuneMerchant();
$conn = $table->establishConnection('r');
@ -50,6 +56,35 @@ final class PhortuneMerchantQuery
$merchant->attachMemberPHIDs($member_phids);
}
if ($this->needProfileImage) {
$default = null;
$file_phids = mpull($merchants, 'getProfileImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($merchants as $merchant) {
$file = idx($files, $merchant->getProfileImagePHID());
if (!$file) {
if (!$default) {
$default = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'merchant.png');
}
$file = $default;
}
$merchant->attachProfileImageFile($file);
}
}
return $merchants;
}

View file

@ -18,7 +18,8 @@ final class PhortuneMerchantSearchEngine
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new PhortuneMerchantQuery());
$query = id(new PhortuneMerchantQuery())
->needProfileImage(true);
return $query;
}
@ -74,7 +75,7 @@ final class PhortuneMerchantSearchEngine
->setHeader($merchant->getName())
->setHref('/phortune/merchant/'.$merchant->getID().'/')
->setObject($merchant)
->setImageIcon('fa-bank');
->setImageURI($merchant->getProfileImageURI());
$list->addItem($item);
}

View file

@ -9,13 +9,20 @@ final class PhortuneMerchant extends PhortuneDAO
protected $viewPolicy;
protected $description;
protected $contactInfo;
protected $invoiceEmail;
protected $invoiceFooter;
protected $profileImagePHID;
private $memberPHIDs = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
public static function initializeNewMerchant(PhabricatorUser $actor) {
return id(new PhortuneMerchant())
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->attachMemberPHIDs(array());
->attachMemberPHIDs(array())
->setContactInfo('')
->setInvoiceEmail('')
->setInvoiceFooter('');
}
protected function getConfiguration() {
@ -25,6 +32,9 @@ final class PhortuneMerchant extends PhortuneDAO
'name' => 'text255',
'description' => 'text',
'contactInfo' => 'text',
'invoiceEmail' => 'text255',
'invoiceFooter' => 'text',
'profileImagePHID' => 'phid?',
),
) + parent::getConfiguration();
}
@ -43,6 +53,22 @@ final class PhortuneMerchant extends PhortuneDAO
return $this;
}
public function getViewURI() {
return '/phortune/merchant/'.$this->getID().'/';
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */

View file

@ -6,6 +6,9 @@ final class PhortuneMerchantTransaction
const TYPE_NAME = 'merchant:name';
const TYPE_DESCRIPTION = 'merchant:description';
const TYPE_CONTACTINFO = 'merchant:contactinfo';
const TYPE_INVOICEEMAIL = 'merchant:invoiceemail';
const TYPE_INVOICEFOOTER = 'merchant:invoicefooter';
const TYPE_PICTURE = 'merchant:picture';
public function getApplicationName() {
return 'phortune';
@ -47,6 +50,14 @@ final class PhortuneMerchantTransaction
return pht(
'%s updated the contact information for this merchant.',
$this->renderHandleLink($author_phid));
case self::TYPE_INVOICEEMAIL:
return pht(
'%s updated the invoice email for this merchant.',
$this->renderHandleLink($author_phid));
case self::TYPE_INVOICEFOOTER:
return pht(
'%s updated the invoice footer for this merchant.',
$this->renderHandleLink($author_phid));
}
return parent::getTitle();
@ -57,6 +68,8 @@ final class PhortuneMerchantTransaction
switch ($this->getTransactionType()) {
case self::TYPE_DESCRIPTION:
case self::TYPE_CONTACTINFO:
case self::TYPE_INVOICEEMAIL:
case self::TYPE_INVOICEFOOTER:
return ($old === null);
}
return parent::shouldHide();
@ -68,6 +81,10 @@ final class PhortuneMerchantTransaction
return ($this->getOldValue() !== null);
case self::TYPE_CONTACTINFO:
return ($this->getOldValue() !== null);
case self::TYPE_INVOICEEMAIL:
return ($this->getOldValue() !== null);
case self::TYPE_INVOICEFOOTER:
return ($this->getOldValue() !== null);
}
return parent::hasChangeDetails();

View file

@ -0,0 +1,159 @@
<?php
final class PhortuneInvoiceView extends AphrontTagView {
private $merchantName;
private $merchantLogo;
private $merchantContact;
private $merchantFooter;
private $accountName;
private $accountContact;
private $status;
private $content;
public function setMerchantName($name) {
$this->merchantName = $name;
return $this;
}
public function setMerchantLogo($logo) {
$this->merchantLogo = $logo;
return $this;
}
public function setMerchantContact($contact) {
$this->merchantContact = $contact;
return $this;
}
public function setMerchantFooter($footer) {
$this->merchantFooter = $footer;
return $this;
}
public function setAccountName($name) {
$this->accountName = $name;
return $this;
}
public function setAccountContact($contact) {
$this->accountContact = $contact;
return $this;
}
public function setStatus($status) {
$this->status = $status;
return $this;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
protected function getTagAttributes() {
$classes = array();
$classes[] = 'phortune-invoice-view';
return array(
'class' => implode(' ', $classes),
);
}
protected function getTagContent() {
require_celerity_resource('phortune-invoice-css');
$logo = phutil_tag(
'div',
array(
'class' => 'phortune-invoice-logo',
),
phutil_tag(
'img',
array(
'height' => '50',
'width' => '50',
'alt' => $this->merchantName,
'src' => $this->merchantLogo,
)));
$to_title = phutil_tag(
'div',
array(
'class' => 'phortune-mini-header',
),
pht('To:'));
$bill_to = phutil_tag(
'td',
array(
'class' => 'phortune-invoice-to',
'width' => '50%',
),
array(
$to_title,
phutil_tag('strong', array(), $this->accountName),
phutil_tag('br', array()),
$this->accountContact,
));
$from_title = phutil_tag(
'div',
array(
'class' => 'phortune-mini-header',
),
pht('From:'));
$bill_from = phutil_tag(
'td',
array(
'class' => 'phortune-invoice-from',
'width' => '50%',
),
array(
$from_title,
phutil_tag('strong', array(), $this->merchantName),
phutil_tag('br', array()),
$this->merchantContact,
));
$contact = phutil_tag(
'table',
array(
'class' => 'phortune-invoice-contact',
'width' => '100%',
),
phutil_tag(
'tr',
array(),
array(
$bill_to,
$bill_from,
)));
$status = null;
if ($this->status) {
$status = phutil_tag(
'div',
array(
'class' => 'phortune-invoice-status',
),
$this->status);
}
$footer = phutil_tag(
'div',
array(
'class' => 'phortune-invoice-footer',
),
$this->merchantFooter);
return array(
$logo,
$contact,
$status,
$this->content,
$footer,
);
}
}

View file

@ -780,7 +780,7 @@ abstract class PhabricatorEditEngine
$controller = $this->getController();
$request = $controller->getRequest();
$action = $request->getURIData('editAction');
$action = $this->getEditAction();
$capabilities = array();
$use_default = false;
@ -996,9 +996,12 @@ abstract class PhabricatorEditEngine
->setContinueOnNoEffect(true);
try {
$xactions = $this->willApplyTransactions($object, $xactions);
$editor->applyTransactions($object, $xactions);
$this->didApplyTransactions($object, $xactions);
return $this->newEditResponse($request, $object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
@ -1349,11 +1352,80 @@ abstract class PhabricatorEditEngine
}
public function newNUXButton($text) {
$specs = $this->newCreateActionSpecifications(array());
$head = head($specs);
return id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($head['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow'])
->setColor(PHUIButtonView::GREEN);
}
final public function addActionToCrumbs(
PHUICrumbsView $crumbs,
array $parameters = array()) {
$viewer = $this->getViewer();
$specs = $this->newCreateActionSpecifications($parameters);
$head = head($specs);
$menu_uri = $head['uri'];
$dropdown = null;
if (count($specs) > 1) {
$menu_icon = 'fa-caret-square-o-down';
$menu_name = $this->getObjectCreateShortText();
$workflow = false;
$disabled = false;
$dropdown = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($specs as $spec) {
$dropdown->addAction(
id(new PhabricatorActionView())
->setName($spec['name'])
->setIcon($spec['icon'])
->setHref($spec['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow']));
}
} else {
$menu_icon = $head['icon'];
$menu_name = $head['name'];
$workflow = $head['workflow'];
$disabled = $head['disabled'];
}
$action = id(new PHUIListItemView())
->setName($menu_name)
->setHref($menu_uri)
->setIcon($menu_icon)
->setWorkflow($workflow)
->setDisabled($disabled);
if ($dropdown) {
$action->setDropdownMenu($dropdown);
}
$crumbs->addAction($action);
}
/**
* Build a raw description of available "Create New Object" UI options so
* other methods can build menus or buttons.
*/
private function newCreateActionSpecifications(array $parameters) {
$viewer = $this->getViewer();
$can_create = $this->hasCreateCapability();
if ($can_create) {
$configs = $this->loadUsableConfigurationsForCreate();
@ -1361,12 +1433,11 @@ abstract class PhabricatorEditEngine
$configs = array();
}
$dropdown = null;
$disabled = false;
$workflow = false;
$menu_icon = 'fa-plus-square';
$specs = array();
if (!$configs) {
if ($viewer->isLoggedIn()) {
$disabled = true;
@ -1382,22 +1453,15 @@ abstract class PhabricatorEditEngine
} else {
$create_uri = $this->getEditURI(null, 'nocreate/');
}
$specs[] = array(
'name' => $this->getObjectCreateShortText(),
'uri' => $create_uri,
'icon' => $menu_icon,
'disabled' => $disabled,
'workflow' => $workflow,
);
} else {
$config = head($configs);
$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';
$dropdown = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($configs as $config) {
$form_key = $config->getIdentifier();
$config_uri = $this->getEditURI(null, "form/{$form_key}/");
@ -1407,29 +1471,17 @@ abstract class PhabricatorEditEngine
->setQueryParams($parameters);
}
$item_icon = 'fa-plus';
$dropdown->addAction(
id(new PhabricatorActionView())
->setName($config->getDisplayName())
->setIcon($item_icon)
->setHref($config_uri));
}
$specs[] = array(
'name' => $config->getDisplayName(),
'uri' => $config_uri,
'icon' => 'fa-plus',
'disabled' => false,
'workflow' => false,
);
}
}
$action = id(new PHUIListItemView())
->setName($this->getObjectCreateShortText())
->setHref($create_uri)
->setIcon($menu_icon)
->setWorkflow($workflow)
->setDisabled($disabled);
if ($dropdown) {
$action->setDropdownMenu($dropdown);
}
$crumbs->addAction($action);
return $specs;
}
final public function buildEditEngineCommentView($object) {
@ -2095,6 +2147,17 @@ abstract class PhabricatorEditEngine
PhabricatorPolicyCapability::CAN_EDIT);
}
public function isCommentAction() {
return ($this->getEditAction() == 'comment');
}
public function getEditAction() {
$controller = $this->getController();
$request = $controller->getRequest();
return $request->getURIData('editAction');
}
/* -( Form Pages )--------------------------------------------------------- */
@ -2176,6 +2239,14 @@ abstract class PhabricatorEditEngine
return $page_map[$selected_key];
}
protected function willApplyTransactions($object, array $xactions) {
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
return;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -4,6 +4,7 @@ final class PhabricatorBoolEditField
extends PhabricatorEditField {
private $options;
private $asCheckbox;
public function setOptions($off_label, $on_label) {
$this->options = array(
@ -17,6 +18,15 @@ final class PhabricatorBoolEditField
return $this->options;
}
public function setAsCheckbox($as_checkbox) {
$this->asCheckbox = $as_checkbox;
return $this;
}
public function getAsCheckbox() {
return $this->asCheckbox;
}
protected function newControl() {
$options = $this->getOptions();
@ -27,10 +37,24 @@ final class PhabricatorBoolEditField
);
}
return id(new AphrontFormSelectControl())
if ($this->getAsCheckbox()) {
$key = $this->getKey();
$value = $this->getValueForControl();
$checkbox_key = $this->newHTTPParameterType()
->getCheckboxKey($key);
$id = $this->getControlID();
$control = id(new AphrontFormCheckboxControl())
->setCheckboxKey($checkbox_key)
->addCheckbox($key, '1', $options['1'], $value, $id);
} else {
$control = id(new AphrontFormSelectControl())
->setOptions($options);
}
return $control;
}
protected function newHTTPParameterType() {
return new AphrontBoolHTTPParameterType();
}

View file

@ -401,8 +401,15 @@ abstract class PhabricatorEditField extends Phobject {
public function setValue($value) {
$this->hasValue = true;
$this->initialValue = $value;
$this->value = $value;
// If we don't have an initial value set yet, use the value as the
// initial value.
$initial_value = $this->getInitialValue();
if ($initial_value === null) {
$this->initialValue = $value;
}
return $this;
}

View file

@ -553,10 +553,18 @@ abstract class PhabricatorApplicationTransaction
return true;
}
if (!is_array($old) && !strlen($old)) {
if (!is_array($old)) {
if (!strlen($old)) {
return true;
}
// The integer 0 is also uninteresting by default; this is often
// an "off" flag for something like "All Day Event".
if ($old === 0) {
return true;
}
}
break;
}
}
@ -1065,12 +1073,23 @@ abstract class PhabricatorApplicationTransaction
break;
default:
// In developer mode, provide a better hint here about which string
// we're missing.
$developer_mode = 'phabricator.developer-mode';
$is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
if ($is_developer) {
return pht(
'%s edited this object (transaction type "%s").',
$this->renderHandleLink($author_phid),
$this->getTransactionType());
} else {
return pht(
'%s edited this %s.',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName());
}
}
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
@ -1607,6 +1626,10 @@ abstract class PhabricatorApplicationTransaction
'editable by the transaction author.');
}
public function getModularType() {
return null;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -7,6 +7,10 @@ abstract class PhabricatorModularTransaction
abstract public function getBaseTransactionClass();
public function getModularType() {
return $this->getTransactionImplementation();
}
final protected function getTransactionImplementation() {
if (!$this->implementation) {
$this->implementation = $this->newTransactionImplementation();

View file

@ -196,12 +196,28 @@ abstract class PhabricatorModularTransactionType
final protected function renderDate($epoch) {
$viewer = $this->getViewer();
// We accept either epoch timestamps or dictionaries describing a
// PhutilCalendarDateTime.
if (is_array($epoch)) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($epoch)
->setViewerTimezone($viewer->getTimezoneIdentifier());
$all_day = $datetime->getIsAllDay();
$epoch = $datetime->getEpoch();
} else {
$all_day = false;
}
if ($all_day) {
$display = phabricator_date($epoch, $viewer);
} else {
$display = phabricator_datetime($epoch, $viewer);
// When rendering to text, we explicitly render the offset from UTC to
// provide context to the date: the mail may be generating with the
// server's settings, or the user may later refer back to it after changing
// timezones.
// server's settings, or the user may later refer back to it after
// changing timezones.
if ($this->isTextMode()) {
$offset = $viewer->getTimeZoneOffsetInHours();
@ -211,6 +227,7 @@ abstract class PhabricatorModularTransactionType
$display = pht('%s (UTC-%d)', $display, abs($offset));
}
}
}
return $this->renderValue($display);
}
@ -262,4 +279,8 @@ abstract class PhabricatorModularTransactionType
->setTransaction($this->getStorage());
}
final protected function isCreateTransaction() {
return $this->getStorage()->getIsCreateTransaction();
}
}

View file

@ -9,75 +9,86 @@ Overview
IMPORTANT: Calendar is a prototype application. See
@{article:User Guide: Prototype Applications}.
The Calendar application is a tool that allows users to schedule group events
and share personal plans.
There are several kinds of events you can create:
- Regular events such as a one-time meeting or a personal appointment.
- All day events such as a company-wide holiday or a vacation.
- Recurring events, which can be regular or all day, such as a weekly 1-1 or
a birthday.
Calendar allows you to schedule parties and invite other users to party with
you. Everyone loves to party. Use Calendar primarily for partying.
Editing Events
==============
Reminders
=========
All fields of basic and all day events can be edited after the event has been
created.
Calendar sends reminder email before events occur. You will receive a reminder
if:
Every instance of a recurring event has an index that maintains its place in
the sequence order. Before an instance of a recurring event is edited, it is
considered a ghost event, or a placeholder. This means that there is no
database entry for that instance. Rather, when querying for events, if a
recurring series of events overlaps with the query range, instance
placeholders of that recurring event are generated and are displayed for
that range. If a placeholder instance of a recurring event is edited, a real
entry in the database is created and all changes are saved. When that
instance falls within a query range, the real instance event replaces the
old placeholder instance.
- you have marked yourself as **attending** the event;
- the event has not been cancelled; and
- the event was not imported from an external source.
To prevent disordering of the recurring sequence of events, parent recurring
events do not allow editing of date-related fields like recurrence frequency
and recurrence start and end dates. If all instances of the recurring event
need to be rescheduled, users are encouraged to cancel a recurring event and
create a new recurring event with the revised date and time.
Reminders are sent 15 minutes before events begin.
Cancelling Events
=================
Availability
============
Cancelling basic events will hide that event from most of the builtin Calendar
queries, unless the query specifies to display cancelled events.
Across all applications, Phabricator shows a red dot next to usernames if the
user is currently attending an event. This provides a hint that they may be in
a meeting (or on vacation) and could take a while to get back to you about a
revision or task.
There are two ways to cancel an instance of a recurring event.
- Cancel an instance of a recurring event.
- Cancel the entire series of a recurring event.
Cancelling a placeholder instance of a recurring event will create a real
cancelled event that will replace the placeholder instance. Consequently,
the cancellation status of that instance of the recurring event will
persist if the parent event is cancelled and subsequently reinstated.
When an entire series of a recurring event is cancelled, all the placeholder
and real instances are also cancelled. An entire series can similarly be
reinstated, but it is currently not possible to reinstate an instance of a
cancelled recurring event series. To reinstate that instance, the entire
series must be reinstated. If an instance of a recurring event has been
cancelled, then the entire recurring event series is also cancelled,
reinstating the series will not reinstate the previously cancelled instances
of that event.
You can click through to a user's profile to see more details about their
availability.
Commenting On Recurring Events
==============================
Importing Events
================
If a placeholder instance of a recurring event has not been converted to a
real instance of the series as a result of editing or cancelling, commenting on
that placeholder instance does not currently save a draft for that instance
only. The draft is saved for the recurring event parent, so the parent
recurring event and all placeholder instances will show that draft. When a
comment is actually added to a placeholder instance, the instance is converted
to a recurrence exception, and the comment will only appear on that instance
of the recurring event.
You can import events from email and from other calendar applications
(like Google Calendar and Calendar.app) into Calendar. For a detailed
guide, see @{article:Calendar User Guide: Importing Events}.
Exporting Events
================
You can export events from Calendar to other applications by downloading
events as `.ics` files or configuring a calendar subscription.
Calendar also attaches `.ics` files containing event information when it sends
email. Most calendar applications can import these files.
For a detailed guide to exporting events, see
@{article:Calendar User Guide: Exporting Events}.
Recurring Events
================
To create a recurring event (like a weekly meeting), first create an event
normally, then select {nav Make Recurring} from the action menu and configure
how often the event should repeat.
**Monthly Events on the 29th, 30th or 31st**: If you configure an event to
repeat monthly and schedule the first instance on the 29th, 30th, or 31st of
the month, it can not occur on the same day every month because some months
do not have enough days.
Instead, these events are internally scheduled to occur relative to the end
of the month. For example, if you schedule a monthly event on the 30th of a
31 day month, it will occur on the second-to-last day of each following month.
**Complex RRULEs**: Calendar supports complex RRULEs internally (like events
that occur every-other Thursday in prime-numbered months) but does not
currently have a UI for scheduling events with complex rules.
Future versions of Calendar may improve support for complex scheduling by using
the UI. In some cases, a partial workaround is to schedule the event in another
application (which has more complex scheduling controls available) and then
import it into Calendar.
Next Steps
==========
Continue by:
- importing events with @{article:Calendar User Guide: Importing Events}; or
- exporting events with @{article:Calendar User Guide: Exporting Events}.

View file

@ -0,0 +1,132 @@
@title Calendar User Guide: Importing Events
@group userguide
Importing events from other calendars.
Overview
========
IMPORTANT: Calendar is a prototype application. See
@{article:User Guide: Prototype Applications}.
You can import events into Phabricator to other calendar applications or from
`.ics` files. This document will guide you through how to importe event data
into Phabricator.
When you import events from another application, they can not be edited in
Phabricator. Importing events allows you to share events or keep track of
events from different sources, but does not let you edit events from other
applications in Phabricator.
Import Policies
===============
When you import events, you select a visibility policy for the import. By
default, imported events are only visible to you (the user importing them).
To share imported events with other users, make the import **Visible To**
a wider set of users, like "All Users".
Importing `.ics` Files
======================
`.ics` files contain information about events, usually either about a single
event or an entire event calendar.
If you have an event or calendar in `.ics` format, you can import it into
Phabricator in two ways:
- Navigate to {nav Calendar > Imports > Import Events > Import .ics File}.
- Drag and drop the file onto a Calendar.
This will create a copy of the event in Phabricator.
If you want to update an imported event later, just repeat this process. The
event will be updated with the latest information.
Many applications send `.ics` files as email attachments. You can import these
into Phabricator.
.ics Files: Google Calendar
===========================
In **Google Calendar**, you can generate a `.ics` file for a calendar by
clicking the dropdown menu next to the calendar and selecting
{nav Calendar Settings > Export Calendar > Export this calendar}.
.ics Files: Calendar.app
========================
In **Calendar.app**, you can generate an `.ics` file for a calendar by
selecting the calendar, then selecting {nav File > Export > Export...} and
saving the calendar as a `.ics` file.
You can also convert an individual event into an `.ics` file by dragging it
from the calendar to your desktop (or any other folder).
When you import an event using an `.ics` file, Phabricator can not
automatically keep the event up to date. You'll need to repeat the process if
there are changes to the event or calendar later, so Phabricator can learn
about the updates.
Importing .ics URIs
=====================
If you have a calendar in another application that supports publishing a
`.ics` URI, you can subscribe to it in Phabricator. This will import the entire
calendar, and can be configured to automatically keep it up to date and in sync
with the external calendar.
First, find the subscription URI for the calendar you want to import (see
below for some guidance on popular calendar applications). Then, browse to
{nav Calendar > Imports > Import Events > Import .ics URI}.
When you import a URI, you can choose to enable automatic updates. If you do,
Phabricator will periodically update the events it imports from this source.
You can stop this later by turning off the automatic updates or disabling
the import.
{icon lock} **Privacy Note**: When you import via URI, the URI often contains
sensitive information (like a username, password, or secret key) which allows
anyone who knows it to access private details about events. Anyone who can edit
the import will also be able to view and edit the URI, so make sure you don't
grant edit access to users who should not have access to the event details.
.ics URIs: Google Calendar
==========================
In **Google Calendar**, you can get the subscription URI for a calendar
by selecting {nav Calendar Settings} from the dropdown next to the calendar,
then copying the URL from the {nav ICAL} link under **Private Address**. This
URI provides access to all event details, including private information.
You may need to adjust the sharing and visibility settings for the calendar
before this option is available.
Alternatively, you can use the URI from the {nav ICAL} link under
**Calendar Address** to access a more limited set of event details. You can
configure which details are available by configuring how the calendar is
shared.
.ics URIs: Calendar.app
=======================
**Calendar.app** does not support subscriptions via `.ics` URIs.
You can export a calendar as an `.ics` file by following the steps above, but
Phabricator can not automatically keep events imported in this way up to date.
Next Steps
==========
Continue by:
- returning to the @{article:Calendar User Guide}.

View file

@ -20,6 +20,8 @@ final class PhabricatorTriggerDaemon
private $nuanceSources;
private $nuanceCursors;
private $calendarEngine;
protected function run() {
// The trigger daemon is a low-level infrastructure daemon which schedules
@ -105,6 +107,7 @@ final class PhabricatorTriggerDaemon
$sleep_duration = $this->getSleepDuration();
$sleep_duration = $this->runNuanceImportCursors($sleep_duration);
$sleep_duration = $this->runGarbageCollection($sleep_duration);
$sleep_duration = $this->runCalendarNotifier($sleep_duration);
$this->sleep($sleep_duration);
} while (!$this->shouldExit());
}
@ -456,4 +459,21 @@ final class PhabricatorTriggerDaemon
return true;
}
/* -( Calendar Notifier )-------------------------------------------------- */
private function runCalendarNotifier($duration) {
$run_until = (PhabricatorTime::getNow() + $duration);
if (!$this->calendarEngine) {
$this->calendarEngine = new PhabricatorCalendarNotificationEngine();
}
$this->calendarEngine->publishNotifications();
$remaining = max(0, $run_until - PhabricatorTime::getNow());
return $remaining;
}
}

View file

@ -1402,9 +1402,11 @@ final class PhabricatorUSEnglishTranslation
),
'Setting retention policy for "%s" to %s day(s).' => array(
array(
'Setting retention policy for "%s" to one day.',
'Setting retention policy for "%s" to %s days.',
),
),
'Waiting %s second(s) for lease to activate.' => array(
'Waiting a second for lease to activate.',

View file

@ -3,6 +3,16 @@
final class AphrontFormCheckboxControl extends AphrontFormControl {
private $boxes = array();
private $checkboxKey;
public function setCheckboxKey($checkbox_key) {
$this->checkboxKey = $checkbox_key;
return $this;
}
public function getCheckboxKey() {
return $this->checkboxKey;
}
public function addCheckbox(
$name,
@ -52,6 +62,23 @@ final class AphrontFormCheckboxControl extends AphrontFormControl {
phutil_tag('th', array(), $label),
));
}
// When a user submits a form with a checkbox unchecked, the browser
// doesn't submit anything to the server. This hidden key lets the server
// know that the checkboxes were present on the client, the user just did
// not select any of them.
$checkbox_key = $this->getCheckboxKey();
if ($checkbox_key) {
$rows[] = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => $checkbox_key,
'value' => 1,
));
}
return phutil_tag(
'table',
array('class' => 'aphront-form-control-checkbox-layout'),

View file

@ -182,7 +182,7 @@ final class AphrontFormDateControlValue extends Phobject {
return null;
}
return $datetime->format('U');
return (int)$datetime->format('U');
}
private function getTimeFormat() {
@ -231,6 +231,32 @@ final class AphrontFormDateControlValue extends Phobject {
return $datetime;
}
public function newPhutilDateTime() {
$datetime = $this->getDateTime();
if (!$datetime) {
return null;
}
$all_day = !strlen($this->valueTime);
$zone_identifier = $this->viewer->getTimezoneIdentifier();
$result = id(new PhutilCalendarAbsoluteDateTime())
->setYear((int)$datetime->format('Y'))
->setMonth((int)$datetime->format('m'))
->setDay((int)$datetime->format('d'))
->setHour((int)$datetime->format('G'))
->setMinute((int)$datetime->format('i'))
->setSecond((int)$datetime->format('s'))
->setTimezone($zone_identifier);
if ($all_day) {
$result->setIsAllDay(true);
}
return $result;
}
private function getFormattedDateFromParts(
$year,
$month,

View file

@ -0,0 +1,75 @@
/**
* @provides phortune-invoice-css
*/
.phortune-invoice-view {
max-width: 800px;
margin: 16px auto;
background: #fff;
}
.phortune-invoice-view .phabricator-main-menu {
display: none;
}
.phortune-invoice-view .phabricator-standard-page-footer {
display: none;
}
.device-desktop .phortune-invoice-view .phui-property-list-key {
width: 16%;
}
.device-desktop .phortune-invoice-view .phui-property-list-value {
width: 80%;
}
.phortune-invoice-logo {
margin-bottom: 24px;
}
.phortune-invoice-logo img {
margin: 0 auto;
}
.phortune-invoice-contact {
margin-bottom: 32px;
}
.phortune-invoice-contact td {
padding: 4px 16px;
}
.phortune-invoice-to {
border-right: 1px solid {$lightblueborder};
}
.phortune-mini-header {
color: {$lightbluetext};
font-weight: bold;
text-transform: uppercase;
margin-bottom: 4px;
letter-spacing: 0.3em;
}
.phortune-invoice-status {
margin-bottom: 24px;
}
.phortune-invoice-status .phui-info-view {
margin: 0;
}
.phortune-invoice-view .phui-box.phui-object-box {
margin-bottom: 24px;
}
.phortune-invoice-footer {
color: {$lightgreytext};
margin: 48px 0 64px;
text-align: center;
}
.phortune-invoice-footer strong {
color: #000;
}

View file

@ -6,7 +6,7 @@ JX.behavior('event-all-day', function(config) {
var all_day = JX.$(config.allDayID);
JX.DOM.listen(all_day, 'change', null, function() {
var is_all_day = !!parseInt(all_day.value, 10);
var is_all_day = !!all_day.checked;
for (var ii = 0; ii < config.controlIDs.length; ii++) {
var control = JX.$(config.controlIDs[ii]);

View file

@ -1,41 +0,0 @@
/**
* @provides javelin-behavior-recurring-edit
*/
JX.behavior('recurring-edit', function(config) {
var checkbox = JX.$(config.isRecurring);
var frequency = JX.$(config.frequency);
var end_date = JX.$(config.recurrenceEndDate);
var end_date_checkbox = JX.DOM.find(end_date, 'input', 'calendar-enable');
JX.DOM.listen(checkbox, 'change', null, function() {
if (checkbox.checked) {
enableRecurring();
} else {
disableRecurring();
}
});
JX.DOM.listen(end_date, 'change', null, function() {
if (end_date_checkbox.checked) {
enableRecurring();
}
});
function enableRecurring() {
checkbox.checked = true;
frequency.disabled = false;
end_date.disabled = false;
}
function disableRecurring() {
checkbox.checked = false;
frequency.disabled = true;
end_date.disabled = true;
end_date_checkbox.checked = false;
JX.DOM.alterClass(end_date, 'datepicker-disabled', true);
}
});