Search code examples
pythonjsonpython-3.xtkinter

Error removing data in json using python tkinter


I'm trying to create a to-do list (TODO) application in Python using tkinter. The problem is that, when trying to remove data from JSON files using the 'delete' function, the code is not removing data from the cards.json file or the ids.json file.

main.py

import datetime
from random import randint
from tkinter import *
from tkinter import messagebox
from extras_without_gui import save_ids, load_ids, clear_terminal, load_cards, save_cards

clear_terminal()
ids_data = load_ids()
IDs = ids_data['IDs']
Cards = load_cards()


def save(id, title, description):
    today_date = datetime.date.today()

    card = {
        f"{id}": {
            "Title": title,
            "Description": description,
            "Tag": [],
            "Creation_Date": today_date.strftime("%d-%m-%Y"),
            "Creation_Time": datetime.datetime.now().strftime("%H:%M"),
            "Last_Modification_Date": "00-00-0000",
            "Last_Modification_Time": "00:00",
            "ID": id
        }
    }

    ids_data['IDs'].append(id)
    save_ids(ids_data)

    Cards.update(card)
    save_cards(Cards)


def delete_task(self):
    task_id = int(self.id_to_delete.get())

    if task_id not in self.IDs:
        messagebox.showerror("Error", "Task ID not found.")
        return

    delete_confirm = messagebox.askquestion("Delete Task", "Are you sure you want to delete this task?")
    if delete_confirm == "yes":
        Cards.pop(str(task_id), " ")
        save_cards(Cards)
        self.IDs.remove(str(task_id))
        self.ids_data['IDs'] = self.IDs
        save_ids(self.ids_data)
        self.id_to_delete.delete(0, 'end')
        messagebox.showinfo("Success", "Task deleted successfully.")
    else:
        self.id_to_delete.delete(0, 'end')
        messagebox.showinfo("Success", "Task not deleted.")


class Application:
    def __init__(self, master=None):
        self.master = master
        self.ids_data = load_ids()
        self.IDs = self.ids_data['IDs']
        self.screen()

    def destroy_window(self):
        try:
            self.master.destroy()
        except AttributeError:
            pass

    def screen(self):
        self.destroy_window()
        self.master = Tk()
        self.master.title("TODO Python")
        self.master.geometry("600x600+250+50")

        screen_1 = Frame(self.master)
        screen_1["pady"] = 10
        screen_1.pack()

        screen_2 = Frame(self.master)
        screen_2["pady"] = 10
        screen_2.pack()

        screen_3 = Frame(self.master)
        screen_3.pack()

        screen_4 = Frame(self.master)
        screen_4["pady"] = 100
        screen_4.pack()

        title = Label(screen_1, text='TODO Python')
        title["font"] = ("Arial", 20, "bold")
        title.pack()

        create_button = Button(screen_2, text='Create', command=self.create_task)
        create_button["font"] = ("Arial", 10, "bold")
        create_button["fg"] = "black"
        create_button["bg"] = "white"
        create_button.pack()

        delete_button = Button(screen_3, text='Delete', command=self.delete_screen)
        delete_button["font"] = ("Arial", 10, "bold")
        delete_button["fg"] = "black"
        delete_button["bg"] = "white"
        delete_button.pack()

        exit_button = Button(screen_4, text='Exit', command=self.destroy_window)
        exit_button["font"] = ("Arial", 10, "bold")
        exit_button["fg"] = "black"
        exit_button["bg"] = "white"
        exit_button.pack()

    def create_task(self):
        today_date = datetime.date.today()

        self.destroy_window()
        self.master = Tk()
        self.master.title("Create New Task")
        self.master.geometry("600x600+250+50")

        title = Label(self.master, text='Create New Task')
        title["font"] = ("Arial", 20, "bold")
        title.pack()

        screen_1 = Frame(self.master)
        screen_1["pady"] = 10
        screen_1.pack()

        screen_2 = Frame(self.master)
        screen_2["pady"] = 10
        screen_2.pack()

        screen_3 = Frame(self.master)
        screen_3["pady"] = 10
        screen_3.pack()

        screen_4 = Frame(self.master)
        screen_4["pady"] = 10
        screen_4.pack()

        screen_5 = Frame(self.master)
        screen_5["pady"] = 10
        screen_5.pack()

        screen_6 = Frame(self.master)
        screen_6["pady"] = 10
        screen_6.pack()

        title_label = Label(screen_1, text='Task Title')
        title_label["font"] = ("Arial", 10, "bold")
        title_label.pack()

        self.title_task = Entry(screen_1)
        self.title_task["font"] = ("Arial", 10, "bold")
        self.title_task["width"] = 50
        self.title_task.pack()

        description_label = Label(screen_2, text='Task Description')
        description_label["font"] = ("Arial", 10, "bold")
        description_label.pack()

        self.description_task = Text(screen_2, width=50, height=10)
        self.description_task["font"] = ("Arial", 10, "bold")
        self.description_task.pack()

        creation_date = Label(screen_3, text=f'Creation Date: {today_date.strftime("%d-%m-%Y")}')
        creation_date["font"] = ("Arial", 10, "bold")
        creation_date.pack()

        creation_time = Label(screen_3, text=f'Creation Time: {datetime.datetime.now().strftime("%H:%M")}')
        creation_time['font'] = ("Arial", 10, "bold")
        creation_time.pack()

        id = self.generate_new_id()

        id_label = Label(screen_4, text=f'ID: {id}')
        id_label["font"] = ("Arial", 10, "bold")
        id_label.pack()

        save_button = Button(screen_5, text='Save', command=self.save_data)
        save_button["font"] = ("Arial", 10, "bold")
        save_button["fg"] = "black"
        save_button["bg"] = "white"
        save_button["width"] = 6
        save_button.pack()

        back_button = Button(screen_6, text='Back', command=self.screen)
        back_button["font"] = ("Arial", 10, "bold")
        back_button["fg"] = "black"
        back_button["bg"] = "white"
        back_button["width"] = 6
        back_button.pack()

    def save_data(self):
        title = self.title_task.get()
        description = self.description_task.get("1.0", "end

-1c")
        if len(title) == 0 or len(description) == 0:
            messagebox.showerror("Error", "Please fill in all fields.")
            return
        id = self.generate_new_id()
        save(id, title, description)
        self.title_task.delete(0, 'end')
        self.description_task.delete("1.0", "end")
        self.screen()

    @staticmethod
    def generate_new_id():
        id = randint(10000, 99999)
        while id in IDs:
            id = randint(10000, 99999)
        return id

    def delete_screen(self):
        self.destroy_window()
        self.master = Tk()
        self.master.title("Delete Task")
        self.master.geometry("600x600+250+50")

        self.screen_delete_1 = Frame(self.master)
        self.screen_delete_1["pady"] = 10
        self.screen_delete_1.pack()

        self.screen_delete_2 = Frame(self.master)
        self.screen_delete_2["pady"] = 10
        self.screen_delete_2.pack()

        self.screen_delete_3 = Frame(self.master)
        self.screen_delete_3["pady"] = 10
        self.screen_delete_3.pack()

        self.screen_delete_4 = Frame(self.master)
        self.screen_delete_4["pady"] = 10
        self.screen_delete_4.pack()

        self.screen_delete_5 = Frame(self.master)
        self.screen_delete_5.pack()

        self.title = Label(self.screen_delete_1, text='Delete Task')
        self.title["font"] = ("Arial", 20, "bold")
        self.title.pack()

        self.label_delete = Label(self.screen_delete_2, text='Task ID: ')
        self.label_delete["font"] = ("Arial", 10, "bold")
        self.label_delete.pack()

        self.id_to_delete = Entry(self.screen_delete_3)
        self.id_to_delete["font"] = ("Arial", 10, "bold")
        self.id_to_delete["width"] = 50
        self.id_to_delete.pack()

        self.delete_button = Button(self.screen_delete_4, text='Delete', command=lambda: delete_task(self))
        self.delete_button["font"] = ("Arial", 10, "bold")
        self.delete_button["fg"] = "black"
        self.delete_button["bg"] = "white"
        self.delete_button["width"] = 6
        self.delete_button.pack()

        self.back_button = Button(self.screen_delete_5, text='Back', command=self.screen)
        self.back_button["font"] = ("Arial", 10, "bold")
        self.back_button["fg"] = "black"
        self.back_button["bg"] = "white"
        self.back_button["width"] = 6
        self.back_button.pack()


def main():
    root = Tk()
    Application(root)
    root.mainloop()


if __name__ == '__main__':
    main()

extras.py

"""
Script to handle loading, saving, and managing card data.

This script includes functions for loading and saving card information from/to JSON files,
as well as functions for clearing the terminal and displaying card details.
"""
from json import load
from json import dump
from json import JSONDecodeError
import platform
from os import system
from os import path
from os import getcwd


def load_cards():
    """
    Load card data from the JSON file.

    Returns:
    - cards (dict): Dictionary containing card data.
    """
    try:
        with open('Data/cards.json', 'r', encoding='utf-8') as cards_info:
            cards = load(cards_info)
    except JSONDecodeError:
        print('JSON formatting error')
    except FileNotFoundError:
        print('File not found')
    except Exception as error:
        print(f'ERROR: {error}')
    else:
        return cards


def save_cards(card):
    """
    Save card data to the JSON file.

    Args:
    - card (dict): Card data to be saved.
    """
    file_path = path.join(getcwd(), 'Data', 'cards.json')
    try:
        with open(file_path, "w", encoding="utf-8") as cards_info:
            dump(card, cards_info, indent=4)
    except JSONDecodeError:
        print('JSON formatting error')
    except FileNotFoundError:
        print('File not found')
    except Exception as error:
        print(f'ERROR: {error}')


def load_ids():
    """
    Load IDs from the JSON file.

    Returns:
    - IDs (dict): Dictionary containing IDs.
    """
    try:
        with open('Data/ids.json', 'r', encoding='utf-8') as data:
            IDs = load(data)
    except JSONDecodeError:
        print('JSON formatting error')
    except FileNotFoundError:
        print('File not found')
    except Exception as error:
        print(f'ERROR: {error}')
    else:
        return IDs


def save_ids(ids):
    """
    Save IDs to the JSON file.

    Args:
    - ids (dict): IDs data to be saved.
    """
    file_path = path.join(getcwd(), 'Data', 'ids.json')
    try:
        with open(file_path, 'w', encoding='utf-8') as data:
            dump(ids, data, indent=4)
    except JSONDecodeError:
        print('JSON formatting error')
    except FileNotFoundError:
        print('File not found')
    except Exception as error:
        print(f'ERROR: {error}')


def clear_terminal():
    """
    Clear the terminal based on the operating system.
    """
    try:
        os_name = str(platform.system())
        try:
            if os_name == "Linux" or os_name == "Darwin":
                system("clear")
            elif os_name == "Windows":
                system("cls")
            else:
                print("Unknown system")
        except Exception as e:
            print(f'Error clearing terminal: {e}')
            exit(1)
    except Exception as os_error:
        print(f'Error identifying system\nError: {os_error}')


def show_cards(cards):
    """
    Show card details.

    Args:
    - Cards (dict): Dictionary containing card data.
    """
    print('All cards: \n')
    for key, card in cards.items():
        for field, content in card.items():
            if isinstance(content, list):
                tags = ', '.join(content)
                print(f'{field}: {tags}')
            else:
                print(f'{field}: {content}')
        print('\n')

cards.json

{
    "15728": {
        "Title": "0",
        "Description": "0",
        "Tag": [],
        "Creation_Date": "19-02-2024",
        "Creation_Time": "19:38",
        "Last_Modification_Date": "00-00-0000",
        "Last_Modification_Time": "00:00",
        "ID": 15728
    }
}

ids.json

{
    "IDs": [
        15728
    ]
}

it must ask for the id and then with the id go to the ids.json file and remove it from the array and in cards.json remove the entire content of the presented id


Solution

  • When I try this code, removing a recently-added task fails with "Task ID not found.".

    The problem is that you have two separate lists of IDs, one loaded here:

    clear_terminal()
    ids_data = load_ids()
    IDs = ids_data['IDs']
    Cards = load_cards()
    

    and the other here:

    class Application:
        def __init__(self, master=None):
            self.master = master
            self.ids_data = load_ids()
            self.IDs = self.ids_data['IDs']
            self.screen()
    

    Furthermore, the save function changes the JSON files, but doesn't cause the IDs to be reloaded, so delete can't find it.

    To fix it, change save to write to Application.IDs:

    # Add the 'self' parameter.
    def save(self, id, title, description):
        # Add this line.
        self.IDs.append(id)
    
        today_date = datetime.date.today()
        ...
    

    and the way you call it:

        def save_data(self):
            title = self.title_task.get()
            description = self.description_task.get("1.0", "end-1c")
            if len(title) == 0 or len(description) == 0:
                messagebox.showerror("Error", "Please fill in all fields.")
                return
            id = self.generate_new_id()
    
            ##############
            # Add the 'self' parameter here.
            save(self, id, title, description)
            ##############
    
            self.title_task.delete(0, 'end')
            self.description_task.delete("1.0", "end")
            self.screen()
    

    And I'd highly recommend you moving the save and load functions inside the Application class, as methods, instead of separate functions. It's unusual to have a free floating function that takes a 'self' parameter.

    Finally, there's another issue where delete_task tries to remove a string ID, when they should be ints:

        delete_confirm = messagebox.askquestion("Delete Task", "Are you sure you want to delete this task?")
        if delete_confirm == "yes":
            Cards.pop(str(task_id), " ")
            save_cards(Cards)
            # Change here:
            self.IDs.remove(task_id)
    

    --

    Alternatively, you can also reload the list of IDs before each operation. As long as the list is not overwhelmingly long, it'd be pretty much instant, and avoid many errors and forms of data corruption.