mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-27 17:22:42 +01:00
(stable) Promote 2016 Week 45
This commit is contained in:
commit
111c639551
87 changed files with 3242 additions and 839 deletions
1
bin/calendar
Symbolic link
1
bin/calendar
Symbolic link
|
@ -0,0 +1 @@
|
|||
../scripts/setup/manage_calendar.php
|
BIN
resources/builtin/merchant.png
Normal file
BIN
resources/builtin/merchant.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_phortune.phortune_merchant
|
||||
ADD profileImagePHID VARBINARY(64);
|
|
@ -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;
|
|
@ -0,0 +1,5 @@
|
|||
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
|
||||
ADD seriesParentPHID VARBINARY(64);
|
||||
|
||||
UPDATE {$NAMESPACE}_calendar.calendar_event
|
||||
SET seriesParentPHID = instanceOfEventPHID;
|
|
@ -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};
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE {$NAMESPACE}_calendar.calendar_holiday;
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_calendar.calendar_eventinvitee
|
||||
ADD availability VARCHAR(64) NOT NULL;
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE {$NAMESPACE}_calendar.calendar_eventinvitee
|
||||
SET availability = 'default'
|
||||
WHERE availability = '';
|
|
@ -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);
|
||||
}
|
21
scripts/setup/manage_calendar.php
Executable file
21
scripts/setup/manage_calendar.php
Executable 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);
|
|
@ -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',
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -32,88 +32,126 @@ 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()) {
|
||||
$xactions = array();
|
||||
|
||||
$xaction = id(new PhabricatorCalendarEventTransaction())
|
||||
->setTransactionType(
|
||||
PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue(!$is_cancelled);
|
||||
$targets = array($event);
|
||||
if ($is_recurring) {
|
||||
$mode = $request->getStr('mode');
|
||||
$is_future = ($mode == 'future');
|
||||
|
||||
$editor = id(new PhabricatorCalendarEventEditor())
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setContinueOnMissingFields(true);
|
||||
// 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();
|
||||
|
||||
try {
|
||||
$editor->applyTransactions($event, array($xaction));
|
||||
$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())
|
||||
->setTransactionType(
|
||||
PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue(!$is_cancelled);
|
||||
|
||||
$editor = id(new PhabricatorCalendarEventEditor())
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setContinueOnMissingFields(true);
|
||||
|
||||
try {
|
||||
$editor->applyTransactions($target, array($xaction));
|
||||
} catch (PhabricatorApplicationTransactionValidationException $ex) {
|
||||
$validation_exception = $ex;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!$validation_exception) {
|
||||
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
|
||||
} catch (PhabricatorApplicationTransactionValidationException $ex) {
|
||||
$validation_exception = $ex;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
$title = 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 {
|
||||
$title = pht('Reinstate Event');
|
||||
$paragraph = pht('Reinstate this event?');
|
||||
$cancel = pht('Back');
|
||||
$submit = pht('Reinstate Event');
|
||||
$body = pht('Reinstate this event?');
|
||||
$show_control = false;
|
||||
}
|
||||
$submit = pht('Reinstate Event');
|
||||
} 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');
|
||||
$title = pht('Cancel Event');
|
||||
if ($is_recurring) {
|
||||
$body = pht(
|
||||
'This event is part of a series. Which events do you want to '.
|
||||
'cancel?');
|
||||
$show_control = true;
|
||||
} else {
|
||||
$title = pht('Cancel Event');
|
||||
$paragraph = pht(
|
||||
'Cancel this event? You can always reinstate the event later.');
|
||||
$cancel = pht('Back');
|
||||
$submit = pht('Cancel Event');
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
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 id(new PhabricatorCalendarEventEditEngine())
|
||||
->setController($this)
|
||||
->buildResponse();
|
||||
return $engine->buildResponse();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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_label = pht('Edit Event');
|
||||
}
|
||||
$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');
|
||||
$cancel_label = pht('Cancel Event');
|
||||
$reinstate_label = pht('Reinstate Event');
|
||||
|
||||
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)
|
||||
|
|
|
@ -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,10 +174,35 @@ final class PhabricatorCalendarEventEditEngine
|
|||
->setConduitTypeDescription(pht('New event invitees.'))
|
||||
->setValue($invitee_phids)
|
||||
->setCommentActionLabel(pht('Change Invitees')),
|
||||
);
|
||||
id(new PhabricatorRemarkupEditField())
|
||||
->setKey('description')
|
||||
->setLabel(pht('Description'))
|
||||
->setDescription(pht('Description of the event.'))
|
||||
->setTransactionType(
|
||||
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())
|
||||
->setTransactionType(
|
||||
PhabricatorCalendarEventIconTransaction::TRANSACTIONTYPE)
|
||||
->setDescription(pht('Event icon.'))
|
||||
->setConduitDescription(pht('Change the event icon.'))
|
||||
->setConduitTypeDescription(pht('New event icon.'))
|
||||
->setValue($object->getIcon()),
|
||||
|
||||
if ($this->getIsCreate()) {
|
||||
$fields[] = id(new PhabricatorBoolEditField())
|
||||
// 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'))
|
||||
|
@ -140,17 +211,8 @@ final class PhabricatorCalendarEventEditEngine
|
|||
->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())
|
||||
->setValue(true),
|
||||
id(new PhabricatorSelectEditField())
|
||||
->setKey('frequency')
|
||||
->setLabel(pht('Frequency'))
|
||||
->setOptions($frequency_options)
|
||||
|
@ -159,14 +221,12 @@ final class PhabricatorCalendarEventEditEngine
|
|||
->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())
|
||||
->setValue($frequency),
|
||||
id(new PhabricatorEpochEditField())
|
||||
->setIsLockable(false)
|
||||
->setIsDefaultable(false)
|
||||
->setAllowNull(true)
|
||||
->setHideTime($object->getIsAllDay())
|
||||
->setKey('until')
|
||||
->setLabel(pht('Repeat Until'))
|
||||
->setTransactionType(
|
||||
|
@ -174,54 +234,8 @@ final class PhabricatorCalendarEventEditEngine
|
|||
->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())
|
||||
->setKey('icon')
|
||||
->setLabel(pht('Icon'))
|
||||
->setIconSet(new PhabricatorCalendarIconSet())
|
||||
->setTransactionType(
|
||||
PhabricatorCalendarEventIconTransaction::TRANSACTIONTYPE)
|
||||
->setDescription(pht('Event icon.'))
|
||||
->setConduitDescription(pht('Change the event icon.'))
|
||||
->setConduitTypeDescription(pht('New event icon.'))
|
||||
->setValue($object->getIcon());
|
||||
->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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
if (strlen($description)) {
|
||||
$body->addRemarkupSection(
|
||||
pht('EVENT DESCRIPTION'),
|
||||
$description);
|
||||
$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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorCalendarManagementWorkflow
|
||||
extends PhabricatorManagementWorkflow {}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
return $this->newDateTimeFromDictionary($datetime);
|
||||
}
|
||||
|
||||
public function getStartDateTimeEpoch() {
|
||||
return $this->newStartDateTime()->getEpoch();
|
||||
}
|
||||
|
||||
public function newEndDateTime() {
|
||||
public function newEndDateTimeForEdit() {
|
||||
$datetime = $this->getParameter('endDateTime');
|
||||
if ($datetime) {
|
||||
return $this->newDateTimeFromDictionary($datetime);
|
||||
return $this->newDateTimeFromDictionary($datetime);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
$epoch = $this->getDateTo();
|
||||
return $this->newDateTimeFromEpoch($epoch);
|
||||
return $datetime;
|
||||
}
|
||||
|
||||
public function getEndDateTimeEpoch() {
|
||||
|
@ -850,11 +865,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
|
|||
return $this->newDateTimeFromDictionary($datetime);
|
||||
}
|
||||
|
||||
$epoch = $this->getRecurrenceEndDate();
|
||||
if (!$epoch) {
|
||||
return null;
|
||||
}
|
||||
return $this->newDateTimeFromEpoch($epoch);
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getUntilDateTimeEpoch() {
|
||||
|
@ -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.');
|
||||
|
|
|
@ -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 )----------------------------------------- */
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
85
src/applications/calendar/view/PHUIUserAvailabilityView.php
Normal file
85
src/applications/calendar/view/PHUIUserAvailabilityView.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorCalendarEventTransactionType
|
||||
extends PhabricatorModularTransactionType {}
|
||||
extends PhabricatorModularTransactionType {
|
||||
|
||||
public function isInheritedEdit() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
$object->setUntilDateTime($datetime);
|
||||
if ($value) {
|
||||
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($value);
|
||||
$datetime->setIsAllDay($editor->getNewIsAllDay());
|
||||
$object->setUntilDateTime($datetime);
|
||||
} else {
|
||||
$object->setUntilDateTime(null);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed this event to repeat until %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderNewDate());
|
||||
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() {
|
||||
return pht(
|
||||
'%s changed %s to repeat until %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderObject(),
|
||||
$this->renderNewDate());
|
||||
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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -66,7 +66,12 @@ final class PhabricatorPeopleUserPHIDType extends PhabricatorPHIDType {
|
|||
} else {
|
||||
$until = $user->getAwayUntil();
|
||||
if ($until) {
|
||||
$availability = PhabricatorObjectHandle::AVAILABILITY_NONE;
|
||||
$away = PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY;
|
||||
if ($user->getDisplayAvailability() == $away) {
|
||||
$availability = PhabricatorObjectHandle::AVAILABILITY_NONE;
|
||||
} else {
|
||||
$availability = PhabricatorObjectHandle::AVAILABILITY_PARTIAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,36 +191,75 @@ final class PhortuneCartViewController
|
|||
$crumbs->addTextCrumb(pht('Cart %d', $cart->getID()));
|
||||
$crumbs->setBorder(true);
|
||||
|
||||
$timeline = $this->buildTransactionTimeline(
|
||||
$cart,
|
||||
new PhortuneCartTransactionQuery());
|
||||
$timeline
|
||||
->setShouldTerminate(true);
|
||||
if (!$this->action) {
|
||||
$class = 'phortune-cart-page';
|
||||
$timeline = $this->buildTransactionTimeline(
|
||||
$cart,
|
||||
new PhortuneCartTransactionQuery());
|
||||
$timeline
|
||||
->setShouldTerminate(true);
|
||||
|
||||
$view = id(new PHUITwoColumnView())
|
||||
->setHeader($header)
|
||||
->setCurtain($curtain)
|
||||
->setMainColumn(array(
|
||||
$error_view,
|
||||
$details,
|
||||
$cart_box,
|
||||
$description,
|
||||
$charges,
|
||||
$timeline,
|
||||
));
|
||||
$view = id(new PHUITwoColumnView())
|
||||
->setHeader($header)
|
||||
->setCurtain($curtain)
|
||||
->setMainColumn(array(
|
||||
$error_view,
|
||||
$details,
|
||||
$cart_box,
|
||||
$description,
|
||||
$charges,
|
||||
$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())
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
$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);
|
||||
|
||||
}
|
||||
|
||||
return id(new PhortuneMerchantEditEngine())
|
||||
->setController($this)
|
||||
->buildResponse();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
|
||||
}
|
||||
}
|
|
@ -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'))
|
||||
|
|
138
src/applications/phortune/editor/PhortuneMerchantEditEngine.php
Normal file
138
src/applications/phortune/editor/PhortuneMerchantEditEngine.php
Normal 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()),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -108,6 +132,30 @@ final class PhortuneMerchantEditor
|
|||
$error->setIsMissingFieldError(true);
|
||||
$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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 )------------------------- */
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
159
src/applications/phortune/view/PhortuneInvoiceView.php
Normal file
159
src/applications/phortune/view/PhortuneInvoiceView.php
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,54 +1453,35 @@ 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}/");
|
||||
foreach ($configs as $config) {
|
||||
$form_key = $config->getIdentifier();
|
||||
$config_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}/");
|
||||
|
||||
if ($parameters) {
|
||||
$config_uri = (string)id(new PhutilURI($config_uri))
|
||||
->setQueryParams($parameters);
|
||||
}
|
||||
|
||||
$item_icon = 'fa-plus';
|
||||
|
||||
$dropdown->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
->setName($config->getDisplayName())
|
||||
->setIcon($item_icon)
|
||||
->setHref($config_uri));
|
||||
if ($parameters) {
|
||||
$config_uri = (string)id(new PhutilURI($config_uri))
|
||||
->setQueryParams($parameters);
|
||||
}
|
||||
|
||||
$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 )----------------------------------------- */
|
||||
|
||||
|
|
|
@ -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,8 +37,22 @@ final class PhabricatorBoolEditField
|
|||
);
|
||||
}
|
||||
|
||||
return id(new AphrontFormSelectControl())
|
||||
->setOptions($options);
|
||||
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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -553,8 +553,16 @@ abstract class PhabricatorApplicationTransaction
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!is_array($old) && !strlen($old)) {
|
||||
return true;
|
||||
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,10 +1073,21 @@ abstract class PhabricatorApplicationTransaction
|
|||
break;
|
||||
|
||||
default:
|
||||
return pht(
|
||||
'%s edited this %s.',
|
||||
$this->renderHandleLink($author_phid),
|
||||
$this->getApplicationObjectTypeName());
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1607,6 +1626,10 @@ abstract class PhabricatorApplicationTransaction
|
|||
'editable by the transaction author.');
|
||||
}
|
||||
|
||||
public function getModularType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorDestructibleInterface )----------------------------------- */
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -196,19 +196,36 @@ abstract class PhabricatorModularTransactionType
|
|||
final protected function renderDate($epoch) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$display = phabricator_datetime($epoch, $viewer);
|
||||
// We accept either epoch timestamps or dictionaries describing a
|
||||
// PhutilCalendarDateTime.
|
||||
if (is_array($epoch)) {
|
||||
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($epoch)
|
||||
->setViewerTimezone($viewer->getTimezoneIdentifier());
|
||||
|
||||
// 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.
|
||||
$all_day = $datetime->getIsAllDay();
|
||||
|
||||
if ($this->isTextMode()) {
|
||||
$offset = $viewer->getTimeZoneOffsetInHours();
|
||||
if ($offset >= 0) {
|
||||
$display = pht('%s (UTC+%d)', $display, $offset);
|
||||
} else {
|
||||
$display = pht('%s (UTC-%d)', $display, abs($offset));
|
||||
$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.
|
||||
|
||||
if ($this->isTextMode()) {
|
||||
$offset = $viewer->getTimeZoneOffsetInHours();
|
||||
if ($offset >= 0) {
|
||||
$display = pht('%s (UTC+%d)', $display, $offset);
|
||||
} else {
|
||||
$display = pht('%s (UTC-%d)', $display, abs($offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -262,4 +279,8 @@ abstract class PhabricatorModularTransactionType
|
|||
->setTransaction($this->getStorage());
|
||||
}
|
||||
|
||||
final protected function isCreateTransaction() {
|
||||
return $this->getStorage()->getIsCreateTransaction();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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}.
|
||||
|
|
132
src/docs/user/userguide/calendar_imports.diviner
Normal file
132
src/docs/user/userguide/calendar_imports.diviner
Normal 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}.
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1402,8 +1402,10 @@ final class PhabricatorUSEnglishTranslation
|
|||
),
|
||||
|
||||
'Setting retention policy for "%s" to %s day(s).' => array(
|
||||
'Setting retention policy for "%s" to one day.',
|
||||
'Setting retention policy for "%s" to %s days.',
|
||||
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(
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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,
|
||||
|
|
75
webroot/rsrc/css/application/phortune/phortune-invoice.css
Normal file
75
webroot/rsrc/css/application/phortune/phortune-invoice.css
Normal 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;
|
||||
}
|
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
Loading…
Reference in a new issue