diff --git a/static/js/task.js b/static/js/task.js index 13902c9..3c40c75 100644 --- a/static/js/task.js +++ b/static/js/task.js @@ -255,6 +255,215 @@ $(document).ready(function(){ }); }); +function getVerificationItemIds(elementTarget) { + const hiddenInput = elementTarget.querySelector('.verification-item-ids'); + if (!hiddenInput || !hiddenInput.value) { + return []; + } + return hiddenInput.value + .split(',') + .map((value) => parseInt(value.trim(), 10)) + .filter((value) => !Number.isNaN(value)); +} + +function getCompletedItemIds(elementTarget) { + const hiddenInput = elementTarget.querySelector('.completed-item-ids'); + if (!hiddenInput || !hiddenInput.value) { + return []; + } + return hiddenInput.value + .split(',') + .map((value) => parseInt(value.trim(), 10)) + .filter((value) => !Number.isNaN(value)); +} + +function getTaskMetaValue(elementTarget, selector) { + const node = elementTarget.querySelector(selector); + return node ? node.value : ''; +} + +function formatUserDateTime(value) { + if (!value) { + return '--/--/---- --:--'; + } + + const parsedDate = new Date(value); + if (Number.isNaN(parsedDate.getTime())) { + return '--/--/---- --:--'; + } + + return new Intl.DateTimeFormat(undefined, { + dateStyle: 'short', + timeStyle: 'short' + }).format(parsedDate); +} + +function renderTaskTimestamps() { + const timestampNodes = document.querySelectorAll('.task-timestamp'); + timestampNodes.forEach((node) => { + const completedAt = node.getAttribute('data-completed-at'); + node.textContent = formatUserDateTime(completedAt); + }); +} + +$(document).ready(function(){ + renderTaskTimestamps(); +}); + +function isoToDateTimeLocal(isoValue) { + const parsedDate = isoValue ? new Date(isoValue) : new Date(); + const safeDate = Number.isNaN(parsedDate.getTime()) ? new Date() : parsedDate; + const pad = (value) => String(value).padStart(2, '0'); + const year = safeDate.getFullYear(); + const month = pad(safeDate.getMonth() + 1); + const day = pad(safeDate.getDate()); + const hours = pad(safeDate.getHours()); + const minutes = pad(safeDate.getMinutes()); + return `${year}-${month}-${day}T${hours}:${minutes}`; +} + +function dateTimeLocalToISO(localValue) { + if (!localValue) { + return null; + } + const parsedDate = new Date(localValue); + if (Number.isNaN(parsedDate.getTime())) { + return null; + } + return parsedDate.toISOString(); +} + +function attachFallbackImage(imgElement, fallbackSrc) { + imgElement.onerror = () => { + imgElement.onerror = null; + imgElement.src = fallbackSrc; + }; +} + +function openTaskActionModal(options) { + const { + actionLabel, + taskName, + taskTip, + taskWikiLink, + itemIds, + recordedItemIds, + currentCompletedAtISO, + allowTimeEdit, + onConfirm, + } = options; + + const modal = document.getElementById('taskActionModal'); + const modalTitle = document.getElementById('taskActionModalTitle'); + const modalTip = document.getElementById('taskActionTip'); + const wikiButton = document.getElementById('taskActionWikiButton'); + const progressFill = document.getElementById('taskActionProgressFill'); + const progressText = document.getElementById('taskActionProgressText'); + const itemContainer = document.getElementById('taskActionModalItems'); + const timeInput = document.getElementById('taskActionTimeInput'); + const confirmButton = document.getElementById('taskActionConfirmButton'); + const backButton = document.getElementById('taskActionBackButton'); + + if (!modal || !modalTitle || !modalTip || !wikiButton || !progressFill || !progressText || !itemContainer || !timeInput || !confirmButton || !backButton) { + onConfirm(); + return; + } + + modalTitle.textContent = taskName || 'Task'; + modalTip.textContent = taskTip || ''; + wikiButton.href = taskWikiLink || '#'; + confirmButton.textContent = actionLabel; + + progressFill.style.width = '100%'; + progressText.textContent = 'Applicable Items'; + + timeInput.value = isoToDateTimeLocal(currentCompletedAtISO); + timeInput.disabled = allowTimeEdit === false; + + itemContainer.innerHTML = ''; + + const normalizedItemIds = Array.from(new Set((itemIds || []) + .map((value) => parseInt(value, 10)) + .filter((value) => !Number.isNaN(value)))); + const selectedItemIds = new Set((recordedItemIds || []) + .map((value) => parseInt(value, 10)) + .filter((value) => !Number.isNaN(value) && normalizedItemIds.includes(value))); + + const renderItems = () => { + itemContainer.innerHTML = ''; + + if (normalizedItemIds.length === 0) { + const empty = document.createElement('p'); + empty.className = 'task-action-empty'; + empty.textContent = 'No verification items for this task.'; + itemContainer.appendChild(empty); + return; + } + + normalizedItemIds.forEach((itemId) => { + const isRecorded = selectedItemIds.has(itemId); + const image = document.createElement('img'); + image.className = `task-action-item ${isRecorded ? 'task-action-item-active' : 'task-action-item-muted'}`; + attachFallbackImage(image, '/static/clog.png'); + image.src = `https://static.runelite.net/cache/item/icon/${itemId}.png`; + image.alt = `Item ${itemId}`; + image.width = 36; + image.height = 32; + image.loading = 'lazy'; + image.title = isRecorded + ? `Item ID: ${itemId} (click to mark incomplete)` + : `Item ID: ${itemId} (click to mark complete)`; + image.setAttribute('role', 'button'); + image.tabIndex = 0; + + const toggleItem = () => { + if (selectedItemIds.has(itemId)) { + selectedItemIds.delete(itemId); + } else { + selectedItemIds.add(itemId); + } + renderItems(); + }; + + image.addEventListener('click', toggleItem); + image.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + toggleItem(); + } + }); + + itemContainer.appendChild(image); + }); + }; + + renderItems(); + + const closeModal = () => { + if (typeof modal.close === 'function') { + modal.close(); + } + }; + + confirmButton.onclick = () => { + closeModal(); + onConfirm( + dateTimeLocalToISO(timeInput.value), + Array.from(selectedItemIds).sort((a, b) => a - b), + ); + }; + + backButton.onclick = () => { + closeModal(); + }; + + if (typeof modal.showModal === 'function') { + modal.showModal(); + } else { + onConfirm(); + } +} + $(document).ready(function(){ $(document).on('click', '.updateButton', function(){ if ($(this).data('type') === 'bossPets' || $(this).data('type') === 'skillPets' || $(this).data('type') === 'otherPets'){ @@ -267,40 +476,72 @@ $(document).ready(function(){ } var elementTarget = this; var parent = elementTarget.parentElement; + const verificationItemIds = getVerificationItemIds(elementTarget); + const completedItemIds = getCompletedItemIds(elementTarget); + const taskName = getTaskMetaValue(elementTarget, '.task-name'); + const taskTip = getTaskMetaValue(elementTarget, '.task-tip'); + const taskWikiLink = getTaskMetaValue(elementTarget, '.task-wiki-link'); + const timestampNode = elementTarget.querySelector('.task-timestamp'); + const currentCompletedAtISO = timestampNode ? timestampNode.getAttribute('data-completed-at') : null; + + openTaskActionModal({ + actionLabel: 'Mark Complete', + taskName: taskName, + taskTip: taskTip, + taskWikiLink: taskWikiLink, + itemIds: verificationItemIds, + recordedItemIds: completedItemIds, + currentCompletedAtISO: currentCompletedAtISO, + allowTimeEdit: true, + onConfirm: function(selectedCompletedAtISO, selectedItemIds) { + $('form').submit(false); + req = $.ajax({ + url : '/update_completed/', + type : 'POST', + data : { + id : elementTarget.id, + tier : tier, + completedAtISO: selectedCompletedAtISO, + completedItemIds: JSON.stringify(selectedItemIds || []), + } + }); + + req.done(function(data){ + $(elementTarget).fadeOut(1000).fadeIn(1000); + $(elementTarget).removeClass('updateButton').addClass('revertButton'); + $(parent).removeClass('incomplete-hover').addClass('complete-hover'); + parent.setAttribute('data-tooltip', 'Mark Incomplete'); + const taskTextNodes = elementTarget.getElementsByTagName('p'); + if (taskTextNodes.length > 1) { + taskTextNodes[1].textContent = formatUserDateTime(data.completedAtISO); + taskTextNodes[1].setAttribute('data-completed-at', data.completedAtISO || ''); + } + const completedIdsNode = elementTarget.querySelector('.completed-item-ids'); + if (completedIdsNode) { + const returnedIds = Array.isArray(data.completedItemIds) ? data.completedItemIds : []; + completedIdsNode.value = returnedIds.join(','); + } + if (tier === 'bossPets' || tier === 'skillPets' || tier === 'otherPets'){ + updatePercent.innerHTML = data["allPets"] + '%'; + } + else { + updatePercent.innerHTML = data[tier] + '%'; + } + for (const child of elementTarget.children) { + if (child.tagName === 'DIV') { + $(child).addClass('square-complete'); + $(child).removeClass('square-incomplete'); + } - - $('form').submit(false); - req = $.ajax({ - url : '/update_completed/', - type : 'POST', - data : {id : this.id, tier : tier} - }); - - req.done(function(data){ - $(elementTarget).fadeOut(1000).fadeIn(1000); - $(elementTarget).removeClass('updateButton').addClass('revertButton'); - $(parent).removeClass('incomplete-hover').addClass('complete-hover'); - parent.setAttribute('data-tooltip', 'Mark Incomplete'); - if (tier === 'bossPets' || tier === 'skillPets' || tier === 'otherPets'){ - updatePercent.innerHTML = data["allPets"] + '%'; - } - else { - updatePercent.innerHTML = data[tier] + '%'; - } - - for (const child of elementTarget.children) { - if (child.tagName === 'DIV') { - $(child).addClass('square-complete'); - $(child).removeClass('square-incomplete'); + if (child.tagName === 'P'){ + $(child).addClass('complete'); + $(child).removeClass('incomplete'); + } } - if (child.tagName === 'P'){ - $(child).addClass('complete'); - $(child).removeClass('incomplete'); - } + }); } - }); }); }); @@ -317,40 +558,63 @@ $(document).ready(function(){ } var elementTarget = this; var parent = elementTarget.parentElement; + const verificationItemIds = getVerificationItemIds(elementTarget); + const completedItemIds = getCompletedItemIds(elementTarget); + const taskName = getTaskMetaValue(elementTarget, '.task-name'); + const taskTip = getTaskMetaValue(elementTarget, '.task-tip'); + const taskWikiLink = getTaskMetaValue(elementTarget, '.task-wiki-link'); + const timestampNode = elementTarget.querySelector('.task-timestamp'); + const currentCompletedAtISO = timestampNode ? timestampNode.getAttribute('data-completed-at') : null; + + openTaskActionModal({ + actionLabel: 'Mark Incomplete', + taskName: taskName, + taskTip: taskTip, + taskWikiLink: taskWikiLink, + itemIds: verificationItemIds, + recordedItemIds: completedItemIds, + currentCompletedAtISO: currentCompletedAtISO, + allowTimeEdit: false, + onConfirm: function() { + $('form').submit(false); + req = $.ajax({ + url : '/revert_completed/', + type : 'POST', + data : {id : elementTarget.id, tier : tier} + }); + + req.done(function(data){ + $(elementTarget).fadeOut(1000).fadeIn(1000); + $(elementTarget).removeClass('revertButton').addClass('updateButton'); + $(parent).removeClass('complete-hover').addClass('incomplete-hover'); + parent.setAttribute('data-tooltip', 'Mark Complete'); + const taskTextNodes = elementTarget.getElementsByTagName('p'); + if (taskTextNodes.length > 1) { + taskTextNodes[1].textContent = '--/--/---- --:--'; + taskTextNodes[1].setAttribute('data-completed-at', ''); + } + if (tier === 'bossPets' || tier === 'skillPets' || tier === 'otherPets'){ + updatePercent.innerHTML = data["allPets"] + '%'; + } + else { + console.log(tier) + updatePercent.innerHTML = data[tier] + '%'; + } + for (const child of elementTarget.children) { + if (child.tagName === 'DIV') { + $(child).addClass('square-incomplete'); + $(child).removeClass('square-complete'); + } - $('form').submit(false); - req = $.ajax({ - url : '/revert_completed/', - type : 'POST', - data : {id : this.id, tier : tier} - }); - - req.done(function(data){ - $(elementTarget).fadeOut(1000).fadeIn(1000); - $(elementTarget).removeClass('revertButton').addClass('updateButton'); - $(parent).removeClass('complete-hover').addClass('incomplete-hover'); - parent.setAttribute('data-tooltip', 'Mark Complete'); - if (tier === 'bossPets' || tier === 'skillPets' || tier === 'otherPets'){ - updatePercent.innerHTML = data["allPets"] + '%'; - } - else { - console.log(tier) - updatePercent.innerHTML = data[tier] + '%'; - } - - for (const child of elementTarget.children) { - if (child.tagName === 'DIV') { - $(child).addClass('square-incomplete'); - $(child).removeClass('square-complete'); + if (child.tagName === 'P'){ + $(child).addClass('incomplete'); + $(child).removeClass('complete'); + } } - if (child.tagName === 'P'){ - $(child).addClass('incomplete'); - $(child).removeClass('complete'); - } + }); } - }); }); }); diff --git a/static/styles.css b/static/styles.css index cc9023e..903ae91 100644 --- a/static/styles.css +++ b/static/styles.css @@ -531,6 +531,179 @@ hr { transition: 100ms; } +.task-action-modal { + border: none; + background: transparent; + padding: 0; + max-width: calc(100vw - 20px); +} + +.task-action-modal::backdrop { + background: rgba(0, 0, 0, 0.7); +} + +.task-action-shell { + padding: 10px; + min-width: 420px; + max-width: 620px; + width: max-content; + min-height: 520px; + max-height: calc(100vh - 40px); + overflow-y: auto; + border: 1px solid #8f8d7d; +} + +.task-action-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.task-action-title { + margin: 0; + text-align: center; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-action-wiki { + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + padding: 0 12px; +} + +.task-action-divider { + margin: 8px 0; +} + +.task-action-tip { + text-align: center; + margin: 0 0 8px 0; + min-height: 18px; +} + +.task-action-progress-wrap { + margin: 0 0 8px 0; +} + +.task-action-time { + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 10px 0; +} + +.task-action-time-label { + min-width: 44px; +} + +.task-action-time-input { + flex: 1; + min-height: 28px; + padding: 4px 8px; + color: var(--current-task-color); + background: var(--main-bg-color); + border: 1px solid #606060; + outline: none; +} + +.task-action-time-input:focus { + border-color: var(--current-task-color); +} + +.task-action-time-input:disabled { + opacity: 0.65; +} + +.task-action-progress-bar { + height: 22px; + border: 1px solid #606060; + position: relative; + overflow: hidden; + background: #3f3930; +} + +.task-action-progress-fill { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 0%; + background: linear-gradient(90deg, #4f6f44 0%, #6d8d5f 100%); +} + +.task-action-progress-text { + position: relative; + z-index: 1; + line-height: 22px; + text-align: center; +} + +.task-action-items { + min-height: 300px; + overflow: visible; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); + gap: 8px; + padding: 8px; + margin-bottom: 10px; + border: 1px solid #606060; + background: #3f3930; +} + +.task-action-item { + width: 38px; + height: 38px; + object-fit: contain; + justify-self: center; + padding: 2px; + border: 1px solid #8f8d7d; + background: #52524d; + image-rendering: pixelated; + cursor: pointer; +} + +.task-action-item-active { + filter: none; + opacity: 1; + border-color: var(--complete-color); +} + +.task-action-item-muted { + filter: grayscale(1); + opacity: 0.45; + border-color: #6b6b6b; +} + +.task-action-empty { + margin: 0; + grid-column: 1 / -1; + text-align: center; +} + +.task-action-footer { + display: flex; + justify-content: center; +} + +@media (max-width: 640px) { + .task-action-shell { + min-width: 320px; + max-width: calc(100vw - 20px); + width: calc(100vw - 20px); + min-height: 460px; + } + + .task-action-items { + min-height: 240px; + } +} + .triangle-top-left { width: 0; diff --git a/task_database.py b/task_database.py index e49b879..361381a 100644 --- a/task_database.py +++ b/task_database.py @@ -2,6 +2,7 @@ # import gspread import re from math import floor +from datetime import datetime, timezone import tasklists import config import user_dao @@ -245,19 +246,75 @@ def __set_current_task(username: str, tier: str, task_id: str, current: bool): ) +def __parse_completed_iso(value: str | None) -> datetime | None: + if not value: + return None + try: + parsed = datetime.fromisoformat(value.replace('Z', '+00:00')) + if parsed.tzinfo is None: + return parsed.replace(tzinfo=timezone.utc) + return parsed.astimezone(timezone.utc) + except ValueError: + return None + + +def __sanitize_item_ids(item_ids) -> list[int]: + if not isinstance(item_ids, list): + return [] + output = [] + seen = set() + for value in item_ids: + try: + parsed = int(value) + except (TypeError, ValueError): + continue + if parsed in seen: + continue + seen.add(parsed) + output.append(parsed) + return output -def __set_task_complete(username: str, tier: str, task_id: int, complete: bool): + + +def __set_task_complete(username: str, tier: str, task_id: str, complete: bool, + completed_at_iso: str | None = None, + completed_item_ids: list[int] | None = None): task_coll = mydb['taskLists'] cleaned_tier = tier.replace("Tasks", "") if complete: + completed_date = __parse_completed_iso(completed_at_iso) or datetime.now(timezone.utc) + safe_completed_item_ids = __sanitize_item_ids(completed_item_ids) + task_coll.update_one( + {"username": username}, + { + "$pull": { + f"tiers.{cleaned_tier}.completedTasks": { + "id": task_id + } + } + } + ) task_coll.update_one( {"username": username}, { "$push": { - f"tiers.{cleaned_tier}.completedTasks": {"id": task_id} + f"tiers.{cleaned_tier}.completedTasks": { + "id": task_id, + "completedDate": completed_date, + "completedItemIds": safe_completed_item_ids + } } } ) + task_coll.update_one( + {"username": username}, + { + "$set": { + f"tiers.{cleaned_tier}.recordedItemIdsByTask.{task_id}": safe_completed_item_ids + } + } + ) + return completed_date, safe_completed_item_ids if not complete: task_coll.update_one( { @@ -268,6 +325,15 @@ def __set_task_complete(username: str, tier: str, task_id: int, complete: bool): f"tiers.{cleaned_tier}.completedTasks" : {"id" : task_id} } }) + return None, [] + + +def __datetime_to_iso(value) -> str | None: + if value is None: + return None + if isinstance(value, datetime): + return value.isoformat() + return str(value) ''' @@ -500,13 +566,29 @@ def get_task_progress(username: str): ''' -def manual_complete_tasks(username, tier, task_id): - __set_task_complete(username, tier, task_id, True) +def manual_complete_tasks(username, tier, task_id, completed_at_iso: str | None = None, + completed_item_ids: list[int] | None = None): + completed_date, stored_completed_item_ids = __set_task_complete( + username, + tier, + task_id, + True, + completed_at_iso, + completed_item_ids, + ) exclude_list = ['bossPetTasks', 'skillPetTasks', 'otherPetTasks'] if tier in exclude_list: tier = tier.replace('Tasks', '') task = user_dao.task_info_for_id(tasklists.list_for_tier(tier), task_id) - return task.name, task.image_link, task.tip, task.wiki_link + return { + 'name': task.name, + 'image_link': task.image_link, + 'tip': task.tip, + 'wiki_link': task.wiki_link, + 'completed_date': completed_date, + 'completed_date_iso': __datetime_to_iso(completed_date), + 'completed_item_ids': stored_completed_item_ids, + } ''' @@ -534,10 +616,12 @@ def manual_revert_tasks(username, tier, task_id): return task.name, task.image_link, task.tip, task.wiki_link -def update_imported_tasks(username: str, all_tasks: list, username2: str): +def update_imported_tasks(username: str, all_tasks: list, username2: str, + recorded_item_ids_by_tier: dict | None = None): coll = mydb['taskLists'] include = {'easy', 'medium', 'hard', 'elite'} tasks_to_check = [] + recorded_item_ids_by_tier = recorded_item_ids_by_tier or {} for tier in include: current_task = get_taskCurrent_tier(username, tier) @@ -615,9 +699,33 @@ def update_imported_tasks(username: str, all_tasks: list, username2: str): 'tiers.easy.completedTasks': 1, 'tiers.medium.completedTasks': 1, 'tiers.hard.completedTasks': 1, - 'tiers.elite.completedTasks': 1 + 'tiers.elite.completedTasks': 1, + 'tiers.easy.recordedItemIdsByTask': 1, + 'tiers.medium.recordedItemIdsByTask': 1, + 'tiers.hard.recordedItemIdsByTask': 1, + 'tiers.elite.recordedItemIdsByTask': 1, }) + def sanitize_recorded_map(value): + if not isinstance(value, dict): + return {} + return {str(task_id): __sanitize_item_ids(item_ids) for task_id, item_ids in value.items()} + + existing_recorded_easy = sanitize_recorded_map(diaries['tiers']['easy'].get('recordedItemIdsByTask', {})) + existing_recorded_medium = sanitize_recorded_map(diaries['tiers']['medium'].get('recordedItemIdsByTask', {})) + existing_recorded_hard = sanitize_recorded_map(diaries['tiers']['hard'].get('recordedItemIdsByTask', {})) + existing_recorded_elite = sanitize_recorded_map(diaries['tiers']['elite'].get('recordedItemIdsByTask', {})) + + imported_recorded_easy = sanitize_recorded_map(recorded_item_ids_by_tier.get('easy', {})) + imported_recorded_medium = sanitize_recorded_map(recorded_item_ids_by_tier.get('medium', {})) + imported_recorded_hard = sanitize_recorded_map(recorded_item_ids_by_tier.get('hard', {})) + imported_recorded_elite = sanitize_recorded_map(recorded_item_ids_by_tier.get('elite', {})) + + merged_recorded_easy = {**existing_recorded_easy, **imported_recorded_easy} + merged_recorded_medium = {**existing_recorded_medium, **imported_recorded_medium} + merged_recorded_hard = {**existing_recorded_hard, **imported_recorded_hard} + merged_recorded_elite = {**existing_recorded_elite, **imported_recorded_elite} + coll.update_one({'username': username}, { '$set': { 'tiers.easy.completedTasks': [], @@ -633,6 +741,10 @@ def update_imported_tasks(username: str, all_tasks: list, username2: str): 'tiers.medium.completedTasks': all_tasks[1], 'tiers.hard.completedTasks': all_tasks[2], 'tiers.elite.completedTasks': all_tasks[3], + 'tiers.easy.recordedItemIdsByTask': merged_recorded_easy, + 'tiers.medium.recordedItemIdsByTask': merged_recorded_medium, + 'tiers.hard.recordedItemIdsByTask': merged_recorded_hard, + 'tiers.elite.recordedItemIdsByTask': merged_recorded_elite, } }) @@ -640,19 +752,47 @@ def update_imported_tasks(username: str, all_tasks: list, username2: str): for diary in diaries['tiers']['easy']['completedTasks']: if diary['id'] in easy_diaries: - __set_task_complete(username, 'easyTasks', diary['id'], True) + __set_task_complete( + username, + 'easyTasks', + diary['id'], + True, + __datetime_to_iso(diary.get('completedDate')), + diary.get('completedItemIds', []), + ) for diary in diaries['tiers']['medium']['completedTasks']: if diary['id'] in medium_diaries: - __set_task_complete(username, 'mediumTasks', diary['id'], True) + __set_task_complete( + username, + 'mediumTasks', + diary['id'], + True, + __datetime_to_iso(diary.get('completedDate')), + diary.get('completedItemIds', []), + ) for diary in diaries['tiers']['hard']['completedTasks']: if diary['id'] in hard_diaries: - __set_task_complete(username, 'hardTasks', diary['id'], True) + __set_task_complete( + username, + 'hardTasks', + diary['id'], + True, + __datetime_to_iso(diary.get('completedDate')), + diary.get('completedItemIds', []), + ) for diary in diaries['tiers']['elite']['completedTasks']: if diary['id'] in elite_diaries: - __set_task_complete(username, 'eliteTasks', diary['id'], True) + __set_task_complete( + username, + 'eliteTasks', + diary['id'], + True, + __datetime_to_iso(diary.get('completedDate')), + diary.get('completedItemIds', []), + ) ''' @@ -1346,7 +1486,6 @@ def test(): valid = False if not valid: - print("Invalid data type for user: ", result['username']) task_coll.update_one( {"username" : result['username']}, {"$set": { @@ -1379,9 +1518,8 @@ def fix_gerni(): data.add(task_id) else: to_remove.add(task_id) - print(result['username'], tier, i, task_id) if to_remove: - print(to_remove) + pass # for _id in to_remove: # task_coll.update_one( # { @@ -1406,7 +1544,6 @@ def copy_tier_tasks(source_username: str, target_username: str, tiers: list[str] source_doc = collection.find_one({"username": source_username}) if not source_doc: - print(f"Source user '{source_username}' not found.") return updates = {} @@ -1422,13 +1559,10 @@ def copy_tier_tasks(source_username: str, target_username: str, tiers: list[str] updates[f"tiers.{tier}.currentTask"] = tier_data["currentTask"] if updates: - result = collection.update_one( + collection.update_one( {"username": target_username}, {"$set": updates} ) - print(f"Updated {result.modified_count} document(s).") - else: - print("No tasks to copy from source document.") if __name__ == "__main__": copy_tier_tasks("AreYaTasking", 'Gerni Task2', tiers=["easy", "medium", "hard", "elite", "master"]) diff --git a/task_types.py b/task_types.py index 3eba243..96dfbb0 100644 --- a/task_types.py +++ b/task_types.py @@ -37,11 +37,13 @@ class UserCompletedTask: id: str assigned_date: datetime = None completed_date: datetime = None + completed_item_ids: list[int] = None @dataclass class UserTaskList: current_task: UserCurrentTask completed_tasks: list[UserCompletedTask] + recorded_item_ids_by_task: dict[str, list[int]] = None @dataclass class TierProgress: @@ -50,7 +52,11 @@ class TierProgress: total_complete: int class PageTask: - def __init__(self, is_completed: bool, is_current: bool, task_data: TaskData): + def __init__(self, is_completed: bool, is_current: bool, task_data: TaskData, + completed_date = None, completed_date_iso: str = None, + verification_item_ids: list[int] = None, + verification_required_count: int = None, + completed_item_ids: list[int] = None): self.name = task_data.name self.is_completed = is_completed self.id = task_data.id @@ -58,6 +64,11 @@ def __init__(self, is_completed: bool, is_current: bool, task_data: TaskData): self.wiki_link = task_data.wiki_link self.image_link = task_data.image_link self.tip = task_data.tip + self.completed_date = completed_date + self.completed_date_iso = completed_date_iso + self.verification_item_ids = verification_item_ids or [] + self.verification_required_count = verification_required_count + self.completed_item_ids = completed_item_ids or [] @dataclass class LeaderboardEntry: diff --git a/taskapp.py b/taskapp.py index 8166d16..e268ff0 100644 --- a/taskapp.py +++ b/taskapp.py @@ -2,6 +2,7 @@ from flask_recaptcha import ReCaptcha # type: ignore import jwt import datetime +import json import bcrypt # type: ignore import config from functools import wraps @@ -146,7 +147,6 @@ def api_task_list(): @app.route('/api/v1/resource/completed_tasks/') def api_completed_tasks(username): user_data = get_user(username) - print(username) return jsonify({'message': { 'easy' : user_data.easy.completed_tasks, 'medium' : user_data.medium.completed_tasks, @@ -318,9 +318,8 @@ def register(): else: return render_template('registerV2.html') - except Exception as e: + except Exception: error = 'An error occurred while processing your request, please try again.' - print(e) return error @@ -350,7 +349,7 @@ def register_user(): error = 'Please fill out the captcha!' return {'success' : False, 'error' : error} - except Exception as e: + except Exception: error = 'An error occurred while processing your request, please try again.' return error @@ -390,9 +389,8 @@ def login(): else: return render_template('loginV2.html') - except Exception as e: + except Exception: error = "An error occurred while processing your request, please try again." - print(e) flash(error) return render_template('loginV2.html') @@ -499,12 +497,27 @@ def collection_log_check(): def collection_log_import(): form_data = request.form rs_username = form_data['username'] - easy_import = check_logs(rs_username, tasklists.list_for_tier('easy'), 'import') - medium_import = check_logs(rs_username, tasklists.list_for_tier('medium'), 'import') - hard_import = check_logs(rs_username, tasklists.list_for_tier('hard'), 'import') - elite_import = check_logs(rs_username, tasklists.list_for_tier('elite'), 'import') + easy_import_result = check_logs(rs_username, tasklists.list_for_tier('easy'), 'import-recorded') + medium_import_result = check_logs(rs_username, tasklists.list_for_tier('medium'), 'import-recorded') + hard_import_result = check_logs(rs_username, tasklists.list_for_tier('hard'), 'import-recorded') + elite_import_result = check_logs(rs_username, tasklists.list_for_tier('elite'), 'import-recorded') + easy_import = easy_import_result.get('completedTasks', []) + medium_import = medium_import_result.get('completedTasks', []) + hard_import = hard_import_result.get('completedTasks', []) + elite_import = elite_import_result.get('completedTasks', []) all_tasks = [easy_import, medium_import, hard_import, elite_import] - update = update_imported_tasks(session['username'], all_tasks, form_data['username']) + recorded_item_ids_by_tier = { + 'easy': easy_import_result.get('recordedItemIdsByTask', {}), + 'medium': medium_import_result.get('recordedItemIdsByTask', {}), + 'hard': hard_import_result.get('recordedItemIdsByTask', {}), + 'elite': elite_import_result.get('recordedItemIdsByTask', {}), + } + update = update_imported_tasks( + session['username'], + all_tasks, + form_data['username'], + recorded_item_ids_by_tier, + ) return render_template('collection_log_import.html', rs_username = rs_username, @@ -629,7 +642,7 @@ def complete_unofficial(): def single_task_list(list_title, task_type): user_info = BasePageInfo() - progress = get_task_progress(user_info.username) + progress = user_info.progress tasks = user_info.user.page_tasks(task_type) context = { 'easy': progress['easy']['percent_complete'], @@ -742,7 +755,24 @@ def update(): user_info = BasePageInfo() task_id = request.form['id'] tier = request.form['tier'] - manual_complete_tasks(session['username'], tier, task_id) + completed_at_iso = request.form.get('completedAtISO') + completed_item_ids_raw = request.form.get('completedItemIds') + completed_item_ids = [] + if completed_item_ids_raw: + try: + parsed = json.loads(completed_item_ids_raw) + if isinstance(parsed, list): + completed_item_ids = parsed + except json.JSONDecodeError: + completed_item_ids = [] + + completed_task = manual_complete_tasks( + session['username'], + tier, + task_id, + completed_at_iso, + completed_item_ids, + ) progress = get_task_progress(user_info.username) data = { 'easy': progress['easy']['percent_complete'], @@ -753,6 +783,8 @@ def update(): 'passive' : progress['passive']['percent_complete'], 'extra' : progress['extra']['percent_complete'], 'allPets' : progress['all_pets']['percent_complete'], + 'completedAtISO': completed_task.get('completed_date_iso') if completed_task else None, + 'completedItemIds': completed_task.get('completed_item_ids') if completed_task else [], } return data @@ -967,8 +999,8 @@ def reset_request(): if session.get('username'): return redirect(url_for('dashboard')) return render_template('password-request.html') - except Exception as e: - print(str(e)) + except Exception: + return None @app.route("/reset-password/request/", methods=['POST']) def reset_password_request(): diff --git a/templates/task-list.html b/templates/task-list.html index e403e01..02fe4d6 100644 --- a/templates/task-list.html +++ b/templates/task-list.html @@ -44,13 +44,20 @@

{{task_type|capitalize}} Task List

+ {% if task.verification_item_ids %} + + {% endif %} + + + +

{{task.name}}

-

--/--/---- --:--

+

--/--/---- --:--

@@ -100,6 +107,13 @@

{{task_type|capitalize}} Task List

+ {% if task.verification_item_ids %} + + {% endif %} + + + +
@@ -114,6 +128,32 @@

{{task_type|capitalize}} Task List

+ +
+ +
+

+
+
+
+
+
+
+
+ + +
+
+ +
+ +