1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-05 12:21:02 +01:00

(stable) Promote 2016 Week 41

This commit is contained in:
epriestley 2016-10-09 06:30:38 -07:00
commit c9cc383291
83 changed files with 3070 additions and 1066 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -7,10 +7,10 @@
*/
return array(
'names' => array(
'conpherence.pkg.css' => 'b1547973',
'conpherence.pkg.css' => '4601645d',
'conpherence.pkg.js' => '11f3e07e',
'core.pkg.css' => 'cfc3eabe',
'core.pkg.js' => '975d6a27',
'core.pkg.css' => 'de918edf',
'core.pkg.js' => '30185d95',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => 'e1d704ce',
'differential.pkg.js' => '634399e9',
@ -35,9 +35,9 @@ return array(
'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af',
'rsrc/css/application/auth/auth.css' => '0877ed6e',
'rsrc/css/application/base/main-menu-view.css' => 'f03e17be',
'rsrc/css/application/base/notification-menu.css' => 'b3ab500d',
'rsrc/css/application/base/notification-menu.css' => '1e055865',
'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601',
'rsrc/css/application/base/phui-theme.css' => '027ba77e',
'rsrc/css/application/base/phui-theme.css' => '798c69b8',
'rsrc/css/application/base/standard-page-view.css' => '79176f5a',
'rsrc/css/application/chatlog/chatlog.css' => 'd295b020',
'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4',
@ -46,11 +46,11 @@ return array(
'rsrc/css/application/config/config-template.css' => '8f18fa41',
'rsrc/css/application/config/setup-issue.css' => 'f794cfc3',
'rsrc/css/application/config/unhandled-exception.css' => '4c96257a',
'rsrc/css/application/conpherence/durable-column.css' => 'af11a2a7',
'rsrc/css/application/conpherence/header-pane.css' => '517de9fe',
'rsrc/css/application/conpherence/menu.css' => '78c7b811',
'rsrc/css/application/conpherence/durable-column.css' => '44bcaa19',
'rsrc/css/application/conpherence/header-pane.css' => '20a7028c',
'rsrc/css/application/conpherence/menu.css' => '4f51db5a',
'rsrc/css/application/conpherence/message-pane.css' => '0d7dff02',
'rsrc/css/application/conpherence/notification.css' => '6cdcc253',
'rsrc/css/application/conpherence/notification.css' => '965db05b',
'rsrc/css/application/conpherence/participant-pane.css' => '7bba0b56',
'rsrc/css/application/conpherence/transaction.css' => '46253e19',
'rsrc/css/application/conpherence/update.css' => '53bc527a',
@ -154,7 +154,7 @@ return array(
'rsrc/css/phui/phui-object-item-list-view.css' => '87278fa0',
'rsrc/css/phui/phui-pager.css' => 'bea33d23',
'rsrc/css/phui/phui-pinboard-view.css' => '2495140e',
'rsrc/css/phui/phui-profile-menu.css' => '8a3fc181',
'rsrc/css/phui/phui-profile-menu.css' => '4768721a',
'rsrc/css/phui/phui-property-list-view.css' => '6d8e58ac',
'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591',
'rsrc/css/phui/phui-segment-bar-view.css' => '46342871',
@ -428,7 +428,7 @@ return array(
'rsrc/js/application/aphlict/Aphlict.js' => '5359e785',
'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '49e20786',
'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'fb20ac8d',
'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761',
'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9',
'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',
@ -438,7 +438,7 @@ return array(
'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408',
'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2',
'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a',
'rsrc/js/application/conpherence/behavior-durable-column.js' => 'e287689f',
'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c5238acb',
'rsrc/js/application/conpherence/behavior-menu.js' => '9eb55204',
'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8',
'rsrc/js/application/conpherence/behavior-pontificate.js' => 'f2e58483',
@ -547,7 +547,6 @@ return array(
'rsrc/js/core/behavior-autofocus.js' => '7319e029',
'rsrc/js/core/behavior-badge-view.js' => '8ff5e24c',
'rsrc/js/core/behavior-choose-control.js' => '327a00d1',
'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2',
'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae',
'rsrc/js/core/behavior-detect-timezone.js' => '4c193c96',
'rsrc/js/core/behavior-device.js' => 'bb1dd507',
@ -618,11 +617,11 @@ return array(
'conduit-api-css' => '7bc725c4',
'config-options-css' => '0ede4c9b',
'config-page-css' => '8798e14f',
'conpherence-durable-column-view' => 'af11a2a7',
'conpherence-header-pane-css' => '517de9fe',
'conpherence-menu-css' => '78c7b811',
'conpherence-durable-column-view' => '44bcaa19',
'conpherence-header-pane-css' => '20a7028c',
'conpherence-menu-css' => '4f51db5a',
'conpherence-message-pane-css' => '0d7dff02',
'conpherence-notification-css' => '6cdcc253',
'conpherence-notification-css' => '965db05b',
'conpherence-participant-pane-css' => '7bba0b56',
'conpherence-thread-manager' => '01774ab2',
'conpherence-transaction-css' => '46253e19',
@ -653,9 +652,8 @@ return array(
'javelin-behavior' => '61cbc29a',
'javelin-behavior-aphlict-dropdown' => '49e20786',
'javelin-behavior-aphlict-listen' => 'fb20ac8d',
'javelin-behavior-aphlict-status' => 'ea681761',
'javelin-behavior-aphlict-status' => '5e2634b9',
'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
'javelin-behavior-aphront-crop' => 'fa0f4fc2',
'javelin-behavior-aphront-drag-and-drop-textarea' => '484a6e22',
'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3',
'javelin-behavior-aphront-more' => 'a80d0378',
@ -699,7 +697,7 @@ return array(
'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc',
'javelin-behavior-doorkeeper-tag' => 'e5822781',
'javelin-behavior-drydock-live-operation-status' => '901935ef',
'javelin-behavior-durable-column' => 'e287689f',
'javelin-behavior-durable-column' => 'c5238acb',
'javelin-behavior-editengine-reorder-configs' => 'd7a74243',
'javelin-behavior-editengine-reorder-fields' => 'b59e1e96',
'javelin-behavior-error-log' => '6882e80a',
@ -860,7 +858,7 @@ return array(
'phabricator-nav-view-css' => 'b29426e9',
'phabricator-notification' => 'ccf1cbf8',
'phabricator-notification-css' => '3f6c89c9',
'phabricator-notification-menu-css' => 'b3ab500d',
'phabricator-notification-menu-css' => '1e055865',
'phabricator-object-selector-css' => '85ee8ce6',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => 'cfd23f37',
@ -932,14 +930,14 @@ return array(
'phui-object-item-list-view-css' => '87278fa0',
'phui-pager-css' => 'bea33d23',
'phui-pinboard-view-css' => '2495140e',
'phui-profile-menu-css' => '8a3fc181',
'phui-profile-menu-css' => '4768721a',
'phui-property-list-view-css' => '6d8e58ac',
'phui-remarkup-preview-css' => '1a8f2591',
'phui-segment-bar-view-css' => '46342871',
'phui-spacing-css' => '042804d6',
'phui-status-list-view-css' => 'd5263e49',
'phui-tag-view-css' => '6bbd83e2',
'phui-theme-css' => '027ba77e',
'phui-theme-css' => '798c69b8',
'phui-timeline-view-css' => 'bc523970',
'phui-two-column-view-css' => 'fcfbe347',
'phui-workboard-color-css' => 'ac6fe6a7',
@ -1424,6 +1422,12 @@ return array(
'javelin-stratcom',
'javelin-dom',
),
'5e2634b9' => array(
'javelin-behavior',
'javelin-aphlict',
'phabricator-phtize',
'javelin-dom',
),
'5e9f347c' => array(
'javelin-behavior',
'multirow-row-manager',
@ -1957,6 +1961,16 @@ return array(
'javelin-install',
'javelin-dom',
),
'c5238acb' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-behavior-device',
'javelin-scrollbar',
'javelin-quicksand',
'phabricator-keyboard-shortcut',
'conpherence-thread-manager',
),
'c587b80f' => array(
'javelin-install',
),
@ -2109,16 +2123,6 @@ return array(
'javelin-stratcom',
'javelin-dom',
),
'e287689f' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-behavior-device',
'javelin-scrollbar',
'javelin-quicksand',
'phabricator-keyboard-shortcut',
'conpherence-thread-manager',
),
'e292eaf4' => array(
'javelin-install',
),
@ -2169,12 +2173,6 @@ return array(
'javelin-dom',
'phabricator-draggable-list',
),
'ea681761' => array(
'javelin-behavior',
'javelin-aphlict',
'phabricator-phtize',
'javelin-dom',
),
'edd1ba66' => array(
'javelin-behavior',
'javelin-stratcom',
@ -2243,12 +2241,6 @@ return array(
'javelin-util',
'phabricator-busy',
),
'fa0f4fc2' => array(
'javelin-behavior',
'javelin-dom',
'javelin-vector',
'javelin-magical-init',
),
'fb20ac8d' => array(
'javelin-behavior',
'javelin-aphlict',

View file

@ -1,52 +1,3 @@
<?php
$table = new PhabricatorCalendarEvent();
$conn = $table->establishConnection('w');
// Previously, "All Day" events were stored with a start and end date set to
// the earliest possible start and end seconds for the corresponding days. We
// now store all day events with their "date" epochs as UTC, separate from
// individual event times.
$zone_min = new DateTimeZone('Pacific/Midway');
$zone_max = new DateTimeZone('Pacific/Kiritimati');
$zone_utc = new DateTimeZone('UTC');
foreach (new LiskMigrationIterator($table) as $event) {
// If this event has already migrated, skip it.
if ($event->getAllDayDateFrom()) {
continue;
}
$is_all_day = $event->getIsAllDay();
$epoch_min = $event->getDateFrom();
$epoch_max = $event->getDateTo();
$date_min = new DateTime('@'.$epoch_min);
$date_max = new DateTime('@'.$epoch_max);
if ($is_all_day) {
$date_min->setTimeZone($zone_min);
$date_min->modify('+2 days');
$date_max->setTimeZone($zone_max);
$date_max->modify('-2 days');
} else {
$date_min->setTimeZone($zone_utc);
$date_max->setTimeZone($zone_utc);
}
$string_min = $date_min->format('Y-m-d');
$string_max = $date_max->format('Y-m-d 23:59:00');
$allday_min = id(new DateTime($string_min, $zone_utc))->format('U');
$allday_max = id(new DateTime($string_max, $zone_utc))->format('U');
queryfx(
$conn,
'UPDATE %T SET allDayDateFrom = %d, allDayDateTo = %d
WHERE id = %d',
$table->getTableName(),
$allday_min,
$allday_max,
$event->getID());
}
// This migration was replaced by "20161004.cal.01.noepoch.php".

View file

@ -0,0 +1,8 @@
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
ADD utcInitialEpoch INT UNSIGNED NOT NULL;
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
ADD utcUntilEpoch INT UNSIGNED;
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
ADD utcInstanceEpoch INT UNSIGNED;

View file

@ -0,0 +1,5 @@
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
ADD parameters LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
UPDATE {$NAMESPACE}_calendar.calendar_event
SET parameters = '{}' WHERE parameters = '';

View file

@ -0,0 +1,125 @@
<?php
$table = new PhabricatorCalendarEvent();
$conn = $table->establishConnection('w');
$table_name = 'calendar_event';
// Long ago, "All Day" events were stored with a start and end date set to
// the earliest possible start and end seconds for the corresponding days. We
// then moved to store all day events with their "date" epochs as UTC, separate
// from individual event times. Both systems were later replaced with use of
// CalendarDateTime.
$zone_min = new DateTimeZone('Pacific/Midway');
$zone_max = new DateTimeZone('Pacific/Kiritimati');
$zone_utc = new DateTimeZone('UTC');
foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) {
$parameters = phutil_json_decode($row['parameters']);
if (isset($parameters['startDateTime'])) {
// This event has already been migrated.
continue;
}
$is_all_day = $row['isAllDay'];
if (empty($row['allDayDateFrom'])) {
// No "allDayDateFrom" means this is an old event which was never migrated
// by the earlier "20160715.event.03.allday.php" migration. The dateFrom
// and dateTo will be minimum and maximum earthly seconds for the event. We
// convert them to UTC if they were in extreme timezones.
$epoch_min = $row['dateFrom'];
$epoch_max = $row['dateTo'];
if ($is_all_day) {
$date_min = new DateTime('@'.$epoch_min);
$date_max = new DateTime('@'.$epoch_max);
$date_min->setTimeZone($zone_min);
$date_min->modify('+2 days');
$date_max->setTimeZone($zone_max);
$date_max->modify('-2 days');
$string_min = $date_min->format('Y-m-d');
$string_max = $date_max->format('Y-m-d 23:59:00');
$utc_min = id(new DateTime($string_min, $zone_utc))->format('U');
$utc_max = id(new DateTime($string_max, $zone_utc))->format('U');
} else {
$utc_min = $epoch_min;
$utc_max = $epoch_max;
}
} else {
// This is an event which was migrated already. We can pick the correct
// epoch timestamps based on the "isAllDay" flag.
if ($is_all_day) {
$utc_min = $row['allDayDateFrom'];
$utc_max = $row['allDayDateTo'];
} else {
$utc_min = $row['dateFrom'];
$utc_max = $row['dateTo'];
}
}
$utc_until = $row['recurrenceEndDate'];
$start_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($utc_min);
if ($is_all_day) {
$start_datetime->setIsAllDay(true);
}
$end_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($utc_max);
if ($is_all_day) {
$end_datetime->setIsAllDay(true);
}
if ($utc_until) {
$until_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($utc_until);
} else {
$until_datetime = null;
}
$parameters['startDateTime'] = $start_datetime->toDictionary();
$parameters['endDateTime'] = $end_datetime->toDictionary();
if ($until_datetime) {
$parameters['untilDateTime'] = $until_datetime->toDictionary();
}
queryfx(
$conn,
'UPDATE %T SET parameters = %s WHERE id = %d',
$table_name,
phutil_json_encode($parameters),
$row['id']);
}
// Generate UTC epochs for all events. We can't readily do this one at a
// time because instance UTC epochs rely on having the parent event.
$viewer = PhabricatorUser::getOmnipotentUser();
$all_events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->execute();
foreach ($all_events as $event) {
if ($event->getUTCInitialEpoch()) {
// Already migrated.
continue;
}
try {
$event->updateUTCEpochs();
} catch (Exception $ex) {
continue;
}
queryfx(
$conn,
'UPDATE %T SET
utcInitialEpoch = %d,
utcUntilEpoch = %nd,
utcInstanceEpoch = %nd WHERE id = %d',
$table_name,
$event->getUTCInitialEpoch(),
$event->getUTCUntilEpoch(),
$event->getUTCInstanceEpoch(),
$event->getID());
}

View file

@ -0,0 +1,44 @@
<?php
$table = new PhabricatorCalendarEvent();
$conn = $table->establishConnection('w');
$table_name = 'calendar_event';
foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) {
$parameters = phutil_json_decode($row['parameters']);
if (isset($parameters['recurrenceRule'])) {
// This event has already been migrated.
continue;
}
if (!$row['isRecurring']) {
continue;
}
$old_rule = $row['recurrenceFrequency'];
if (!$old_rule) {
continue;
}
try {
$frequency = phutil_json_decode($old_rule);
if ($frequency) {
$frequency_rule = $frequency['rule'];
$frequency_rule = phutil_utf8_strtoupper($frequency_rule);
$rrule = id(new PhutilCalendarRecurrenceRule())
->setFrequency($frequency_rule);
}
} catch (Exception $ex) {
continue;
}
$parameters['recurrenceRule'] = $rrule->toDictionary();
queryfx(
$conn,
'UPDATE %T SET parameters = %s WHERE id = %d',
$table_name,
phutil_json_encode($parameters),
$row['id']);
}

View file

@ -0,0 +1,14 @@
CREATE TABLE {$NAMESPACE}_calendar.calendar_export (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
name LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
authorPHID VARBINARY(64) NOT NULL,
policyMode VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
queryKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
secretKey BINARY(20) NOT NULL,
isDisabled BOOL NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
KEY `key_author` (authorPHID),
UNIQUE KEY `key_secret` (secretKey)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,19 @@
CREATE TABLE {$NAMESPACE}_calendar.calendar_exporttransaction (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
authorPHID VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
viewPolicy VARBINARY(64) NOT NULL,
editPolicy VARBINARY(64) NOT NULL,
commentPHID VARBINARY(64) DEFAULT NULL,
commentVersion INT UNSIGNED NOT NULL,
transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (`phid`),
KEY `key_object` (`objectPHID`)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

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

View file

@ -0,0 +1,34 @@
<?php
// Rebuild all Conpherence Room images to profile standards
//
$table = new ConpherenceThread();
$conn = $table->establishConnection('w');
$table_name = 'conpherence_thread';
foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) {
$images = phutil_json_decode($row['imagePHIDs']);
if (!$images) {
continue;
}
$file_phid = idx($images, 'original');
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($file_phid))
->executeOne();
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
$xformed = $xform->executeTransform($file);
$new_phid = $xformed->getPHID();
queryfx(
$conn,
'UPDATE %T SET profileImagePHID = %s WHERE id = %d',
$table->getTableName(),
$new_phid,
$row['id']);
}

View file

@ -292,7 +292,6 @@ phutil_register_library_map(array(
'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php',
'ConpherenceFormDragAndDropUploadControl' => 'applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php',
'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php',
'ConpherenceImageData' => 'applications/conpherence/constants/ConpherenceImageData.php',
'ConpherenceIndex' => 'applications/conpherence/storage/ConpherenceIndex.php',
'ConpherenceLayoutView' => 'applications/conpherence/view/ConpherenceLayoutView.php',
'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php',
@ -305,11 +304,11 @@ phutil_register_library_map(array(
'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php',
'ConpherenceParticipantView' => 'applications/conpherence/view/ConpherenceParticipantView.php',
'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php',
'ConpherencePicCropControl' => 'applications/conpherence/view/ConpherencePicCropControl.php',
'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php',
'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php',
'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php',
'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php',
'ConpherenceRoomPictureController' => 'applications/conpherence/controller/ConpherenceRoomPictureController.php',
'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php',
'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php',
'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php',
@ -2076,8 +2075,27 @@ phutil_register_library_map(array(
'PhabricatorCalendarEventTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarEventTransactionType.php',
'PhabricatorCalendarEventUntilDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php',
'PhabricatorCalendarEventViewController' => 'applications/calendar/controller/PhabricatorCalendarEventViewController.php',
'PhabricatorCalendarExport' => 'applications/calendar/storage/PhabricatorCalendarExport.php',
'PhabricatorCalendarExportDisableController' => 'applications/calendar/controller/PhabricatorCalendarExportDisableController.php',
'PhabricatorCalendarExportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportDisableTransaction.php',
'PhabricatorCalendarExportEditController' => 'applications/calendar/controller/PhabricatorCalendarExportEditController.php',
'PhabricatorCalendarExportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarExportEditEngine.php',
'PhabricatorCalendarExportEditor' => 'applications/calendar/editor/PhabricatorCalendarExportEditor.php',
'PhabricatorCalendarExportICSController' => 'applications/calendar/controller/PhabricatorCalendarExportICSController.php',
'PhabricatorCalendarExportListController' => 'applications/calendar/controller/PhabricatorCalendarExportListController.php',
'PhabricatorCalendarExportModeTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php',
'PhabricatorCalendarExportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php',
'PhabricatorCalendarExportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarExportPHIDType.php',
'PhabricatorCalendarExportQuery' => 'applications/calendar/query/PhabricatorCalendarExportQuery.php',
'PhabricatorCalendarExportQueryKeyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php',
'PhabricatorCalendarExportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarExportSearchEngine.php',
'PhabricatorCalendarExportTransaction' => 'applications/calendar/storage/PhabricatorCalendarExportTransaction.php',
'PhabricatorCalendarExportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php',
'PhabricatorCalendarExportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php',
'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php',
'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php',
'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php',
'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php',
'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php',
'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php',
@ -4769,7 +4787,6 @@ phutil_register_library_map(array(
'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor',
'ConpherenceFormDragAndDropUploadControl' => 'AphrontFormControl',
'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceImageData' => 'ConpherenceConstants',
'ConpherenceIndex' => 'ConpherenceDAO',
'ConpherenceLayoutView' => 'AphrontTagView',
'ConpherenceListController' => 'ConpherenceController',
@ -4782,11 +4799,11 @@ phutil_register_library_map(array(
'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipantView' => 'AphrontView',
'ConpherenceParticipationStatus' => 'ConpherenceConstants',
'ConpherencePicCropControl' => 'AphrontFormControl',
'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler',
'ConpherenceRoomListController' => 'ConpherenceController',
'ConpherenceRoomPictureController' => 'ConpherenceController',
'ConpherenceRoomTestCase' => 'ConpherenceTestCase',
'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ConpherenceTestCase' => 'PhabricatorTestCase',
@ -6827,8 +6844,32 @@ phutil_register_library_map(array(
'PhabricatorCalendarEventTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarEventUntilDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExport' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarExportDisableController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportDisableTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarExportEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarExportICSController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportModeTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportNameTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarExportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarExportQueryKeyTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarExportTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarExportTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarExportTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO',
'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase',
'PhabricatorCalendarICSWriter' => 'Phobject',
'PhabricatorCalendarIconSet' => 'PhabricatorIconSet',
'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',

View file

@ -62,6 +62,19 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication {
'export/(?P<id>[1-9]\d*)/(?P<filename>[^/]*)'
=> 'PhabricatorCalendarEventExportController',
),
'export/' => array(
$this->getQueryRoutePattern()
=> 'PhabricatorCalendarExportListController',
$this->getEditRoutePattern('edit/')
=> 'PhabricatorCalendarExportEditController',
'(?P<id>[1-9]\d*)/'
=> 'PhabricatorCalendarExportViewController',
'ics/(?P<secretKey>[^/]+)/(?P<filename>[^/]*)'
=> 'PhabricatorCalendarExportICSController',
'disable/(?P<id>[1-9]\d*)/'
=> 'PhabricatorCalendarExportDisableController',
),
),
);
}

View file

@ -2,4 +2,20 @@
abstract class PhabricatorCalendarController extends PhabricatorController {
protected function newICSResponse(
PhabricatorUser $viewer,
$file_name,
array $events) {
$ics_data = id(new PhabricatorCalendarICSWriter())
->setViewer($viewer)
->setEvents($events)
->writeICSDocument();
return id(new AphrontFileResponse())
->setDownload($file_name)
->setMimeType('text/calendar')
->setContent($ics_data);
}
}

View file

@ -19,22 +19,16 @@ final class PhabricatorCalendarEventExportController
return new Aphront404Response();
}
$file_name = $event->getICSFilename();
$event_node = $event->newIntermediateEventNode($viewer);
if ($event->isChildEvent()) {
$target = $event->getParentEvent();
} else {
$target = $event;
}
$document_node = id(new PhutilCalendarDocumentNode())
->appendChild($event_node);
$root_node = id(new PhutilCalendarRootNode())
->appendChild($document_node);
$ics_data = id(new PhutilICSWriter())
->writeICSDocument($root_node);
return id(new AphrontFileResponse())
->setDownload($file_name)
->setMimeType('text/calendar')
->setContent($ics_data);
return $this->newICSResponse(
$viewer,
$target->getICSFileName(),
array($target));
}
}

View file

@ -11,17 +11,19 @@ final class PhabricatorCalendarEventListController
$year = $request->getURIData('year');
$month = $request->getURIData('month');
$day = $request->getURIData('day');
$engine = new PhabricatorCalendarEventSearchEngine();
if ($month && $year) {
$engine->setCalendarYearAndMonthAndDay($year, $month, $day);
}
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($request->getURIData('queryKey'))
->setSearchEngine($engine);
$nav_items = $this->buildNavigationItems();
return $this->delegateToController($controller);
return $engine
->setNavigationItems($nav_items)
->setController($this)
->buildResponse();
}
protected function buildApplicationCrumbs() {
@ -34,4 +36,18 @@ final class PhabricatorCalendarEventListController
return $crumbs;
}
protected function buildNavigationItems() {
$items = array();
$items[] = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_LABEL)
->setName(pht('Import/Export'));
$items[] = id(new PHUIListItemView())
->setName('Exports')
->setHref('/calendar/export/');
return $items;
}
}

View file

@ -27,8 +27,8 @@ final class PhabricatorCalendarEventViewController
$page_title = $monogram.' '.$event->getName();
$crumbs = $this->buildApplicationCrumbs();
$start = new DateTime('@'.$event->getViewerDateFrom());
$start->setTimeZone($viewer->getTimeZone());
$start = $event->newStartDateTime()
->newPHPDateTime();
$crumbs->addTextCrumb(
$start->format('F Y'),
@ -72,9 +72,9 @@ final class PhabricatorCalendarEventViewController
$comment_view,
))
->setCurtain($curtain)
->addPropertySection($details_header, $details)
->addPropertySection(pht('Description'), $description)
->addPropertySection($recurring_header, $recurring)
->addPropertySection(pht('Description'), $description);
->addPropertySection($details_header, $details);
return $this->newPage()
->setTitle($page_title)
@ -348,9 +348,16 @@ final class PhabricatorCalendarEventViewController
->render();
}
$rule = $event->getFrequencyRule();
switch ($rule) {
case PhabricatorCalendarEvent::FREQUENCY_DAILY:
$rrule = $event->newRecurrenceRule();
if ($rrule) {
$frequency = $rrule->getFrequency();
} else {
$frequency = null;
}
switch ($frequency) {
case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY:
if ($is_parent) {
$message = pht('This event repeats every day.');
} else {
@ -359,7 +366,7 @@ final class PhabricatorCalendarEventViewController
$parent_link);
}
break;
case PhabricatorCalendarEvent::FREQUENCY_WEEKLY:
case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY:
if ($is_parent) {
$message = pht('This event repeats every week.');
} else {
@ -368,7 +375,7 @@ final class PhabricatorCalendarEventViewController
$parent_link);
}
break;
case PhabricatorCalendarEvent::FREQUENCY_MONTHLY:
case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY:
if ($is_parent) {
$message = pht('This event repeats every month.');
} else {
@ -377,7 +384,7 @@ final class PhabricatorCalendarEventViewController
$parent_link);
}
break;
case PhabricatorCalendarEvent::FREQUENCY_YEARLY:
case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY:
if ($is_parent) {
$message = pht('This event repeats every year.');
} else {

View file

@ -0,0 +1,63 @@
<?php
final class PhabricatorCalendarExportDisableController
extends PhabricatorCalendarController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$export = id(new PhabricatorCalendarExportQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$export) {
return new Aphront404Response();
}
$export_uri = $export->getURI();
$is_disable = !$export->getIsDisabled();
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PhabricatorCalendarExportTransaction())
->setTransactionType(
PhabricatorCalendarExportDisableTransaction::TRANSACTIONTYPE)
->setNewValue($is_disable ? 1 : 0);
$editor = id(new PhabricatorCalendarExportEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($export, $xactions);
return id(new AphrontRedirectResponse())->setURI($export_uri);
}
if ($is_disable) {
$title = pht('Disable Export');
$body = pht(
'Disable this export? The export URI will no longer function.');
$button = pht('Disable Export');
} else {
$title = pht('Enable Export');
$body = pht(
'Enable this export? Anyone who knows the export URI will be able '.
'to export the data.');
$button = pht('Enable Export');
}
return $this->newDialog()
->setTitle($title)
->appendParagraph($body)
->addCancelButton($export_uri)
->addSubmitButton($button);
}
}

View file

@ -0,0 +1,12 @@
<?php
final class PhabricatorCalendarExportEditController
extends PhabricatorCalendarController {
public function handleRequest(AphrontRequest $request) {
return id(new PhabricatorCalendarExportEditEngine())
->setController($this)
->buildResponse();
}
}

View file

@ -0,0 +1,97 @@
<?php
final class PhabricatorCalendarExportICSController
extends PhabricatorCalendarController {
public function shouldRequireLogin() {
// Export URIs are available if you know the secret key. We can't do any
// other kind of authentication because third-party applications like
// Google Calendar and Calendar.app need to be able to fetch these URIs.
return false;
}
public function handleRequest(AphrontRequest $request) {
$omnipotent = PhabricatorUser::getOmnipotentUser();
// NOTE: We're using the omnipotent viewer to fetch the export, but the
// URI must contain the secret key. Once we load the export we'll figure
// out who the effective viewer is.
$export = id(new PhabricatorCalendarExportQuery())
->setViewer($omnipotent)
->withSecretKeys(array($request->getURIData('secretKey')))
->executeOne();
if (!$export) {
return new Aphront404Response();
}
if ($export->getIsDisabled()) {
return new Aphront404Response();
}
$author = id(new PhabricatorPeopleQuery())
->setViewer($omnipotent)
->withPHIDs(array($export->getAuthorPHID()))
->needUserSettings(true)
->executeOne();
if (!$author) {
return new Aphront404Response();
}
$mode = $export->getPolicyMode();
switch ($mode) {
case PhabricatorCalendarExport::MODE_PUBLIC:
$viewer = new PhabricatorUser();
break;
case PhabricatorCalendarExport::MODE_PRIVILEGED:
$viewer = $author;
break;
default:
throw new Exception(
pht(
'This export has an invalid mode ("%s").',
$mode));
}
$engine = id(new PhabricatorCalendarEventSearchEngine())
->setViewer($viewer);
$query_key = $export->getQueryKey();
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($omnipotent)
->withEngineClassNames(array(get_class($engine)))
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved) {
$saved = $engine->buildSavedQueryFromBuiltin($query_key);
}
if (!$saved) {
return new Aphront404Response();
}
$saved = clone $saved;
// Mark this as a query for export, so we get the correct ghost/recurring
// behaviors. We also want to load all matching events.
$saved->setParameter('export', true);
$saved->setParameter('limit', 0xFFFF);
// Remove any range constraints. We always export all matching events into
// ICS files.
$saved->setParameter('rangeStart', null);
$saved->setParameter('rangeEnd', null);
$saved->setParameter('upcoming', null);
$query = $engine->buildQueryFromSavedQuery($saved);
$events = $query
->setViewer($viewer)
->execute();
return $this->newICSResponse(
$viewer,
$export->getICSFilename(),
$events);
}
}

View file

@ -0,0 +1,27 @@
<?php
final class PhabricatorCalendarExportListController
extends PhabricatorCalendarController {
public function handleRequest(AphrontRequest $request) {
return id(new PhabricatorCalendarExportSearchEngine())
->setController($this)
->buildResponse();
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$doc_name = 'Calendar User Guide: Exporting Events';
$doc_href = PhabricatorEnv::getDoclink($doc_name);
$crumbs->addAction(
id(new PHUIListItemView())
->setName(pht('Guide: Exporting Events'))
->setIcon('fa-book')
->setHref($doc_href));
return $crumbs;
}
}

View file

@ -0,0 +1,176 @@
<?php
final class PhabricatorCalendarExportViewController
extends PhabricatorCalendarController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$export = id(new PhabricatorCalendarExportQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$export) {
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
pht('Exports'),
'/calendar/export/');
$crumbs->addTextCrumb(pht('Export %d', $export->getID()));
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$export,
new PhabricatorCalendarExportTransactionQuery());
$timeline->setShouldTerminate(true);
$header = $this->buildHeaderView($export);
$curtain = $this->buildCurtain($export);
$details = $this->buildPropertySection($export);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setMainColumn(
array(
$timeline,
))
->setCurtain($curtain)
->addPropertySection(pht('Details'), $details);
$page_title = pht('Export %d %s', $export->getID(), $export->getName());
return $this->newPage()
->setTitle($page_title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($export->getPHID()))
->appendChild($view);
}
private function buildHeaderView(
PhabricatorCalendarExport $export) {
$viewer = $this->getViewer();
$id = $export->getID();
if ($export->getIsDisabled()) {
$icon = 'fa-ban';
$color = 'red';
$status = pht('Disabled');
} else {
$icon = 'fa-check';
$color = 'bluegrey';
$status = pht('Active');
}
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($export->getName())
->setStatus($icon, $color, $status)
->setPolicyObject($export);
return $header;
}
private function buildCurtain(PhabricatorCalendarExport $export) {
$viewer = $this->getRequest()->getUser();
$id = $export->getID();
$curtain = $this->newCurtainView($export);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$export,
PhabricatorPolicyCapability::CAN_EDIT);
$ics_uri = $export->getICSURI();
$edit_uri = "export/edit/{$id}/";
$edit_uri = $this->getApplicationURI($edit_uri);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Export'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($edit_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Export as .ics'))
->setIcon('fa-download')
->setHref($ics_uri));
$disable_uri = "export/disable/{$id}/";
$disable_uri = $this->getApplicationURI($disable_uri);
if ($export->getIsDisabled()) {
$disable_name = pht('Enable Export');
$disable_icon = 'fa-check';
} else {
$disable_name = pht('Disable Export');
$disable_icon = 'fa-ban';
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName($disable_name)
->setIcon($disable_icon)
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($disable_uri));
return $curtain;
}
private function buildPropertySection(
PhabricatorCalendarExport $export) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$mode = $export->getPolicyMode();
$policy_icon = PhabricatorCalendarExport::getPolicyModeIcon($mode);
$policy_name = PhabricatorCalendarExport::getPolicyModeName($mode);
$policy_desc = PhabricatorCalendarExport::getPolicyModeDescription($mode);
$policy_color = PhabricatorCalendarExport::getPolicyModeColor($mode);
$policy_view = id(new PHUIStatusListView())
->addItem(
id(new PHUIStatusItemView())
->setIcon($policy_icon, $policy_color)
->setTarget($policy_name)
->setNote($policy_desc));
$properties->addProperty(pht('Mode'), $policy_view);
$query_key = $export->getQueryKey();
$query_link = phutil_tag(
'a',
array(
'href' => $this->getApplicationURI("/query/{$query_key}/"),
),
$query_key);
$properties->addProperty(pht('Query'), $query_link);
$ics_uri = $export->getICSURI();
$ics_uri = PhabricatorEnv::getURI($ics_uri);
if ($export->getIsDisabled()) {
$ics_href = phutil_tag('em', array(), $ics_uri);
} else {
$ics_href = phutil_tag(
'a',
array(
'href' => $ics_uri,
),
$ics_uri);
}
$properties->addProperty(pht('ICS URI'), $ics_href);
return $properties;
}
}

View file

@ -68,10 +68,10 @@ final class PhabricatorCalendarEventEditEngine
}
$frequency_options = array(
PhabricatorCalendarEvent::FREQUENCY_DAILY => pht('Daily'),
PhabricatorCalendarEvent::FREQUENCY_WEEKLY => pht('Weekly'),
PhabricatorCalendarEvent::FREQUENCY_MONTHLY => pht('Monthly'),
PhabricatorCalendarEvent::FREQUENCY_YEARLY => pht('Yearly'),
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => pht('Daily'),
PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => pht('Weekly'),
PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => pht('Monthly'),
PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => pht('Yearly'),
);
$fields = array(
@ -142,6 +142,14 @@ final class PhabricatorCalendarEventEditEngine
->setConduitTypeDescription(pht('Mark the event as a recurring event.'))
->setValue($object->getIsRecurring());
$rrule = $object->newRecurrenceRule();
if ($rrule) {
$frequency = $rrule->getFrequency();
} else {
$frequency = null;
}
$fields[] = id(new PhabricatorSelectEditField())
->setKey('frequency')
->setLabel(pht('Frequency'))
@ -151,7 +159,7 @@ final class PhabricatorCalendarEventEditEngine
->setDescription(pht('Recurring event frequency.'))
->setConduitDescription(pht('Change the event frequency.'))
->setConduitTypeDescription(pht('New event frequency.'))
->setValue($object->getFrequencyRule());
->setValue($frequency);
}
if ($this->getIsCreate() || $object->getIsRecurring()) {
@ -164,50 +172,50 @@ 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->getRecurrenceEndDate());
->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());
->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'))
->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->getViewerDateFrom());
->setKey('start')
->setLabel(pht('Start'))
->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'))
->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->getViewerDateTo());
->setKey('end')
->setLabel(pht('End'))
->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());
->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());
return $fields;
}

View file

@ -139,7 +139,7 @@ final class PhabricatorCalendarEventEditor
WHERE phid IN (%Ls) AND availabilityCacheTTL >= %d',
$user->getTableName(),
$phids,
$object->getDateFromForCache());
$object->getStartDateTimeEpochForCache());
}
return $xactions;
@ -159,9 +159,9 @@ final class PhabricatorCalendarEventEditor
$recurrence_end_xaction =
PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE;
$start_date = $object->getDateFrom();
$end_date = $object->getDateTo();
$recurrence_end = $object->getRecurrenceEndDate();
$start_date = $object->getStartDateTimeEpoch();
$end_date = $object->getEndDateTimeEpoch();
$recurrence_end = $object->getUntilDateTimeEpoch();
$is_recurring = $object->getIsRecurring();
$errors = array();
@ -309,16 +309,10 @@ final class PhabricatorCalendarEventEditor
PhabricatorCalendarEvent $event) {
$actor = $this->getActor();
$event_node = $event->newIntermediateEventNode($actor);
$document_node = id(new PhutilCalendarDocumentNode())
->appendChild($event_node);
$root_node = id(new PhutilCalendarRootNode())
->appendChild($document_node);
$ics_data = id(new PhutilICSWriter())
->writeICSDocument($root_node);
$ics_data = id(new PhabricatorCalendarICSWriter())
->setViewer($actor)
->setEvents(array($event))
->writeICSDocument();
$ics_attachment = new PhabricatorMetaMTAAttachment(
$ics_data,

View file

@ -0,0 +1,133 @@
<?php
final class PhabricatorCalendarExportEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'calendar.export';
public function getEngineName() {
return pht('Calendar Exports');
}
public function isEngineConfigurable() {
return false;
}
public function getSummaryHeader() {
return pht('Configure Calendar Export Forms');
}
public function getSummaryText() {
return pht('Configure how users create and edit exports.');
}
public function getEngineApplicationClass() {
return 'PhabricatorCalendarApplication';
}
protected function newEditableObject() {
return PhabricatorCalendarExport::initializeNewCalendarExport(
$this->getViewer());
}
protected function newObjectQuery() {
return new PhabricatorCalendarExportQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Export');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Export: %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return pht('Export %d', $object->getID());
}
protected function getObjectCreateShortText() {
return pht('Create Export');
}
protected function getObjectName() {
return pht('Export');
}
protected function getObjectViewURI($object) {
return $object->getURI();
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('export/edit/');
}
protected function buildCustomEditFields($object) {
$viewer = $this->getViewer();
$export_modes = PhabricatorCalendarExport::getAvailablePolicyModes();
$export_modes = array_fuse($export_modes);
$current_mode = $object->getPolicyMode();
if (empty($export_modes[$current_mode])) {
array_shift($export_modes, $current_mode);
}
$mode_options = array();
foreach ($export_modes as $export_mode) {
$mode_name = PhabricatorCalendarExport::getPolicyModeName($export_mode);
$mode_summary = PhabricatorCalendarExport::getPolicyModeSummary(
$export_mode);
$mode_options[$export_mode] = pht('%s: %s', $mode_name, $mode_summary);
}
$fields = array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setDescription(pht('Name of the export.'))
->setIsRequired(true)
->setTransactionType(
PhabricatorCalendarExportNameTransaction::TRANSACTIONTYPE)
->setConduitDescription(pht('Rename the export.'))
->setConduitTypeDescription(pht('New export name.'))
->setValue($object->getName()),
id(new PhabricatorBoolEditField())
->setKey('disabled')
->setOptions(pht('Active'), pht('Disabled'))
->setLabel(pht('Disabled'))
->setDescription(pht('Disable the export.'))
->setTransactionType(
PhabricatorCalendarExportDisableTransaction::TRANSACTIONTYPE)
->setIsConduitOnly(true)
->setConduitDescription(pht('Disable or restore the export.'))
->setConduitTypeDescription(pht('True to cancel the export.'))
->setValue($object->getIsDisabled()),
id(new PhabricatorTextEditField())
->setKey('queryKey')
->setLabel(pht('Query Key'))
->setDescription(pht('Query to execute.'))
->setIsRequired(true)
->setTransactionType(
PhabricatorCalendarExportQueryKeyTransaction::TRANSACTIONTYPE)
->setConduitDescription(pht('Change the export query key.'))
->setConduitTypeDescription(pht('New export query key.'))
->setValue($object->getQueryKey()),
id(new PhabricatorSelectEditField())
->setKey('mode')
->setLabel(pht('Mode'))
->setTransactionType(
PhabricatorCalendarExportModeTransaction::TRANSACTIONTYPE)
->setOptions($mode_options)
->setDescription(pht('Change the policy mode for the export.'))
->setConduitDescription(pht('Adjust export mode.'))
->setConduitTypeDescription(pht('New export mode.'))
->setValue($current_mode),
);
return $fields;
}
}

View file

@ -0,0 +1,18 @@
<?php
final class PhabricatorCalendarExportEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorCalendarApplication';
}
public function getEditorObjectsDescription() {
return pht('Calendar Exports');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this export.', $author);
}
}

View file

@ -0,0 +1,50 @@
<?php
final class PhabricatorCalendarExportPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'CEXP';
public function getTypeName() {
return pht('Calendar Export');
}
public function newObject() {
return new PhabricatorCalendarExport();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorCalendarApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorCalendarExportQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$export = $objects[$phid];
$id = $export->getID();
$name = $export->getName();
$uri = $export->getURI();
$handle
->setName($name)
->setFullName(pht('Calendar Export %s: %s', $id, $name))
->setURI($uri);
if ($export->getIsDisabled()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
}
}
}
}

View file

@ -13,6 +13,7 @@ final class PhabricatorCalendarEventQuery
private $eventsWithNoParent;
private $instanceSequencePairs;
private $isStub;
private $parentEventPHIDs;
private $generateGhosts = false;
@ -71,6 +72,11 @@ final class PhabricatorCalendarEventQuery
return $this;
}
public function withParentEventPHIDs(array $parent_phids) {
$this->parentEventPHIDs = $parent_phids;
return $this;
}
protected function getDefaultOrderVector() {
return array('start', 'id');
}
@ -99,7 +105,7 @@ final class PhabricatorCalendarEventQuery
protected function getPagingValueMap($cursor, array $keys) {
$event = $this->loadCursorObject($cursor);
return array(
'start' => $event->getViewerDateFrom(),
'start' => $event->getStartDateTimeEpoch(),
'id' => $event->getID(),
);
}
@ -165,70 +171,54 @@ final class PhabricatorCalendarEventQuery
// discard anything outside of the time window.
$events = $this->getEventsInRange($events);
$enforced_end = null;
$generate_from = $this->rangeBegin;
$generate_until = $this->rangeEnd;
foreach ($parents as $key => $event) {
$sequence_start = 0;
$sequence_end = null;
$start = null;
$duration = $event->getDuration();
$frequency = $event->getFrequencyUnit();
$modify_key = '+1 '.$frequency;
$start_date = $this->getRecurrenceWindowStart(
$event,
$generate_from - $duration);
if (($this->rangeBegin !== null) &&
($this->rangeBegin > $event->getViewerDateFrom())) {
$max_date = $this->rangeBegin - $duration;
$date = $event->getViewerDateFrom();
$datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
$end_date = $this->getRecurrenceWindowEnd(
$event,
$generate_until);
while ($date < $max_date) {
// TODO: optimize this to not loop through all off-screen events
$sequence_start++;
$datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
$date = $datetime->modify($modify_key)->format('U');
$limit = $this->getRecurrenceLimit($event, $raw_limit);
$set = $event->newRecurrenceSet();
$recurrences = $set->getEventsBetween(
null,
$end_date,
$limit + 1);
// We're generating events from the beginning and then filtering them
// here (instead of only generating events starting at the start date)
// because we need to know the proper sequence indexes to generate ghost
// events. This may change after RDATE support.
if ($start_date) {
$start_epoch = $start_date->getEpoch();
} else {
$start_epoch = null;
}
foreach ($recurrences as $sequence_index => $sequence_datetime) {
if (!$sequence_index) {
// This is the parent event, which we already have.
continue;
}
$start = $this->rangeBegin;
} else {
$start = $event->getViewerDateFrom() - $duration;
}
$date = $start;
$datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
// Select the minimum end time we need to generate events until.
$end_times = array();
if ($this->rangeEnd) {
$end_times[] = $this->rangeEnd;
}
if ($event->getRecurrenceEndDate()) {
$end_times[] = $event->getRecurrenceEndDate();
}
if ($enforced_end) {
$end_times[] = $enforced_end;
}
if ($end_times) {
$end = min($end_times);
$sequence_end = $sequence_start;
while ($date < $end) {
$sequence_end++;
$datetime->modify($modify_key);
$date = $datetime->format('U');
if ($sequence_end > $raw_limit + $sequence_start) {
break;
if ($start_epoch) {
if ($sequence_datetime->getEpoch() < $start_epoch) {
continue;
}
}
} else {
$sequence_end = $raw_limit + $sequence_start;
}
$sequence_start = max(1, $sequence_start);
for ($index = $sequence_start; $index < $sequence_end; $index++) {
$events[] = $event->newGhost($viewer, $index);
$events[] = $event->newGhost(
$viewer,
$sequence_index,
$sequence_datetime);
}
// NOTE: We're slicing results every time because this makes it cheaper
@ -238,9 +228,9 @@ final class PhabricatorCalendarEventQuery
if ($raw_limit) {
if (count($events) > $raw_limit) {
$events = msort($events, 'getViewerDateFrom');
$events = msort($events, 'getStartDateTimeEpoch');
$events = array_slice($events, 0, $raw_limit, true);
$enforced_end = last($events)->getViewerDateFrom();
$generate_until = last($events)->getEndDateTimeEpoch();
}
}
}
@ -308,7 +298,7 @@ final class PhabricatorCalendarEventQuery
}
}
$events = msort($events, 'getViewerDateFrom');
$events = msort($events, 'getStartDateTimeEpoch');
return $events;
}
@ -331,14 +321,14 @@ final class PhabricatorCalendarEventQuery
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids) {
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'event.id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'event.phid IN (%Ls)',
@ -352,14 +342,14 @@ final class PhabricatorCalendarEventQuery
if ($this->rangeBegin) {
$where[] = qsprintf(
$conn,
'event.dateTo >= %d OR event.isRecurring = 1',
'(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)',
$this->rangeBegin - phutil_units('16 hours in seconds'));
}
if ($this->rangeEnd) {
$where[] = qsprintf(
$conn,
'event.dateFrom <= %d',
'event.utcInitialEpoch <= %d',
$this->rangeEnd + phutil_units('16 hours in seconds'));
}
@ -370,7 +360,7 @@ final class PhabricatorCalendarEventQuery
$this->inviteePHIDs);
}
if ($this->hostPHIDs) {
if ($this->hostPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.hostPHID IN (%Ls)',
@ -414,6 +404,13 @@ final class PhabricatorCalendarEventQuery
(int)$this->isStub);
}
if ($this->parentEventPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.instanceOfEventPHID IN (%Ls)',
$this->parentEventPHIDs);
}
return $where;
}
@ -500,7 +497,7 @@ final class PhabricatorCalendarEventQuery
}
}
$events = msort($events, 'getViewerDateFrom');
$events = msort($events, 'getStartDateTimeEpoch');
return $events;
}
@ -510,8 +507,8 @@ final class PhabricatorCalendarEventQuery
$range_end = $this->rangeEnd;
foreach ($events as $key => $event) {
$event_start = $event->getViewerDateFrom();
$event_end = $event->getViewerDateTo();
$event_start = $event->getStartDateTimeEpoch();
$event_end = $event->getEndDateTimeEpoch();
if ($range_start && $event_end < $range_start) {
unset($events[$key]);
@ -525,4 +522,44 @@ final class PhabricatorCalendarEventQuery
return $events;
}
private function getRecurrenceWindowStart(
PhabricatorCalendarEvent $event,
$generate_from) {
if (!$generate_from) {
return null;
}
return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from);
}
private function getRecurrenceWindowEnd(
PhabricatorCalendarEvent $event,
$generate_until) {
$end_epochs = array();
if ($generate_until) {
$end_epochs[] = $generate_until;
}
$until_epoch = $event->getUntilDateTimeEpoch();
if ($until_epoch) {
$end_epochs[] = $until_epoch;
}
if (!$end_epochs) {
return null;
}
return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs));
}
private function getRecurrenceLimit(
PhabricatorCalendarEvent $event,
$raw_limit) {
return $raw_limit;
}
}

View file

@ -115,8 +115,12 @@ final class PhabricatorCalendarEventSearchEngine
}
// Generate ghosts (and ignore stub events) if we aren't querying for
// specific events.
if (!$map['ids'] && !$map['phids']) {
// specific events or exporting.
if (!empty($map['export'])) {
// This is a specific mode enabled by event exports.
$query
->withIsStub(false);
} else if (!$map['ids'] && !$map['phids']) {
$query
->withIsStub(false)
->setGenerateGhosts(true);
@ -255,11 +259,20 @@ final class PhabricatorCalendarEventSearchEngine
array $handles) {
if ($this->isMonthView($query)) {
return $this->buildCalendarMonthView($events, $query);
$result = $this->buildCalendarMonthView($events, $query);
} else if ($this->isDayView($query)) {
return $this->buildCalendarDayView($events, $query);
$result = $this->buildCalendarDayView($events, $query);
} else {
$result = $this->buildCalendarListView($events, $query);
}
return $result;
}
private function buildCalendarListView(
array $events,
PhabricatorSavedQuery $query) {
assert_instances_of($events, 'PhabricatorCalendarEvent');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
@ -343,8 +356,8 @@ final class PhabricatorCalendarEventSearchEngine
$month_view->setUser($viewer);
foreach ($events as $event) {
$epoch_min = $event->getViewerDateFrom();
$epoch_max = $event->getViewerDateTo();
$epoch_min = $event->getStartDateTimeEpoch();
$epoch_max = $event->getEndDateTimeEpoch();
$event_view = id(new AphrontCalendarEventView())
->setHostPHID($event->getHostPHID())
@ -408,8 +421,8 @@ final class PhabricatorCalendarEventSearchEngine
$event,
PhabricatorPolicyCapability::CAN_EDIT);
$epoch_min = $event->getViewerDateFrom();
$epoch_max = $event->getViewerDateTo();
$epoch_min = $event->getStartDateTimeEpoch();
$epoch_max = $event->getEndDateTimeEpoch();
$status_icon = $event->getDisplayIcon($viewer);
$status_color = $event->getDisplayIconColor($viewer);
@ -562,4 +575,17 @@ final class PhabricatorCalendarEventSearchEngine
return false;
}
public function newUseResultsActions(PhabricatorSavedQuery $saved) {
$viewer = $this->requireViewer();
$can_export = $viewer->isLoggedIn();
return array(
id(new PhabricatorActionView())
->setIcon('fa-download')
->setName(pht('Export Query as .ics'))
->setDisabled(!$can_export)
->setHref('/calendar/export/edit/?queryKey='.$saved->getQueryKey()),
);
}
}

View file

@ -0,0 +1,94 @@
<?php
final class PhabricatorCalendarExportQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $secretKeys;
private $isDisabled;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
public function withIsDisabled($is_disabled) {
$this->isDisabled = $is_disabled;
return $this;
}
public function withSecretKeys(array $keys) {
$this->secretKeys = $keys;
return $this;
}
public function newResultObject() {
return new PhabricatorCalendarExport();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'export.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'export.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'export.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->isDisabled !== null) {
$where[] = qsprintf(
$conn,
'export.isDisabled = %d',
(int)$this->isDisabled);
}
if ($this->secretKeys !== null) {
$where[] = qsprintf(
$conn,
'export.secretKey IN (%Ls)',
$this->secretKeys);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'export';
}
public function getQueryApplicationClass() {
return 'PhabricatorCalendarApplication';
}
}

View file

@ -0,0 +1,119 @@
<?php
final class PhabricatorCalendarExportSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Calendar Exports');
}
public function getApplicationClassName() {
return 'PhabricatorCalendarApplication';
}
public function newQuery() {
$viewer = $this->requireViewer();
return id(new PhabricatorCalendarExportQuery())
->withAuthorPHIDs(array($viewer->getPHID()));
}
protected function buildCustomSearchFields() {
return array();
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
return $query;
}
protected function getURI($path) {
return '/calendar/export/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'all' => pht('All Exports'),
);
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $exports,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($exports, 'PhabricatorCalendarExport');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
foreach ($exports as $export) {
$item = id(new PHUIObjectItemView())
->setViewer($viewer)
->setObjectName(pht('Export %d', $export->getID()))
->setHeader($export->getName())
->setHref($export->getURI());
if ($export->getIsDisabled()) {
$item->setDisabled(true);
}
$mode = $export->getPolicyMode();
$policy_icon = PhabricatorCalendarExport::getPolicyModeIcon($mode);
$policy_name = PhabricatorCalendarExport::getPolicyModeName($mode);
$policy_color = PhabricatorCalendarExport::getPolicyModeColor($mode);
$item->addIcon(
"{$policy_icon} {$policy_color}",
$policy_name);
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No exports found.'));
return $result;
}
protected function getNewUserBody() {
$doc_name = 'Calendar User Guide: Exporting Events';
$doc_href = PhabricatorEnv::getDoclink($doc_name);
$create_button = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-book white')
->setText($doc_name)
->setHref($doc_href)
->setColor(PHUIButtonView::GREEN);
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();
$view = id(new PHUIBigInfoView())
->setIcon('fa-download')
->setTitle(pht('No Exports Configured'))
->setDescription(
pht(
'You have not set up any events for export from Calendar yet. '.
'See the documentation for instructions on how to get started.'))
->addAction($create_button);
return $view;
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorCalendarExportTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new PhabricatorCalendarExportTransaction();
}
}

View file

@ -17,10 +17,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
protected $name;
protected $hostPHID;
protected $dateFrom;
protected $dateTo;
protected $allDayDateFrom;
protected $allDayDateTo;
protected $description;
protected $isCancelled;
protected $isAllDay;
@ -29,8 +25,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
protected $isStub;
protected $isRecurring = 0;
protected $recurrenceFrequency = array();
protected $recurrenceEndDate;
private $isGhostEvent = false;
protected $instanceOfEventPHID;
@ -41,17 +35,23 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
protected $spacePHID;
protected $utcInitialEpoch;
protected $utcUntilEpoch;
protected $utcInstanceEpoch;
protected $parameters = array();
private $parentEvent = self::ATTACHABLE;
private $invitees = self::ATTACHABLE;
private $viewerDateFrom;
private $viewerDateTo;
private $viewerTimezone;
// Frequency Constants
const FREQUENCY_DAILY = 'daily';
const FREQUENCY_WEEKLY = 'weekly';
const FREQUENCY_MONTHLY = 'monthly';
const FREQUENCY_YEARLY = 'yearly';
// TODO: DEPRECATED. Remove once we're sure the migrations worked.
protected $allDayDateFrom;
protected $allDayDateTo;
protected $dateFrom;
protected $dateTo;
protected $recurrenceEndDate;
protected $recurrenceFrequency = array();
public static function initializeNewCalendarEvent(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
@ -66,45 +66,37 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
$now = PhabricatorTime::getNow();
$start = new DateTime('@'.$now);
$start->setTimeZone($actor->getTimeZone());
$start->setTime($start->format('H'), 0, 0);
$start->modify('+1 hour');
$end = id(clone $start)->modify('+1 hour');
$epoch_min = $start->format('U');
$epoch_max = $end->format('U');
$now_date = new DateTime('@'.$now);
$now_min = id(clone $now_date)->setTime(0, 0)->format('U');
$now_max = id(clone $now_date)->setTime(23, 59)->format('U');
$default_icon = 'fa-calendar';
$datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$now,
$actor->getTimezoneIdentifier());
$datetime_end = $datetime_start->newRelativeDateTime('PT1H');
return id(new PhabricatorCalendarEvent())
->setHostPHID($actor->getPHID())
->setIsCancelled(0)
->setIsAllDay(0)
->setIsStub(0)
->setIsRecurring(0)
->setRecurrenceFrequency(
array(
'rule' => self::FREQUENCY_WEEKLY,
))
->setIcon($default_icon)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID())
->attachInvitees(array())
->setDateFrom($epoch_min)
->setDateTo($epoch_max)
->setAllDayDateFrom($now_min)
->setAllDayDateTo($now_max)
->setDateFrom(0)
->setDateTo(0)
->setAllDayDateFrom(0)
->setAllDayDateTo(0)
->setStartDateTime($datetime_start)
->setEndDateTime($datetime_end)
->applyViewerTimezone($actor);
}
private function newChild(PhabricatorUser $actor, $sequence) {
private function newChild(
PhabricatorUser $actor,
$sequence,
PhutilCalendarDateTime $start = null) {
if (!$this->isParentEvent()) {
throw new Exception(
pht(
@ -118,10 +110,13 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
->setInstanceOfEventPHID($this->getPHID())
->setSequenceIndex($sequence)
->setIsRecurring(true)
->setRecurrenceFrequency($this->getRecurrenceFrequency())
->attachParentEvent($this);
->attachParentEvent($this)
->setAllDayDateFrom(0)
->setAllDayDateTo(0)
->setDateFrom(0)
->setDateTo(0);
return $child->copyFromParent($actor);
return $child->copyFromParent($actor, $start);
}
protected function readField($field) {
@ -153,7 +148,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
}
public function copyFromParent(PhabricatorUser $actor) {
public function copyFromParent(
PhabricatorUser $actor,
PhutilCalendarDateTime $start = null) {
if (!$this->isChildEvent()) {
throw new Exception(
pht(
@ -173,65 +171,46 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
->setDescription($parent->getDescription());
$sequence = $this->getSequenceIndex();
$duration = $this->getDuration();
$epochs = $parent->getSequenceIndexEpochs($actor, $sequence, $duration);
if ($start) {
$start_datetime = $start;
} else {
$start_datetime = $parent->newSequenceIndexDateTime($sequence);
if (!$start_datetime) {
throw new Exception(
pht(
'Sequence "%s" is not valid for event!',
$sequence));
}
}
$duration = $parent->newDuration();
$end_datetime = $start_datetime->newRelativeDateTime($duration);
$this
->setDateFrom($epochs['dateFrom'])
->setDateTo($epochs['dateTo'])
->setAllDayDateFrom($epochs['allDayDateFrom'])
->setAllDayDateTo($epochs['allDayDateTo']);
->setStartDateTime($start_datetime)
->setEndDateTime($end_datetime);
return $this;
}
public function isValidSequenceIndex(PhabricatorUser $viewer, $sequence) {
try {
$this->getSequenceIndexEpochs($viewer, $sequence, $this->getDuration());
return true;
} catch (Exception $ex) {
return false;
}
return (bool)$this->newSequenceIndexDateTime($sequence);
}
private function getSequenceIndexEpochs(
PhabricatorUser $viewer,
$sequence,
$duration) {
$frequency = $this->getFrequencyUnit();
$modify_key = '+'.$sequence.' '.$frequency;
$date = $this->getDateFrom();
$date_time = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
$date_time->modify($modify_key);
$date = $date_time->format('U');
$end_date = $this->getRecurrenceEndDate();
if ($end_date && $date > $end_date) {
throw new Exception(
pht(
'Sequence "%s" is invalid for this event: it would occur after '.
'the event stops repeating.',
$sequence));
public function newSequenceIndexDateTime($sequence) {
$set = $this->newRecurrenceSet();
if (!$set) {
return null;
}
$utc = new DateTimeZone('UTC');
$instances = $set->getEventsBetween(
null,
$this->newUntilDateTime(),
$sequence + 1);
$allday_from = $this->getAllDayDateFrom();
$allday_date = new DateTime('@'.$allday_from, $utc);
$allday_date->setTimeZone($utc);
$allday_date->modify($modify_key);
$allday_min = $allday_date->format('U');
$allday_duration = ($this->getAllDayDateTo() - $allday_from);
return array(
'dateFrom' => $date,
'dateTo' => $date + $duration,
'allDayDateFrom' => $allday_min,
'allDayDateTo' => $allday_min + $allday_duration,
);
return idx($instances, $sequence, null);
}
public function newStub(PhabricatorUser $actor, $sequence) {
@ -248,8 +227,12 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $stub;
}
public function newGhost(PhabricatorUser $actor, $sequence) {
$ghost = $this->newChild($actor, $sequence);
public function newGhost(
PhabricatorUser $actor,
$sequence,
PhutilCalendarDateTime $start = null) {
$ghost = $this->newChild($actor, $sequence, $start);
$ghost
->setIsGhostEvent(true)
@ -260,67 +243,62 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $ghost;
}
public function getViewerDateFrom() {
if ($this->viewerDateFrom === null) {
throw new PhutilInvalidStateException('applyViewerTimezone');
}
return $this->viewerDateFrom;
}
public function getViewerDateTo() {
if ($this->viewerDateTo === null) {
throw new PhutilInvalidStateException('applyViewerTimezone');
}
return $this->viewerDateTo;
}
public function applyViewerTimezone(PhabricatorUser $viewer) {
if (!$this->getIsAllDay()) {
$this->viewerDateFrom = $this->getDateFrom();
$this->viewerDateTo = $this->getDateTo();
} else {
$zone = $viewer->getTimeZone();
$this->viewerDateFrom = $this->getDateEpochForTimezone(
$this->getAllDayDateFrom(),
new DateTimeZone('UTC'),
'Y-m-d',
null,
$zone);
$this->viewerDateTo = $this->getDateEpochForTimezone(
$this->getAllDayDateTo(),
new DateTimeZone('UTC'),
'Y-m-d 23:59:00',
null,
$zone);
}
$this->viewerTimezone = $viewer->getTimezoneIdentifier();
return $this;
}
public function getDuration() {
return $this->getDateTo() - $this->getDateFrom();
return ($this->getEndDateTimeEpoch() - $this->getStartDateTimeEpoch());
}
public function getDateEpochForTimezone(
$epoch,
$src_zone,
$format,
$adjust,
$dst_zone) {
public function updateUTCEpochs() {
// The "intitial" epoch is the start time of the event, in UTC.
$start_date = $this->newStartDateTime()
->setViewerTimezone('UTC');
$start_epoch = $start_date->getEpoch();
$this->setUTCInitialEpoch($start_epoch);
$src = new DateTime('@'.$epoch);
$src->setTimeZone($src_zone);
// The "until" epoch is the last UTC epoch on which any instance of this
// event occurs. For infinitely recurring events, it is `null`.
if (strlen($adjust)) {
$adjust = ' '.$adjust;
if (!$this->getIsRecurring()) {
$end_date = $this->newEndDateTime()
->setViewerTimezone('UTC');
$until_epoch = $end_date->getEpoch();
} else {
$until_epoch = null;
$until_date = $this->newUntilDateTime();
if ($until_date) {
$until_date->setViewerTimezone('UTC');
$duration = $this->newDuration();
$until_epoch = id(new PhutilCalendarRelativeDateTime())
->setOrigin($until_date)
->setDuration($duration)
->getEpoch();
}
}
$this->setUTCUntilEpoch($until_epoch);
$dst = new DateTime($src->format($format).$adjust, $dst_zone);
return $dst->format('U');
// The "instance" epoch is a property of instances of recurring events.
// It's the original UTC epoch on which the instance started. Usually that
// is the same as the start date, but they may be different if the instance
// has been edited.
// The ICS format uses this value (original start time) to identify event
// instances, and must do so because it allows additional arbitrary
// instances to be added (with "RDATE").
$instance_epoch = null;
$instance_date = $this->newInstanceDateTime();
if ($instance_date) {
$instance_epoch = $instance_date
->setViewerTimezone('UTC')
->getEpoch();
}
$this->setUTCInstanceEpoch($instance_epoch);
return $this;
}
public function save() {
@ -328,6 +306,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$this->updateUTCEpochs();
return parent::save();
}
@ -340,8 +320,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
*
* @return int Event start date for availability caches.
*/
public function getDateFromForCache() {
return ($this->getViewerDateFrom() - phutil_units('15 minutes in seconds'));
public function getStartDateTimeEpochForCache() {
$epoch = $this->getStartDateTimeEpoch();
$window = phutil_units('15 minutes in seconds');
return ($epoch - $window);
}
protected function getConfiguration() {
@ -349,20 +331,25 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text',
'dateFrom' => 'epoch',
'dateTo' => 'epoch',
'allDayDateFrom' => 'epoch',
'allDayDateTo' => 'epoch',
'description' => 'text',
'isCancelled' => 'bool',
'isAllDay' => 'bool',
'icon' => 'text32',
'mailKey' => 'bytes20',
'isRecurring' => 'bool',
'recurrenceEndDate' => 'epoch?',
'instanceOfEventPHID' => 'phid?',
'sequenceIndex' => 'uint32?',
'isStub' => 'bool',
'utcInitialEpoch' => 'epoch',
'utcUntilEpoch' => 'epoch?',
'utcInstanceEpoch' => 'epoch?',
// TODO: DEPRECATED.
'allDayDateFrom' => 'epoch',
'allDayDateTo' => 'epoch',
'dateFrom' => 'epoch',
'dateTo' => 'epoch',
'recurrenceEndDate' => 'epoch?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_date' => array(
@ -372,9 +359,17 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
'columns' => array('instanceOfEventPHID', 'sequenceIndex'),
'unique' => true,
),
'key_epoch' => array(
'columns' => array('utcInitialEpoch', 'utcUntilEpoch'),
),
'key_rdate' => array(
'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'),
'unique' => true,
),
),
self::CONFIG_SERIALIZATION => array(
'recurrenceFrequency' => self::SERIALIZATION_JSON,
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
@ -450,27 +445,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $this;
}
public function getFrequencyRule() {
return idx($this->recurrenceFrequency, 'rule');
}
public function getFrequencyUnit() {
$frequency = $this->getFrequencyRule();
switch ($frequency) {
case 'daily':
return 'day';
case 'weekly':
return 'week';
case 'monthly':
return 'month';
case 'yearly':
return 'year';
default:
return 'day';
}
}
public function getURI() {
if ($this->getIsGhostEvent()) {
$base = $this->getParentEvent()->getURI();
@ -516,14 +490,12 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
PhabricatorUser $viewer,
$show_end) {
if ($show_end) {
$min_date = PhabricatorTime::getDateTimeFromEpoch(
$this->getViewerDateFrom(),
$viewer);
$start = $this->newStartDateTime();
$end = $this->newEndDateTime();
$max_date = PhabricatorTime::getDateTimeFromEpoch(
$this->getViewerDateTo(),
$viewer);
if ($show_end) {
$min_date = $start->newPHPDateTime();
$max_date = $end->newPHPDateTime();
$min_day = $min_date->format('Y m d');
$max_day = $max_date->format('Y m d');
@ -533,8 +505,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
$show_end_date = false;
}
$min_epoch = $this->getViewerDateFrom();
$max_epoch = $this->getViewerDateTo();
$min_epoch = $start->getEpoch();
$max_epoch = $end->getEpoch();
if ($this->getIsAllDay()) {
if ($show_end_date) {
@ -629,11 +601,28 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $this->getMonogram().'.ics';
}
public function newIntermediateEventNode(PhabricatorUser $viewer) {
public function newIntermediateEventNode(
PhabricatorUser $viewer,
array $children) {
$base_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
$domain = $base_uri->getDomain();
$uid = $this->getPHID().'@'.$domain;
// NOTE: For recurring events, all of the events in the series have the
// same UID (the UID of the parent). The child event instances are
// differentiated by the "RECURRENCE-ID" field.
if ($this->isChildEvent()) {
$parent = $this->getParentEvent();
$instance_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$this->getUTCInstanceEpoch());
$recurrence_id = $instance_datetime->getISO8601();
$rrule = null;
} else {
$parent = $this;
$recurrence_id = null;
$rrule = $this->newRecurrenceRule();
}
$uid = $parent->getPHID().'@'.$domain;
$created = $this->getDateCreated();
$created = PhutilCalendarAbsoluteDateTime::newFromEpoch($created);
@ -641,11 +630,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
$modified = $this->getDateModified();
$modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified);
$date_start = $this->getDateFrom();
$date_start = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_start);
$date_end = $this->getDateTo();
$date_end = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_end);
$date_start = $this->newStartDateTime();
$date_end = $this->newEndDateTime();
if ($this->getIsAllDay()) {
$date_start->setIsAllDay(true);
@ -705,6 +691,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
->setStatus($status);
}
// TODO: Use $children to generate EXDATE/RDATE information.
$node = id(new PhutilCalendarEventNode())
->setUID($uid)
->setName($this->getName())
@ -716,10 +704,186 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
->setOrganizer($organizer)
->setAttendees($attendees);
if ($rrule) {
$node->setRecurrenceRule($rrule);
}
if ($recurrence_id) {
$node->setRecurrenceID($recurrence_id);
}
return $node;
}
public function newStartDateTime() {
$datetime = $this->getParameter('startDateTime');
if ($datetime) {
return $this->newDateTimeFromDictionary($datetime);
}
$epoch = $this->getDateFrom();
return $this->newDateTimeFromEpoch($epoch);
}
public function getStartDateTimeEpoch() {
return $this->newStartDateTime()->getEpoch();
}
public function newEndDateTime() {
$datetime = $this->getParameter('endDateTime');
if ($datetime) {
return $this->newDateTimeFromDictionary($datetime);
}
$epoch = $this->getDateTo();
return $this->newDateTimeFromEpoch($epoch);
}
public function getEndDateTimeEpoch() {
return $this->newEndDateTime()->getEpoch();
}
public function newUntilDateTime() {
$datetime = $this->getParameter('untilDateTime');
if ($datetime) {
return $this->newDateTimeFromDictionary($datetime);
}
$epoch = $this->getRecurrenceEndDate();
if (!$epoch) {
return null;
}
return $this->newDateTimeFromEpoch($epoch);
}
public function getUntilDateTimeEpoch() {
$datetime = $this->newUntilDateTime();
if (!$datetime) {
return null;
}
return $datetime->getEpoch();
}
public function newDuration() {
return id(new PhutilCalendarDuration())
->setSeconds($this->getDuration());
}
public function newInstanceDateTime() {
if (!$this->getIsRecurring()) {
return null;
}
$index = $this->getSequenceIndex();
if (!$index) {
return null;
}
return $this->newSequenceIndexDateTime($index);
}
private function newDateTimeFromEpoch($epoch) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch);
if ($this->getIsAllDay()) {
$datetime->setIsAllDay(true);
}
return $this->newDateTimeFromDateTime($datetime);
}
private function newDateTimeFromDictionary(array $dict) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($dict);
return $this->newDateTimeFromDateTime($datetime);
}
private function newDateTimeFromDateTime(PhutilCalendarDateTime $datetime) {
$viewer_timezone = $this->viewerTimezone;
if ($viewer_timezone) {
$datetime->setViewerTimezone($viewer_timezone);
}
return $datetime;
}
public function getParameter($key, $default = null) {
return idx($this->parameters, $key, $default);
}
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
return $this;
}
public function setStartDateTime(PhutilCalendarDateTime $datetime) {
return $this->setParameter(
'startDateTime',
$datetime->newAbsoluteDateTime()->toDictionary());
}
public function setEndDateTime(PhutilCalendarDateTime $datetime) {
return $this->setParameter(
'endDateTime',
$datetime->newAbsoluteDateTime()->toDictionary());
}
public function setUntilDateTime(PhutilCalendarDateTime $datetime) {
return $this->setParameter(
'untilDateTime',
$datetime->newAbsoluteDateTime()->toDictionary());
}
public function setRecurrenceRule(PhutilCalendarRecurrenceRule $rrule) {
return $this->setParameter(
'recurrenceRule',
$rrule->toDictionary());
}
public function newRecurrenceRule() {
if ($this->isChildEvent()) {
return $this->getParentEvent()->newRecurrenceRule();
}
if (!$this->getIsRecurring()) {
return null;
}
$dict = $this->getParameter('recurrenceRule');
if (!$dict) {
return null;
}
$rrule = PhutilCalendarRecurrenceRule::newFromDictionary($dict);
$start = $this->newStartDateTime();
$rrule->setStartDateTime($start);
$until = $this->newUntilDateTime();
if ($until) {
$rrule->setUntil($until);
}
return $rrule;
}
public function newRecurrenceSet() {
if ($this->isChildEvent()) {
return $this->getParentEvent()->newRecurrenceSet();
}
$set = new PhutilCalendarRecurrenceSet();
$rrule = $this->newRecurrenceRule();
if (!$rrule) {
return null;
}
$set->addSource($rrule);
return $set;
}
/* -( Markup Interface )--------------------------------------------------- */

View file

@ -0,0 +1,198 @@
<?php
final class PhabricatorCalendarExport extends PhabricatorCalendarDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $authorPHID;
protected $policyMode;
protected $queryKey;
protected $secretKey;
protected $isDisabled = 0;
const MODE_PUBLIC = 'public';
const MODE_PRIVILEGED = 'privileged';
public static function initializeNewCalendarExport(PhabricatorUser $actor) {
return id(new self())
->setAuthorPHID($actor->getPHID())
->setPolicyMode(self::MODE_PRIVILEGED)
->setIsDisabled(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text',
'policyMode' => 'text64',
'queryKey' => 'text64',
'secretKey' => 'bytes20',
'isDisabled' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_secret' => array(
'columns' => array('secretKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorCalendarExportPHIDType::TYPECONST;
}
public function save() {
if (!$this->getSecretKey()) {
$this->setSecretKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getURI() {
$id = $this->getID();
return "/calendar/export/{$id}/";
}
private static function getPolicyModeMap() {
return array(
self::MODE_PUBLIC => array(
'icon' => 'fa-globe',
'name' => pht('Public'),
'color' => 'bluegrey',
'summary' => pht(
'Export only public data.'),
'description' => pht(
'Only publicly available data is exported.'),
),
self::MODE_PRIVILEGED => array(
'icon' => 'fa-unlock-alt',
'name' => pht('Privileged'),
'color' => 'red',
'summary' => pht(
'Export private data.'),
'description' => pht(
'Anyone who knows the URI for this export can view all event '.
'details as though they were logged in with your account.'),
),
);
}
private static function getPolicyModeSpec($const) {
return idx(self::getPolicyModeMap(), $const, array());
}
public static function getPolicyModeName($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'name', $const);
}
public static function getPolicyModeIcon($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'icon', $const);
}
public static function getPolicyModeColor($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'color', $const);
}
public static function getPolicyModeSummary($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'summary', $const);
}
public static function getPolicyModeDescription($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'description', $const);
}
public static function getPolicyModes() {
return array_keys(self::getPolicyModeMap());
}
public static function getAvailablePolicyModes() {
$modes = array();
if (PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$modes[] = self::MODE_PUBLIC;
}
$modes[] = self::MODE_PRIVILEGED;
return $modes;
}
public function getICSFilename() {
return PhabricatorSlug::normalizeProjectSlug($this->getName()).'.ics';
}
public function getICSURI() {
$secret_key = $this->getSecretKey();
$ics_name = $this->getICSFilename();
return "/calendar/export/ics/{$secret_key}/{$ics_name}";
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getAuthorPHID();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorCalendarExportEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorCalendarExportTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
}

View file

@ -0,0 +1,18 @@
<?php
final class PhabricatorCalendarExportTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'calendar';
}
public function getApplicationTransactionType() {
return PhabricatorCalendarExportPHIDType::TYPECONST;
}
public function getBaseTransactionClass() {
return 'PhabricatorCalendarExportTransactionType';
}
}

View file

@ -0,0 +1,60 @@
<?php
final class PhabricatorCalendarICSWriter extends Phobject {
private $viewer;
private $events = array();
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setEvents(array $events) {
assert_instances_of($events, 'PhabricatorCalendarEvent');
$this->events = $events;
return $this;
}
public function getEvents() {
return $this->events;
}
public function writeICSDocument() {
$viewer = $this->getViewer();
$events = $this->getEvents();
$events = mpull($events, null, 'getPHID');
if ($events) {
$child_map = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withParentEventPHIDs(array_keys($events))
->execute();
$child_map = mpull($child_map, null, 'getPHID');
} else {
$child_map = array();
}
$all_events = $events + $child_map;
$child_groups = mgroup($child_map, 'getInstanceOfEventPHID');
$document_node = new PhutilCalendarDocumentNode();
foreach ($all_events as $event) {
$child_events = idx($child_groups, $event->getPHID(), array());
$event_node = $event->newIntermediateEventNode($viewer, $child_events);
$document_node->appendChild($event_node);
}
$root_node = id(new PhutilCalendarRootNode())
->appendChild($document_node);
return id(new PhutilICSWriter())
->writeICSDocument($root_node);
}
}

View file

@ -15,6 +15,25 @@ final class PhabricatorCalendarEventAllDayTransaction
public function applyInternalEffects($object, $value) {
$object->setIsAllDay($value);
// Adjust the flags on any other dates the event has.
$keys = array(
'startDateTime',
'endDateTime',
'untilDateTime',
);
foreach ($keys as $key) {
$dict = $object->getParameter($key);
if (!$dict) {
continue;
}
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($dict);
$datetime->setIsAllDay($value);
$object->setParameter($key, $datetime->toDictionary());
}
}
public function getTitle() {

View file

@ -6,21 +6,18 @@ final class PhabricatorCalendarEventEndDateTransaction
const TRANSACTIONTYPE = 'calendar.enddate';
public function generateOldValue($object) {
return $object->getDateTo();
// TODO: Upgrade this.
return $object->getEndDateTimeEpoch();
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
$object->setDateTo($value);
$object->setAllDayDateTo(
$object->getDateEpochForTimezone(
$value,
$actor->getTimeZone(),
'Y-m-d 23:59:00',
null,
new DateTimeZone('UTC')));
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$value,
$actor->getTimezoneIdentifier());
$datetime->setIsAllDay($object->getIsAllDay());
$object->setEndDateTime($datetime);
}
public function getTitle() {

View file

@ -6,32 +6,65 @@ final class PhabricatorCalendarEventFrequencyTransaction
const TRANSACTIONTYPE = 'calendar.frequency';
public function generateOldValue($object) {
return $object->getFrequencyRule();
$rrule = $object->newRecurrenceRule();
if (!$rrule) {
return null;
}
return $rrule->getFrequency();
}
public function applyInternalEffects($object, $value) {
$object->setRecurrenceFrequency(
array(
'rule' => $value,
));
$rrule = id(new PhutilCalendarRecurrenceRule())
->setFrequency($value);
$object->setRecurrenceRule($rrule);
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$valid = array(
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY,
PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY,
PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY,
PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY,
);
$valid = array_fuse($valid);
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if (!isset($valid[$value])) {
$errors[] = $this->newInvalidError(
pht(
'Event frequency "%s" is not valid. Valid frequences are: %s.',
$value,
implode(', ', $valid)),
$xaction);
}
}
return $errors;
}
public function getTitle() {
$frequency = $this->getFrequencyRule($this->getNewValue());
$frequency = $this->getFrequency($this->getNewValue());
switch ($frequency) {
case PhabricatorCalendarEvent::FREQUENCY_DAILY:
case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY:
return pht(
'%s set this event to repeat daily.',
$this->renderAuthor());
case PhabricatorCalendarEvent::FREQUENCY_WEEKLY:
case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY:
return pht(
'%s set this event to repeat weekly.',
$this->renderAuthor());
case PhabricatorCalendarEvent::FREQUENCY_MONTHLY:
case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY:
return pht(
'%s set this event to repeat monthly.',
$this->renderAuthor());
case PhabricatorCalendarEvent::FREQUENCY_YEARLY:
case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY:
return pht(
'%s set this event to repeat yearly.',
$this->renderAuthor());
@ -39,24 +72,24 @@ final class PhabricatorCalendarEventFrequencyTransaction
}
public function getTitleForFeed() {
$frequency = $this->getFrequencyRule($this->getNewValue());
$frequency = $this->getFrequency($this->getNewValue());
switch ($frequency) {
case PhabricatorCalendarEvent::FREQUENCY_DAILY:
case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY:
return pht(
'%s set %s to repeat daily.',
$this->renderAuthor(),
$this->renderObject());
case PhabricatorCalendarEvent::FREQUENCY_WEEKLY:
case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY:
return pht(
'%s set %s to repeat weekly.',
$this->renderAuthor(),
$this->renderObject());
case PhabricatorCalendarEvent::FREQUENCY_MONTHLY:
case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY:
return pht(
'%s set %s to repeat monthly.',
$this->renderAuthor(),
$this->renderObject());
case PhabricatorCalendarEvent::FREQUENCY_YEARLY:
case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY:
return pht(
'%s set %s to repeat yearly.',
$this->renderAuthor(),
@ -64,12 +97,18 @@ final class PhabricatorCalendarEventFrequencyTransaction
}
}
private function getFrequencyRule($value) {
private function getFrequency($value) {
// NOTE: This is normalizing three generations of these transactions
// to use RRULE constants. It would be vaguely nice to migrate them
// for consistency.
if (is_array($value)) {
$value = idx($value, 'rule');
} else {
return $value;
$value = $value;
}
return phutil_utf8_strtoupper($value);
}
}

View file

@ -6,21 +6,18 @@ final class PhabricatorCalendarEventStartDateTransaction
const TRANSACTIONTYPE = 'calendar.startdate';
public function generateOldValue($object) {
return $object->getDateFrom();
// TODO: Upgrade this.
return $object->getStartDateTimeEpoch();
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
$object->setDateFrom($value);
$object->setAllDayDateFrom(
$object->getDateEpochForTimezone(
$value,
$actor->getTimeZone(),
'Y-m-d',
null,
new DateTimeZone('UTC')));
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$value,
$actor->getTimezoneIdentifier());
$datetime->setIsAllDay($object->getIsAllDay());
$object->setStartDateTime($datetime);
}
public function getTitle() {

View file

@ -6,11 +6,21 @@ final class PhabricatorCalendarEventUntilDateTransaction
const TRANSACTIONTYPE = 'calendar.recurrenceenddate';
public function generateOldValue($object) {
return $object->getRecurrenceEndDate();
// TODO: Upgrade this.
return $object->getUntilDateTimeEpoch();
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
// TODO: DEPRECATED.
$object->setRecurrenceEndDate($value);
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$value,
$actor->getTimezoneIdentifier());
$datetime->setIsAllDay($object->getIsAllDay());
$object->setUntilDateTime($datetime);
}
public function getTitle() {

View file

@ -0,0 +1,28 @@
<?php
final class PhabricatorCalendarExportDisableTransaction
extends PhabricatorCalendarExportTransactionType {
const TRANSACTIONTYPE = 'calendar.export.disable';
public function generateOldValue($object) {
return (int)$object->getIsDisabled();
}
public function applyInternalEffects($object, $value) {
$object->setIsDisabled((int)$value);
}
public function getTitle() {
if ($this->getNewValue()) {
return pht(
'%s disabled this export.',
$this->renderAuthor());
} else {
return pht(
'%s enabled this export.',
$this->renderAuthor());
}
}
}

View file

@ -0,0 +1,54 @@
<?php
final class PhabricatorCalendarExportModeTransaction
extends PhabricatorCalendarExportTransactionType {
const TRANSACTIONTYPE = 'calendar.export.mode';
public function generateOldValue($object) {
return $object->getPolicyMode();
}
public function applyInternalEffects($object, $value) {
$object->setPolicyMode($value);
}
public function getTitle() {
$old_value = $this->getOldValue();
$new_value = $this->getNewValue();
$old_name = PhabricatorCalendarExport::getPolicyModeName($old_value);
$new_name = PhabricatorCalendarExport::getPolicyModeName($new_value);
return pht(
'%s changed the policy mode for this export from %s to %s.',
$this->renderAuthor(),
$this->renderValue($old_name),
$this->renderValue($new_name));
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$valid = PhabricatorCalendarExport::getPolicyModes();
$valid = array_fuse($valid);
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if (isset($valid[$value])) {
continue;
}
$errors[] = $this->newInvalidError(
pht(
'Mode "%s" is not a valid policy mode. Valid modes are: %s.',
$value,
implode(', ', $valid)),
$xaction);
}
return $errors;
}
}

View file

@ -0,0 +1,35 @@
<?php
final class PhabricatorCalendarExportNameTransaction
extends PhabricatorCalendarExportTransactionType {
const TRANSACTIONTYPE = 'calendar.export.name';
public function generateOldValue($object) {
return $object->getName();
}
public function applyInternalEffects($object, $value) {
$object->setName($value);
}
public function getTitle() {
return pht(
'%s renamed this export from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
$errors[] = $this->newRequiredError(
pht('Calendar exports must have a name.'));
}
return $errors;
}
}

View file

@ -0,0 +1,61 @@
<?php
final class PhabricatorCalendarExportQueryKeyTransaction
extends PhabricatorCalendarExportTransactionType {
const TRANSACTIONTYPE = 'calendar.export.querykey';
public function generateOldValue($object) {
return $object->getQueryKey();
}
public function applyInternalEffects($object, $value) {
$object->setQueryKey($value);
}
public function getTitle() {
return pht(
'%s changed the query for this export.',
$this->renderAuthor());
}
public function validateTransactions($object, array $xactions) {
$actor = $this->getActor();
$errors = array();
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
$query = id(new PhabricatorSavedQueryQuery())
->setViewer($actor)
->withEngineClassNames(array('PhabricatorCalendarEventSearchEngine'))
->withQueryKeys(array($value))
->executeOne();
if ($query) {
continue;
}
$builtin = id(new PhabricatorCalendarEventSearchEngine())
->setViewer($actor)
->getBuiltinQueries($actor);
if (isset($builtin[$value])) {
continue;
}
$errors[] = $this->newInvalidError(
pht(
'Query key "%s" does not identify a valid event query.',
$value),
$xaction);
}
if ($this->isEmptyTextTransaction($object->getQueryKey(), $xactions)) {
$errors[] = $this->newRequiredError(
pht('Calendar exports must have a query key.'));
}
return $errors;
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class PhabricatorCalendarExportTransactionType
extends PhabricatorModularTransactionType {}

View file

@ -404,19 +404,48 @@ final class PhabricatorSetupIssueView extends AphrontView {
implode("\n", $more_loc));
}
$info[] = phutil_tag(
'p',
array(),
pht(
'You can find more information about PHP configuration values in the '.
'%s.',
phutil_tag(
'a',
array(
'href' => 'http://php.net/manual/ini.list.php',
'target' => '_blank',
),
pht('PHP Documentation'))));
$show_standard = false;
$show_opcache = false;
foreach ($configs as $key) {
if (preg_match('/^opcache\./', $key)) {
$show_opcache = true;
} else {
$show_standard = true;
}
}
if ($show_standard) {
$info[] = phutil_tag(
'p',
array(),
pht(
'You can find more information about PHP configuration values '.
'in the %s.',
phutil_tag(
'a',
array(
'href' => 'http://php.net/manual/ini.list.php',
'target' => '_blank',
),
pht('PHP Documentation'))));
}
if ($show_opcache) {
$info[] = phutil_tag(
'p',
array(),
pht(
'You can find more information about configuring OPCache in '.
'the %s.',
phutil_tag(
'a',
array(
'href' => 'http://php.net/manual/opcache.configuration.php',
'target' => '_blank',
),
pht('PHP OPCache Documentation'))));
}
$info[] = phutil_tag(
'p',

View file

@ -30,20 +30,31 @@ final class PhabricatorConpherenceApplication extends PhabricatorApplication {
public function getRoutes() {
return array(
'/Z(?P<id>[1-9]\d*)' => 'ConpherenceViewController',
'/Z(?P<id>[1-9]\d*)'
=> 'ConpherenceViewController',
'/conpherence/' => array(
'' => 'ConpherenceListController',
'thread/(?P<id>[1-9]\d*)/' => 'ConpherenceListController',
'(?P<id>[1-9]\d*)/' => 'ConpherenceViewController',
''
=> 'ConpherenceListController',
'thread/(?P<id>[1-9]\d*)/'
=> 'ConpherenceListController',
'(?P<id>[1-9]\d*)/'
=> 'ConpherenceViewController',
'(?P<id>[1-9]\d*)/(?P<messageID>[1-9]\d*)/'
=> 'ConpherenceViewController',
'columnview/' => 'ConpherenceColumnViewController',
'new/' => 'ConpherenceNewRoomController',
=> 'ConpherenceViewController',
'columnview/'
=> 'ConpherenceColumnViewController',
'new/'
=> 'ConpherenceNewRoomController',
'picture/(?P<id>[1-9]\d*)/'
=> 'ConpherenceRoomPictureController',
'search/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'ConpherenceRoomListController',
'panel/' => 'ConpherenceNotificationPanelController',
'participant/(?P<id>[1-9]\d*)/' => 'ConpherenceParticipantController',
'update/(?P<id>[1-9]\d*)/' => 'ConpherenceUpdateController',
=> 'ConpherenceRoomListController',
'panel/'
=> 'ConpherenceNotificationPanelController',
'participant/(?P<id>[1-9]\d*)/'
=> 'ConpherenceParticipantController',
'update/(?P<id>[1-9]\d*)/'
=> 'ConpherenceUpdateController',
),
);
}

View file

@ -1,11 +0,0 @@
<?php
final class ConpherenceImageData extends ConpherenceConstants {
const SIZE_ORIG = 'original';
const SIZE_CROP = 'crop';
const CROP_WIDTH = 35;
const CROP_HEIGHT = 35;
}

View file

@ -16,7 +16,7 @@ final class ConpherenceColumnViewController extends
$latest_conpherences = id(new ConpherenceThreadQuery())
->setViewer($user)
->withPHIDs($conpherence_phids)
->needCropPics(true)
->needProfileImage(true)
->needParticipantCache(true)
->execute();
$latest_conpherences = mpull($latest_conpherences, null, 'getPHID');
@ -31,7 +31,7 @@ final class ConpherenceColumnViewController extends
$conpherence = id(new ConpherenceThreadQuery())
->setViewer($user)
->withIDs(array($request->getInt('id')))
->needCropPics(true)
->needProfileImage(true)
->needTransactions(true)
->setTransactionLimit(ConpherenceThreadQuery::TRANSACTION_LIMIT)
->executeOne();
@ -41,7 +41,7 @@ final class ConpherenceColumnViewController extends
$conpherence = id(new ConpherenceThreadQuery())
->setViewer($user)
->withPHIDs(array($participant->getConpherencePHID()))
->needCropPics(true)
->needProfileImage(true)
->needTransactions(true)
->setTransactionLimit(ConpherenceThreadQuery::TRANSACTION_LIMIT)
->executeOne();

View file

@ -59,13 +59,18 @@ abstract class ConpherenceController extends PhabricatorController {
$header = id(new PHUIHeaderView())
->setHeader($data['title'])
->setSubheader($data['topic'])
->addClass((!$data['topic']) ? 'conpherence-no-topic' : null);
->setImage($data['image']);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$conpherence,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
$header->setImageURL(
$this->getApplicationURI('picture/'.$conpherence->getID().'/'));
}
$participating = $conpherence->getParticipantIfExists($viewer->getPHID());
$can_join = PhabricatorPolicyFilter::hasCapability(
$viewer,

View file

@ -158,7 +158,7 @@ final class ConpherenceListController extends ConpherenceController {
$conpherences = id(new ConpherenceThreadQuery())
->setViewer($user)
->withPHIDs($conpherence_phids)
->needCropPics(true)
->needProfileImage(true)
->needParticipantCache(true)
->execute();

View file

@ -6,6 +6,7 @@ final class ConpherenceNotificationPanelController
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$conpherences = array();
require_celerity_resource('conpherence-notification-css');
$unread_status = ConpherenceParticipationStatus::BEHIND;
$participant_data = id(new ConpherenceParticipantQuery())
@ -17,7 +18,7 @@ final class ConpherenceNotificationPanelController
$conpherences = id(new ConpherenceThreadQuery())
->setViewer($user)
->withPHIDs(array_keys($participant_data))
->needCropPics(true)
->needProfileImage(true)
->needTransactions(true)
->setTransactionLimit(3 * 5)
->needParticipantCache(true)
@ -25,7 +26,6 @@ final class ConpherenceNotificationPanelController
}
if ($conpherences) {
require_celerity_resource('conpherence-notification-css');
// re-order the conpherences based on participation data
$conpherences = array_select_keys(
$conpherences, array_keys($participant_data));
@ -70,13 +70,20 @@ final class ConpherenceNotificationPanelController
}
$content = $view->render();
} else {
$rooms_uri = phutil_tag(
'a',
array(
'href' => '/conpherence/',
'class' => 'no-room-notification',
),
pht('You have joined no rooms.'));
$content = phutil_tag_div(
'phabricator-notification no-notifications',
pht('You have no messages.'));
'phabricator-notification no-notifications', $rooms_uri);
}
$content = hsprintf(
'<div class="phabricator-notification-header">%s</div>'.
'<div class="phabricator-notification-header grouped">%s%s</div>'.
'%s',
phutil_tag(
'a',
@ -84,6 +91,7 @@ final class ConpherenceNotificationPanelController
'href' => '/conpherence/',
),
pht('Rooms')),
$this->renderPersistentOption(),
$content);
$unread = id(new ConpherenceParticipantCountQuery())
@ -100,4 +108,32 @@ final class ConpherenceNotificationPanelController
return id(new AphrontAjaxResponse())->setContent($json);
}
private function renderPersistentOption() {
$viewer = $this->getViewer();
$column_key = PhabricatorConpherenceColumnVisibleSetting::SETTINGKEY;
$show = (bool)$viewer->getUserSetting($column_key, false);
$view = phutil_tag(
'div',
array(
'class' => 'persistent-option',
),
array(
javelin_tag(
'input',
array(
'type' => 'checkbox',
'checked' => ($show) ? 'checked' : null,
'value' => !$show,
'sigil' => 'conpherence-persist-column',
)),
phutil_tag(
'span',
array(),
pht('Persistent Chat')),
));
return $view;
}
}

View file

@ -0,0 +1,234 @@
<?php
final class ConpherenceRoomPictureController
extends ConpherenceController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$conpherence = id(new ConpherenceThreadQuery())
->setViewer($viewer)
->withIDs(array($id))
->needProfileImage(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$conpherence) {
return new Aphront404Response();
}
$monogram = $conpherence->getMonogram();
$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 new room picture.');
}
}
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($conpherence->getPHID());
$new_value = $xformed->getPHID();
}
$xactions = array();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(ConpherenceTransaction::TYPE_PICTURE)
->setNewValue($new_value);
$editor = id(new ConpherenceEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($conpherence, $xactions);
return id(new AphrontRedirectResponse())->setURI('/'.$monogram);
}
}
$title = pht('Edit Room Picture');
$form = id(new PHUIFormLayoutView())
->setUser($viewer);
$default_image = PhabricatorFile::loadBuiltin($viewer, 'conpherence.png');
$images = array();
$current = $conpherence->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 Picture'))
->setValue(array_shift($buttons)));
}
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Use Picture'))
->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 Picture'))
->setError($e_file)
->setCaption(
pht('Supported formats: %s', implode(', ', $supported_formats))))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/'.$monogram)
->setValue(pht('Upload Picture')));
$upload_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Upload New Picture'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($upload_form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($conpherence->getTitle(), '/'.$monogram);
$crumbs->addTextCrumb(pht('Room Picture'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Room Picture'))
->setHeaderIcon('fa-camera');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$form_box,
$upload_box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
}

View file

@ -36,8 +36,6 @@ final class ConpherenceUpdateController
$conpherence = id(new ConpherenceThreadQuery())
->setViewer($user)
->withIDs(array($conpherence_id))
->needOrigPics(true)
->needCropPics(true)
->needParticipants($need_participants)
->requireCapabilities($needed_capabilities)
->executeOne();
@ -131,57 +129,14 @@ final class ConpherenceUpdateController
break;
case ConpherenceUpdateActions::METADATA:
$top = $request->getInt('image_y');
$left = $request->getInt('image_x');
$file_id = $request->getInt('file_id');
$title = $request->getStr('title');
$topic = $request->getStr('topic');
if ($file_id) {
$orig_file = id(new PhabricatorFileQuery())
->setViewer($user)
->withIDs(array($file_id))
->executeOne();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(ConpherenceTransaction::TYPE_PICTURE)
->setNewValue($orig_file);
$okay = $orig_file->isTransformableImage();
if ($okay) {
$xformer = new PhabricatorImageTransformer();
$crop_file = $xformer->executeConpherenceTransform(
$orig_file,
0,
0,
ConpherenceImageData::CROP_WIDTH,
ConpherenceImageData::CROP_HEIGHT);
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceTransaction::TYPE_PICTURE_CROP)
->setNewValue($crop_file->getPHID());
}
$response_mode = 'redirect';
}
// all other metadata updates are continue requests
if (!$request->isContinueRequest()) {
break;
}
if ($top !== null || $left !== null) {
$file = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG);
$xformer = new PhabricatorImageTransformer();
$xformed = $xformer->executeConpherenceTransform(
$file,
$top,
$left,
ConpherenceImageData::CROP_WIDTH,
ConpherenceImageData::CROP_HEIGHT);
$image_phid = $xformed->getPHID();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceTransaction::TYPE_PICTURE_CROP)
->setNewValue($image_phid);
}
$title = $request->getStr('title');
$topic = $request->getStr('topic');
$xactions[] = id(new ConpherenceTransaction())
@ -491,35 +446,6 @@ final class ConpherenceUpdateController
->setName('topic')
->setValue($conpherence->getTopic()));
$nopic = $this->getRequest()->getExists('nopic');
$image = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG);
if ($nopic) {
// do not render any pic related controls
} else if ($image) {
$crop_uri = $conpherence->loadImageURI(ConpherenceImageData::SIZE_CROP);
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Image'))
->setValue(phutil_tag(
'img',
array(
'src' => $crop_uri,
))))
->appendChild(
id(new ConpherencePicCropControl())
->setLabel(pht('Crop Image'))
->setValue($image))
->appendChild(
id(new ConpherenceFormDragAndDropUploadControl())
->setLabel(pht('Change Image')));
} else {
$form
->appendChild(
id(new ConpherenceFormDragAndDropUploadControl())
->setLabel(pht('Image')));
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($conpherence)
@ -571,7 +497,6 @@ final class ConpherenceUpdateController
$latest_transaction_id) {
$minimal_display = $this->getRequest()->getExists('minimal_display');
$need_widget_data = false;
$need_transactions = false;
$need_participant_cache = true;
switch ($action) {
@ -582,7 +507,6 @@ final class ConpherenceUpdateController
case ConpherenceUpdateActions::MESSAGE:
case ConpherenceUpdateActions::ADD_PERSON:
$need_transactions = true;
$need_widget_data = !$minimal_display;
break;
case ConpherenceUpdateActions::REMOVE_PERSON:
case ConpherenceUpdateActions::NOTIFICATIONS:
@ -594,7 +518,7 @@ final class ConpherenceUpdateController
$conpherence = id(new ConpherenceThreadQuery())
->setViewer($user)
->setAfterTransactionID($latest_transaction_id)
->needCropPics(true)
->needProfileImage(true)
->needParticipantCache($need_participant_cache)
->needParticipants(true)
->needTransactions($need_transactions)
@ -607,8 +531,13 @@ final class ConpherenceUpdateController
$user,
$conpherence,
!$minimal_display);
$participant_obj = $conpherence->getParticipant($user->getPHID());
$participant_obj->markUpToDate($conpherence, $data['latest_transaction']);
$key = PhabricatorConpherenceColumnMinimizeSetting::SETTINGKEY;
$minimized = $user->getUserSetting($key);
if (!$minimized) {
$participant_obj = $conpherence->getParticipant($user->getPHID());
$participant_obj
->markUpToDate($conpherence, $data['latest_transaction']);
}
} else if ($need_transactions) {
$non_update = true;
$data = array();

View file

@ -19,7 +19,7 @@ final class ConpherenceViewController extends
$query = id(new ConpherenceThreadQuery())
->setViewer($user)
->withIDs(array($conpherence_id))
->needCropPics(true)
->needProfileImage(true)
->needParticipantCache(true)
->needTransactions(true)
->setTransactionLimit($this->getMainQueryLimit());

View file

@ -91,7 +91,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
$types[] = ConpherenceTransaction::TYPE_TOPIC;
$types[] = ConpherenceTransaction::TYPE_PARTICIPANTS;
$types[] = ConpherenceTransaction::TYPE_PICTURE;
$types[] = ConpherenceTransaction::TYPE_PICTURE_CROP;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_JOIN_POLICY;
@ -109,9 +108,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
case ConpherenceTransaction::TYPE_TOPIC:
return $object->getTopic();
case ConpherenceTransaction::TYPE_PICTURE:
return $object->getImagePHID(ConpherenceImageData::SIZE_ORIG);
case ConpherenceTransaction::TYPE_PICTURE_CROP:
return $object->getImagePHID(ConpherenceImageData::SIZE_CROP);
return $object->getProfileImagePHID();
case ConpherenceTransaction::TYPE_PARTICIPANTS:
if ($this->getIsNewObject()) {
return array();
@ -127,11 +124,8 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
switch ($xaction->getTransactionType()) {
case ConpherenceTransaction::TYPE_TITLE:
case ConpherenceTransaction::TYPE_TOPIC:
case ConpherenceTransaction::TYPE_PICTURE_CROP:
return $xaction->getNewValue();
case ConpherenceTransaction::TYPE_PICTURE:
$file = $xaction->getNewValue();
return $file->getPHID();
return $xaction->getNewValue();
case ConpherenceTransaction::TYPE_PARTICIPANTS:
return $this->getPHIDTransactionNewValue($xaction);
}
@ -224,14 +218,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
$object->setTopic($xaction->getNewValue());
break;
case ConpherenceTransaction::TYPE_PICTURE:
$object->setImagePHID(
$xaction->getNewValue(),
ConpherenceImageData::SIZE_ORIG);
break;
case ConpherenceTransaction::TYPE_PICTURE_CROP:
$object->setImagePHID(
$xaction->getNewValue(),
ConpherenceImageData::SIZE_CROP);
$object->setProfileImagePHID($xaction->getNewValue());
break;
case ConpherenceTransaction::TYPE_PARTICIPANTS:
if (!$this->getIsNewObject()) {
@ -339,6 +326,10 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
PhabricatorLiskDAO $object,
array $xactions) {
if (!$xactions) {
return $xactions;
}
$message_count = 0;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
@ -571,19 +562,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
return true;
}
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ConpherenceTransaction::TYPE_PICTURE:
case ConpherenceTransaction::TYPE_PICTURE_CROP:
return array($xaction->getNewValue());
}
return parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
@ -612,21 +590,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
$errors[] = $error;
}
break;
case ConpherenceTransaction::TYPE_PICTURE:
foreach ($xactions as $xaction) {
$file = $xaction->getNewValue();
if (!$file->isTransformableImage()) {
$detail = pht('This server only supports these image formats: %s.',
implode(', ', PhabricatorFile::getTransformableImageFormats()));
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$detail,
last($xactions));
$errors[] = $error;
}
}
break;
case ConpherenceTransaction::TYPE_PARTICIPANTS:
foreach ($xactions as $xaction) {
$new_phids = $this->getPHIDTransactionNewValue($xaction, array());

View file

@ -9,14 +9,13 @@ final class ConpherenceThreadQuery
private $ids;
private $participantPHIDs;
private $needParticipants;
private $needCropPics;
private $needOrigPics;
private $needTransactions;
private $needParticipantCache;
private $afterTransactionID;
private $beforeTransactionID;
private $transactionLimit;
private $fulltext;
private $needProfileImage;
public function needParticipantCache($participant_cache) {
$this->needParticipantCache = $participant_cache;
@ -28,13 +27,8 @@ final class ConpherenceThreadQuery
return $this;
}
public function needCropPics($need) {
$this->needCropPics = $need;
return $this;
}
public function needOrigPics($need_widget_data) {
$this->needOrigPics = $need_widget_data;
public function needProfileImage($need) {
$this->needProfileImage = $need;
return $this;
}
@ -110,14 +104,33 @@ final class ConpherenceThreadQuery
if ($this->needTransactions) {
$this->loadTransactionsAndHandles($conpherences);
}
if ($this->needOrigPics || $this->needCropPics) {
$this->initImages($conpherences);
}
if ($this->needOrigPics) {
$this->loadOrigPics($conpherences);
}
if ($this->needCropPics) {
$this->loadCropPics($conpherences);
if ($this->needProfileImage) {
$default = null;
$file_phids = mpull($conpherences, '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 ($conpherences as $conpherence) {
$file = idx($files, $conpherence->getProfileImagePHID());
if (!$file) {
if (!$default) {
$default = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'conpherence.png');
}
$file = $default;
}
$conpherence->attachProfileImageFile($file);
}
}
}
@ -266,50 +279,6 @@ final class ConpherenceThreadQuery
return $this;
}
private function loadOrigPics(array $conpherences) {
return $this->loadPics(
$conpherences,
ConpherenceImageData::SIZE_ORIG);
}
private function loadCropPics(array $conpherences) {
return $this->loadPics(
$conpherences,
ConpherenceImageData::SIZE_CROP);
}
private function initImages($conpherences) {
foreach ($conpherences as $conpherence) {
$conpherence->attachImages(array());
}
}
private function loadPics(array $conpherences, $size) {
$conpherence_pic_phids = array();
foreach ($conpherences as $conpherence) {
$phid = $conpherence->getImagePHID($size);
if ($phid) {
$conpherence_pic_phids[$conpherence->getPHID()] = $phid;
}
}
if (!$conpherence_pic_phids) {
return $this;
}
$files = id(new PhabricatorFileQuery())
->setViewer($this->getViewer())
->withPHIDs($conpherence_pic_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($conpherence_pic_phids as $conpherence_phid => $pic_phid) {
$conpherences[$conpherence_phid]->setImage($files[$pic_phid], $size);
}
return $this;
}
public function getQueryApplicationClass() {
return 'PhabricatorConpherenceApplication';
}

View file

@ -9,7 +9,8 @@ final class ConpherenceThread extends ConpherenceDAO
protected $title;
protected $topic;
protected $imagePHIDs = array();
protected $imagePHIDs = array(); // TODO; nuke after migrations
protected $profileImagePHID;
protected $messageCount;
protected $recentParticipantPHIDs = array();
protected $mailKey;
@ -19,8 +20,8 @@ final class ConpherenceThread extends ConpherenceDAO
private $participants = self::ATTACHABLE;
private $transactions = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
private $handles = self::ATTACHABLE;
private $images = self::ATTACHABLE;
public static function initializeNewRoom(PhabricatorUser $sender) {
$default_policy = id(new ConpherenceThreadMembersPolicyRule())
@ -30,7 +31,6 @@ final class ConpherenceThread extends ConpherenceDAO
->setTitle('')
->setTopic('')
->attachParticipants(array())
->attachImages(array())
->setViewPolicy($default_policy)
->setEditPolicy($default_policy)
->setJoinPolicy($default_policy);
@ -49,6 +49,7 @@ final class ConpherenceThread extends ConpherenceDAO
'messageCount' => 'uint64',
'mailKey' => 'text20',
'joinPolicy' => 'policy',
'profileImagePHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
@ -76,46 +77,21 @@ final class ConpherenceThread extends ConpherenceDAO
return 'Z'.$this->getID();
}
public function getImagePHID($size) {
$image_phids = $this->getImagePHIDs();
return idx($image_phids, $size);
}
public function setImagePHID($phid, $size) {
$image_phids = $this->getImagePHIDs();
$image_phids[$size] = $phid;
return $this->setImagePHIDs($image_phids);
}
public function getImage($size) {
$images = $this->getImages();
return idx($images, $size);
}
public function setImage(PhabricatorFile $file, $size) {
$files = $this->getImages();
$files[$size] = $file;
return $this->attachImages($files);
}
public function attachImages(array $files) {
assert_instances_of($files, 'PhabricatorFile');
$this->images = $files;
return $this;
}
private function getImages() {
return $this->assertAttached($this->images);
}
public function attachParticipants(array $participants) {
assert_instances_of($participants, 'ConpherenceParticipant');
$this->participants = $participants;
return $this;
}
public function getParticipants() {
return $this->assertAttached($this->participants);
}
public function getParticipant($phid) {
$participants = $this->getParticipants();
return $participants[$phid];
}
public function getParticipantIfExists($phid, $default = null) {
$participants = $this->getParticipants();
return idx($participants, $phid, $default);
@ -131,6 +107,7 @@ final class ConpherenceThread extends ConpherenceDAO
$this->handles = $handles;
return $this;
}
public function getHandles() {
return $this->assertAttached($this->handles);
}
@ -140,9 +117,11 @@ final class ConpherenceThread extends ConpherenceDAO
$this->transactions = $transactions;
return $this;
}
public function getTransactions($assert_attached = true) {
return $this->assertAttached($this->transactions);
}
public function hasAttachedTransactions() {
return $this->transactions !== self::ATTACHABLE;
}
@ -156,14 +135,17 @@ final class ConpherenceThread extends ConpherenceDAO
$amount);
}
public function loadImageURI($size) {
$file = $this->getImage($size);
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
if ($file) {
return $file->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
return PhabricatorUser::getDefaultProfileImageURI();
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
/**
@ -273,22 +255,26 @@ final class ConpherenceThread extends ConpherenceDAO
$lucky_handle = reset($handles);
}
$img_src = null;
$size = ConpherenceImageData::SIZE_CROP;
if ($this->getImagePHID($size)) {
$img_src = $this->getImage($size)->getBestURI();
} else if ($lucky_handle) {
$img_src = $lucky_handle->getImageURI();
}
$img_src = $this->getProfileImageURI();
$message_title = null;
if ($subtitle_mode == 'message') {
$message_transaction = null;
$action_transaction = null;
foreach ($transactions as $transaction) {
if ($message_transaction || $action_transaction) {
break;
}
switch ($transaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$message_transaction = $transaction;
break 2;
break;
case ConpherenceTransaction::TYPE_TITLE:
case ConpherenceTransaction::TYPE_TOPIC:
case ConpherenceTransaction::TYPE_PICTURE:
case ConpherenceTransaction::TYPE_PARTICIPANTS:
$action_transaction = $transaction;
break;
default:
break;
}
@ -303,6 +289,11 @@ final class ConpherenceThread extends ConpherenceDAO
->truncateString(
$message_transaction->getComment()->getContent()));
}
if ($action_transaction) {
$message_title = id(clone $action_transaction)
->setRenderingTarget(PhabricatorApplicationTransaction::TARGET_TEXT)
->getTitle();
}
}
switch ($subtitle_mode) {
case 'recent':

View file

@ -7,7 +7,7 @@ final class ConpherenceTransaction extends PhabricatorApplicationTransaction {
const TYPE_PARTICIPANTS = 'participants';
const TYPE_DATE_MARKER = 'date-marker';
const TYPE_PICTURE = 'picture';
const TYPE_PICTURE_CROP = 'picture-crop';
const TYPE_PICTURE_CROP = 'picture-crop'; // TODO: Nuke these from DB.
public function getApplicationName() {
return 'conpherence';

View file

@ -150,13 +150,14 @@ final class ConpherenceDurableColumnView extends AphrontTagView {
$icon_bar = null;
if ($this->conpherences) {
$icon_bar = phutil_tag(
'div',
array(
'class' => 'conpherence-durable-column-icon-bar',
),
$this->buildIconBar());
$icon_bar = $this->buildIconBar();
}
$icon_bar = phutil_tag(
'div',
array(
'class' => 'conpherence-durable-column-icon-bar',
),
$icon_bar);
$transactions = $this->buildTransactions();
@ -198,19 +199,6 @@ final class ConpherenceDurableColumnView extends AphrontTagView {
);
}
private function getPolicyIcon(
ConpherenceThread $conpherence,
array $policy_objects) {
assert_instances_of($policy_objects, 'PhabricatorPolicy');
$icon = $conpherence->getPolicyIconName($policy_objects);
$icon = id(new PHUIIconView())
->addClass('mmr')
->setIcon($icon);
return $icon;
}
private function buildIconBar() {
$icons = array();
$selected_conpherence = $this->getSelectedConpherence();
@ -222,12 +210,10 @@ final class ConpherenceDurableColumnView extends AphrontTagView {
$classes[] = 'selected';
}
$data = $conpherence->getDisplayData($this->getUser());
$icon = $this->getPolicyIcon($conpherence, $this->getPolicyObjects());
$thread_title = phutil_tag(
'span',
array(),
array(
$icon,
$data['title'],
));
$image = $data['image'];
@ -324,17 +310,20 @@ final class ConpherenceDurableColumnView extends AphrontTagView {
->addMenuItem($minimize)
->addClass('phabricator-application-menu');
$header = null;
if ($conpherence) {
$data = $conpherence->getDisplayData($this->getUser());
$header = phutil_tag(
'span',
array(),
array(
$this->getPolicyIcon($conpherence, $this->getPolicyObjects()),
$data['title'],
));
}
$data['title']);
} else {
$header = phutil_tag(
'span',
array(),
pht('Conpherence'));
}
$status = new PhabricatorNotificationStatusView();
return
phutil_tag(
@ -343,6 +332,7 @@ final class ConpherenceDurableColumnView extends AphrontTagView {
'class' => 'conpherence-durable-column-header-inner',
),
array(
$status,
javelin_tag(
'div',
array(
@ -403,22 +393,22 @@ final class ConpherenceDurableColumnView extends AphrontTagView {
if (!$this->getVisible() || $this->getInitialLoad()) {
return pht('Loading...');
}
return array(
$view = array(
phutil_tag(
'div',
array(
'class' => 'mmb',
'class' => 'column-no-rooms-text',
),
pht('You are not in any rooms yet.')),
pht('You have not joined any rooms yet.')),
javelin_tag(
'a',
array(
'href' => '/conpherence/new/',
'href' => '/conpherence/search/',
'class' => 'button grey',
'sigil' => 'workflow',
),
pht('Create a Room')),
pht('Find Rooms')),
);
return phutil_tag_div('column-no-rooms', $view);
}
$data = ConpherenceTransactionRenderer::renderTransactions(

View file

@ -1,78 +0,0 @@
<?php
final class ConpherencePicCropControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-crop';
}
protected function renderInput() {
$width = ConpherenceImageData::CROP_WIDTH;
$height = ConpherenceImageData::CROP_HEIGHT;
$file = $this->getValue();
if ($file === null) {
return phutil_tag(
'img',
array(
'src' => PhabricatorUser::getDefaultProfileImageURI(),
),
'');
}
$c_id = celerity_generate_unique_node_id();
$metadata = $file->getMetadata();
$scale = PhabricatorImageTransformer::getScaleForCrop(
$file,
$width,
$height);
Javelin::initBehavior(
'aphront-crop',
array(
'cropBoxID' => $c_id,
'width' => $width,
'height' => $height,
'scale' => $scale,
'imageH' => $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT],
'imageW' => $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH],
));
return javelin_tag(
'div',
array(
'id' => $c_id,
'sigil' => 'crop-box',
'mustcapture' => true,
'class' => 'crop-box',
),
array(
javelin_tag(
'img',
array(
'src' => $file->getBestURI(),
'class' => 'crop-image',
'sigil' => 'crop-image',
),
''),
javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => 'image_x',
'sigil' => 'crop-x',
),
''),
javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => 'image_y',
'sigil' => 'crop-y',
),
''),
));
}
}

View file

@ -228,7 +228,6 @@ final class ConpherenceTransactionView extends AphrontView {
case ConpherenceTransaction::TYPE_TITLE:
case ConpherenceTransaction::TYPE_TOPIC:
case ConpherenceTransaction::TYPE_PICTURE:
case ConpherenceTransaction::TYPE_PICTURE_CROP:
case ConpherenceTransaction::TYPE_PARTICIPANTS:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:

View file

@ -78,7 +78,7 @@ final class DiffusionRepositoryActionsManagementPanel
$autoclose = phutil_tag('em', array(), $autoclose);
$view->addProperty(pht('Autoclose'), $autoclose);
return $this->newBox(pht('Branches'), $view);
return $this->newBox(pht('Actions'), $view);
}
}

View file

@ -421,7 +421,7 @@ final class ManiphestTaskSearchEngine
'you need to get done. Tasks assigned to you will appear here.'))
->addAction($create_button);
return $view;
return $view;
}
}

View file

@ -16,6 +16,20 @@ final class PhabricatorNotificationStatusView extends AphrontTagView {
'open' => pht('Connected'),
'closed' => pht('Disconnected'),
),
'icon' => array(
'open' => array(
'icon' => 'fa-circle',
'color' => 'green',
),
'setup' => array(
'icon' => 'fa-circle',
'color' => 'yellow',
),
'closed' => array(
'icon' => 'fa-circle',
'color' => 'red',
),
),
));
return array(
@ -26,9 +40,33 @@ final class PhabricatorNotificationStatusView extends AphrontTagView {
protected function getTagContent() {
$have = PhabricatorEnv::getEnvConfig('notification.servers');
if ($have) {
return pht('Connecting...');
$icon = id(new PHUIIconView())
->setIcon('fa-circle-o yellow');
$text = pht('Connecting...');
return phutil_tag(
'span',
array(
'class' => 'connection-status-text '.
'aphlict-connection-status-connecting',
),
array(
$icon,
$text,
));
} else {
return pht('Notification server not enabled');
$text = pht('Notification server not enabled');
$icon = id(new PHUIIconView())
->setIcon('fa-circle-o grey');
return phutil_tag(
'span',
array(
'class' => 'connection-status-text '.
'aphlict-connection-status-notenabled',
),
array(
$icon,
$text,
));
}
}

View file

@ -205,8 +205,8 @@ final class PhabricatorPeopleProfileViewController
$event,
PhabricatorPolicyCapability::CAN_EDIT);
$epoch_min = $event->getViewerDateFrom();
$epoch_max = $event->getViewerDateTo();
$epoch_min = $event->getStartDateTimeEpoch();
$epoch_max = $event->getEndDateTimeEpoch();
$event_view = id(new AphrontCalendarEventView())
->setCanEdit($can_edit)

View file

@ -431,8 +431,8 @@ final class PhabricatorPeopleQuery
// because of an event, we check again for events after that one ends.
while (true) {
foreach ($events as $event) {
$from = $event->getDateFromForCache();
$to = $event->getViewerDateTo();
$from = $event->getStartDateTimeEpochForCache();
$to = $event->getEndDateTimeEpoch();
if (($from <= $cursor) && ($to > $cursor)) {
$cursor = $to;
continue 2;

View file

@ -252,12 +252,6 @@ final class PhabricatorApplicationSearchController
get_class($engine)));
}
if ($list->getActions()) {
foreach ($list->getActions() as $action) {
$header->addActionLink($action);
}
}
if ($list->getObjectList()) {
$box->setObjectList($list->getObjectList());
}
@ -274,6 +268,21 @@ final class PhabricatorApplicationSearchController
$result_header = $list->getHeader();
if ($result_header) {
$box->setHeader($result_header);
$header = $result_header;
}
if ($list->getActions()) {
foreach ($list->getActions() as $action) {
$header->addActionLink($action);
}
}
$use_actions = $engine->newUseResultsActions($saved_query);
if ($use_actions) {
$use_dropdown = $this->newUseResultsDropdown(
$saved_query,
$use_actions);
$header->addActionLink($use_dropdown);
}
$more_crumbs = $list->getCrumbs();
@ -496,5 +505,24 @@ final class PhabricatorApplicationSearchController
return $nux_view;
}
private function newUseResultsDropdown(
PhabricatorSavedQuery $query,
array $dropdown_items) {
$viewer = $this->getViewer();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($dropdown_items as $dropdown_item) {
$action_list->addAction($dropdown_item);
}
return id(new PHUIButtonView())
->setTag('a')
->setHref('#')
->setText(pht('Use Results...'))
->setIcon('fa-road')
->setDropdownMenu($action_list);
}
}

View file

@ -1390,4 +1390,8 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
return null;
}
public function newUseResultsActions(PhabricatorSavedQuery $saved) {
return array();
}
}

View file

@ -0,0 +1,97 @@
@title Calendar User Guide: Exporting Events
@group userguide
Exporting events to other calendars.
Overview
========
IMPORTANT: Calendar is a prototype application. See
@{article:User Guide: Prototype Applications}.
You can export events from Phabricator to other calendar applications like
**Google Calendar** or **Calendar.app**. This document will guide you through
how to export event data from Phabricator.
When you export events into another application, they generally will not be
editable from that application. Exporting events allows you to create one
calendar that shows all the events you care about in whatever application you
prefer (so you can keep track of everything you need to do), but does not let
you edit Phabricator events from another application.
When exporting events, you can either export individual events one at a time
or export an entire group of events (for example, all events you are attending).
Exporting a Single Event
========================
To export a single event, visit the event detail page and click
{nav Export as .ics}. This will download an `.ics` file which you can import
into most other calendar applications.
Mail you receive about events also has a copy of this `.ics` file attached to
it. You can import this `.ics` file directly.
In **Google Calendar**, use {nav Other Calendars > Import Calendar} to import
the `.ics` file.
In **Calendar.app**, use {nav File > Import...} to import the `.ics` file, or
drag the `.ics` file onto your calendar.
When you export a recurring event, the `.ics` file will contain information
about the entire event series.
If you want to update event information later, you can just repeat this
process. Calendar applications will update the existing event if you've
previously imported an older version of it.
Exporting a Group of Events
===========================
You can export a group of events matching an arbitrary query (like all events
you are attending) to keep different calendars in sync.
To export a group of events:
- Run a query in Calendar which selects the events you want to export.
- Example: All events you are attending.
- Example: All events you are invited to.
- Example: All events tagged `#meetup`.
- Select the {nav Use Results... > Export Query as .ics} action to turn
the query into an export.
- Name the export with a descritive name.
- Select a policy mode for the export (see below for discussion).
- Click {nav Create New Export} to finish the process.
The **policy modes** for exports are:
- **Public**: Only public information (visible to logged-out users) will
be exported. This mode is not available if your install does not have
public information (per `policy.allow-public` in Config).
- **Privileged**: All event information will be exported. This means that
anyone who knows the export URI can see ALL of the related event
information, as though they were logged in with your account.
WARNING: Anyone who learns the URI for an export can see the data you choose
to export, even if they don't have a Phabricator account! Be careful about how
much data you export and treat the URI as a secret. If you accidentally share
a URI, you can disable the export.
After finishing the process, you'll see a screen with some details about the
export and an **ICS URI**. This URI allows you to import the events which match
the query into another calendar application.
In **Google Calendar**, use {nav Other Calendars > Add by URL} to import the
URI.
In **Calendar.app**, use {nav File > New Calendar Subscription...} to subscribe
to the URI.
Next Steps
==========
Continue by:
- returning to the @{article:Calendar User Guide}.

View file

@ -1339,11 +1339,12 @@ abstract class LiskDAO extends Phobject {
* @task hook
*/
public function generatePHID() {
throw new Exception(
pht(
'To use %s, you need to overload %s to perform PHID generation.',
'CONFIG_AUX_PHID',
'generatePHID()'));
$type = $this->getPHIDType();
return PhabricatorPHID::generateNewPHID($type);
}
public function getPHIDType() {
throw new PhutilMethodNotImplementedException();
}

View file

@ -805,6 +805,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
return array(
'title' => $this->getTitle(),
'bodyClasses' => $this->getBodyClasses(),
'aphlictDropdownData' => array(
$dropdown_query->getNotificationData(),
$dropdown_query->getConpherenceData(),

View file

@ -124,10 +124,16 @@
color: {$lightgreytext};
}
.aphlict-connection-status .aphlict-connection-status-connected {
color: {$green};
.aphlict-connection-status {
position: relative;
}
.aphlict-connection-status .aphlict-connection-status-error {
color: {$red};
.aphlict-connection-status .phui-icon-view {
font-size: 9px;
position: absolute;
top: 4px;
}
.aphlict-connection-status .connection-status-text {
margin-left: 12px;
}

View file

@ -28,36 +28,3 @@
background: #124A1B;
}
/*--- Profile Nav Colors -----------------------------------------------------*/
.phui-theme-blindigo .phui-profile-menu .phabricator-side-menu,
.phui-theme-blindigo .phabricator-side-menu .phui-profile-menu-footer-1 {
background: #525867;
}
.phui-theme-dark .phui-profile-menu .phabricator-side-menu,
.phui-theme-dark .phabricator-side-menu .phui-profile-menu-footer-1 {
background: #30333A;
}
.phui-theme-indigo .phui-profile-menu .phabricator-side-menu,
.phui-theme-indigo .phabricator-side-menu .phui-profile-menu-footer-1 {
background: #423658;
}
.phui-theme-red .phui-profile-menu .phabricator-side-menu,
.phui-theme-red .phabricator-side-menu .phui-profile-menu-footer-1 {
background: #420C0C;
}
.phui-theme-blue .phui-profile-menu .phabricator-side-menu,
.phui-theme-blue .phabricator-side-menu .phui-profile-menu-footer-1 {
background: #062842;
}
.phui-theme-green .phui-profile-menu .phabricator-side-menu,
.phui-theme-green .phabricator-side-menu .phui-profile-menu-footer-1 {
background: #122916;
}

View file

@ -45,6 +45,24 @@
display: none;
}
.conpherence-durable-column-header .aphlict-connection-status {
width: 6px;
float: left;
height: 18px;
margin-left: 10px;
margin-top: 8px;
}
.conpherence-durable-column-header .aphlict-connection-status
.connection-status-text {
display: none;
}
.conpherence-durable-column-header .aphlict-connection-status
.phui-icon-view {
font-size: 10px;
}
.conpherence-durable-column-header .phabricator-application-menu
.phui-list-item-view {
margin: 0;
@ -100,11 +118,6 @@
padding: 10px 8px 10px 8px;
}
.conpherence-durable-column-header-text .phui-icon-view {
color: #fff;
text-shadow: 1px 1px 0 rgba(0,0,0,.6);
}
.conpherence-durable-column-icon-bar {
width: 36px;
background-color: {$lightgreybackground};
@ -153,6 +166,16 @@
overflow-x: hidden;
}
.conpherence-durable-column .column-no-rooms {
padding: 12px 8px;
}
.conpherence-durable-column .column-no-rooms-text {
color: {$greytext};
font-style: italic;
margin-bottom: 16px;
}
.conpherence-durable-column-transactions {
padding: 8px 12px 0;
}
@ -300,12 +323,6 @@ img {
text-shadow: none;
}
.minimize-column .conpherence-durable-column
.conpherence-durable-column-header-text .phui-icon-view {
color: {$darkbluetext};
text-shadow: none;
}
.minimize-column .conpherence-durable-column
.conpherence-durable-column-header .phabricator-application-menu
.phui-list-item-icon.phui-font-fa {

View file

@ -2,9 +2,6 @@
* @provides conpherence-header-pane-css
*/
.conpherence-header-pane {
}
.conpherence-header-pane .phui-header-shell {
padding: 8px 16px 10px;
min-height: 38px;
@ -23,8 +20,26 @@
margin: 0;
}
.conpherence-header-pane .phui-header-shell.conpherence-no-topic {
padding: 15px 16px 5px;
.conpherence-header-pane .phui-header-col1 {
width: 46px;
height: 35px;
}
.conpherence-header-pane .phui-header-image {
height: 35px;
width: 35px;
background-size: 35px;
position: absolute;
top: 4px;
left: 0;
}
.conpherence-header-pane .phui-header-image-href {
position: inherit;
}
.conpherence-header-pane .phui-header-col2 {
height: 40px;
}
.conpherence-header-pane .phui-header-action-list .phui-header-action-item

View file

@ -26,10 +26,14 @@
padding: 4px 0 4px 8px;
}
.conpherence-menu-pane .phui-list-item-view.hidden {
.conpherence-menu-pane.phabricator-side-menu .phui-list-item-view.hidden {
display: none;
}
.phui-list-view.conpherence-menu {
margin-bottom: 20px;
}
.conpherence-menu-pane.phabricator-side-menu .room-list-href {
padding: 10px 0 9px 8px;
display: inline-block;

View file

@ -2,8 +2,7 @@
* @provides conpherence-notification-css
*/
/* kill styles on phabricator-notification */
.conpherence-notification {
.phabricator-notification.conpherence-notification {
padding: 0;
}
@ -27,7 +26,6 @@
width: 30px;
height: 30px;
background-size: 100%;
box-shadow: {$borderinset};
border-radius: 3px;
}
@ -71,3 +69,19 @@
padding: 0 5px 1px;
font-size: {$smallestfontsize};
}
.phabricator-notification .no-room-notification {
color: {$lightgreytext};
display: block;
}
.phabricator-notification-header .persistent-option {
white-space: nowrap;
float: right;
}
.phabricator-notification-header .persistent-option span {
margin-left: 4px;
font-weight: normal;
color: {$greytext};
}

View file

@ -17,10 +17,14 @@
}
.phui-profile-menu .phabricator-side-menu {
background: #525867;
background: #dee0e7;
width: 240px;
}
.phabricator-side-menu .phui-profile-menu-footer-1 {
background: #dee0e7;
}
.phui-profile-menu .phabricator-side-menu .phui-list-item-view {
position: relative;
}
@ -32,9 +36,9 @@
height: 48px;
font-size: {$biggerfontsize};
-webkit-font-smoothing: antialiased;
color: {$menu.profile.text};
line-height: 22px;
overflow: hidden;
color: {$darkbluetext};
text-overflow: ellipsis;
line-height: 48px;
}
@ -50,7 +54,7 @@
height: 24px;
line-height: 24px;
text-align: center;
color: {$menu.profile.text};
color: {$darkbluetext};
background-size: 100%;
}
@ -81,7 +85,7 @@
.phui-profile-menu .phabricator-side-menu
.phui-list-item-disabled
.phui-list-item-icon {
color: {$menu.profile.icon.disabled};
color: {$lightgreytext};
}
.phui-profile-menu .phabricator-side-menu .phui-icon-view {
@ -90,28 +94,17 @@
.device-desktop .phui-profile-menu .phabricator-side-menu
.phui-list-item-href:hover {
background-color: rgba({$alphablack},0.15);
color: {$menu.profile.text.selected};
}
.phui-profile-menu .phabricator-side-menu
.phui-list-item-selected
.phui-list-item-icon,
.device-desktop .phui-profile-menu .phabricator-side-menu
.phui-list-item-href:hover
.phui-list-item-icon {
color: {$menu.profile.text.selected};
background-color: rgba({$alphablack},0.05);
}
.phui-profile-menu .phabricator-side-menu .phui-list-item-selected
.phui-list-item-href {
background-color: rgba({$alphablack},0.3);
color: {$menu.profile.text.selected};
background-color: rgba({$alphablack},0.1);
}
.phui-profile-menu .phabricator-side-menu .phui-list-item-selected
.phui-list-item-href:hover {
background-color: rgba({$alphablack},0.45);
background-color: rgba({$alphablack},0.15);
}
.phui-profile-menu .phabricator-side-menu .phui-divider {
@ -123,13 +116,13 @@
white-space: normal;
padding: 18px 15px;
font-size: 12px;
color: {$menu.profile.text};
color: {$darkbluetext};
}
.phui-profile-menu .phabricator-side-menu .phui-motivator .phui-icon-view {
position: static;
font-size: 12px;
color: {$menu.profile.text};
color: {$darkbluetext};
}
.phui-profile-menu .phabricator-side-menu .phui-profile-menu-error {
@ -150,11 +143,11 @@
.phui-list-item-href,
.phui-profile-menu .phui-list-sidenav .phui-list-item-disabled
.phui-list-item-href {
color: rgba({$alphawhite}, 0.5);
color: rgba({$lightgreytext});
}
.phui-profile-menu .phabricator-side-menu .phui-profile-segment-bar {
color: {$menu.profile.text};
color: {$darkbluetext};
font-size: {$smallerfontsize};
-webkit-font-smoothing: antialiased;
padding: 8px 12px 16px;
@ -201,25 +194,11 @@
}
.phui-profile-menu .phui-profile-menu-footer .phui-icon-circle {
border-color: {$menu.profile.text};
border-color: {$darkbluetext};
}
.phui-profile-menu .phui-profile-menu-footer .phui-icon-circle .phui-icon-view {
color: {$menu.profile.text};
}
.phui-profile-menu .phui-profile-menu-footer .phui-list-item-href:hover
.phui-icon-circle,
.phui-profile-menu .phui-list-item-selected.phui-profile-menu-footer
.phui-icon-circle {
border-color: {$menu.profile.text.selected};
}
.phui-profile-menu .phui-profile-menu-footer .phui-list-item-href:hover
.phui-icon-circle .phui-icon-view,
.phui-profile-menu .phui-list-item-selected.phui-profile-menu-footer
.phui-icon-circle .phui-icon-view {
color: {$menu.profile.text.selected};
color: {$darkbluetext};
}
.phui-profile-menu .phui-profile-menu-footer

View file

@ -24,14 +24,22 @@ JX.behavior('aphlict-status', function(config) {
}
var status = client.getStatus();
var icon = config.icon[status];
var status_node = JX.$N(
'span',
{
className: 'aphlict-connection-status-' + status
className: 'connection-status-text aphlict-connection-status-' + status
},
pht(status));
JX.DOM.setContent(node, status_node);
var icon_node = new JX.PHUIXIconView()
.setIcon(icon['icon'])
.setColor(icon['color'])
.getNode();
var content = [icon_node, ' ', status_node];
JX.DOM.setContent(node, content);
}
JX.Aphlict.listen('didChangeStatus', update);

View file

@ -66,6 +66,11 @@ JX.behavior('durable-column', function(config, statics) {
JX.DOM.alterClass(document.body, 'minimize-column', userMinimize);
JX.Stratcom.invoke('resize');
if (!userMinimize) {
var messages = _getColumnMessagesNode();
scrollbar.scrollTo(messages.scrollHeight);
}
new JX.Request(config.minimizeURI)
.setData({value: (userMinimize ? 1 : 0)})
.send();
@ -89,9 +94,10 @@ JX.behavior('durable-column', function(config, statics) {
JX.Stratcom.invoke('resize');
}
new JX.KeyboardShortcut('\\', 'Toggle Conpherence Column')
.setHandler(_toggleColumn)
.register();
JX.Stratcom.listen(
'click',
'conpherence-persist-column',
_toggleColumn);
JX.Stratcom.listen(
'click',
@ -346,6 +352,11 @@ JX.behavior('durable-column', function(config, statics) {
null,
function (e) {
var new_data = e.getData().newResponse;
var new_classes = new_data.bodyClasses;
if (userMinimize) {
new_classes = new_classes + ' minimize-column';
}
document.body.className = new_classes;
JX.Title.setTitle(new_data.title);
});

View file

@ -1,82 +0,0 @@
/**
* @provides javelin-behavior-aphront-crop
* @requires javelin-behavior
* javelin-dom
* javelin-vector
* javelin-magical-init
*/
JX.behavior('aphront-crop', function(config) {
var dragging = false;
var startX, startY;
var finalX, finalY;
var cropBox = JX.$(config.cropBoxID);
var basePos = JX.$V(cropBox);
cropBox.style.height = config.height + 'px';
cropBox.style.width = config.width + 'px';
var baseD = JX.$V(config.width, config.height);
var image = JX.DOM.find(cropBox, 'img', 'crop-image');
image.style.height = (config.imageH * config.scale) + 'px';
image.style.width = (config.imageW * config.scale) + 'px';
var imageD = JX.$V(
config.imageW * config.scale,
config.imageH * config.scale
);
var minLeft = baseD.x - imageD.x;
var minTop = baseD.y - imageD.y;
var ondrag = function(e) {
e.kill();
dragging = true;
var p = JX.$V(e);
startX = p.x;
startY = p.y;
};
var onmove = function(e) {
if (!dragging) {
return;
}
e.kill();
var p = JX.$V(e);
var dx = startX - p.x;
var dy = startY - p.y;
var imagePos = JX.$V(image);
var moveLeft = imagePos.x - basePos.x - dx;
var moveTop = imagePos.y - basePos.y - dy;
image.style.left = Math.min(Math.max(minLeft, moveLeft), 0) + 'px';
image.style.top = Math.min(Math.max(minTop, moveTop), 0) + 'px';
// reset these; a new beginning!
startX = p.x;
startY = p.y;
// save off where we are right now
imagePos = JX.$V(image);
finalX = Math.abs(imagePos.x - basePos.x);
finalY = Math.abs(imagePos.y - basePos.y);
JX.DOM.find(cropBox, 'input', 'crop-x').value = finalX;
JX.DOM.find(cropBox, 'input', 'crop-y').value = finalY;
};
var ondrop = function() {
if (!dragging) {
return;
}
dragging = false;
};
// NOTE: Javelin does not dispatch mousemove by default.
JX.enableDispatch(cropBox, 'mousemove');
JX.DOM.listen(cropBox, 'mousedown', [], ondrag);
JX.DOM.listen(cropBox, 'mousemove', [], onmove);
JX.DOM.listen(cropBox, 'mouseup', [], ondrop);
JX.DOM.listen(cropBox, 'mouseout', [], ondrop);
});