diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 7added162f..4f65202a2c 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -1,499 +1,503 @@ setParameter( 'rangeStart', $this->readDateFromRequest($request, 'rangeStart')); $saved->setParameter( 'rangeEnd', $this->readDateFromRequest($request, 'rangeEnd')); $saved->setParameter( 'upcoming', $this->readBoolFromRequest($request, 'upcoming')); $saved->setParameter( 'invitedPHIDs', $this->readUsersFromRequest($request, 'invited')); $saved->setParameter( 'creatorPHIDs', $this->readUsersFromRequest($request, 'creators')); $saved->setParameter( 'isCancelled', $request->getStr('isCancelled')); $saved->setParameter( 'display', $request->getStr('display')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorCalendarEventQuery()); $viewer = $this->requireViewer(); $timezone = new DateTimeZone($viewer->getTimezoneIdentifier()); $min_range = $this->getDateFrom($saved)->getEpoch(); $max_range = $this->getDateTo($saved)->getEpoch(); if ($this->isMonthView($saved) || $this->isDayView($saved)) { list($start_year, $start_month, $start_day) = $this->getDisplayYearAndMonthAndDay($saved); $start_day = new DateTime( "{$start_year}-{$start_month}-{$start_day}", $timezone); $next = clone $start_day; if ($this->isMonthView($saved)) { $next->modify('+1 month'); } else if ($this->isDayView($saved)) { $next->modify('+6 day'); } $display_start = $start_day->format('U'); $display_end = $next->format('U'); - // 0 = Sunday is always the start of the week, for now - $start_of_week = 0; - $end_of_week = 6 - $start_of_week; + $preferences = $viewer->loadPreferences(); + $pref_week_day = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; + + $start_of_week = $preferences->getPreference($pref_week_day, 0); + $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 ($this->isMonthView($saved) && - $first_of_month > $start_of_week) { + $first_of_month !== $start_of_week) { + $interim_day_num = ($first_of_month + 7 - $start_of_week) % 7; $min_range = id(clone $start_day) - ->modify('-'.$first_of_month.' days') + ->modify('-'.$interim_day_num.' days') ->format('U'); } } if (!$max_range || ($max_range > $display_end)) { $max_range = $display_end; if ($this->isMonthView($saved) && - $last_of_month < $end_of_week) { + $last_of_month !== $end_of_week) { + $interim_day_num = ($end_of_week + 7 - $last_of_month) % 7; $max_range = id(clone $next) - ->modify('+'.(6 - $first_of_month).' days') + ->modify('+'.$interim_day_num.' days') ->format('U'); } } } if ($saved->getParameter('upcoming')) { if ($min_range) { $min_range = max(time(), $min_range); } else { $min_range = time(); } } if ($min_range || $max_range) { $query->withDateRange($min_range, $max_range); } $invited_phids = $saved->getParameter('invitedPHIDs'); if ($invited_phids) { $query->withInvitedPHIDs($invited_phids); } $creator_phids = $saved->getParameter('creatorPHIDs'); if ($creator_phids) { $query->withCreatorPHIDs($creator_phids); } $is_cancelled = $saved->getParameter('isCancelled'); switch ($is_cancelled) { case 'active': $query->withIsCancelled(false); break; case 'cancelled': $query->withIsCancelled(true); break; } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $range_start = $this->getDateFrom($saved); $e_start = null; $range_end = $this->getDateTo($saved); $e_end = null; if (!$range_start->isValid()) { $this->addError(pht('Start date is not valid.')); $e_start = pht('Invalid'); } if (!$range_end->isValid()) { $this->addError(pht('End date is not valid.')); $e_end = pht('Invalid'); } $start_epoch = $range_start->getEpoch(); $end_epoch = $range_end->getEpoch(); if ($start_epoch && $end_epoch && ($start_epoch > $end_epoch)) { $this->addError(pht('End date must be after start date.')); $e_start = pht('Invalid'); $e_end = pht('Invalid'); } $upcoming = $saved->getParameter('upcoming'); $is_cancelled = $saved->getParameter('isCancelled', 'active'); $display = $saved->getParameter('display', 'month'); $invited_phids = $saved->getParameter('invitedPHIDs', array()); $creator_phids = $saved->getParameter('creatorPHIDs', array()); $resolution_types = array( 'active' => pht('Active Events Only'), 'cancelled' => pht('Cancelled Events Only'), 'both' => pht('Both Cancelled and Active Events'), ); $display_options = array( 'month' => pht('Month View'), 'day' => pht('Day View (beta)'), 'list' => pht('List View'), ); $form ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('creators') ->setLabel(pht('Created By')) ->setValue($creator_phids)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('invited') ->setLabel(pht('Invited')) ->setValue($invited_phids)) ->appendChild( id(new AphrontFormDateControl()) ->setLabel(pht('Occurs After')) ->setUser($this->requireViewer()) ->setName('rangeStart') ->setError($e_start) ->setValue($range_start)) ->appendChild( id(new AphrontFormDateControl()) ->setLabel(pht('Occurs Before')) ->setUser($this->requireViewer()) ->setName('rangeEnd') ->setError($e_end) ->setValue($range_end)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'upcoming', 1, pht('Show only upcoming events.'), $upcoming)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Cancelled Events')) ->setName('isCancelled') ->setValue($is_cancelled) ->setOptions($resolution_types)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Display Options')) ->setName('display') ->setValue($display) ->setOptions($display_options)); } 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('upcoming', true); case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { $phids = array(); foreach ($objects as $event) { $phids[$event->getUserPHID()] = 1; } return array_keys($phids); } protected function renderResultList( array $events, PhabricatorSavedQuery $query, array $handles) { if ($this->isMonthView($query)) { return $this->buildCalendarView($events, $query, $handles); } else if ($this->isDayView($query)) { return $this->buildCalendarDayView($events, $query, $handles); } assert_instances_of($events, 'PhabricatorCalendarEvent'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); foreach ($events as $event) { $href = '/E'.$event->getID(); $from = phabricator_datetime($event->getDateFrom(), $viewer); $to = phabricator_datetime($event->getDateTo(), $viewer); $creator_handle = $handles[$event->getUserPHID()]; $item = id(new PHUIObjectItemView()) ->setHeader($event->getName()) ->setHref($href) ->addByline(pht('Creator: %s', $creator_handle->renderLink())) ->addAttribute(pht('From %s to %s', $from, $to)) ->addAttribute(id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(64) ->truncateString($event->getDescription())); $list->addItem($item); } return $list; } private function buildCalendarView( array $statuses, PhabricatorSavedQuery $query, array $handles) { $viewer = $this->requireViewer(); $now = time(); list($start_year, $start_month) = $this->getDisplayYearAndMonthAndDay($query); $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->getDateFrom($query), $this->getDateTo($query), $start_month, $start_year, $now_day); } else { $month_view = new PHUICalendarMonthView( $this->getDateFrom($query), $this->getDateTo($query), $start_month, $start_year); } $month_view->setUser($viewer); $phids = mpull($statuses, 'getUserPHID'); foreach ($statuses as $status) { $viewer_is_invited = $status->getIsUserInvited($viewer->getPHID()); $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $event->setIsAllDay($status->getIsAllDay()); $name_text = $handles[$status->getUserPHID()]->getName(); $status_text = $status->getName(); $event->setUserPHID($status->getUserPHID()); $event->setDescription(pht('%s (%s)', $name_text, $status_text)); $event->setName($status_text); $event->setEventID($status->getID()); $event->setViewerIsInvited($viewer_is_invited); $month_view->addEvent($event); } $month_view->setBrowseURI( $this->getURI('query/'.$query->getQueryKey().'/')); return $month_view; } private function buildCalendarDayView( array $statuses, PhabricatorSavedQuery $query, array $handles) { $viewer = $this->requireViewer(); list($start_year, $start_month, $start_day) = $this->getDisplayYearAndMonthAndDay($query); $day_view = new PHUICalendarDayView( $this->getDateFrom($query), $this->getDateTo($query), $start_year, $start_month, $start_day); $day_view->setUser($viewer); $phids = mpull($statuses, 'getUserPHID'); foreach ($statuses as $status) { if ($status->getIsCancelled()) { continue; } $viewer_is_invited = $status->getIsUserInvited($viewer->getPHID()); $event = new AphrontCalendarEventView(); $event->setEventID($status->getID()); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $event->setIsAllDay($status->getIsAllDay()); $event->setViewerIsInvited($viewer_is_invited); $event->setName($status->getName()); $event->setURI('/'.$status->getMonogram()); $day_view->addEvent($event); } $day_view->setBrowseURI( $this->getURI('query/'.$query->getQueryKey().'/')); return $day_view; } private function getDisplayYearAndMonthAndDay( PhabricatorSavedQuery $query) { $viewer = $this->requireViewer(); if ($this->calendarYear && $this->calendarMonth) { $start_year = $this->calendarYear; $start_month = $this->calendarMonth; $start_day = $this->calendarDay ? $this->calendarDay : 1; } else { $epoch = $this->getDateFrom($query)->getEpoch(); if (!$epoch) { $epoch = $this->getDateTo($query)->getEpoch(); if (!$epoch) { $epoch = time(); } } if ($this->isMonthView($query)) { $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) { return $saved->getParameter('limit', 1000); } private function getDateFrom(PhabricatorSavedQuery $saved) { return $this->getDate($saved, 'rangeStart'); } private function getDateTo(PhabricatorSavedQuery $saved) { return $this->getDate($saved, 'rangeEnd'); } private function getDate(PhabricatorSavedQuery $saved, $key) { $viewer = $this->requireViewer(); $wild = $saved->getParameter($key); if ($wild) { $value = AphrontFormDateControlValue::newFromWild($viewer, $wild); } 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; } } diff --git a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php index 661a1172b7..5fbf825d22 100644 --- a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php @@ -1,89 +1,115 @@ getUser(); $username = $user->getUsername(); $pref_time = PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT; + $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; $preferences = $user->loadPreferences(); $errors = array(); if ($request->isFormPost()) { $new_timezone = $request->getStr('timezone'); if (in_array($new_timezone, DateTimeZone::listIdentifiers(), true)) { $user->setTimezoneIdentifier($new_timezone); } else { $errors[] = pht('The selected timezone is not a valid timezone.'); } - $preferences->setPreference($pref_time, $request->getStr($pref_time)); + $preferences->setPreference( + $pref_time, + $request->getStr($pref_time)); + $preferences->setPreference( + $pref_week_start, + $request->getStr($pref_week_start)); if (!$errors) { $preferences->save(); $user->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } } $timezone_ids = DateTimeZone::listIdentifiers(); $timezone_id_map = array_fuse($timezone_ids); $form = new AphrontFormView(); $form ->setUser($user) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Timezone')) ->setName('timezone') ->setOptions($timezone_id_map) ->setValue($user->getTimezoneIdentifier())) ->appendRemarkupInstructions( pht( "**Custom Date and Time Formats**\n\n". "You can specify custom formats which will be used when ". "rendering dates and times of day. Examples:\n\n". "| Format | Example | Notes |\n". "| ------ | -------- | ----- |\n". "| `g:i A` | 2:34 PM | Default 12-hour time. |\n". "| `G.i a` | 02.34 pm | Alternate 12-hour time. |\n". "| `H:i` | 14:34 | 24-hour time. |\n". "\n\n". "You can find a [[%s | full reference in the PHP manual]].", 'http://www.php.net/manual/en/function.date.php')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Time-of-Day Format')) ->setName($pref_time) ->setCaption( pht('Format used when rendering a time of day.')) ->setValue($preferences->getPreference($pref_time))) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Week Starts On')) + ->setOptions($this->getWeekDays()) + ->setName($pref_week_start) + ->setCaption( + pht('Calendar weeks will start with this day.')) + ->setValue($preferences->getPreference($pref_week_start, 0))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Account Settings'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Date and Time Settings')) ->setFormSaved($request->getStr('saved')) ->setFormErrors($errors) ->setForm($form); return array( $form_box, ); } + + private function getWeekDays() { + return array( + pht('Sunday'), + pht('Monday'), + pht('Tuesday'), + pht('Wednesday'), + pht('Thursday'), + pht('Friday'), + pht('Saturday'), + ); + } } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index 3248dc8b2a..7732df5568 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -1,111 +1,112 @@ array( 'preferences' => self::SERIALIZATION_JSON, ), self::CONFIG_TIMESTAMPS => false, self::CONFIG_KEY_SCHEMA => array( 'userPHID' => array( 'columns' => array('userPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPreference($key, $default = null) { return idx($this->preferences, $key, $default); } public function setPreference($key, $value) { $this->preferences[$key] = $value; return $this; } public function unsetPreference($key) { unset($this->preferences[$key]); return $this; } public function getPinnedApplications(array $apps, PhabricatorUser $viewer) { $pref_pinned = self::PREFERENCE_APP_PINNED; $pinned = $this->getPreference($pref_pinned); if ($pinned) { return $pinned; } $pref_tiles = self::PREFERENCE_APP_TILES; $tiles = $this->getPreference($pref_tiles, array()); $full_tile = 'full'; $large = array(); foreach ($apps as $app) { $show = $app->isPinnedByDefault($viewer); // TODO: This is legacy stuff, clean it up eventually. This approximately // retains the old "tiles" preference. if (isset($tiles[get_class($app)])) { $show = ($tiles[get_class($app)] == $full_tile); } if ($show) { $large[] = get_class($app); } } return $large; } public static function filterMonospacedCSSRule($monospaced) { // Prevent the user from doing dangerous things. return preg_replace('/[^a-z0-9 ,".]+/i', '', $monospaced); } } diff --git a/src/view/phui/calendar/PHUICalendarMonthView.php b/src/view/phui/calendar/PHUICalendarMonthView.php index fd2ca4e458..d6bb7d1a32 100644 --- a/src/view/phui/calendar/PHUICalendarMonthView.php +++ b/src/view/phui/calendar/PHUICalendarMonthView.php @@ -1,545 +1,549 @@ browseURI = $browse_uri; return $this; } private function getBrowseURI() { return $this->browseURI; } public function addEvent(AphrontCalendarEventView $event) { $this->events[] = $event; return $this; } public function setImage($uri) { $this->image = $uri; return $this; } public function setInfoView(PHUIInfoView $error) { $this->error = $error; return $this; } public function __construct( $range_start, $range_end, $month, $year, $day = null) { $this->rangeStart = $range_start; $this->rangeEnd = $range_end; $this->day = $day; $this->month = $month; $this->year = $year; } public function render() { if (empty($this->user)) { throw new PhutilInvalidStateException('setUser'); } $events = msort($this->events, 'getEpochStart'); $days = $this->getDatesInMonth(); $cell_lists = array(); - $empty_cell = array( - 'list' => null, - 'date' => null, - 'uri' => null, - 'count' => 0, - 'class' => null, - ); require_celerity_resource('phui-calendar-month-css'); $first = reset($days); - $start_of_week = 0; - - $empty = $first->format('w'); - - for ($ii = 0; $ii < $empty; $ii++) { - $cell_lists[] = $empty_cell; - } foreach ($days as $day) { $day_number = $day->format('j'); $class = 'phui-calendar-month-day'; $weekday = $day->format('w'); $day->setTime(0, 0, 0); $day_start_epoch = $day->format('U'); $day_end_epoch = id(clone $day)->modify('+1 day')->format('U'); $list_events = array(); $all_day_events = array(); foreach ($events as $event) { if ($event->getEpochStart() >= $day_end_epoch) { break; } if ($event->getEpochStart() < $day_end_epoch && $event->getEpochEnd() > $day_start_epoch) { if ($event->getIsAllDay()) { $all_day_events[] = $event; } else { $list_events[] = $event; } } } $list = new PHUICalendarListView(); $list->setUser($this->user); foreach ($all_day_events as $item) { $list->addEvent($item); } foreach ($list_events as $item) { $list->addEvent($item); } $uri = $this->getBrowseURI(); $uri = $uri.$day->format('Y').'/'. $day->format('m').'/'. $day->format('d').'/'; $cell_lists[] = array( 'list' => $list, 'date' => $day, 'uri' => $uri, 'count' => count($all_day_events) + count($list_events), 'class' => $class, ); } $rows = array(); $cell_lists_by_week = array_chunk($cell_lists, 7); foreach ($cell_lists_by_week as $week_of_cell_lists) { $cells = array(); - while (count($week_of_cell_lists) < 7) { - $week_of_cell_lists[] = $empty_cell; - } foreach ($week_of_cell_lists as $cell_list) { $cells[] = $this->getEventListCell($cell_list); } $rows[] = phutil_tag('tr', array(), $cells); $cells = array(); foreach ($week_of_cell_lists as $cell_list) { $cells[] = $this->getDayNumberCell($cell_list); } $rows[] = phutil_tag('tr', array(), $cells); } $header = $this->getDayNamesHeader(); $table = phutil_tag( 'table', array('class' => 'phui-calendar-view'), array( $header, $rows, )); $warnings = $this->getQueryRangeWarning(); $box = id(new PHUIObjectBoxView()) ->setHeader($this->renderCalendarHeader($first)) ->appendChild($table) ->setFormErrors($warnings); if ($this->error) { $box->setInfoView($this->error); } return $box; } private function getEventListCell($event_list) { $list = $event_list['list']; $class = $event_list['class']; $uri = $event_list['uri']; $count = $event_list['count']; $viewer_is_invited = $list->getIsViewerInvitedOnList(); $event_count_badge = $this->getEventCountBadge($count, $viewer_is_invited); $cell_day_secret_link = $this->getHiddenDayLink($uri); $cell_data_div = phutil_tag( 'div', array( 'class' => 'phui-calendar-month-cell-div', ), array( $cell_day_secret_link, $event_count_badge, $list, )); return phutil_tag( 'td', array( 'class' => 'phui-calendar-month-event-list '.$class, ), $cell_data_div); } private function getDayNumberCell($event_list) { $class = $event_list['class']; $date = $event_list['date']; $cell_day_secret_link = null; if ($date) { $uri = $event_list['uri']; $cell_day_secret_link = $this->getHiddenDayLink($uri); $cell_day = phutil_tag( 'a', array( 'class' => 'phui-calendar-date-number', 'href' => $uri, ), $date->format('j')); } else { $cell_day = null; } if ($date && $date->format('j') == $this->day) { $today_class = 'phui-calendar-today-slot phui-calendar-today'; } else { $today_class = 'phui-calendar-today-slot'; } if ($this->isDateInCurrentWeek($date)) { $today_class .= ' phui-calendar-this-week'; } $last_week_day = 6; if ($date->format('w') == $last_week_day) { $today_class .= ' last-weekday'; } $today_slot = phutil_tag ( 'div', array( 'class' => $today_class, ), null); $cell_div = phutil_tag( 'div', array( 'class' => 'phui-calendar-month-cell-div', ), array( $cell_day_secret_link, $cell_day, $today_slot, )); return phutil_tag( 'td', array( 'class' => 'phui-calendar-date-number-container '.$class, ), $cell_div); } private function isDateInCurrentWeek($date) { list($week_start_date, $week_end_date) = $this->getThisWeekRange(); if ($date->format('U') < $week_end_date->format('U') && $date->format('U') >= $week_start_date->format('U')) { return true; } return false; } private function getEventCountBadge($count, $viewer_is_invited) { $class = 'phui-calendar-month-count-badge'; if ($viewer_is_invited) { $class = $class.' viewer-invited-day-badge'; } $event_count = null; if ($count > 0) { $event_count = phutil_tag( 'div', array( 'class' => $class, ), $count); } return phutil_tag( 'div', array( 'class' => 'phui-calendar-month-event-count', ), $event_count); } private function getHiddenDayLink($uri) { return phutil_tag( 'a', array( 'class' => 'phui-calendar-month-secret-link', 'href' => $uri, ), null); } private function getDayNamesHeader() { + list($week_start, $week_end) = $this->getWeekStartAndEnd(); + + $weekday_names = array( + $this->getDayHeader(pht('Sun'), pht('Sunday'), true), + $this->getDayHeader(pht('Mon'), pht('Monday')), + $this->getDayHeader(pht('Tue'), pht('Tuesday')), + $this->getDayHeader(pht('Wed'), pht('Wednesday')), + $this->getDayHeader(pht('Thu'), pht('Thursday')), + $this->getDayHeader(pht('Fri'), pht('Friday')), + $this->getDayHeader(pht('Sat'), pht('Saturday'), true), + ); + + $sorted_weekday_names = array(); + + for ($i = $week_start; $i < ($week_start + 7); $i++) { + $sorted_weekday_names[] = $weekday_names[$i % 7]; + } + return phutil_tag( 'tr', array('class' => 'phui-calendar-day-of-week-header'), - array( - $this->getDayHeader(pht('Sun'), pht('Sunday'), true), - $this->getDayHeader(pht('Mon'), pht('Monday')), - $this->getDayHeader(pht('Tue'), pht('Tuesday')), - $this->getDayHeader(pht('Wed'), pht('Wednesday')), - $this->getDayHeader(pht('Thu'), pht('Thursday')), - $this->getDayHeader(pht('Fri'), pht('Friday')), - $this->getDayHeader(pht('Sat'), pht('Saturday'), true), - )); + $sorted_weekday_names); } private function getDayHeader($short, $long, $is_weekend = false) { $class = null; if ($is_weekend) { $class = 'weekend-day-header'; } $day = array(); $day[] = phutil_tag( 'span', array( 'class' => 'long-weekday-name', ), $long); $day[] = phutil_tag( 'span', array( 'class' => 'short-weekday-name', ), $short); return phutil_tag( 'th', array( 'class' => $class, ), $day); } private function renderCalendarHeader(DateTime $date) { $button_bar = null; // check for a browseURI, which means we need "fancy" prev / next UI $uri = $this->getBrowseURI(); if ($uri) { list($prev_year, $prev_month) = $this->getPrevYearAndMonth(); $prev_uri = $uri.$prev_year.'/'.$prev_month.'/'; list($next_year, $next_month) = $this->getNextYearAndMonth(); $next_uri = $uri.$next_year.'/'.$next_month.'/'; $button_bar = new PHUIButtonBarView(); $left_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-left bluegrey'); $left = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($prev_uri) ->setTitle(pht('Previous Month')) ->setIcon($left_icon); $right_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-right bluegrey'); $right = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($next_uri) ->setTitle(pht('Next Month')) ->setIcon($right_icon); $button_bar->addButton($left); $button_bar->addButton($right); } $header = id(new PHUIHeaderView()) ->setHeader($date->format('F Y')); if ($button_bar) { $header->setButtonBar($button_bar); } if ($this->image) { $header->setImage($this->image); } return $header; } private function getQueryRangeWarning() { $errors = array(); $range_start_epoch = $this->rangeStart->getEpoch(); $range_end_epoch = $this->rangeEnd->getEpoch(); $month_start = $this->getDateTime(); $month_end = id(clone $month_start)->modify('+1 month'); $month_start = $month_start->format('U'); $month_end = $month_end->format('U') - 1; if (($range_start_epoch != null && $range_start_epoch < $month_end && $range_start_epoch > $month_start) || ($range_end_epoch != null && $range_end_epoch < $month_end && $range_end_epoch > $month_start)) { $errors[] = pht('Part of the month is out of range'); } if (($this->rangeEnd->getEpoch() != null && $this->rangeEnd->getEpoch() < $month_start) || ($this->rangeStart->getEpoch() != null && $this->rangeStart->getEpoch() > $month_end)) { $errors[] = pht('Month is out of query range'); } return $errors; } private function getNextYearAndMonth() { $next = $this->getDateTime(); $next->modify('+1 month'); return array( $next->format('Y'), $next->format('m'), ); } private function getPrevYearAndMonth() { $prev = $this->getDateTime(); $prev->modify('-1 month'); return array( $prev->format('Y'), $prev->format('m'), ); } /** * Return a DateTime object representing the first moment in each day in the * month, according to the user's locale. * * @return list List of DateTimes, one for each day. */ private function getDatesInMonth() { $user = $this->user; $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $month = $this->month; $year = $this->year; list($next_year, $next_month) = $this->getNextYearAndMonth(); $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone); - $start_of_week = 0; - $end_of_week = 6 - $start_of_week; + list($start_of_week, $end_of_week) = $this->getWeekStartAndEnd(); + $days_in_month = id(clone $end_date)->modify('-1 day')->format('d'); $first_month_day_date = new DateTime("{$year}-{$month}-01", $timezone); $last_month_day_date = id(clone $end_date)->modify('-1 day'); $first_weekday_of_month = $first_month_day_date->format('w'); $last_weekday_of_month = $last_month_day_date->format('w'); $num_days_display = $days_in_month; - if ($start_of_week < $first_weekday_of_month) { - $num_days_display += $first_weekday_of_month; + if ($start_of_week !== $first_weekday_of_month) { + $interim_start_num = ($first_weekday_of_month + 7 - $start_of_week) % 7; + $num_days_display += $interim_start_num; + $day_date = id(clone $first_month_day_date) + ->modify('-'.$interim_start_num.' days'); } - if ($end_of_week > $last_weekday_of_month) { - $num_days_display += (6 - $last_weekday_of_month); - $end_date->modify('+'.(6 - $last_weekday_of_month).' days'); + if ($end_of_week !== $last_weekday_of_month) { + $interim_end_day_num = ($end_of_week - $last_weekday_of_month + 7) % 7; + $num_days_display += $interim_end_day_num; + $end_date->modify('+'.$interim_end_day_num.' days'); } $days = array(); - $day_date = id(clone $first_month_day_date) - ->modify('-'.$first_weekday_of_month.' days'); for ($day = 1; $day <= $num_days_display; $day++) { $day_epoch = $day_date->format('U'); $end_epoch = $end_date->format('U'); if ($day_epoch >= $end_epoch) { break; } else { $days[] = clone $day_date; } $day_date->modify('+1 day'); } return $days; } private function getTodayMidnight() { $viewer = $this->getUser(); $today = new DateTime('@'.time()); $today->setTimeZone($viewer->getTimeZone()); $today->setTime(0, 0, 0); return $today; } private function getThisWeekRange() { - $week_start = 0; - $week_end = 6; + list($week_start, $week_end) = $this->getWeekStartAndEnd(); $today = $this->getTodayMidnight(); $date_weekday = $today->format('w'); - $days_from_week_start = $date_weekday - $week_start; - $days_to_week_end = $week_end - $date_weekday + 1; + $days_from_week_start = ($date_weekday + 7 - $week_start) % 7; + $days_to_week_end = 7 - $days_from_week_start; $modify = '-'.$days_from_week_start.' days'; $week_start_date = id(clone $today)->modify($modify); $modify = '+'.$days_to_week_end.' days'; $week_end_date = id(clone $today)->modify($modify); return array($week_start_date, $week_end_date); } + private function getWeekStartAndEnd() { + $preferences = $this->user->loadPreferences(); + $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; + + $week_start = $preferences->getPreference($pref_week_start, 0); + $week_end = ($week_start + 6) % 7; + + return array($week_start, $week_end); + } + private function getDateTime() { $user = $this->user; $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $month = $this->month; $year = $this->year; $date = new DateTime("{$year}-{$month}-01 ", $timezone); return $date; } }