Skip to content

Commit

Permalink
Display timezone on answers page (indico#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
Diego-Zulu authored Sep 9, 2020
1 parent bdf1567 commit d64b7d9
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 41 deletions.
18 changes: 15 additions & 3 deletions newdle/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@
from itsdangerous import BadData, SignatureExpired
from marshmallow import fields
from marshmallow.validate import OneOf
from pytz import common_timezones_set
from pytz import common_timezones_set, timezone
from sqlalchemy.orm import selectinload
from werkzeug.exceptions import Forbidden, ServiceUnavailable, UnprocessableEntity

from .core.auth import search_users, user_info_from_app_token
from .core.db import db
from .core.util import DATE_FORMAT, format_dt, range_union, sign_user
from .core.util import (
DATE_FORMAT,
change_dt_timezone,
format_dt,
range_union,
sign_user,
)
from .core.webargs import abort, use_args, use_kwargs
from .models import Newdle, Participant
from .notifications import notify_newdle_participants
Expand Down Expand Up @@ -187,7 +193,12 @@ def get_participant_busy_times(date, code, tz, participant_code=None):
).first_or_404('Specified participant does not exist')
if participant.auth_uid is None:
abort(422, messages={'participant_code': ['Participant is an unknown user']})
if not any(date == ts.date() for ts in participant.newdle.timeslots):
target_tz = timezone(tz)
newdle_tz = timezone(participant.newdle.timezone)
if not any(
date == change_dt_timezone(ts, newdle_tz, target_tz).date()
for ts in participant.newdle.timeslots
):
abort(422, messages={'date': ['Date has no timeslots']})
return _get_busy_times(date, tz, participant.auth_uid)

Expand All @@ -205,6 +216,7 @@ def _get_busy_times(date, tz, uid):
[
['{:02}:{:02}'.format(*r[0]), '{:02}:{:02}'.format(*r[1])]
for r in merged_ranges
if r[0] != r[1]
]
)

Expand Down
33 changes: 27 additions & 6 deletions newdle/client/src/answerSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import _ from 'lodash';
import moment from 'moment';
import {createSelector} from 'reselect';
import {overlaps, serializeDate, toMoment} from './util/date';
import {getUserTimezone} from './selectors';

export const getNewdle = state => state.answer.newdle;
export const getHandpickedAnswers = state => state.answer.answers;
Expand All @@ -18,13 +19,33 @@ export const getNumberOfTimeslots = createSelector(
getNewdleTimeslots,
slots => slots.length
);
export const getCalendarDates = createSelector(
export const getLocalNewdleTimeslots = createSelector(
getNewdleTimeslots,
getUserTimezone,
getNewdleTimezone,
(timeslots, userTz, newdleTz) =>
timeslots.map(timeslot =>
serializeDate(
toMoment(timeslot, moment.HTML5_FMT.DATETIME_LOCAL, newdleTz),
moment.HTML5_FMT.DATETIME_LOCAL,
userTz
)
)
);
/** A mapping from user-tz timeslots to newdle-tz timeslots */
const getLocalNewdleTimeslotsMap = createSelector(
getNewdleTimeslots,
getLocalNewdleTimeslots,
(timeslots, localTimeslots) => _.zipObject(localTimeslots, timeslots)
);
export const getCalendarDates = createSelector(
getLocalNewdleTimeslots,
timeslots =>
_.uniq(
timeslots.map(timeslot => serializeDate(toMoment(timeslot, moment.HTML5_FMT.DATETIME_LOCAL)))
)
);

export const getActiveDate = state =>
state.answer.calendarActiveDate || getCalendarDates(state)[0] || serializeDate(moment());

Expand Down Expand Up @@ -66,17 +87,17 @@ export const isAllAvailableSelectedExplicitly = state => state.answer.allAvailab
/** All time slots during which the person is free */
const getFreeTimeslots = createSelector(
getFlatBusyTimes,
getNewdleTimeslots,
getLocalNewdleTimeslotsMap,
getNewdleDuration,
(busyTimes, timeslots, duration) => {
(busyTimes, timeslotMap, duration) => {
busyTimes = busyTimes.map(pair => pair.map(t => toMoment(t, moment.HTML5_FMT.DATETIME_LOCAL)));
timeslots = timeslots.map(t => [
const localTimeslots = Object.keys(timeslotMap).map(t => [
toMoment(t, moment.HTML5_FMT.DATETIME_LOCAL),
toMoment(t, moment.HTML5_FMT.DATETIME_LOCAL).add(duration, 'm'),
]);
return timeslots
return localTimeslots
.filter(ts => !busyTimes.some(bt => overlaps(ts, bt)))
.map(([start]) => serializeDate(start, moment.HTML5_FMT.DATETIME_LOCAL))
.map(([start]) => timeslotMap[serializeDate(start, moment.HTML5_FMT.DATETIME_LOCAL)])
.sort();
}
);
Expand Down
10 changes: 9 additions & 1 deletion newdle/client/src/components/ParticipantTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {Radio, Icon, Label, Table} from 'semantic-ui-react';
import AvailabilityRing from './AvailabilityRing';
import {serializeDate, toMoment} from '../util/date';
import {useIsMobile} from 'src/util/hooks';
import {getNewdleDuration, getNumberOfParticipants, getParticipantAvailability} from '../selectors';
import {
getNewdleDuration,
getNewdleTimezone,
getNumberOfParticipants,
getParticipantAvailability,
} from '../selectors';
import styles from './ParticipantTable.module.scss';

const MAX_PARTICIPANTS_SHOWN = 4;
Expand Down Expand Up @@ -69,6 +74,7 @@ function AvailabilityRow({
children,
}) {
const numberOfParticipants = useSelector(getNumberOfParticipants);
const newdleTimezone = useSelector(getNewdleTimezone);
const duration = useSelector(getNewdleDuration);
const startTime = toMoment(startDt, 'YYYY-MM-DDTHH:mm');

Expand Down Expand Up @@ -109,13 +115,15 @@ function AvailabilityRow({
<div>
<div className={styles['date']}>{startTime.format('D MMM')}</div>
<div className={styles['time']}>{formatMeetingTime(startTime, duration)}</div>
<div className={styles['timezone']}>{newdleTimezone}</div>
</div>
{!finalized && isCreator && <Radio name="slot-id" value={startDt} checked={active} />}
</div>
) : (
<>
<div className={styles['date']}>{startTime.format('D MMM')}</div>
<div className={styles['time']}>{formatMeetingTime(startTime, duration)}</div>
<div className={styles['timezone']}>({newdleTimezone})</div>
</>
)}
</Table.Cell>
Expand Down
6 changes: 6 additions & 0 deletions newdle/client/src/components/ParticipantTable.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
color: gray;
}

.timezone {
font-size: 10px;
font-weight: normal;
color: gray;
}

.available-participants {
.wrapper {
display: flex;
Expand Down
16 changes: 12 additions & 4 deletions newdle/client/src/components/answer/AnswerPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
haveParticipantAnswersChanged,
hasBusyTimes,
} from '../../answerSelectors';
import {getUserInfo} from '../../selectors';
import {getUserInfo, getUserTimezone} from '../../selectors';
import {
chooseAllAvailable,
fetchBusyTimesForAnswer,
Expand Down Expand Up @@ -91,7 +91,8 @@ export default function AnswerPage() {
const participantUnknown = useSelector(isParticipantUnknown);
const participantAnswersChanged = useSelector(haveParticipantAnswersChanged);
const busyTimesLoaded = useSelector(hasBusyTimes);
const tz = useSelector(getNewdleTimezone);
const newdleTz = useSelector(getNewdleTimezone);
const userTz = useSelector(getUserTimezone);
usePageTitle(newdle && newdle.title, true);

const [submitAnswer, submitting, , submitResult] = participantCode
Expand Down Expand Up @@ -139,9 +140,10 @@ export default function AnswerPage() {

useEffect(() => {
if ((participantCode && !participantUnknown) || (!participantCode && user)) {
dispatch(fetchBusyTimesForAnswer(newdleCode, participantCode || null, dates, tz));
// Fetching busy times for user's timezone so no timezone conversion needed later
dispatch(fetchBusyTimesForAnswer(newdleCode, participantCode || null, dates, userTz));
}
}, [dates, newdleCode, participantCode, participantUnknown, user, tz, dispatch]);
}, [dates, newdleCode, participantCode, participantUnknown, user, userTz, dispatch]);

if (!newdle || (participantCode && !participant)) {
return null;
Expand Down Expand Up @@ -200,6 +202,12 @@ export default function AnswerPage() {
/>
</Segment>
)}
<div className={styles.timezone}>
<span className={styles['timezone-title']}>Newdle timezone:</span> {newdleTz}
</div>
<div className={styles.timezone}>
<span className={styles['timezone-title']}>Displayed timezone:</span> {userTz}
</div>
</Grid.Column>
<Grid.Column computer={11} tablet={8}>
<Calendar />
Expand Down
46 changes: 31 additions & 15 deletions newdle/client/src/components/answer/Calendar.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from 'lodash';
import React from 'react';
import HTML5_FMT from 'moment';
import {HTML5_FMT} from 'moment';
import PropTypes from 'prop-types';
import {Grid} from 'semantic-ui-react';
import {useDispatch, useSelector} from 'react-redux';
Expand All @@ -9,14 +9,16 @@ import {useIsSmallScreen} from '../../util/hooks';
import DayTimeline from './DayTimeline';
import {
getActiveDate,
getNewdleTimeslots,
getLocalNewdleTimeslots,
getNewdleDuration,
getAnswers,
getBusyTimes,
getNewdleTimezone,
} from '../../answerSelectors';
import {setAnswer, setAnswerActiveDate} from '../../actions';
import DayCarousel from '../DayCarousel';
import styles from './answer.module.scss';
import {getUserTimezone} from '../../selectors';

const OVERFLOW_HEIGHT = 0.5;
const DEFAULT_FORMAT = HTML5_FMT.DATETIME_LOCAL;
Expand Down Expand Up @@ -97,17 +99,21 @@ function getAnswerProps(slot, answer) {
}
}

function getSlotProps(slot, duration, minHour, maxHour, answer) {
const start = toMoment(slot, DEFAULT_FORMAT);
const end = toMoment(start).add(duration, 'm');
const answerProps = getAnswerProps(slot, answer);
function getSlotProps(slot, duration, minHour, maxHour, answers, newdleTz, userTz) {
const start = toMoment(slot, DEFAULT_FORMAT, userTz);
const end = start.clone().add(duration, 'm');

const newdleSlot = serializeDate(start, DEFAULT_FORMAT, newdleTz);
const answer = answers[newdleSlot];
const answerProps = getAnswerProps(newdleSlot, answer);
const height = calculateHeight(start, end, minHour, maxHour);
const pos = calculatePosition(start, minHour, maxHour);

return {
slot,
startTime: serializeDate(start, 'HH:mm'),
endTime: serializeDate(end, 'HH:mm'),
startTime: serializeDate(start, 'H:mm'),
endTime: serializeDate(end, 'H:mm'),
groupDateKey: start,
height,
pos,
key: slot,
Expand All @@ -116,10 +122,10 @@ function getSlotProps(slot, duration, minHour, maxHour, answer) {
};
}

function calculateOptionsPositions(options, duration, minHour, maxHour, answers) {
function calculateOptionsPositions(options, duration, minHour, maxHour, answers, newdleTz, userTz) {
const optionsByDate = _.groupBy(
options.map(slot => getSlotProps(slot, duration, minHour, maxHour, answers[slot])),
slot => serializeDate(toMoment(slot.slot, DEFAULT_FORMAT))
options.map(slot => getSlotProps(slot, duration, minHour, maxHour, answers, newdleTz, userTz)),
slot => serializeDate(slot.groupDateKey, HTML5_FMT.DATE)
);

return Object.entries(optionsByDate).map(([date, options]) => {
Expand All @@ -132,8 +138,8 @@ function getBusySlotProps(slot, minHour, maxHour) {
const start = toMoment(startTime, 'HH:mm');
const end = toMoment(endTime, 'HH:mm');
return {
startTime,
endTime,
startTime: serializeDate(start, 'H:mm'),
endTime: serializeDate(end, 'H:mm'),
height: calculateHeight(start, end, minHour, maxHour),
pos: calculatePosition(start, minHour, maxHour),
key: `${startTime}-${endTime}`,
Expand Down Expand Up @@ -178,9 +184,11 @@ Hours.defaultProps = {

export default function Calendar() {
const answers = useSelector(getAnswers);
const timeSlots = useSelector(getNewdleTimeslots);
const timeSlots = useSelector(getLocalNewdleTimeslots);
const duration = useSelector(getNewdleDuration);
const busyTimes = useSelector(getBusyTimes);
const newdleTz = useSelector(getNewdleTimezone);
const userTz = useSelector(getUserTimezone);
const activeDate = toMoment(useSelector(getActiveDate), HTML5_FMT.DATE);
const dispatch = useDispatch();

Expand All @@ -201,7 +209,15 @@ export default function Calendar() {
format,
};
const [minHour, maxHour] = getHourSpan(input);
const optionsByDay = calculateOptionsPositions(timeSlots, duration, minHour, maxHour, answers);
const optionsByDay = calculateOptionsPositions(
timeSlots,
duration,
minHour,
maxHour,
answers,
newdleTz,
userTz
);
const busyByDay = calculateBusyPositions(busyTimes, minHour, maxHour);
const activeDateIndex = optionsByDay.findIndex(({date: timeSlotDate}) =>
toMoment(timeSlotDate, HTML5_FMT.DATE).isSame(activeDate, 'day')
Expand Down
5 changes: 3 additions & 2 deletions newdle/client/src/components/answer/DayTimeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import styles from './answer.module.scss';

export default function DayTimeline({options, busySlots}) {
const dispatch = useDispatch();
const date = serializeDate(toMoment(options.date, 'YYYY-MM-DD'), 'dddd D MMM');
// This date does not need to be timezone casted as we are only format correcting
const formattedDate = serializeDate(toMoment(options.date, 'YYYY-MM-DD'), 'dddd D MMM');
return (
<>
<Header as="h3" className={styles.date}>
{date}
{formattedDate}
</Header>
<div className={styles['options-column']}>
{options.optionGroups.map(group => {
Expand Down
6 changes: 1 addition & 5 deletions newdle/client/src/components/answer/Option.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Icon} from 'semantic-ui-react';
import {serializeDate, toMoment} from '../../util/date';
import styles from './answer.module.scss';

export default function Option({startTime, endTime, icon, onClick, className, styles: moreStyles}) {
const start = serializeDate(toMoment(startTime, 'H:mm'), 'H:mm');
const end = serializeDate(toMoment(endTime, 'H:mm'), 'H:mm');

return (
<div className={`${styles.option} ${className}`} onClick={onClick} style={moreStyles}>
<span className={styles.times}>
{start} - {end}
{startTime} - {endTime}
</span>
<Icon name={icon} />
</div>
Expand Down
9 changes: 9 additions & 0 deletions newdle/client/src/components/answer/answer.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,12 @@
.on-behalf {
color: $purple;
}

.timezone {
margin: 10px;
text-align: center;

.timezone-title {
font-weight: bold;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function getCandidateSlotProps(startTime, duration, minHour, maxHour) {
const endTime = toMoment(startTime, DEFAULT_TIME_FORMAT)
.add(duration, 'm')
.format(DEFAULT_TIME_FORMAT);
return getSlotProps(startTime, endTime, minHour, maxHour, duration);
return getSlotProps(startTime, endTime, minHour, maxHour);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions newdle/client/src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const isAcquiringToken = state => !!state.auth.acquiringToken;

// user
export const getUserInfo = state => state.user;
export const getUserTimezone = () => moment.tz.guess();

// creation
export const getCreationCalendarDates = state => Object.keys(state.creation.timeslots);
Expand Down Expand Up @@ -83,6 +84,7 @@ export const getCreatedNewdle = state => state.creation.createdNewdle;

// newdle
export const getNewdle = state => state.newdle;
export const getNewdleTimezone = state => state.newdle && state.newdle.timezone;
export const getNewdleTimeslots = state => (state.newdle && state.newdle.timeslots) || [];
export const getNewdleDuration = state => state.newdle && state.newdle.duration;
export const getNewdleParticipants = state => (state.newdle && state.newdle.participants) || [];
Expand Down
Loading

0 comments on commit d64b7d9

Please sign in to comment.