From 4c38ed8dedf790d527cb6a7cfdeb521713e1a325 Mon Sep 17 00:00:00 2001 From: Leo Furnari Date: Thu, 8 Oct 2026 13:02:29 -0400 Subject: [PATCH 1/2] Receive request body with hide_below to generate-task POST. Utilize to narrow incomplete task criteria. --- task_api.py | 14 ++++++++++++-- task_database.py | 4 ++-- user_dao.py | 9 +++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/task_api.py b/task_api.py index 51ac5f0..8580160 100644 --- a/task_api.py +++ b/task_api.py @@ -10,7 +10,7 @@ from app_setup import app, db from tasklists import get_task_tier, list_for_tier from templesync import sync_user_tasks -from user_dao import UserDatabaseObject +from user_dao import ROLLABLE_TIERS, UserDatabaseObject import discord_service @@ -132,7 +132,17 @@ def apiv2_generate_task(user: UserDatabaseObject): if user.current_task(): return { 'error': 'User already has an active task' }, HTTPStatus.BAD_REQUEST - generated_task = generate_task(user.username) + body = request.get_json() if request.data else {} + if not isinstance(body, dict): + return { 'error': 'Request body must be a JSON object' }, HTTPStatus.BAD_REQUEST + + hide_below = body.get('hide_below', 'easy') + if not isinstance(hide_below, str) or hide_below not in ROLLABLE_TIERS: + return { + 'error': f"hide_below must be one of: {', '.join(ROLLABLE_TIERS)}" + }, HTTPStatus.BAD_REQUEST + + generated_task = generate_task(user.username, hide_below) if generated_task: return { 'task_id': generated_task.id } diff --git a/task_database.py b/task_database.py index 0ac1009..1c5d94b 100644 --- a/task_database.py +++ b/task_database.py @@ -472,7 +472,7 @@ def get_roll_candidates_for_tier(username: str, tier: str, min_candidates: int = ''' -def generate_task(username: str) -> TaskData | None: +def generate_task(username: str, hide_below: str = 'easy') -> TaskData | None: user = get_user(username) if user.current_task() is not None: return @@ -484,7 +484,7 @@ def get_first_task_instance(generated_task: TaskData, incomplete_tasks: list[Tas return first_task_instance return generated_task - tier = user.current_rollable_tier() + tier = user.current_rollable_tier(hide_below) if tier is None: return None diff --git a/user_dao.py b/user_dao.py index 20b4bc5..f66136f 100644 --- a/user_dao.py +++ b/user_dao.py @@ -30,6 +30,7 @@ "5cbcc790-10d1-4c17-8b39-cad7b48dadf8", "4a387bb0-dfc3-4374-aa35-9bacfc2fc92d", } +ROLLABLE_TIERS = ('easy', 'medium', 'hard', 'elite', 'master') @dataclass class UserDatabaseObject: @@ -99,8 +100,12 @@ def current_tier(self) -> str: else: return None - def current_rollable_tier(self) -> str | None: - for tier in ['easy', 'medium', 'hard', 'elite', 'master']: + def current_rollable_tier(self, hide_below: str = 'easy') -> str | None: + if hide_below not in ROLLABLE_TIERS: + raise ValueError(f"Invalid minimum rollable tier: {hide_below}") + + first_tier_index = ROLLABLE_TIERS.index(hide_below) + for tier in ROLLABLE_TIERS[first_tier_index:]: tasks = self.get_incomplete_tasks(tier) if len(tasks) != 0: return tier From b2fb7c005c794f5ad359c955971224a9c9e17e32 Mon Sep 17 00:00:00 2001 From: Leo Furnari Date: Thu, 8 Oct 2026 21:45:44 -0400 Subject: [PATCH 2/2] Add hideBelow as new property in Mongo Return hideBelow from profile endpoint Initialize new accounts to default hideBelow of easy Add Hide Tasks Below dropdown to User Profile, only shown for unofficial accounts. --- task_api.py | 15 +++------------ task_database.py | 26 ++++++++++++++++++++++++-- taskapp.py | 26 +++++++++++++++++++++++++- templates/profile.html | 26 +++++++++++++++++++++++--- user_dao.py | 12 +++++++++++- 5 files changed, 86 insertions(+), 19 deletions(-) diff --git a/task_api.py b/task_api.py index 8580160..61f2dae 100644 --- a/task_api.py +++ b/task_api.py @@ -10,7 +10,7 @@ from app_setup import app, db from tasklists import get_task_tier, list_for_tier from templesync import sync_user_tasks -from user_dao import ROLLABLE_TIERS, UserDatabaseObject +from user_dao import UserDatabaseObject import discord_service @@ -93,6 +93,7 @@ def apiv2_get_user_profile(user: UserDatabaseObject): 'username': user.username, 'is_official': user.is_official, 'is_lms_enabled': user.lms_enabled, + 'hide_below': user.effective_hide_below, 'has_migrated': user.has_migrated, 'active_task_id': user.current_task_id(), 'completed_tasks': [ @@ -132,17 +133,7 @@ def apiv2_generate_task(user: UserDatabaseObject): if user.current_task(): return { 'error': 'User already has an active task' }, HTTPStatus.BAD_REQUEST - body = request.get_json() if request.data else {} - if not isinstance(body, dict): - return { 'error': 'Request body must be a JSON object' }, HTTPStatus.BAD_REQUEST - - hide_below = body.get('hide_below', 'easy') - if not isinstance(hide_below, str) or hide_below not in ROLLABLE_TIERS: - return { - 'error': f"hide_below must be one of: {', '.join(ROLLABLE_TIERS)}" - }, HTTPStatus.BAD_REQUEST - - generated_task = generate_task(user.username, hide_below) + generated_task = generate_task(user.username) if generated_task: return { 'task_id': generated_task.id } diff --git a/task_database.py b/task_database.py index 1c5d94b..d85643f 100644 --- a/task_database.py +++ b/task_database.py @@ -96,6 +96,7 @@ def add_task_account(username, is_official, lms_enabled): "username": str(username), "isOfficial": bool(is_official), "lmsEnabled": bool(lms_enabled), + "hideBelow": 'easy', "discordLinked": False, "discordNameSyncEnabled": False, "completedTasks": [], @@ -472,7 +473,7 @@ def get_roll_candidates_for_tier(username: str, tier: str, min_candidates: int = ''' -def generate_task(username: str, hide_below: str = 'easy') -> TaskData | None: +def generate_task(username: str) -> TaskData | None: user = get_user(username) if user.current_task() is not None: return @@ -484,7 +485,7 @@ def get_first_task_instance(generated_task: TaskData, incomplete_tasks: list[Tas return first_task_instance return generated_task - tier = user.current_rollable_tier(hide_below) + tier = user.current_rollable_tier() if tier is None: return None @@ -495,6 +496,27 @@ def get_first_task_instance(generated_task: TaskData, incomplete_tasks: list[Tas __set_current_task(username, f'{tier}Tasks', first_task_instance.id, True) return first_task_instance + +def set_hide_below(username: str, hide_below: str) -> None: + if hide_below not in user_dao.ROLLABLE_TIERS: + raise ValueError(f"Invalid minimum rollable tier: {hide_below}") + + result = mydb['taskLists'].update_one( + {'username': username, 'isOfficial': False}, + {'$set': {'hideBelow': hide_below}} + ) + if result.matched_count == 0: + user = mydb['taskLists'].find_one( + {'username': username}, + {'_id': 0, 'isOfficial': 1} + ) + if user is None: + raise LookupError(f"No task account found for username {username}") + if user.get('isOfficial', False): + raise PermissionError("Hide Tasks Below is only available for unofficial accounts.") + raise LookupError(f"Could not update task account for username {username}") + + # If user has just completed a task of the given tier and the progress is 100, then they've just completed the last task def __get_firework_variables(username, tier): user = get_user(username) diff --git a/taskapp.py b/taskapp.py index 61c31d8..3ccd2ba 100644 --- a/taskapp.py +++ b/taskapp.py @@ -13,7 +13,8 @@ get_lms_status, lms_status_change, update_imported_tasks, official_status_change, username_change, get_taskCurrent_tier, generate_task_for_tier, complete_task_unofficial_tier, get_user, get_leaderboard, - get_roll_candidates_for_tier, get_discord_name_sync_enabled, clear_leaderboard_cache) + get_roll_candidates_for_tier, get_discord_name_sync_enabled, clear_leaderboard_cache, + set_hide_below) import send_grid_email from templesync import check_logs, temple_player_data, import_logs from task_types import CollectionLogVerificationData @@ -22,6 +23,7 @@ import task_api import discord_api import command_api +from user_dao import ROLLABLE_TIERS ''' @@ -1081,6 +1083,8 @@ def profile(): email_val=user_info.email_val, official=user_info.official, lms_status=user_info.user.lms_enabled, + hide_below=user_info.user.hide_below, + rollable_tiers=ROLLABLE_TIERS, discord_status=user_info.user.discord_linked, discord_default_username=discord_default_username, discord_name_sync_enabled=user_info.user.discord_name_sync_enabled, @@ -1140,6 +1144,26 @@ def change_lms_status(): lms_status_change(username, data) return {'success' : True} + +@app.route("/profile/change-hide-below/", methods=['POST']) +@login_required +def change_hide_below(): + user = get_user(session['username']) + if user.is_official: + return {'success': False, 'error': 'Hide Tasks Below is only available for unofficial accounts.'}, 403 + + hide_below = request.form.get('hide_below') + if hide_below not in ROLLABLE_TIERS: + return {'success': False, 'error': 'Invalid minimum task tier.'}, 400 + + try: + set_hide_below(session['username'], hide_below) + except PermissionError as error: + return {'success': False, 'error': str(error)}, 403 + + return {'success': True} + + # AJAX route to change offical status @app.route("/profile/change-official/", methods=['POST']) @login_required diff --git a/templates/profile.html b/templates/profile.html index 0841556..9c3ef0b 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -82,6 +82,14 @@

{{username}} Profile

+ {% if not official %} +

Hide Tasks Below

+ + {% endif %}
@@ -366,7 +374,21 @@

Discord Settings

}); } - + $('#hide_below').on('change', function() { + const hideBelow = $(this).val(); + $.ajax({ + url: '/profile/change-hide-below/', + type: 'POST', + data: {hide_below: hideBelow} + }).done(function(data) { + if (!data.success) { + alert(data.error || 'Could not update the minimum task tier.'); + } + }).fail(function() { + alert('Could not update the minimum task tier.'); + }); + }); + $('#discord_name_sync_enabled').on('change', function(e) { var discord_name_sync_enabled = e.target.checked; req = $.ajax({ @@ -418,5 +440,3 @@

Discord Settings

}); {% endblock%} - - diff --git a/user_dao.py b/user_dao.py index f66136f..6571fa6 100644 --- a/user_dao.py +++ b/user_dao.py @@ -38,6 +38,7 @@ class UserDatabaseObject: username: str is_official: bool lms_enabled: bool + hide_below: str discord_linked: bool discord_name_sync_enabled: bool has_migrated: bool @@ -100,7 +101,8 @@ def current_tier(self) -> str: else: return None - def current_rollable_tier(self, hide_below: str = 'easy') -> str | None: + def current_rollable_tier(self) -> str | None: + hide_below = self.effective_hide_below if hide_below not in ROLLABLE_TIERS: raise ValueError(f"Invalid minimum rollable tier: {hide_below}") @@ -112,6 +114,10 @@ def current_rollable_tier(self, hide_below: str = 'easy') -> str | None: return None + @property + def effective_hide_below(self) -> str: + return self.hide_below if not self.is_official else ROLLABLE_TIERS[0] + def get_incomplete_tasks(self, tier: str) -> list[TaskData]: all_tasks = tasklists.list_for_tier(tier, self.lms_enabled) completed_task_ids = list(map(lambda x: x.id, self.get_task_list(tier).completed_tasks)) @@ -257,6 +263,9 @@ def task_info_for_id(task_list: list[TaskData], task_id: str) -> tasklists.TaskD def convert_database_user(user_data: dict) -> UserDatabaseObject: tiers = user_data['tiers'] lms_enabled = user_data.get('lmsEnabled', False) + hide_below = user_data.get('hideBelow', 'easy') + if hide_below not in ROLLABLE_TIERS: + raise ValueError(f"Invalid stored minimum rollable tier: {hide_below}") def to_item_ids(value) -> list[int]: if not isinstance(value, list): @@ -348,6 +357,7 @@ def convert_database_tier(tier: str) -> UserTaskList: username=user_data['username'], is_official=user_data['isOfficial'], lms_enabled=user_data['lmsEnabled'], + hide_below=hide_below, has_migrated=user_data.get('hasMigrated', False), discord_linked=user_data.get('discordLinked', False), discord_name_sync_enabled=user_data.get('discordNameSyncEnabled', False),