Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
index 6ecc9376d7..fc4161d6cf 100644
--- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
@@ -1,621 +1,623 @@
<?php
final class PhabricatorCalendarEventSearchEngine
extends PhabricatorApplicationSearchEngine {
private $calendarYear;
private $calendarMonth;
private $calendarDay;
public function getResultTypeDescription() {
return pht('Calendar Events');
}
public function getApplicationClassName() {
return 'PhabricatorCalendarApplication';
}
public function newQuery() {
$viewer = $this->requireViewer();
return id(new PhabricatorCalendarEventQuery())
->needRSVPs(array($viewer->getPHID()));
}
protected function shouldShowOrderField() {
return false;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Hosts'))
->setKey('hostPHIDs')
->setAliases(array('host', 'hostPHID', 'hosts'))
->setDatasource(new PhabricatorPeopleUserFunctionDatasource()),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Invited'))
->setKey('invitedPHIDs')
->setDatasource(new PhabricatorCalendarInviteeDatasource()),
id(new PhabricatorSearchDateControlField())
->setLabel(pht('Occurs After'))
->setKey('rangeStart'),
id(new PhabricatorSearchDateControlField())
->setLabel(pht('Occurs Before'))
->setKey('rangeEnd')
->setAliases(array('rangeEnd')),
id(new PhabricatorSearchCheckboxesField())
->setKey('upcoming')
->setOptions(array(
'upcoming' => pht('Show only upcoming events.'),
)),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Cancelled Events'))
->setKey('isCancelled')
->setOptions($this->getCancelledOptions())
->setDefault('active'),
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Import Sources'))
->setKey('importSourcePHIDs')
->setAliases(array('importSourcePHID')),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Display Options'))
->setKey('display')
->setOptions($this->getViewOptions())
->setDefault('month'),
);
}
private function getCancelledOptions() {
return array(
'active' => pht('Active Events Only'),
'cancelled' => pht('Cancelled Events Only'),
'both' => pht('Both Cancelled and Active Events'),
);
}
private function getViewOptions() {
return array(
'month' => pht('Month View'),
'day' => pht('Day View'),
'list' => pht('List View'),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
$viewer = $this->requireViewer();
if ($map['hostPHIDs']) {
$query->withHostPHIDs($map['hostPHIDs']);
}
if ($map['invitedPHIDs']) {
$query->withInvitedPHIDs($map['invitedPHIDs']);
}
$range_start = $map['rangeStart'];
$range_end = $map['rangeEnd'];
$display = $map['display'];
if ($map['upcoming'] && $map['upcoming'][0] == 'upcoming') {
$upcoming = true;
} else {
$upcoming = false;
}
list($range_start, $range_end) = $this->getQueryDateRange(
$range_start,
$range_end,
$display,
$upcoming);
$query->withDateRange($range_start, $range_end);
switch ($map['isCancelled']) {
case 'active':
$query->withIsCancelled(false);
break;
case 'cancelled':
$query->withIsCancelled(true);
break;
}
if ($map['importSourcePHIDs']) {
$query->withImportSourcePHIDs($map['importSourcePHIDs']);
}
// Generate ghosts (and ignore stub events) if we aren't querying for
// 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);
}
return $query;
}
private function getQueryDateRange(
$start_date_wild,
$end_date_wild,
$display,
$upcoming) {
$start_date_value = $this->getSafeDate($start_date_wild);
$end_date_value = $this->getSafeDate($end_date_wild);
$viewer = $this->requireViewer();
$timezone = new DateTimeZone($viewer->getTimezoneIdentifier());
$min_range = null;
$max_range = null;
$min_range = $start_date_value->getEpoch();
$max_range = $end_date_value->getEpoch();
if ($display == 'month' || $display == 'day') {
list($start_year, $start_month, $start_day) =
$this->getDisplayYearAndMonthAndDay($min_range, $max_range, $display);
$start_day = new DateTime(
"{$start_year}-{$start_month}-{$start_day}",
$timezone);
$next = clone $start_day;
if ($display == 'month') {
$next->modify('+1 month');
} else if ($display == 'day') {
$next->modify('+7 day');
}
$display_start = $start_day->format('U');
$display_end = $next->format('U');
$start_of_week = $viewer->getUserSetting(
PhabricatorWeekStartDaySetting::SETTINGKEY);
$end_of_week = ($start_of_week + 6) % 7;
$first_of_month = $start_day->format('w');
$last_of_month = id(clone $next)->modify('-1 day')->format('w');
if (!$min_range || ($min_range < $display_start)) {
$min_range = $display_start;
if ($display == 'month' &&
$first_of_month !== $start_of_week) {
$interim_day_num = ($first_of_month + 7 - $start_of_week) % 7;
$min_range = id(clone $start_day)
->modify('-'.$interim_day_num.' days')
->format('U');
}
}
if (!$max_range || ($max_range > $display_end)) {
$max_range = $display_end;
if ($display == 'month' &&
$last_of_month !== $end_of_week) {
$interim_day_num = ($end_of_week + 7 - $last_of_month) % 7;
$max_range = id(clone $next)
->modify('+'.$interim_day_num.' days')
->format('U');
}
}
}
if ($upcoming) {
$now = PhabricatorTime::getNow();
if ($min_range) {
$min_range = max($now, $min_range);
} else {
$min_range = $now;
}
}
return array($min_range, $max_range);
}
protected function getURI($path) {
return '/calendar/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'month' => pht('Month View'),
'day' => pht('Day View'),
'upcoming' => pht('Upcoming Events'),
'all' => pht('All Events'),
);
return $names;
}
public function setCalendarYearAndMonthAndDay($year, $month, $day = null) {
$this->calendarYear = $year;
$this->calendarMonth = $month;
$this->calendarDay = $day;
return $this;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'month':
return $query->setParameter('display', 'month');
case 'day':
return $query->setParameter('display', 'day');
case 'upcoming':
return $query
->setParameter('display', 'list')
->setParameter('upcoming', array(
0 => 'upcoming',
));
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $events,
PhabricatorSavedQuery $query,
array $handles) {
if ($this->isMonthView($query)) {
$result = $this->buildCalendarMonthView($events, $query);
} else if ($this->isDayView($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();
foreach ($events as $event) {
if ($event->getIsGhostEvent()) {
$monogram = $event->getParentEvent()->getMonogram();
$index = $event->getSequenceIndex();
$monogram = "{$monogram}/{$index}";
} else {
$monogram = $event->getMonogram();
}
$item = id(new PHUIObjectItemView())
->setUser($viewer)
->setObject($event)
->setObjectName($monogram)
->setHeader($event->getName())
->setHref($event->getURI());
$item->addAttribute($event->renderEventDate($viewer, false));
if ($event->getIsCancelled()) {
$item->setDisabled(true);
}
$status_icon = $event->getDisplayIcon($viewer);
$status_color = $event->getDisplayIconColor($viewer);
$status_label = $event->getDisplayIconLabel($viewer);
$item->setStatusIcon("{$status_icon} {$status_color}", $status_label);
$host = pht(
'Hosted by %s',
$viewer->renderHandle($event->getHostPHID()));
$item->addByline($host);
$list->addItem($item);
}
return $this->newResultView()
->setObjectList($list)
->setNoDataString(pht('No events found.'));
}
private function buildCalendarMonthView(
array $events,
PhabricatorSavedQuery $query) {
assert_instances_of($events, 'PhabricatorCalendarEvent');
$viewer = $this->requireViewer();
$now = PhabricatorTime::getNow();
list($start_year, $start_month) =
$this->getDisplayYearAndMonthAndDay(
$this->getQueryDateFrom($query)->getEpoch(),
$this->getQueryDateTo($query)->getEpoch(),
$query->getParameter('display'));
$now_year = phabricator_format_local_time($now, $viewer, 'Y');
$now_month = phabricator_format_local_time($now, $viewer, 'm');
$now_day = phabricator_format_local_time($now, $viewer, 'j');
if ($start_month == $now_month && $start_year == $now_year) {
$month_view = new PHUICalendarMonthView(
$this->getQueryDateFrom($query),
$this->getQueryDateTo($query),
$start_month,
$start_year,
$now_day);
} else {
$month_view = new PHUICalendarMonthView(
$this->getQueryDateFrom($query),
$this->getQueryDateTo($query),
$start_month,
$start_year);
}
$month_view->setUser($viewer);
foreach ($events as $event) {
$epoch_min = $event->getStartDateTimeEpoch();
$epoch_max = $event->getEndDateTimeEpoch();
+ $is_invited = $event->isRSVPInvited($viewer->getPHID());
+
$event_view = id(new AphrontCalendarEventView())
->setHostPHID($event->getHostPHID())
->setEpochRange($epoch_min, $epoch_max)
->setIsCancelled($event->getIsCancelled())
->setName($event->getName())
->setURI($event->getURI())
->setIsAllDay($event->getIsAllDay())
->setIcon($event->getDisplayIcon($viewer))
- ->setViewerIsInvited($event->getIsUserInvited($viewer->getPHID()))
+ ->setViewerIsInvited($is_invited)
->setIconColor($event->getDisplayIconColor($viewer));
$month_view->addEvent($event_view);
}
$month_view->setBrowseURI(
$this->getURI('query/'.$query->getQueryKey().'/'));
$from = $this->getQueryDateFrom($query)->getDateTime();
$crumbs = array();
$crumbs[] = id(new PHUICrumbView())
->setName($from->format('F Y'));
$header = id(new PHUIHeaderView())
->setProfileHeader(true)
->setHeader($from->format('F Y'));
return $this->newResultView($month_view)
->setCrumbs($crumbs)
->setHeader($header);
}
private function buildCalendarDayView(
array $events,
PhabricatorSavedQuery $query) {
$viewer = $this->requireViewer();
list($start_year, $start_month, $start_day) =
$this->getDisplayYearAndMonthAndDay(
$this->getQueryDateFrom($query)->getEpoch(),
$this->getQueryDateTo($query)->getEpoch(),
$query->getParameter('display'));
$day_view = id(new PHUICalendarDayView(
$this->getQueryDateFrom($query),
$this->getQueryDateTo($query),
$start_year,
$start_month,
$start_day))
->setQuery($query->getQueryKey());
$day_view->setUser($viewer);
$phids = mpull($events, 'getHostPHID');
foreach ($events as $event) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$event,
PhabricatorPolicyCapability::CAN_EDIT);
$epoch_min = $event->getStartDateTimeEpoch();
$epoch_max = $event->getEndDateTimeEpoch();
$status_icon = $event->getDisplayIcon($viewer);
$status_color = $event->getDisplayIconColor($viewer);
$event_view = id(new AphrontCalendarEventView())
->setCanEdit($can_edit)
->setEventID($event->getID())
->setEpochRange($epoch_min, $epoch_max)
->setIsAllDay($event->getIsAllDay())
->setIcon($status_icon)
->setIconColor($status_color)
->setName($event->getName())
->setURI($event->getURI())
->setIsCancelled($event->getIsCancelled());
$day_view->addEvent($event_view);
}
$browse_uri = $this->getURI('query/'.$query->getQueryKey().'/');
$day_view->setBrowseURI($browse_uri);
$from = $this->getQueryDateFrom($query)->getDateTime();
$month_uri = $browse_uri.$from->format('Y/m/');
$crumbs = array(
id(new PHUICrumbView())
->setName($from->format('F Y'))
->setHref($month_uri),
id(new PHUICrumbView())
->setName($from->format('D jS')),
);
$header = id(new PHUIHeaderView())
->setProfileHeader(true)
->setHeader($from->format('D, F jS'));
return $this->newResultView($day_view)
->setCrumbs($crumbs)
->setHeader($header);
}
private function getDisplayYearAndMonthAndDay(
$range_start,
$range_end,
$display) {
$viewer = $this->requireViewer();
$epoch = null;
if ($this->calendarYear && $this->calendarMonth) {
$start_year = $this->calendarYear;
$start_month = $this->calendarMonth;
$start_day = $this->calendarDay ? $this->calendarDay : 1;
} else {
if ($range_start) {
$epoch = $range_start;
} else if ($range_end) {
$epoch = $range_end;
} else {
$epoch = time();
}
if ($display == 'month') {
$day = 1;
} else {
$day = phabricator_format_local_time($epoch, $viewer, 'd');
}
$start_year = phabricator_format_local_time($epoch, $viewer, 'Y');
$start_month = phabricator_format_local_time($epoch, $viewer, 'm');
$start_day = $day;
}
return array($start_year, $start_month, $start_day);
}
public function getPageSize(PhabricatorSavedQuery $saved) {
if ($this->isMonthView($saved) || $this->isDayView($saved)) {
return $saved->getParameter('limit', 1000);
} else {
return $saved->getParameter('limit', 100);
}
}
private function getQueryDateFrom(PhabricatorSavedQuery $saved) {
if ($this->calendarYear && $this->calendarMonth) {
$viewer = $this->requireViewer();
$start_year = $this->calendarYear;
$start_month = $this->calendarMonth;
$start_day = $this->calendarDay ? $this->calendarDay : 1;
return AphrontFormDateControlValue::newFromDictionary(
$viewer,
array(
'd' => "{$start_year}-{$start_month}-{$start_day}",
));
}
return $this->getQueryDate($saved, 'rangeStart');
}
private function getQueryDateTo(PhabricatorSavedQuery $saved) {
return $this->getQueryDate($saved, 'rangeEnd');
}
private function getQueryDate(PhabricatorSavedQuery $saved, $key) {
$viewer = $this->requireViewer();
$wild = $saved->getParameter($key);
return $this->getSafeDate($wild);
}
private function getSafeDate($value) {
$viewer = $this->requireViewer();
if ($value) {
// ideally this would be consistent and always pass in the same type
if ($value instanceof AphrontFormDateControlValue) {
return $value;
} else {
$value = AphrontFormDateControlValue::newFromWild($viewer, $value);
}
} else {
$value = AphrontFormDateControlValue::newFromEpoch(
$viewer,
PhabricatorTime::getTodayMidnightDateTime($viewer)->format('U'));
$value->setEnabled(false);
}
$value->setOptional(true);
return $value;
}
private function isMonthView(PhabricatorSavedQuery $query) {
if ($this->isDayView($query)) {
return false;
}
if ($query->getParameter('display') == 'month') {
return true;
}
}
private function isDayView(PhabricatorSavedQuery $query) {
if ($query->getParameter('display') == 'day') {
return true;
}
if ($this->calendarDay) {
return true;
}
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()),
);
}
private function newResultView($content = null) {
// If we aren't rendering a dashboard panel, activate global drag-and-drop
// so you can import ".ics" files by dropping them directly onto the
// calendar.
if (!$this->isPanelContext()) {
$drop_upload = id(new PhabricatorGlobalUploadTargetView())
->setViewer($this->requireViewer())
->setHintText("\xE2\x87\xAA ".pht('Drop .ics Files to Import'))
->setSubmitURI('/calendar/import/drop/')
->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
$content = array(
$drop_upload,
$content,
);
}
return id(new PhabricatorApplicationSearchResultView())
->setContent($content);
}
}
diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
index ca41fc8cf1..adcaa6f316 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
@@ -1,1437 +1,1427 @@
<?php
final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorPolicyCodexInterface,
PhabricatorProjectInterface,
PhabricatorMarkupInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorMentionableInterface,
PhabricatorFlaggableInterface,
PhabricatorSpacesInterface,
PhabricatorFulltextInterface,
PhabricatorConduitResultInterface {
protected $name;
protected $hostPHID;
protected $description;
protected $isCancelled;
protected $isAllDay;
protected $icon;
protected $mailKey;
protected $isStub;
protected $isRecurring = 0;
protected $seriesParentPHID;
protected $instanceOfEventPHID;
protected $sequenceIndex;
protected $viewPolicy;
protected $editPolicy;
protected $spacePHID;
protected $utcInitialEpoch;
protected $utcUntilEpoch;
protected $utcInstanceEpoch;
protected $parameters = array();
protected $importAuthorPHID;
protected $importSourcePHID;
protected $importUIDIndex;
protected $importUID;
private $parentEvent = self::ATTACHABLE;
private $invitees = self::ATTACHABLE;
private $importSource = self::ATTACHABLE;
private $rsvps = self::ATTACHABLE;
private $viewerTimezone;
private $isGhostEvent = false;
private $stubInvitees;
public static function initializeNewCalendarEvent(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorCalendarApplication'))
->executeOne();
$view_default = PhabricatorCalendarEventDefaultViewCapability::CAPABILITY;
$edit_default = PhabricatorCalendarEventDefaultEditCapability::CAPABILITY;
$view_policy = $app->getPolicy($view_default);
$edit_policy = $app->getPolicy($edit_default);
$now = PhabricatorTime::getNow();
$default_icon = 'fa-calendar';
$datetime_defaults = self::newDefaultEventDateTimes(
$actor,
$now);
list($datetime_start, $datetime_end) = $datetime_defaults;
// When importing events from a context like "bin/calendar reload", we may
// be acting as the omnipotent user.
$host_phid = $actor->getPHID();
if (!$host_phid) {
$host_phid = $app->getPHID();
}
return id(new PhabricatorCalendarEvent())
->setDescription('')
->setHostPHID($host_phid)
->setIsCancelled(0)
->setIsAllDay(0)
->setIsStub(0)
->setIsRecurring(0)
->setIcon($default_icon)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID())
->attachInvitees(array())
->setStartDateTime($datetime_start)
->setEndDateTime($datetime_end)
->attachImportSource(null)
->applyViewerTimezone($actor);
}
public static function newDefaultEventDateTimes(
PhabricatorUser $viewer,
$now) {
$datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$now,
$viewer->getTimezoneIdentifier());
// Advance the time by an hour, then round downwards to the nearest hour.
// For example, if it is currently 3:25 PM, we suggest a default start time
// of 4 PM.
$datetime_start = $datetime_start
->newRelativeDateTime('PT1H')
->newAbsoluteDateTime();
$datetime_start->setMinute(0);
$datetime_start->setSecond(0);
// Default the end time to an hour after the start time.
$datetime_end = $datetime_start
->newRelativeDateTime('PT1H')
->newAbsoluteDateTime();
return array($datetime_start, $datetime_end);
}
private function newChild(
PhabricatorUser $actor,
$sequence,
PhutilCalendarDateTime $start = null) {
if (!$this->isParentEvent()) {
throw new Exception(
pht(
'Unable to generate a new child event for an event which is not '.
'a recurring parent event!'));
}
$series_phid = $this->getSeriesParentPHID();
if (!$series_phid) {
$series_phid = $this->getPHID();
}
$child = id(new self())
->setIsCancelled(0)
->setIsStub(0)
->setInstanceOfEventPHID($this->getPHID())
->setSeriesParentPHID($series_phid)
->setSequenceIndex($sequence)
->setIsRecurring(true)
->attachParentEvent($this)
->attachImportSource(null);
return $child->copyFromParent($actor, $start);
}
protected function readField($field) {
static $inherit = array(
'hostPHID' => true,
'isAllDay' => true,
'icon' => true,
'spacePHID' => true,
'viewPolicy' => true,
'editPolicy' => true,
'name' => true,
'description' => true,
'isCancelled' => true,
);
// Read these fields from the parent event instead of this event. For
// example, we want any changes to the parent event's name to apply to
// the child.
if (isset($inherit[$field])) {
if ($this->getIsStub()) {
// TODO: This should be unconditional, but the execution order of
// CalendarEventQuery and applyViewerTimezone() are currently odd.
if ($this->parentEvent !== self::ATTACHABLE) {
return $this->getParentEvent()->readField($field);
}
}
}
return parent::readField($field);
}
public function copyFromParent(
PhabricatorUser $actor,
PhutilCalendarDateTime $start = null) {
if (!$this->isChildEvent()) {
throw new Exception(
pht(
'Unable to copy from parent event: this is not a child event.'));
}
$parent = $this->getParentEvent();
$this
->setHostPHID($parent->getHostPHID())
->setIsAllDay($parent->getIsAllDay())
->setIcon($parent->getIcon())
->setSpacePHID($parent->getSpacePHID())
->setViewPolicy($parent->getViewPolicy())
->setEditPolicy($parent->getEditPolicy())
->setName($parent->getName())
->setDescription($parent->getDescription())
->setIsCancelled($parent->getIsCancelled());
if ($start) {
$start_datetime = $start;
} else {
$sequence = $this->getSequenceIndex();
$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
->setStartDateTime($start_datetime)
->setEndDateTime($end_datetime);
if ($parent->isImportedEvent()) {
$full_uid = $parent->getImportUID().'/'.$start_datetime->getEpoch();
// NOTE: We don't attach the import source because this gets called
// from CalendarEventQuery while building ghosts, before we've loaded
// and attached sources. Possibly this sequence should be flipped.
$this
->setImportAuthorPHID($parent->getImportAuthorPHID())
->setImportSourcePHID($parent->getImportSourcePHID())
->setImportUID($full_uid);
}
return $this;
}
public function isValidSequenceIndex(PhabricatorUser $viewer, $sequence) {
return (bool)$this->newSequenceIndexDateTime($sequence);
}
public function newSequenceIndexDateTime($sequence) {
$set = $this->newRecurrenceSet();
if (!$set) {
return null;
}
$limit = $sequence + 1;
$count = $this->getRecurrenceCount();
if ($count && ($count < $limit)) {
return null;
}
$instances = $set->getEventsBetween(
null,
$this->newUntilDateTime(),
$limit);
return idx($instances, $sequence, null);
}
public function newStub(PhabricatorUser $actor, $sequence) {
$stub = $this->newChild($actor, $sequence);
$stub->setIsStub(1);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$stub->save();
unset($unguarded);
$stub->applyViewerTimezone($actor);
return $stub;
}
public function newGhost(
PhabricatorUser $actor,
$sequence,
PhutilCalendarDateTime $start = null) {
$ghost = $this->newChild($actor, $sequence, $start);
$ghost
->setIsGhostEvent(true)
->makeEphemeral();
$ghost->applyViewerTimezone($actor);
return $ghost;
}
public function applyViewerTimezone(PhabricatorUser $viewer) {
$this->viewerTimezone = $viewer->getTimezoneIdentifier();
return $this;
}
public function getDuration() {
return ($this->getEndDateTimeEpoch() - $this->getStartDateTimeEpoch());
}
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);
// The "until" epoch is the last UTC epoch on which any instance of this
// event occurs. For infinitely recurring events, it is `null`.
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);
// 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() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$import_uid = $this->getImportUID();
if ($import_uid !== null) {
$index = PhabricatorHash::digestForIndex($import_uid);
} else {
$index = null;
}
$this->setImportUIDIndex($index);
$this->updateUTCEpochs();
return parent::save();
}
/**
* Get the event start epoch for evaluating invitee availability.
*
* When assessing availability, we pretend events start earlier than they
* really do. This allows us to mark users away for the entire duration of a
* series of back-to-back meetings, even if they don't strictly overlap.
*
* @return int Event start date for availability caches.
*/
public function getStartDateTimeEpochForCache() {
$epoch = $this->getStartDateTimeEpoch();
$window = phutil_units('15 minutes in seconds');
return ($epoch - $window);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text',
'description' => 'text',
'isCancelled' => 'bool',
'isAllDay' => 'bool',
'icon' => 'text32',
'mailKey' => 'bytes20',
'isRecurring' => 'bool',
'seriesParentPHID' => 'phid?',
'instanceOfEventPHID' => 'phid?',
'sequenceIndex' => 'uint32?',
'isStub' => 'bool',
'utcInitialEpoch' => 'epoch',
'utcUntilEpoch' => 'epoch?',
'utcInstanceEpoch' => 'epoch?',
'importAuthorPHID' => 'phid?',
'importSourcePHID' => 'phid?',
'importUIDIndex' => 'bytes12?',
'importUID' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_instance' => array(
'columns' => array('instanceOfEventPHID', 'sequenceIndex'),
'unique' => true,
),
'key_epoch' => array(
'columns' => array('utcInitialEpoch', 'utcUntilEpoch'),
),
'key_rdate' => array(
'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'),
'unique' => true,
),
'key_series' => array(
'columns' => array('seriesParentPHID', 'utcInitialEpoch'),
),
),
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorCalendarEventPHIDType::TYPECONST);
}
public function getMonogram() {
return 'E'.$this->getID();
}
public function getInvitees() {
if ($this->getIsGhostEvent() || $this->getIsStub()) {
if ($this->stubInvitees === null) {
$this->stubInvitees = $this->newStubInvitees();
}
return $this->stubInvitees;
}
return $this->assertAttached($this->invitees);
}
public function getInviteeForPHID($phid) {
$invitees = $this->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
return idx($invitees, $phid);
}
public static function getFrequencyMap() {
return array(
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array(
'label' => pht('Daily'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => array(
'label' => pht('Weekly'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => array(
'label' => pht('Monthly'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => array(
'label' => pht('Yearly'),
),
);
}
private function newStubInvitees() {
$parent = $this->getParentEvent();
$parent_invitees = $parent->getInvitees();
$stub_invitees = array();
foreach ($parent_invitees as $invitee) {
$stub_invitee = id(new PhabricatorCalendarEventInvitee())
->setInviteePHID($invitee->getInviteePHID())
->setInviterPHID($invitee->getInviterPHID())
->setStatus(PhabricatorCalendarEventInvitee::STATUS_INVITED);
$stub_invitees[] = $stub_invitee;
}
return $stub_invitees;
}
public function attachInvitees(array $invitees) {
$this->invitees = $invitees;
return $this;
}
public function getInviteePHIDsForEdit() {
$invitees = array();
foreach ($this->getInvitees() as $invitee) {
if ($invitee->isUninvited()) {
continue;
}
$invitees[] = $invitee->getInviteePHID();
}
return $invitees;
}
public function getUserInviteStatus($phid) {
$invitees = $this->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
$invited = idx($invitees, $phid);
if (!$invited) {
return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
}
$invited = $invited->getStatus();
return $invited;
}
public function getIsUserAttending($phid) {
$attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
$old_status = $this->getUserInviteStatus($phid);
$is_attending = ($old_status == $attending_status);
return $is_attending;
}
- public function getIsUserInvited($phid) {
- $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
- $declined_status = PhabricatorCalendarEventInvitee::STATUS_DECLINED;
- $status = $this->getUserInviteStatus($phid);
- if ($status == $uninvited_status || $status == $declined_status) {
- return false;
- }
- return true;
- }
-
public function getIsGhostEvent() {
return $this->isGhostEvent;
}
public function setIsGhostEvent($is_ghost_event) {
$this->isGhostEvent = $is_ghost_event;
return $this;
}
public function getURI() {
if ($this->getIsGhostEvent()) {
$base = $this->getParentEvent()->getURI();
$sequence = $this->getSequenceIndex();
return "{$base}/{$sequence}/";
}
return '/'.$this->getMonogram();
}
public function getParentEvent() {
return $this->assertAttached($this->parentEvent);
}
public function attachParentEvent(PhabricatorCalendarEvent $event = null) {
$this->parentEvent = $event;
return $this;
}
public function isParentEvent() {
return ($this->getIsRecurring() && !$this->getInstanceOfEventPHID());
}
public function isChildEvent() {
return ($this->instanceOfEventPHID !== null);
}
public function renderEventDate(
PhabricatorUser $viewer,
$show_end) {
$start = $this->newStartDateTime();
$end = $this->newEndDateTime();
$min_date = $start->newPHPDateTime();
$max_date = $end->newPHPDateTime();
if ($show_end) {
// Subtract one second since the stored date is exclusive.
$max_date = $max_date->modify('-1 second');
$min_day = $min_date->format('Y m d');
$max_day = $max_date->format('Y m d');
$show_end_date = ($min_day != $max_day);
} else {
$show_end_date = false;
}
$min_epoch = $min_date->format('U');
$max_epoch = $max_date->format('U');
if ($this->getIsAllDay()) {
if ($show_end_date) {
return pht(
'%s - %s, All Day',
phabricator_date($min_epoch, $viewer),
phabricator_date($max_epoch, $viewer));
} else {
return pht(
'%s, All Day',
phabricator_date($min_epoch, $viewer));
}
} else if ($show_end_date) {
return pht(
'%s - %s',
phabricator_datetime($min_epoch, $viewer),
phabricator_datetime($max_epoch, $viewer));
} else if ($show_end) {
return pht(
'%s - %s',
phabricator_datetime($min_epoch, $viewer),
phabricator_time($max_epoch, $viewer));
} else {
return pht(
'%s',
phabricator_datetime($min_epoch, $viewer));
}
}
public function getDisplayIcon(PhabricatorUser $viewer) {
if ($this->getIsCancelled()) {
return 'fa-times';
}
if ($viewer->isLoggedIn()) {
$viewer_phid = $viewer->getPHID();
if ($this->isRSVPInvited($viewer_phid)) {
return 'fa-users';
} else {
$status = $this->getUserInviteStatus($viewer_phid);
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return 'fa-check-circle';
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return 'fa-user-plus';
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
- return 'fa-times';
+ return 'fa-times-circle';
}
}
}
if ($this->isImportedEvent()) {
return 'fa-download';
}
return $this->getIcon();
}
public function getDisplayIconColor(PhabricatorUser $viewer) {
if ($this->getIsCancelled()) {
return 'red';
}
if ($this->isImportedEvent()) {
return 'orange';
}
if ($viewer->isLoggedIn()) {
$viewer_phid = $viewer->getPHID();
if ($this->isRSVPInvited($viewer_phid)) {
return 'green';
}
$status = $this->getUserInviteStatus($viewer_phid);
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return 'green';
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return 'green';
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return 'grey';
}
}
return 'bluegrey';
}
public function getDisplayIconLabel(PhabricatorUser $viewer) {
if ($this->getIsCancelled()) {
return pht('Cancelled');
}
if ($viewer->isLoggedIn()) {
$status = $this->getUserInviteStatus($viewer->getPHID());
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return pht('Attending');
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return pht('Invited');
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return pht('Declined');
}
}
return null;
}
public function getICSFilename() {
return $this->getMonogram().'.ics';
}
public function newIntermediateEventNode(
PhabricatorUser $viewer,
array $children) {
$base_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
$domain = $base_uri->getDomain();
// 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);
$modified = $this->getDateModified();
$modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified);
$date_start = $this->newStartDateTime();
$date_end = $this->newEndDateTime();
if ($this->getIsAllDay()) {
$date_start->setIsAllDay(true);
$date_end->setIsAllDay(true);
}
$host_phid = $this->getHostPHID();
$invitees = $this->getInvitees();
foreach ($invitees as $key => $invitee) {
if ($invitee->isUninvited()) {
unset($invitees[$key]);
}
}
$phids = array();
$phids[] = $host_phid;
foreach ($invitees as $invitee) {
$phids[] = $invitee->getInviteePHID();
}
$handles = $viewer->loadHandles($phids);
$host_handle = $handles[$host_phid];
$host_name = $host_handle->getFullName();
// NOTE: Gmail shows "Who: Unknown Organizer*" if the organizer URI does
// not look like an email address. Use a synthetic address so it shows
// the host name instead.
$install_uri = PhabricatorEnv::getProductionURI('/');
$install_uri = new PhutilURI($install_uri);
// This should possibly use "metamta.reply-handler-domain" instead, but
// we do not currently accept mail for users anyway, and that option may
// not be configured.
$mail_domain = $install_uri->getDomain();
$host_uri = "mailto:{$host_phid}@{$mail_domain}";
$organizer = id(new PhutilCalendarUserNode())
->setName($host_name)
->setURI($host_uri);
$attendees = array();
foreach ($invitees as $invitee) {
$invitee_phid = $invitee->getInviteePHID();
$invitee_handle = $handles[$invitee_phid];
$invitee_name = $invitee_handle->getFullName();
$invitee_uri = $invitee_handle->getURI();
$invitee_uri = PhabricatorEnv::getURI($invitee_uri);
switch ($invitee->getStatus()) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
$status = PhutilCalendarUserNode::STATUS_ACCEPTED;
break;
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
$status = PhutilCalendarUserNode::STATUS_DECLINED;
break;
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
default:
$status = PhutilCalendarUserNode::STATUS_INVITED;
break;
}
$attendees[] = id(new PhutilCalendarUserNode())
->setName($invitee_name)
->setURI($invitee_uri)
->setStatus($status);
}
// TODO: Use $children to generate EXDATE/RDATE information.
$node = id(new PhutilCalendarEventNode())
->setUID($uid)
->setName($this->getName())
->setDescription($this->getDescription())
->setCreatedDateTime($created)
->setModifiedDateTime($modified)
->setStartDateTime($date_start)
->setEndDateTime($date_end)
->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');
return $this->newDateTimeFromDictionary($datetime);
}
public function getStartDateTimeEpoch() {
return $this->newStartDateTime()->getEpoch();
}
public function newEndDateTimeForEdit() {
$datetime = $this->getParameter('endDateTime');
return $this->newDateTimeFromDictionary($datetime);
}
public function newEndDateTime() {
$datetime = $this->newEndDateTimeForEdit();
// If this is an all day event, we move the end date time forward to the
// first second of the following day. This is consistent with what users
// expect: an all day event from "Nov 1" to "Nov 1" lasts the entire day.
// For imported events, the end date is already stored with this
// adjustment.
if ($this->getIsAllDay() && !$this->isImportedEvent()) {
$datetime = $datetime
->newAbsoluteDateTime()
->setHour(0)
->setMinute(0)
->setSecond(0)
->newRelativeDateTime('P1D')
->newAbsoluteDateTime();
}
return $datetime;
}
public function getEndDateTimeEpoch() {
return $this->newEndDateTime()->getEpoch();
}
public function newUntilDateTime() {
$datetime = $this->getParameter('untilDateTime');
if ($datetime) {
return $this->newDateTimeFromDictionary($datetime);
}
return null;
}
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 = null) {
if ($datetime) {
$value = $datetime->newAbsoluteDateTime()->toDictionary();
} else {
$value = null;
}
return $this->setParameter('untilDateTime', $value);
}
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);
}
$count = $this->getRecurrenceCount();
if ($count) {
$rrule->setCount($count);
}
return $rrule;
}
public function getRecurrenceCount() {
$count = (int)$this->getParameter('recurrenceCount');
if (!$count) {
return null;
}
return $count;
}
public function newRecurrenceSet() {
if ($this->isChildEvent()) {
return $this->getParentEvent()->newRecurrenceSet();
}
$set = new PhutilCalendarRecurrenceSet();
$rrule = $this->newRecurrenceRule();
if (!$rrule) {
return null;
}
$set->addSource($rrule);
return $set;
}
public function isImportedEvent() {
return (bool)$this->getImportSourcePHID();
}
public function getImportSource() {
return $this->assertAttached($this->importSource);
}
public function attachImportSource(
PhabricatorCalendarImport $import = null) {
$this->importSource = $import;
return $this;
}
public function loadForkTarget(PhabricatorUser $viewer) {
if (!$this->getIsRecurring()) {
// Can't fork an event which isn't recurring.
return null;
}
if ($this->isChildEvent()) {
// If this is a child event, this is the fork target.
return $this;
}
if (!$this->isValidSequenceIndex($viewer, 1)) {
// This appears to be a "recurring" event with no valid instances: for
// example, its "until" date is before the second instance would occur.
// This can happen if we already forked the event or if users entered
// silly stuff. Just edit the event directly without forking anything.
return null;
}
$next_event = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withInstanceSequencePairs(
array(
array($this->getPHID(), 1),
))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$next_event) {
$next_event = $this->newStub($viewer, 1);
}
return $next_event;
}
public function loadFutureEvents(PhabricatorUser $viewer) {
// NOTE: If you can't edit some of the future events, we just
// don't try to update them. This seems like it's probably what
// users are likely to expect.
// NOTE: This only affects events that are currently in the same
// series, not all events that were ever in the original series.
// We could use series PHIDs instead of parent PHIDs to affect more
// events if this turns out to be counterintuitive. Other
// applications differ in their behavior.
return id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withParentEventPHIDs(array($this->getPHID()))
->withUTCInitialEpochBetween($this->getUTCInitialEpoch(), null)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
}
public function getNotificationPHIDs() {
$phids = array();
if ($this->getPHID()) {
$phids[] = $this->getPHID();
}
if ($this->getSeriesParentPHID()) {
$phids[] = $this->getSeriesParentPHID();
}
return $phids;
}
public function getRSVPs($phid) {
return $this->assertAttachedKey($this->rsvps, $phid);
}
public function attachRSVPs(array $rsvps) {
$this->rsvps = $rsvps;
return $this;
}
public function isRSVPInvited($phid) {
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
return ($this->getRSVPStatus($phid) == $status_invited);
}
public function hasRSVPAuthority($phid, $other_phid) {
foreach ($this->getRSVPs($phid) as $rsvp) {
if ($rsvp->getInviteePHID() == $other_phid) {
return true;
}
}
return false;
}
public function getRSVPStatus($phid) {
// Check for an individual invitee record first.
$invitees = $this->invitees;
$invitees = mpull($invitees, null, 'getInviteePHID');
$invitee = idx($invitees, $phid);
if ($invitee) {
return $invitee->getStatus();
}
// If we don't have one, try to find an invited status for the user's
// projects.
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
foreach ($this->getRSVPs($phid) as $rsvp) {
if ($rsvp->getStatus() == $status_invited) {
return $status_invited;
}
}
return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "calendar:T{$id}:{$field}:{$hash}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newCalendarMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->isImportedEvent()) {
return PhabricatorPolicies::POLICY_NOONE;
} else {
return $this->getEditPolicy();
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isImportedEvent()) {
return false;
}
// The host of an event can always view and edit it.
$user_phid = $this->getHostPHID();
if ($user_phid) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid == $user_phid) {
return true;
}
}
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$status = $this->getUserInviteStatus($viewer->getPHID());
if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED ||
$status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING ||
$status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) {
return true;
}
}
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$import_source = $this->getImportSource();
if ($import_source) {
$extended[] = array(
$import_source,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
public function newPolicyCodex() {
return new PhabricatorCalendarEventPolicyCodex();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorCalendarEventEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorCalendarEventTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getHostPHID());
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array($this->getHostPHID());
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorCalendarEventFulltextEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the event.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('string')
->setDescription(pht('The event description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isAllDay')
->setType('bool')
->setDescription(pht('True if the event is an all day event.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('startDateTime')
->setType('datetime')
->setDescription(pht('Start date and time of the event.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('endDateTime')
->setType('datetime')
->setDescription(pht('End date and time of the event.')),
);
}
public function getFieldValuesForConduit() {
$start_datetime = $this->newStartDateTime();
$end_datetime = $this->newEndDateTime();
return array(
'name' => $this->getName(),
'description' => $this->getDescription(),
'isAllDay' => (bool)$this->getIsAllDay(),
'startDateTime' => $this->getConduitDateTime($start_datetime),
'endDateTime' => $this->getConduitDateTime($end_datetime),
);
}
public function getConduitSearchAttachments() {
return array();
}
private function getConduitDateTime($datetime) {
if (!$datetime) {
return null;
}
$epoch = $datetime->getEpoch();
// TODO: Possibly pass the actual viewer in from the Conduit stuff, or
// retain it when setting the viewer timezone?
$viewer = id(new PhabricatorUser())
->overrideTimezoneIdentifier($this->viewerTimezone);
return array(
'epoch' => (int)$epoch,
'display' => array(
'default' => phabricator_datetime($epoch, $viewer),
),
'iso8601' => $datetime->getISO8601(),
'timezone' => $this->viewerTimezone,
);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php
index f04bc1c5fa..28e6e464a3 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php
@@ -1,376 +1,377 @@
<?php
final class PhabricatorPeopleProfileViewController
extends PhabricatorPeopleProfileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$username = $request->getURIData('username');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($username))
->needBadges(true)
->needProfileImage(true)
->needAvailability(true)
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$this->setUser($user);
$profile = $user->loadUserProfile();
$picture = $user->getProfileImageURI();
$profile_icon = PhabricatorPeopleIconSet::getIconIcon($profile->getIcon());
$profile_icon = id(new PHUIIconView())
->setIcon($profile_icon);
$profile_title = $profile->getDisplayTitle();
$header = id(new PHUIHeaderView())
->setHeader($user->getFullName())
->setSubheader(array($profile_icon, $profile_title))
->setImage($picture)
->setProfileHeader(true);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$user,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
$id = $user->getID();
$header->setImageEditURL($this->getApplicationURI("picture/{$id}/"));
}
$properties = $this->buildPropertyView($user);
$name = $user->getUsername();
$feed = $this->buildPeopleFeed($user, $viewer);
$feed = id(new PHUIObjectBoxView())
->setHeaderText(pht('Recent Activity'))
->addClass('project-view-feed')
->appendChild($feed);
$projects = $this->buildProjectsView($user);
$calendar = $this->buildCalendarDayView($user);
$badges = $this->buildBadgesView($user);
require_celerity_resource('project-view-css');
$home = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->setMainColumn(
array(
$properties,
$feed,
))
->setSideColumn(
array(
$projects,
$calendar,
$badges,
));
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorPeopleProfilePanelEngine::PANEL_PROFILE);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
return $this->newPage()
->setTitle($user->getUsername())
->setNavigation($nav)
->setCrumbs($crumbs)
->appendChild(
array(
$home,
));
}
private function buildPropertyView(
PhabricatorUser $user) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($user);
$field_list = PhabricatorCustomField::getObjectFields(
$user,
PhabricatorCustomField::ROLE_VIEW);
$field_list->appendFieldsToPropertyList($user, $viewer, $view);
if (!$view->hasAnyProperties()) {
return null;
}
$view = id(new PHUIObjectBoxView())
->appendChild($view)
->addClass('project-view-properties');
return $view;
}
private function buildProjectsView(
PhabricatorUser $user) {
$viewer = $this->getViewer();
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($user->getPHID()))
->needImages(true)
->withStatuses(
array(
PhabricatorProjectStatus::STATUS_ACTIVE,
))
->execute();
$header = id(new PHUIHeaderView())
->setHeader(pht('Projects'));
if (!empty($projects)) {
$limit = 5;
$render_phids = array_slice($projects, 0, $limit);
$list = id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($render_phids);
if (count($projects) > $limit) {
$header_text = pht(
'Projects (%s)',
phutil_count($projects));
$header = id(new PHUIHeaderView())
->setHeader($header_text)
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-list-ul')
->setText(pht('View All'))
->setHref('/project/?member='.$user->getPHID()));
}
} else {
$error = id(new PHUIBoxView())
->addClass('mlb')
->appendChild(pht('User does not belong to any projects.'));
$list = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild($error);
}
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($list)
->setBackground(PHUIObjectBoxView::GREY);
return $box;
}
private function buildCalendarDayView(PhabricatorUser $user) {
$viewer = $this->getViewer();
$class = 'PhabricatorCalendarApplication';
if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
return null;
}
$midnight = PhabricatorTime::getTodayMidnightDateTime($viewer);
$week_end = clone $midnight;
$week_end = $week_end->modify('+3 days');
$range_start = $midnight->format('U');
$range_end = $week_end->format('U');
$events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withDateRange($range_start, $range_end)
->withInvitedPHIDs(array($user->getPHID()))
->withIsCancelled(false)
+ ->needRSVPs(array($viewer->getPHID()))
->execute();
$event_views = array();
foreach ($events as $event) {
- $viewer_is_invited = $event->getIsUserInvited($viewer->getPHID());
+ $viewer_is_invited = $event->isRSVPInvited($viewer->getPHID());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$event,
PhabricatorPolicyCapability::CAN_EDIT);
$epoch_min = $event->getStartDateTimeEpoch();
$epoch_max = $event->getEndDateTimeEpoch();
$event_view = id(new AphrontCalendarEventView())
->setCanEdit($can_edit)
->setEventID($event->getID())
->setEpochRange($epoch_min, $epoch_max)
->setIsAllDay($event->getIsAllDay())
->setIcon($event->getIcon())
->setViewerIsInvited($viewer_is_invited)
->setName($event->getName())
->setURI($event->getURI());
$event_views[] = $event_view;
}
$event_views = msort($event_views, 'getEpochStart');
$day_view = id(new PHUICalendarWeekView())
->setViewer($viewer)
->setView('week')
->setEvents($event_views)
->setWeekLength(3)
->render();
$header = id(new PHUIHeaderView())
->setHeader(pht('Calendar'))
->setHref(
urisprintf(
'/calendar/?invited=%s#R',
$user->getUsername()));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($day_view)
->addClass('calendar-profile-box')
->setBackground(PHUIObjectBoxView::GREY);
return $box;
}
private function buildBadgesView(PhabricatorUser $user) {
$viewer = $this->getViewer();
$class = 'PhabricatorBadgesApplication';
if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
return null;
}
$awards = array();
$badges = array();
if ($user->getBadgePHIDs()) {
$awards = id(new PhabricatorBadgesAwardQuery())
->setViewer($viewer)
->withRecipientPHIDs(array($user->getPHID()))
->execute();
$awards = mpull($awards, null, 'getBadgePHID');
$badges = array();
foreach ($awards as $award) {
$badge = $award->getBadge();
if ($badge->getStatus() == PhabricatorBadgesBadge::STATUS_ACTIVE) {
$badges[$award->getBadgePHID()] = $badge;
}
}
}
if (count($badges)) {
$flex = new PHUIBadgeBoxView();
foreach ($badges as $badge) {
if ($badge) {
$awarder_info = array();
$award = idx($awards, $badge->getPHID(), null);
$awarder_phid = $award->getAwarderPHID();
$awarder_handle = $viewer->renderHandle($awarder_phid);
$awarder_info = pht(
'Awarded by %s',
$awarder_handle->render());
$item = id(new PHUIBadgeView())
->setIcon($badge->getIcon())
->setHeader($badge->getName())
->setSubhead($badge->getFlavor())
->setQuality($badge->getQuality())
->setHref($badge->getViewURI())
->addByLine($awarder_info);
$flex->addItem($item);
}
}
} else {
$error = id(new PHUIBoxView())
->addClass('mlb')
->appendChild(pht('User does not have any badges.'));
$flex = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild($error);
}
// Best option?
$badges = id(new PhabricatorBadgesQuery())
->setViewer($viewer)
->withStatuses(array(
PhabricatorBadgesBadge::STATUS_ACTIVE,
))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
$button = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-plus')
->setText(pht('Award'))
->setWorkflow(true)
->setHref('/badges/award/'.$user->getID().'/');
$can_award = false;
if (count($badges)) {
$can_award = true;
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Badges'));
if (count($badges)) {
$header->addActionLink($button);
}
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->addClass('project-view-badges')
->appendChild($flex)
->setBackground(PHUIObjectBoxView::GREY);
return $box;
}
private function buildPeopleFeed(
PhabricatorUser $user,
$viewer) {
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(
array(
$user->getPHID(),
));
$query->setLimit(100);
$query->setViewer($viewer);
$stories = $query->execute();
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($viewer);
$builder->setShowHovercards(true);
$builder->setNoDataString(pht('To begin on such a grand journey, '.
'requires but just a single step.'));
$view = $builder->buildView();
return $view->render();
}
}
diff --git a/src/docs/user/userguide/calendar.diviner b/src/docs/user/userguide/calendar.diviner
index dd0a31fe5d..5c09a753b7 100644
--- a/src/docs/user/userguide/calendar.diviner
+++ b/src/docs/user/userguide/calendar.diviner
@@ -1,94 +1,110 @@
@title Calendar User Guide
@group userguide
Guide to the Calendar application.
Overview
========
IMPORTANT: Calendar is a prototype application. See
@{article:User Guide: Prototype Applications}.
Calendar allows you to schedule parties and invite other users to party with
you. Everyone loves to party. Use Calendar primarily for partying.
Reminders
=========
Calendar sends reminder email before events occur. You will receive a reminder
if:
- you have marked yourself as **attending** the event;
- the event has not been cancelled; and
- the event was not imported from an external source.
Reminders are sent 15 minutes before events begin.
Availability
============
Across all applications, Phabricator shows a red dot next to usernames if the
user is currently attending an event. This provides a hint that they may be in
a meeting (or on vacation) and could take a while to get back to you about a
revision or task.
You can click through to a user's profile to see more details about their
availability.
+Status Icons
+============
+
+On the month and day views, Calendar shows an icon next to each event to
+indicate status. The icons are:
+
+ - {icon user-plus, color=green} **Invited, Individual**: You're personally
+ invited to the event.
+ - {icon users, color=green} **Invited, Group**: A project you are a member
+ of is invited to the event.
+ - {icon check-circle, color=green} **Attending**: You're attending the event.
+ - {icon times-circle, color=grey} **Declined**: You've declined the event.
+ - {icon times, color=red} **Cancelled**: The event has been cancelled.
+
+If you don't have any special relationship to the event and the event does not
+have any special status, an event-specific icon is shown instead.
Importing Events
================
You can import events from email and from other calendar applications
(like Google Calendar and Calendar.app) into Calendar. For a detailed
guide, see @{article:Calendar User Guide: Importing Events}.
Exporting Events
================
You can export events from Calendar to other applications by downloading
events as `.ics` files or configuring a calendar subscription.
Calendar also attaches `.ics` files containing event information when it sends
email. Most calendar applications can import these files.
For a detailed guide to exporting events, see
@{article:Calendar User Guide: Exporting Events}.
Recurring Events
================
To create a recurring event (like a weekly meeting), first create an event
normally, then select {nav Make Recurring} from the action menu and configure
how often the event should repeat.
**Monthly Events on the 29th, 30th or 31st**: If you configure an event to
repeat monthly and schedule the first instance on the 29th, 30th, or 31st of
the month, it can not occur on the same day every month because some months
do not have enough days.
Instead, these events are internally scheduled to occur relative to the end
of the month. For example, if you schedule a monthly event on the 30th of a
31 day month, it will occur on the second-to-last day of each following month.
**Complex RRULEs**: Calendar supports complex RRULEs internally (like events
that occur every-other Thursday in prime-numbered months) but does not
currently have a UI for scheduling events with complex rules.
Future versions of Calendar may improve support for complex scheduling by using
the UI. In some cases, a partial workaround is to schedule the event in another
application (which has more complex scheduling controls available) and then
import it into Calendar.
Next Steps
==========
Continue by:
- importing events with @{article:Calendar User Guide: Importing Events}; or
- exporting events with @{article:Calendar User Guide: Exporting Events}.
diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
index d89b5c8f71..291097a6c0 100644
--- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
+++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
@@ -1,1586 +1,1594 @@
<?php
final class PhabricatorUSEnglishTranslation
extends PhutilTranslation {
public function getLocaleCode() {
return 'en_US';
}
protected function getTranslations() {
return array(
'No daemon(s) with id(s) "%s" exist!' => array(
'No daemon with id %s exists!',
'No daemons with ids %s exist!',
),
'These %d configuration value(s) are related:' => array(
'This configuration value is related:',
'These configuration values are related:',
),
'%s Task(s)' => array('Task', 'Tasks'),
'%s ERROR(S)' => array('ERROR', 'ERRORS'),
'%d Error(s)' => array('%d Error', '%d Errors'),
'%d Warning(s)' => array('%d Warning', '%d Warnings'),
'%d Auto-Fix(es)' => array('%d Auto-Fix', '%d Auto-Fixes'),
'%d Advice(s)' => array('%d Advice', '%d Pieces of Advice'),
'%d Detail(s)' => array('%d Detail', '%d Details'),
'(%d line(s))' => array('(%d line)', '(%d lines)'),
'%d line(s)' => array('%d line', '%d lines'),
'%d path(s)' => array('%d path', '%d paths'),
'%d diff(s)' => array('%d diff', '%d diffs'),
'%s Answer(s)' => array('%s Answer', '%s Answers'),
'Show %d Comment(s)' => array('Show %d Comment', 'Show %d Comments'),
'%s DIFF LINK(S)' => array('DIFF LINK', 'DIFF LINKS'),
'You successfully created %d diff(s).' => array(
'You successfully created %d diff.',
'You successfully created %d diffs.',
),
'Diff creation failed; see body for %s error(s).' => array(
'Diff creation failed; see body for error.',
'Diff creation failed; see body for errors.',
),
'There are %d raw fact(s) in storage.' => array(
'There is %d raw fact in storage.',
'There are %d raw facts in storage.',
),
'There are %d aggregate fact(s) in storage.' => array(
'There is %d aggregate fact in storage.',
'There are %d aggregate facts in storage.',
),
'%s Commit(s) Awaiting Audit' => array(
'%s Commit Awaiting Audit',
'%s Commits Awaiting Audit',
),
'%s Problem Commit(s)' => array(
'%s Problem Commit',
'%s Problem Commits',
),
'%s Review(s) Blocking Others' => array(
'%s Review Blocking Others',
'%s Reviews Blocking Others',
),
'%s Review(s) Need Attention' => array(
'%s Review Needs Attention',
'%s Reviews Need Attention',
),
'%s Review(s) Waiting on Others' => array(
'%s Review Waiting on Others',
'%s Reviews Waiting on Others',
),
'%s Active Review(s)' => array(
'%s Active Review',
'%s Active Reviews',
),
'%s Flagged Object(s)' => array(
'%s Flagged Object',
'%s Flagged Objects',
),
'%s Object(s) Tracked' => array(
'%s Object Tracked',
'%s Objects Tracked',
),
'%s Assigned Task(s)' => array(
'%s Assigned Task',
'%s Assigned Tasks',
),
'Show %d Lint Message(s)' => array(
'Show %d Lint Message',
'Show %d Lint Messages',
),
'Hide %d Lint Message(s)' => array(
'Hide %d Lint Message',
'Hide %d Lint Messages',
),
'This is a binary file. It is %s byte(s) in length.' => array(
'This is a binary file. It is %s byte in length.',
'This is a binary file. It is %s bytes in length.',
),
'%s Action(s) Have No Effect' => array(
'Action Has No Effect',
'Actions Have No Effect',
),
'%s Action(s) With No Effect' => array(
'Action With No Effect',
'Actions With No Effect',
),
'Some of your %s action(s) have no effect:' => array(
'One of your actions has no effect:',
'Some of your actions have no effect:',
),
'Apply remaining %d action(s)?' => array(
'Apply remaining action?',
'Apply remaining actions?',
),
'Apply %d Other Action(s)' => array(
'Apply Remaining Action',
'Apply Remaining Actions',
),
'The %s action(s) you are taking have no effect:' => array(
'The action you are taking has no effect:',
'The actions you are taking have no effect:',
),
'%s edited member(s), added %d: %s; removed %d: %s.' =>
'%s edited members, added: %3$s; removed: %5$s.',
'%s added %s member(s): %s.' => array(
array(
'%s added a member: %3$s.',
'%s added members: %3$s.',
),
),
'%s removed %s member(s): %s.' => array(
array(
'%s removed a member: %3$s.',
'%s removed members: %3$s.',
),
),
'%s edited project(s), added %s: %s; removed %s: %s.' =>
'%s edited projects, added: %3$s; removed: %5$s.',
'%s added %s project(s): %s.' => array(
array(
'%s added a project: %3$s.',
'%s added projects: %3$s.',
),
),
'%s removed %s project(s): %s.' => array(
array(
'%s removed a project: %3$s.',
'%s removed projects: %3$s.',
),
),
'%s merged %s task(s): %s.' => array(
array(
'%s merged a task: %3$s.',
'%s merged tasks: %3$s.',
),
),
'%s merged %s task(s) %s into %s.' => array(
array(
'%s merged %3$s into %4$s.',
'%s merged tasks %3$s into %4$s.',
),
),
'%s added %s voting user(s): %s.' => array(
array(
'%s added a voting user: %3$s.',
'%s added voting users: %3$s.',
),
),
'%s removed %s voting user(s): %s.' => array(
array(
'%s removed a voting user: %3$s.',
'%s removed voting users: %3$s.',
),
),
'%s added %s subtask(s): %s.' => array(
array(
'%s added a subtask: %3$s.',
'%s added subtasks: %3$s.',
),
),
'%s added %s parent task(s): %s.' => array(
array(
'%s added a parent task: %3$s.',
'%s added parent tasks: %3$s.',
),
),
'%s removed %s subtask(s): %s.' => array(
array(
'%s removed a subtask: %3$s.',
'%s removed subtasks: %3$s.',
),
),
'%s removed %s parent task(s): %s.' => array(
array(
'%s removed a parent task: %3$s.',
'%s removed parent tasks: %3$s.',
),
),
'%s added %s subtask(s) for %s: %s.' => array(
array(
'%s added a subtask for %3$s: %4$s.',
'%s added subtasks for %3$s: %4$s.',
),
),
'%s added %s parent task(s) for %s: %s.' => array(
array(
'%s added a parent task for %3$s: %4$s.',
'%s added parent tasks for %3$s: %4$s.',
),
),
'%s removed %s subtask(s) for %s: %s.' => array(
array(
'%s removed a subtask for %3$s: %4$s.',
'%s removed subtasks for %3$s: %4$s.',
),
),
'%s removed %s parent task(s) for %s: %s.' => array(
array(
'%s removed a parent task for %3$s: %4$s.',
'%s removed parent tasks for %3$s: %4$s.',
),
),
'%s edited subtask(s), added %s: %s; removed %s: %s.' =>
'%s edited subtasks, added: %3$s; removed: %5$s.',
'%s edited subtask(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited subtasks for %s, added: %4$s; removed: %6$s.',
'%s edited parent task(s), added %s: %s; removed %s: %s.' =>
'%s edited parent tasks, added: %3$s; removed: %5$s.',
'%s edited parent task(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited parent tasks for %s, added: %4$s; removed: %6$s.',
'%s edited answer(s), added %s: %s; removed %d: %s.' =>
'%s edited answers, added: %3$s; removed: %5$s.',
'%s added %s answer(s): %s.' => array(
array(
'%s added an answer: %3$s.',
'%s added answers: %3$s.',
),
),
'%s removed %s answer(s): %s.' => array(
array(
'%s removed a answer: %3$s.',
'%s removed answers: %3$s.',
),
),
'%s edited question(s), added %s: %s; removed %s: %s.' =>
'%s edited questions, added: %3$s; removed: %5$s.',
'%s added %s question(s): %s.' => array(
array(
'%s added a question: %3$s.',
'%s added questions: %3$s.',
),
),
'%s removed %s question(s): %s.' => array(
array(
'%s removed a question: %3$s.',
'%s removed questions: %3$s.',
),
),
'%s edited mock(s), added %s: %s; removed %s: %s.' =>
'%s edited mocks, added: %3$s; removed: %5$s.',
'%s added %s mock(s): %s.' => array(
array(
'%s added a mock: %3$s.',
'%s added mocks: %3$s.',
),
),
'%s removed %s mock(s): %s.' => array(
array(
'%s removed a mock: %3$s.',
'%s removed mocks: %3$s.',
),
),
'%s added %s task(s): %s.' => array(
array(
'%s added a task: %3$s.',
'%s added tasks: %3$s.',
),
),
'%s removed %s task(s): %s.' => array(
array(
'%s removed a task: %3$s.',
'%s removed tasks: %3$s.',
),
),
'%s edited file(s), added %s: %s; removed %s: %s.' =>
'%s edited files, added: %3$s; removed: %5$s.',
'%s added %s file(s): %s.' => array(
array(
'%s added a file: %3$s.',
'%s added files: %3$s.',
),
),
'%s removed %s file(s): %s.' => array(
array(
'%s removed a file: %3$s.',
'%s removed files: %3$s.',
),
),
'%s edited contributor(s), added %s: %s; removed %s: %s.' =>
'%s edited contributors, added: %3$s; removed: %5$s.',
'%s added %s contributor(s): %s.' => array(
array(
'%s added a contributor: %3$s.',
'%s added contributors: %3$s.',
),
),
'%s removed %s contributor(s): %s.' => array(
array(
'%s removed a contributor: %3$s.',
'%s removed contributors: %3$s.',
),
),
'%s edited %s reviewer(s), added %s: %s; removed %s: %s.' =>
'%s edited reviewers, added: %4$s; removed: %6$s.',
'%s edited %s reviewer(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited reviewers for %3$s, added: %5$s; removed: %7$s.',
'%s added %s reviewer(s): %s.' => array(
array(
'%s added a reviewer: %3$s.',
'%s added reviewers: %3$s.',
),
),
'%s added %s reviewer(s) for %s: %s.' => array(
array(
'%s added a reviewer for %3$s: %4$s.',
'%s added reviewers for %3$s: %4$s.',
),
),
'%s removed %s reviewer(s): %s.' => array(
array(
'%s removed a reviewer: %3$s.',
'%s removed reviewers: %3$s.',
),
),
'%s removed %s reviewer(s) for %s: %s.' => array(
array(
'%s removed a reviewer for %3$s: %4$s.',
'%s removed reviewers for %3$s: %4$s.',
),
),
'%d other(s)' => array(
'1 other',
'%d others',
),
'%s edited subscriber(s), added %d: %s; removed %d: %s.' =>
'%s edited subscribers, added: %3$s; removed: %5$s.',
'%s added %d subscriber(s): %s.' => array(
array(
'%s added a subscriber: %3$s.',
'%s added subscribers: %3$s.',
),
),
'%s removed %d subscriber(s): %s.' => array(
array(
'%s removed a subscriber: %3$s.',
'%s removed subscribers: %3$s.',
),
),
'%s edited watcher(s), added %s: %s; removed %d: %s.' =>
'%s edited watchers, added: %3$s; removed: %5$s.',
'%s added %s watcher(s): %s.' => array(
array(
'%s added a watcher: %3$s.',
'%s added watchers: %3$s.',
),
),
'%s removed %s watcher(s): %s.' => array(
array(
'%s removed a watcher: %3$s.',
'%s removed watchers: %3$s.',
),
),
'%s edited participant(s), added %d: %s; removed %d: %s.' =>
'%s edited participants, added: %3$s; removed: %5$s.',
'%s added %d participant(s): %s.' => array(
array(
'%s added a participant: %3$s.',
'%s added participants: %3$s.',
),
),
'%s removed %d participant(s): %s.' => array(
array(
'%s removed a participant: %3$s.',
'%s removed participants: %3$s.',
),
),
'%s edited image(s), added %d: %s; removed %d: %s.' =>
'%s edited images, added: %3$s; removed: %5$s',
'%s added %d image(s): %s.' => array(
array(
'%s added an image: %3$s.',
'%s added images: %3$s.',
),
),
'%s removed %d image(s): %s.' => array(
array(
'%s removed an image: %3$s.',
'%s removed images: %3$s.',
),
),
'%s Line(s)' => array(
'%s Line',
'%s Lines',
),
'Indexing %d object(s) of type %s.' => array(
'Indexing %d object of type %s.',
'Indexing %d object of type %s.',
),
'Run these %d command(s):' => array(
'Run this command:',
'Run these commands:',
),
'Install these %d PHP extension(s):' => array(
'Install this PHP extension:',
'Install these PHP extensions:',
),
'The current Phabricator configuration has these %d value(s):' => array(
'The current Phabricator configuration has this value:',
'The current Phabricator configuration has these values:',
),
'The current MySQL configuration has these %d value(s):' => array(
'The current MySQL configuration has this value:',
'The current MySQL configuration has these values:',
),
'You can update these %d value(s) here:' => array(
'You can update this value here:',
'You can update these values here:',
),
'The current PHP configuration has these %d value(s):' => array(
'The current PHP configuration has this value:',
'The current PHP configuration has these values:',
),
'To update these %d value(s), edit your PHP configuration file.' => array(
'To update this %d value, edit your PHP configuration file.',
'To update these %d values, edit your PHP configuration file.',
),
'To update these %d value(s), edit your PHP configuration file, located '.
'here:' => array(
'To update this value, edit your PHP configuration file, located '.
'here:',
'To update these values, edit your PHP configuration file, located '.
'here:',
),
'PHP also loaded these %s configuration file(s):' => array(
'PHP also loaded this configuration file:',
'PHP also loaded these configuration files:',
),
'%s added %d inline comment(s).' => array(
array(
'%s added an inline comment.',
'%s added inline comments.',
),
),
'%s comment(s)' => array('%s comment', '%s comments'),
'%s rejection(s)' => array('%s rejection', '%s rejections'),
'%s update(s)' => array('%s update', '%s updates'),
'This configuration value is defined in these %d '.
'configuration source(s): %s.' => array(
'This configuration value is defined in this '.
'configuration source: %2$s.',
'This configuration value is defined in these %d '.
'configuration sources: %s.',
),
'%s Open Pull Request(s)' => array(
'%s Open Pull Request',
'%s Open Pull Requests',
),
'Stale (%s day(s))' => array(
'Stale (%s day)',
'Stale (%s days)',
),
'Old (%s day(s))' => array(
'Old (%s day)',
'Old (%s days)',
),
'%s Commit(s)' => array(
'%s Commit',
'%s Commits',
),
'%s attached %d file(s): %s.' => array(
array(
'%s attached a file: %3$s.',
'%s attached files: %3$s.',
),
),
'%s detached %d file(s): %s.' => array(
array(
'%s detached a file: %3$s.',
'%s detached files: %3$s.',
),
),
'%s changed file(s), attached %d: %s; detached %d: %s.' =>
'%s changed files, attached: %3$s; detached: %5$s.',
'%s added %s dependencie(s): %s.' => array(
array(
'%s added a dependency: %3$s.',
'%s added dependencies: %3$s.',
),
),
'%s added %s dependencie(s) for %s: %s.' => array(
array(
'%s added a dependency for %3$s: %4$s.',
'%s added dependencies for %3$s: %4$s.',
),
),
'%s removed %s dependencie(s): %s.' => array(
array(
'%s removed a dependency: %3$s.',
'%s removed dependencies: %3$s.',
),
),
'%s removed %s dependencie(s) for %s: %s.' => array(
array(
'%s removed a dependency for %3$s: %4$s.',
'%s removed dependencies for %3$s: %4$s.',
),
),
'%s edited dependencie(s), added %s: %s; removed %s: %s.' => array(
'%s edited dependencies, added: %3$s; removed: %5$s.',
),
'%s edited dependencie(s) for %s, added %s: %s; removed %s: %s.' => array(
'%s edited dependencies for %s, added: %3$s; removed: %5$s.',
),
'%s added %s dependent revision(s): %s.' => array(
array(
'%s added a dependent revision: %3$s.',
'%s added dependent revisions: %3$s.',
),
),
'%s added %s dependent revision(s) for %s: %s.' => array(
array(
'%s added a dependent revision for %3$s: %4$s.',
'%s added dependent revisions for %3$s: %4$s.',
),
),
'%s removed %s dependent revision(s): %s.' => array(
array(
'%s removed a dependent revision: %3$s.',
'%s removed dependent revisions: %3$s.',
),
),
'%s removed %s dependent revision(s) for %s: %s.' => array(
array(
'%s removed a dependent revision for %3$s: %4$s.',
'%s removed dependent revisions for %3$s: %4$s.',
),
),
'%s added %s commit(s): %s.' => array(
array(
'%s added a commit: %3$s.',
'%s added commits: %3$s.',
),
),
'%s removed %s commit(s): %s.' => array(
array(
'%s removed a commit: %3$s.',
'%s removed commits: %3$s.',
),
),
'%s edited commit(s), added %s: %s; removed %s: %s.' =>
'%s edited commits, added %3$s; removed %5$s.',
'%s added %s reverted commit(s): %s.' => array(
array(
'%s added a reverted commit: %3$s.',
'%s added reverted commits: %3$s.',
),
),
'%s removed %s reverted commit(s): %s.' => array(
array(
'%s removed a reverted commit: %3$s.',
'%s removed reverted commits: %3$s.',
),
),
'%s edited reverted commit(s), added %s: %s; removed %s: %s.' =>
'%s edited reverted commits, added %3$s; removed %5$s.',
'%s added %s reverted commit(s) for %s: %s.' => array(
array(
'%s added a reverted commit for %3$s: %4$s.',
'%s added reverted commits for %3$s: %4$s.',
),
),
'%s removed %s reverted commit(s) for %s: %s.' => array(
array(
'%s removed a reverted commit for %3$s: %4$s.',
'%s removed reverted commits for %3$s: %4$s.',
),
),
'%s edited reverted commit(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited reverted commits for %2$s, added %4$s; removed %6$s.',
'%s added %s reverting commit(s): %s.' => array(
array(
'%s added a reverting commit: %3$s.',
'%s added reverting commits: %3$s.',
),
),
'%s removed %s reverting commit(s): %s.' => array(
array(
'%s removed a reverting commit: %3$s.',
'%s removed reverting commits: %3$s.',
),
),
'%s edited reverting commit(s), added %s: %s; removed %s: %s.' =>
'%s edited reverting commits, added %3$s; removed %5$s.',
'%s added %s reverting commit(s) for %s: %s.' => array(
array(
'%s added a reverting commit for %3$s: %4$s.',
'%s added reverting commitsi for %3$s: %4$s.',
),
),
'%s removed %s reverting commit(s) for %s: %s.' => array(
array(
'%s removed a reverting commit for %3$s: %4$s.',
'%s removed reverting commits for %3$s: %4$s.',
),
),
'%s edited reverting commit(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited reverting commits for %s, added %4$s; removed %6$s.',
'%s changed project member(s), added %d: %s; removed %d: %s.' =>
'%s changed project members, added %3$s; removed %5$s.',
'%s added %d project member(s): %s.' => array(
array(
'%s added a member: %3$s.',
'%s added members: %3$s.',
),
),
'%s removed %d project member(s): %s.' => array(
array(
'%s removed a member: %3$s.',
'%s removed members: %3$s.',
),
),
'%s project hashtag(s) are already used by other projects: %s.' => array(
'Project hashtag "%2$s" is already used by another project.',
'Some project hashtags are already used by other projects: %2$s.',
),
'%s changed project hashtag(s), added %d: %s; removed %d: %s.' =>
'%s changed project hashtags, added %3$s; removed %5$s.',
'Hashtags must contain at least one letter or number. %s '.
'project hashtag(s) are invalid: %s.' => array(
'Hashtags must contain at least one letter or number. The '.
'hashtag "%2$s" is not valid.',
'Hashtags must contain at least one letter or number. These '.
'hashtags are invalid: %2$s.',
),
'%s added %d project hashtag(s): %s.' => array(
array(
'%s added a hashtag: %3$s.',
'%s added hashtags: %3$s.',
),
),
'%s removed %d project hashtag(s): %s.' => array(
array(
'%s removed a hashtag: %3$s.',
'%s removed hashtags: %3$s.',
),
),
'%s changed %s hashtag(s), added %d: %s; removed %d: %s.' =>
'%s changed hashtags for %s, added %4$s; removed %6$s.',
'%s added %d %s hashtag(s): %s.' => array(
array(
'%s added a hashtag to %3$s: %4$s.',
'%s added hashtags to %3$s: %4$s.',
),
),
'%s removed %d %s hashtag(s): %s.' => array(
array(
'%s removed a hashtag from %3$s: %4$s.',
'%s removed hashtags from %3$s: %4$s.',
),
),
'%d User(s) Need Approval' => array(
'%d User Needs Approval',
'%d Users Need Approval',
),
'%s, %s line(s)' => array(
array(
'%s, %s line',
'%s, %s lines',
),
),
'%s pushed %d commit(s) to %s.' => array(
array(
'%s pushed a commit to %3$s.',
'%s pushed %d commits to %s.',
),
),
'%s commit(s)' => array(
'1 commit',
'%s commits',
),
'%s removed %s JIRA issue(s): %s.' => array(
array(
'%s removed a JIRA issue: %3$s.',
'%s removed JIRA issues: %3$s.',
),
),
'%s added %s JIRA issue(s): %s.' => array(
array(
'%s added a JIRA issue: %3$s.',
'%s added JIRA issues: %3$s.',
),
),
'%s added %s required legal document(s): %s.' => array(
array(
'%s added a required legal document: %3$s.',
'%s added required legal documents: %3$s.',
),
),
'%s updated JIRA issue(s): added %s %s; removed %d %s.' =>
'%s updated JIRA issues: added %3$s; removed %5$s.',
'%s edited %s task(s), added %s: %s; removed %s: %s.' =>
'%s edited tasks, added %4$s; removed %6$s.',
'%s added %s task(s) to %s: %s.' => array(
array(
'%s added a task to %3$s: %4$s.',
'%s added tasks to %3$s: %4$s.',
),
),
'%s removed %s task(s) from %s: %s.' => array(
array(
'%s removed a task from %3$s: %4$s.',
'%s removed tasks from %3$s: %4$s.',
),
),
'%s edited %s task(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited tasks for %3$s, added: %5$s; removed %7$s.',
'%s edited %s commit(s), added %s: %s; removed %s: %s.' =>
'%s edited commits, added %4$s; removed %6$s.',
'%s added %s commit(s) to %s: %s.' => array(
array(
'%s added a commit to %3$s: %4$s.',
'%s added commits to %3$s: %4$s.',
),
),
'%s removed %s commit(s) from %s: %s.' => array(
array(
'%s removed a commit from %3$s: %4$s.',
'%s removed commits from %3$s: %4$s.',
),
),
'%s edited %s commit(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited commits for %3$s, added: %5$s; removed %7$s.',
'%s added %s revision(s): %s.' => array(
array(
'%s added a revision: %3$s.',
'%s added revisions: %3$s.',
),
),
'%s removed %s revision(s): %s.' => array(
array(
'%s removed a revision: %3$s.',
'%s removed revisions: %3$s.',
),
),
'%s edited %s revision(s), added %s: %s; removed %s: %s.' =>
'%s edited revisions, added %4$s; removed %6$s.',
'%s added %s revision(s) to %s: %s.' => array(
array(
'%s added a revision to %3$s: %4$s.',
'%s added revisions to %3$s: %4$s.',
),
),
'%s removed %s revision(s) from %s: %s.' => array(
array(
'%s removed a revision from %3$s: %4$s.',
'%s removed revisions from %3$s: %4$s.',
),
),
'%s edited %s revision(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited revisions for %3$s, added: %5$s; removed %7$s.',
'%s edited %s project(s), added %s: %s; removed %s: %s.' =>
'%s edited projects, added %4$s; removed %6$s.',
'%s added %s project(s) to %s: %s.' => array(
array(
'%s added a project to %3$s: %4$s.',
'%s added projects to %3$s: %4$s.',
),
),
'%s removed %s project(s) from %s: %s.' => array(
array(
'%s removed a project from %3$s: %4$s.',
'%s removed projects from %3$s: %4$s.',
),
),
'%s edited %s project(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited projects for %3$s, added: %5$s; removed %7$s.',
'%s added %s panel(s): %s.' => array(
array(
'%s added a panel: %3$s.',
'%s added panels: %3$s.',
),
),
'%s removed %s panel(s): %s.' => array(
array(
'%s removed a panel: %3$s.',
'%s removed panels: %3$s.',
),
),
'%s edited %s panel(s), added %s: %s; removed %s: %s.' =>
'%s edited panels, added %4$s; removed %6$s.',
'%s added %s dashboard(s): %s.' => array(
array(
'%s added a dashboard: %3$s.',
'%s added dashboards: %3$s.',
),
),
'%s removed %s dashboard(s): %s.' => array(
array(
'%s removed a dashboard: %3$s.',
'%s removed dashboards: %3$s.',
),
),
'%s edited %s dashboard(s), added %s: %s; removed %s: %s.' =>
'%s edited dashboards, added %4$s; removed %6$s.',
'%s added %s edge(s): %s.' => array(
array(
'%s added an edge: %3$s.',
'%s added edges: %3$s.',
),
),
'%s added %s edge(s) to %s: %s.' => array(
array(
'%s added an edge to %3$s: %4$s.',
'%s added edges to %3$s: %4$s.',
),
),
'%s removed %s edge(s): %s.' => array(
array(
'%s removed an edge: %3$s.',
'%s removed edges: %3$s.',
),
),
'%s removed %s edge(s) from %s: %s.' => array(
array(
'%s removed an edge from %3$s: %4$s.',
'%s removed edges from %3$s: %4$s.',
),
),
'%s edited edge(s), added %s: %s; removed %s: %s.' =>
'%s edited edges, added: %3$s; removed: %5$s.',
'%s edited %s edge(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited edges for %3$s, added: %5$s; removed %7$s.',
'%s added %s member(s) for %s: %s.' => array(
array(
'%s added a member for %3$s: %4$s.',
'%s added members for %3$s: %4$s.',
),
),
'%s removed %s member(s) for %s: %s.' => array(
array(
'%s removed a member for %3$s: %4$s.',
'%s removed members for %3$s: %4$s.',
),
),
'%s edited %s member(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited members for %3$s, added: %5$s; removed %7$s.',
'%d related link(s):' => array(
'Related link:',
'Related links:',
),
'You have %d unpaid invoice(s).' => array(
'You have an unpaid invoice.',
'You have unpaid invoices.',
),
'The configurations differ in the following %s way(s):' => array(
'The configurations differ:',
'The configurations differ in these ways:',
),
'Phabricator is configured with an email domain whitelist (in %s), so '.
'only users with a verified email address at one of these %s '.
'allowed domain(s) will be able to register an account: %s' => array(
array(
'Phabricator is configured with an email domain whitelist (in %s), '.
'so only users with a verified email address at %3$s will be '.
'allowed to register an account.',
'Phabricator is configured with an email domain whitelist (in %s), '.
'so only users with a verified email address at one of these '.
'allowed domains will be able to register an account: %3$s',
),
),
'Show First %d Line(s)' => array(
'Show First Line',
'Show First %d Lines',
),
"\xE2\x96\xB2 Show %d Line(s)" => array(
"\xE2\x96\xB2 Show Line",
"\xE2\x96\xB2 Show %d Lines",
),
'Show All %d Line(s)' => array(
'Show Line',
'Show All %d Lines',
),
"\xE2\x96\xBC Show %d Line(s)" => array(
"\xE2\x96\xBC Show Line",
"\xE2\x96\xBC Show %d Lines",
),
'Show Last %d Line(s)' => array(
'Show Last Line',
'Show Last %d Lines',
),
'%s marked %s inline comment(s) as done and %s inline comment(s) as '.
'not done.' => array(
array(
array(
'%s marked an inline comment as done and an inline comment '.
'as not done.',
'%s marked an inline comment as done and %3$s inline comments '.
'as not done.',
),
array(
'%s marked %s inline comments as done and an inline comment '.
'as not done.',
'%s marked %s inline comments as done and %s inline comments '.
'as done.',
),
),
),
'%s marked %s inline comment(s) as done.' => array(
array(
'%s marked an inline comment as done.',
'%s marked %s inline comments as done.',
),
),
'%s marked %s inline comment(s) as not done.' => array(
array(
'%s marked an inline comment as not done.',
'%s marked %s inline comments as not done.',
),
),
'These %s object(s) will be destroyed forever:' => array(
'This object will be destroyed forever:',
'These objects will be destroyed forever:',
),
'Are you absolutely certain you want to destroy these %s '.
'object(s)?' => array(
'Are you absolutely certain you want to destroy this object?',
'Are you absolutely certain you want to destroy these objects?',
),
'%s added %s owner(s): %s.' => array(
array(
'%s added an owner: %3$s.',
'%s added owners: %3$s.',
),
),
'%s removed %s owner(s): %s.' => array(
array(
'%s removed an owner: %3$s.',
'%s removed owners: %3$s.',
),
),
'%s changed %s package owner(s), added %s: %s; removed %s: %s.' => array(
'%s changed package owners, added: %4$s; removed: %6$s.',
),
'Found %s book(s).' => array(
'Found %s book.',
'Found %s books.',
),
'Found %s file(s)...' => array(
'Found %s file...',
'Found %s files...',
),
'Found %s file(s) in project.' => array(
'Found %s file in project.',
'Found %s files in project.',
),
'Found %s unatomized, uncached file(s).' => array(
'Found %s unatomized, uncached file.',
'Found %s unatomized, uncached files.',
),
'Found %s file(s) to atomize.' => array(
'Found %s file to atomize.',
'Found %s files to atomize.',
),
'Atomizing %s file(s).' => array(
'Atomizing %s file.',
'Atomizing %s files.',
),
'Creating %s document(s).' => array(
'Creating %s document.',
'Creating %s documents.',
),
'Deleting %s document(s).' => array(
'Deleting %s document.',
'Deleting %s documents.',
),
'Found %s obsolete atom(s) in graph.' => array(
'Found %s obsolete atom in graph.',
'Found %s obsolete atoms in graph.',
),
'Found %s new atom(s) in graph.' => array(
'Found %s new atom in graph.',
'Found %s new atoms in graph.',
),
'This call takes %s parameter(s), but only %s are documented.' => array(
array(
'This call takes %s parameter, but only %s is documented.',
'This call takes %s parameter, but only %s are documented.',
),
array(
'This call takes %s parameters, but only %s is documented.',
'This call takes %s parameters, but only %s are documented.',
),
),
'%s Passed Test(s)' => '%s Passed',
'%s Failed Test(s)' => '%s Failed',
'%s Skipped Test(s)' => '%s Skipped',
'%s Broken Test(s)' => '%s Broken',
'%s Unsound Test(s)' => '%s Unsound',
'%s Other Test(s)' => '%s Other',
'%s Bulk Task(s)' => array(
'%s Task',
'%s Tasks',
),
'%s added %s badge(s) for %s: %s.' => array(
array(
'%s added a badge for %s: %3$s.',
'%s added badges for %s: %3$s.',
),
),
'%s added %s badge(s): %s.' => array(
array(
'%s added a badge: %3$s.',
'%s added badges: %3$s.',
),
),
'%s awarded %s recipient(s) for %s: %s.' => array(
array(
'%s awarded %3$s to %4$s.',
'%s awarded %3$s to multiple recipients: %4$s.',
),
),
'%s awarded %s recipients(s): %s.' => array(
array(
'%s awarded a recipient: %3$s.',
'%s awarded multiple recipients: %3$s.',
),
),
'%s edited badge(s) for %s, added %s: %s; revoked %s: %s.' => array(
array(
'%s edited badges for %s, added %s: %s; revoked %s: %s.',
'%s edited badges for %s, added %s: %s; revoked %s: %s.',
),
),
'%s edited badge(s), added %s: %s; revoked %s: %s.' => array(
array(
'%s edited badges, added %s: %s; revoked %s: %s.',
'%s edited badges, added %s: %s; revoked %s: %s.',
),
),
'%s edited recipient(s) for %s, awarded %s: %s; revoked %s: %s.' => array(
array(
'%s edited recipients for %s, awarded %s: %s; revoked %s: %s.',
'%s edited recipients for %s, awarded %s: %s; revoked %s: %s.',
),
),
'%s edited recipient(s), awarded %s: %s; revoked %s: %s.' => array(
array(
'%s edited recipients, awarded %s: %s; revoked %s: %s.',
'%s edited recipients, awarded %s: %s; revoked %s: %s.',
),
),
'%s revoked %s badge(s) for %s: %s.' => array(
array(
'%s revoked a badge for %3$s: %4$s.',
'%s revoked multiple badges for %3$s: %4$s.',
),
),
'%s revoked %s badge(s): %s.' => array(
array(
'%s revoked a badge: %3$s.',
'%s revoked multiple badges: %3$s.',
),
),
'%s revoked %s recipient(s) for %s: %s.' => array(
array(
'%s revoked %3$s from %4$s.',
'%s revoked multiple recipients for %3$s: %4$s.',
),
),
'%s revoked %s recipients(s): %s.' => array(
array(
'%s revoked a recipient: %3$s.',
'%s revoked multiple recipients: %3$s.',
),
),
'%s automatically subscribed target(s) were not affected: %s.' => array(
'An automatically subscribed target was not affected: %2$s.',
'Automatically subscribed targets were not affected: %2$s.',
),
'Declined to resubscribe %s target(s) because they previously '.
'unsubscribed: %s.' => array(
'Delined to resubscribe a target because they previously '.
'unsubscribed: %2$s.',
'Declined to resubscribe targets because they previously '.
'unsubscribed: %2$s.',
),
'%s target(s) are not subscribed: %s.' => array(
'A target is not subscribed: %2$s.',
'Targets are not subscribed: %2$s.',
),
'%s target(s) are already subscribed: %s.' => array(
'A target is already subscribed: %2$s.',
'Targets are already subscribed: %2$s.',
),
'Added %s subscriber(s): %s.' => array(
'Added a subscriber: %2$s.',
'Added subscribers: %2$s.',
),
'Removed %s subscriber(s): %s.' => array(
'Removed a subscriber: %2$s.',
'Removed subscribers: %2$s.',
),
'Queued email to be delivered to %s target(s): %s.' => array(
'Queued email to be delivered to target: %2$s.',
'Queued email to be delivered to targets: %2$s.',
),
'Queued email to be delivered to %s target(s), ignoring their '.
'notification preferences: %s.' => array(
'Queued email to be delivered to target, ignoring notification '.
'preferences: %2$s.',
'Queued email to be delivered to targets, ignoring notification '.
'preferences: %2$s.',
),
'%s project(s) are not associated: %s.' => array(
'A project is not associated: %2$s.',
'Projects are not associated: %2$s.',
),
'%s project(s) are already associated: %s.' => array(
'A project is already associated: %2$s.',
'Projects are already associated: %2$s.',
),
'Added %s project(s): %s.' => array(
'Added a project: %2$s.',
'Added projects: %2$s.',
),
'Removed %s project(s): %s.' => array(
'Removed a project: %2$s.',
'Removed projects: %2$s.',
),
'Added %s reviewer(s): %s.' => array(
'Added a reviewer: %2$s.',
'Added reviewers: %2$s.',
),
'Added %s blocking reviewer(s): %s.' => array(
'Added a blocking reviewer: %2$s.',
'Added blocking reviewers: %2$s.',
),
'Required %s signature(s): %s.' => array(
'Required a signature: %2$s.',
'Required signatures: %2$s.',
),
'Started %s build(s): %s.' => array(
'Started a build: %2$s.',
'Started builds: %2$s.',
),
'Added %s auditor(s): %s.' => array(
'Added an auditor: %2$s.',
'Added auditors: %2$s.',
),
'%s target(s) do not have permission to see this object: %s.' => array(
'A target does not have permission to see this object: %2$s.',
'Targets do not have permission to see this object: %2$s.',
),
'This action has no effect on %s target(s): %s.' => array(
'This action has no effect on a target: %2$s.',
'This action has no effect on targets: %2$s.',
),
'Mail sent in the last %s day(s).' => array(
'Mail sent in the last day.',
'Mail sent in the last %s days.',
),
'%s Day(s)' => array(
'%s Day',
'%s Days',
),
'%s Day(s) Ago' => array(
'%s Day Ago',
'%s Days Ago',
),
'Setting retention policy for "%s" to %s day(s).' => array(
array(
'Setting retention policy for "%s" to one day.',
'Setting retention policy for "%s" to %s days.',
),
),
'Waiting %s second(s) for lease to activate.' => array(
'Waiting a second for lease to activate.',
'Waiting %s seconds for lease to activate.',
),
'%s changed %s automation blueprint(s), added %s: %s; removed %s: %s.' =>
'%s changed automation blueprints, added: %4$s; removed: %6$s.',
'%s added %s automation blueprint(s): %s.' => array(
array(
'%s added an automation blueprint: %3$s.',
'%s added automation blueprints: %3$s.',
),
),
'%s removed %s automation blueprint(s): %s.' => array(
array(
'%s removed an automation blueprint: %3$s.',
'%s removed automation blueprints: %3$s.',
),
),
'WARNING: There are %s unapproved authorization(s)!' => array(
'WARNING: There is an unapproved authorization!',
'WARNING: There are unapproved authorizations!',
),
'Found %s Open Resource(s)' => array(
'Found %s Open Resource',
'Found %s Open Resources',
),
'%s Open Resource(s) Remain' => array(
'%s Open Resource Remain',
'%s Open Resources Remain',
),
'Found %s Blueprint(s)' => array(
'Found %s Blueprint',
'Found %s Blueprints',
),
'%s Blueprint(s) Can Allocate' => array(
'%s Blueprint Can Allocate',
'%s Blueprints Can Allocate',
),
'%s Blueprint(s) Enabled' => array(
'%s Blueprint Enabled',
'%s Blueprints Enabled',
),
'%s Event(s)' => array(
'%s Event',
'%s Events',
),
'%s Unit(s)' => array(
'%s Unit',
'%s Units',
),
'QUEUEING TASKS (%s Commit(s)):' => array(
'QUEUEING TASKS (%s Commit):',
'QUEUEING TASKS (%s Commits):',
),
'Found %s total commit(s); updating...' => array(
'Found %s total commit; updating...',
'Found %s total commits; updating...',
),
'Not enough process slots to schedule the other %s '.
'repository(s) for updates yet.' => array(
'Not enough process slots to schedule the other '.'
repository for update yet.',
'Not enough process slots to schedule the other %s '.
'repositories for updates yet.',
),
'%s updated %s, added %d: %s.' =>
'%s updated %s, added: %4$s.',
'%s updated %s, removed %s: %s.' =>
'%s updated %s, removed: %4$s.',
'%s updated %s, added %s: %s; removed %s: %s.' =>
'%s updated %s, added: %4$s; removed: %6$s.',
'%s updated %s for %s, added %d: %s.' =>
'%s updated %s for %s, added: %5$s.',
'%s updated %s for %s, removed %s: %s.' =>
'%s updated %s for %s, removed: %5$s.',
'%s updated %s for %s, added %s: %s; removed %s: %s.' =>
'%s updated %s for %s, added: %5$s; removed; %7$s.',
'Permanently destroyed %s object(s).' => array(
'Permanently destroyed %s object.',
'Permanently destroyed %s objects.',
),
'%s added %s watcher(s) for %s: %s.' => array(
array(
'%s added a watcher for %3$s: %4$s.',
'%s added watchers for %3$s: %4$s.',
),
),
'%s removed %s watcher(s) for %s: %s.' => array(
array(
'%s removed a watcher for %3$s: %4$s.',
'%s removed watchers for %3$s: %4$s.',
),
),
'%s awarded this badge to %s recipient(s): %s.' => array(
array(
'%s awarded this badge to recipient: %3$s.',
'%s awarded this badge to recipients: %3$s.',
),
),
'%s revoked this badge from %s recipient(s): %s.' => array(
array(
'%s revoked this badge from recipient: %3$s.',
'%s revoked this badge from recipients: %3$s.',
),
),
'%s awarded %s to %s recipient(s): %s.' => array(
array(
array(
'%s awarded %s to recipient: %4$s.',
'%s awarded %s to recipients: %4$s.',
),
),
),
'%s revoked %s from %s recipient(s): %s.' => array(
array(
array(
'%s revoked %s from recipient: %4$s.',
'%s revoked %s from recipients: %4$s.',
),
),
),
'%s invited %s attendee(s): %s.' =>
'%s invited: %3$s.',
'%s uninvited %s attendee(s): %s.' =>
'%s uninvited: %3$s.',
'%s invited %s attendee(s): %s; uninvinted %s attendee(s): %s.' =>
'%s invited: %3$s; uninvited: %5$s.',
'%s invited %s attendee(s) to %s: %s.' =>
'%s added invites for %3$s: %4$s.',
'%s uninvited %s attendee(s) to %s: %s.' =>
'%s removed invites for %3$s: %4$s.',
'%s updated the invite list for %s, invited %s: %s; uninvinted %s: %s.' =>
'%s updated the invite list for %s, invited: %4$s; uninvited: %6$s.',
'Restart %s build(s)?' => array(
'Restart %s build?',
'Restart %s builds?',
),
+
+ '%s is starting in %s minute(s), at %s.' => array(
+ array(
+ '%s is starting in one minute, at %3$s.',
+ '%s is starting in %s minutes, at %s.',
+ ),
+ ),
+
);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jul 29, 9:01 AM (3 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
188406
Default Alt Text
(121 KB)

Event Timeline