Skip to content

Conversation

@lsabor
Copy link
Contributor

@lsabor lsabor commented Jan 11, 2026

closes #3799

This is the final branch for the MC options changing project
This took all of the separate branches and molded them into one
Each of the individual branches are commits on this PR and have individually been QA'd

This branch addresses the final QA steps in preparation for merging into main
commits after "MC 3805 UI" are added after this branch existed and is bug fixing and tweaks so do not need to be looked at separately

TODO:

  • notifications
    • emails remain untested
    • copy
      • delete copy: '''Options “Paris”, “Tokyo”, “La Paz” were removed on 18 December 2025 09:45 UTC. Their probability was folded into the “Other Capital” option.'''
      • add copy: '''Options “Berlin”, “Seoul” were added on 18 December 2025 09:45 UTC. Please update before 19 December 2025 09:45 UTC to keep an active forecast on this question.'''
  • admin form
    • grace period end
      • show current timezone offset
        • this is proving to be frustrating...
      • prefill with now + 3 days (3 from spec even though feedback on slack says 7)
  • forecasting
    • It's a bit weird that the latest forecast I've made during the grace period is shown with the new option in grey, but the moment I forecast again grey disappears in my previous forecast.
      • I'm not 100% sure what this means, but I might have solved it, so I'll have to ask Nikitas to test this again
    • triple check slider behavior in all scenarios
  • graphs
    • Mouseovering the graph should only list options that were present at the corresponding point. I.e., here it should have no row for the green option because it hadn't been added yet.
      • I'm not sure I agree with this, but if we do implement this, it should only disregard options before they're added. I think it should maintain a "-" for options that have since been deleted, no?
    • Deleted options are not differentiated from non-deleted options in the legend
    • Graph implies I forecasted when the option was deleted. (It does update my forecast, so is this ok?)
  • question creation
    • disable editing options once question has forecasts
  • User experience
    • No immediately visible indication that an option was deleted when I first visit the question.
  • Condition QA
    • MC Delete - With Predictions --- eliminated option has infinitesimal but non-zero width
    • having your prediction withdrawn isn't showing anything can you can see
    • MC Delete - With Predictions - Resolved: Deleted Option --- resolved to deleted option showing probability when it should not... seems like colors don't track options properly - not the case.
    • MC Delete then Add - Reforecasted - Resolved: new option --- new option d never gets assigned probability by graph (are we still in grace period?)
    • MC Delete then Delete then Add - Reforecasted - Resolved: d --- after first delete, color for c is wrong (has deleted b's color) - Same problem as MC Delete - With Predictions
    • Can't resolve to deleted options!

lsabor and others added 8 commits January 10, 2026 14:49
add options_history field to question model and migration
add options_history to question serialization
add options_history initialization to question creation
add helper functions to question/services/multiple_choice_handlers.py and add 'automatic' to forecast.source selection
fix build_question_forecasts import and remove options & options_history from admin panel edit
tests for question creation, multiple_choice_rename_option, multiple_choice_options_reorder,  multiple_choice_delete_options, multiple_choice_add_options
mark some expected failures
add options_history to openapi docs
add csv reporting support for options_history
update logic to play well with back/forward filling 0s
add all_options_ever to serializer and api docs
add current options to csv return
add support for datetime isoformat in options_history
fix file restructure
fix datetime iso format in history conflicts
add support for None values in MC predictions
fix tests and source logic
fix test_comment_edit_include_forecast to explicitly set forecast times
mark xfail tests
mc/3806/aggregations

adjust aggregations to play nicely with placeholders
improve test for comput_weighted_semi_standard_deviations
add support for None s in prediction difference for sorting plus tests
update prediction difference for display to handle placeholders
reactivate skipped tests
mc/3801/scoring

add OptionsHistoryType, multiple_choice_interpret_forecast, and test
update test for change to function
update string_location_to_scaled_location to accept all historical option values, and related test
multiple choice forecasts require interpretation before scoring
remove double written definition
support the new MC format scoring
tests for mc with placeholder values
add support for None values
fix some linting errors left from previous commit
mc/3802/backend/notifications

add notification logic
add mjml/html and update tasks to setup for notifications
add withdrawal notifications and bulletin deactivation
fix grace period end bug
mc/3804/backend/updating

add admin form for changing options
add comment author and text to admin panel action and mc change methods
mc/3805/frontend/graphing

forecast only current option values
aggregation explorer
disclaimer to forecast during grace period
add option reordering (should be in mc/3804)
mc/3805/ui

Added tooltip and highlight for newly added MC options
Updated highlight method and message copy
grace period timer
translation strings

Co-authored-by: aseckin <atakanseckin@gmail.com>
Comment on lines +108 to +126
const pluralize = (count: number, singular: string) =>
count === 1 ? singular : `${singular}s`;

if (days > 0) {
setTimeRemaining(
`${days} ${pluralize(days, "day")}, ${hours} ${pluralize(hours, "hour")}`
);
} else if (hours > 0) {
setTimeRemaining(
`${hours} ${pluralize(hours, "hour")}, ${minutes} ${pluralize(minutes, "minute")}`
);
} else if (minutes > 0) {
setTimeRemaining(
`${minutes} ${pluralize(minutes, "minute")}, ${seconds} ${pluralize(seconds, "second")}`
);
} else {
setTimeRemaining(`${seconds} ${pluralize(seconds, "second")}`);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your pluralize adds "s" in English. That will be wrong for basically every other locale. You can check how to do it properly in tournaments_hero.tsx file for its translation keys.

Comment on lines +327 to +339
const getNewOptions = useCallback(() => {
if (!activeUserForecast) return [];

return choicesForecasts
.filter((choice, index) => {
const isCurrentOption = question.options.includes(choice.name);
const hasForecast = activeUserForecast.forecast_values[index] !== null;
return isCurrentOption && !hasForecast;
})
.map((c) => ({ name: c.name, color: c.color }));
}, [activeUserForecast, choicesForecasts, question.options]);

const newOptions = getNewOptions();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newOptions computed via callback invoked at render. It defeats most of the value of useCallback and makes it easier to accidentally recompute more than needed. Make newOptions a useMemo

Comment on lines +96 to +99
if (diff <= 0) {
setTimeRemaining("expired");
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's better to stop ticking immediately when expired and do not wait until unmount.

Comment on lines +611 to +614
const isFirstNewOption =
newOptions.length > 0 && choice.name === newOptions[0]?.name;
const isNewOption = newOptions.some(
(opt) => opt.name === choice.name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can compute inside a component

const newOptionNames = useMemo(() => new Set(newOptions.map(o => o.name)), [newOptions]);
const firstNewOptionName = newOptions[0]?.name;

and then you'll have

const isNewOption = newOptionNames.has(choice.name);
const isFirstNewOption = choice.name === firstNewOptionName;

I believe it's better from the performance standpoint.

Comment on lines +332 to +334
const isCurrentOption = question.options.includes(choice.name);
const hasForecast = activeUserForecast.forecast_values[index] !== null;
return isCurrentOption && !hasForecast;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that your

const hasForecast = activeUserForecast.forecast_values[index] !== null;

assumes: the index of an item in choicesForecasts === the index of the same option in activeUserForecast.forecast_values, but choicesForecasts is built by generateChoiceOptions(), and that function reorders the array:

const resolutionIndex = allOptions.findIndex((_, index) => allOptions[index] === question.resolution);
...
choiceItems.unshift(resolutionItem);

So even if choiceItems originally lined up by index with forecast_values, once you move the resolution option to the front, indices no longer match. I believe it can be fragile, correct me if I'm wrong.

I guess it's better to build a map from the canonical ordering that forecast_values uses and look up index by option name, not by render position.

Comment on lines +78 to +86

/**
* Hook to display remaining time until grace period ends
* Updates every second for a live countdown
*/
const useGracePeriodCountdown = (gracePeriodEnd: Date | null) => {
const [timeRemaining, setTimeRemaining] = useState<string>("");

useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it would be better to separate hook from the component implementation for easier navigation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adding and removing options from live Multiple Choice questions

3 participants