Exercício 13-06:

Modifique o programa 13.13 para ler e gravar dados de um banco de dados. Modificando a classe GerenteDeSites ou criando outra, você deve facilmente poder trocar o armazenamento de JSON para um banco de dados como o Sqlite.

Resposta:

##############################################################################
# Parte do livro Introdução à Programação com Python
# Autor: Nilo Ney Coutinho Menezes
# Editora Novatec (c) 2010-2024
# Quarta Edição - Março/2024 - ISBN 978-85-7522-886-9
#
# Site: https://python.nilo.pro.br/
#
# Arquivo: capitulo 13/exercicio-13-06.py
##############################################################################
import os.path
import sqlite3
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.messagebox import askquestion, showinfo, showerror

from uuid import uuid4
from datetime import date

# Todas as classes estão incluídas na resposta do exercício
# Apenas a classe App foi alteradas e
# a classe GerenteDeSitesDB foi criada


class Data(ttk.Frame):
    def __init__(self, parent, min_ano=00, max_ano=40):
        super().__init__(parent)
        self.min_ano = min_ano
        self.max_ano = max_ano
        self.dia = tk.StringVar()
        self.mês = tk.StringVar()
        self.ano = tk.StringVar()
        self.cria_controles()

    def set(self, data):
        dia, mês, ano = data.split("-")
        self.dia.set(dia)
        self.mês.set(mês)
        self.ano.set(ano)

    def get(self):
        return f"{self.dia.get()}-{self.mês.get()}-{self.ano.get()}"

    def cria_controles(self):
        self.c_dia = ttk.Combobox(
            self,
            textvariable=self.dia,
            width=3,
            values=[f"{d:02d}" for d in range(1, 32)],
            state="readonly",
        )
        self.c_dia.pack(side=tk.LEFT)
        self.c_mes = ttk.Combobox(
            self,
            textvariable=self.mês,
            values=[f"{m:02d}" for m in range(1, 13)],
            width=3,
            state="readonly",
        )
        self.c_mes.pack(side=tk.LEFT)
        self.c_ano = ttk.Combobox(
            self,
            textvariable=self.ano,
            values=[f"{m:02d}" for m in range(self.min_ano, self.max_ano + 1)],
            width=6,
            state="readonly",
        )
        self.c_ano.pack(side=tk.LEFT)


class Site:
    def __init__(self, /, url=None, categoria=None, data=None, id=None, notas=None):
        if id is None:
            id = str(uuid4())
        self.id = id
        if data is None:
            data = date.today().strftime("%d-%m-%y")
        self.data = data
        self.url = url
        self.categoria = categoria
        self.notas = notas

    def __str__(self):
        return f"Site {self.id} {self.url} {self.categoria} {self.notas}"


class GerenteDeSitesDB:
    def __init__(self):
        self.nome = "agenda.db"
        existe = os.path.exists(self.nome)
        self.conecta()
        if not existe:
            self.cria_tabela()

    def cria_tabela(self):
        self.conexão.execute(
            "CREATE TABLE sites (id TEXT PRIMARY KEY, url TEXT, categoria TEXT, data TEXT, notas TEXT)"
        )
        self.conexão.commit()

    def conecta(self):
        self.conexão = sqlite3.connect(self.nome)

    def desconecta(self):
        self.conexão.close()

    def salva(self, site):
        self.conexão.execute(
            """INSERT INTO sites (id, url, categoria, data, notas) VALUES (?, ?, ?, ?, ?)
               ON CONFLICT(id) DO UPDATE SET url=excluded.url, categoria=excluded.categoria, data=excluded.data, notas=excluded.notas""",
            (site.id, site.url, site.categoria, site.data, site.notas),
        )
        self.conexão.commit()
        self.sites[site.id] = site

    def apaga(self, id):
        self.conexão.execute("DELETE FROM sites WHERE id=?", (id,))
        self.conexão.commit()
        if id in self.sites:
            del self.sites[id]

    def carrega(self):
        self.sites = {}
        q = self.conexão.execute("SELECT * FROM sites")
        for site in q.fetchall():
            novo_site = Site(
                id=site[0],
                url=site[1],
                categoria=site[2],
                data=site[3],
                notas=site[4],
            )
            self.sites[novo_site.id] = novo_site
        return self.sites


class Janela(tk.Toplevel):
    MIN_X = 300
    MIN_Y = 300
    PADXY = 10

    def __init__(self, parent, site, on_change=None):
        super().__init__(parent)
        self.geometry(f"{self.MIN_X}x{self.MIN_Y}")
        self.title("Site")
        self.padding = {"padx": self.PADXY, "pady": self.PADXY}
        self.on_change = on_change
        self.cria_controles()
        self.minsize(self.MIN_X, self.MIN_Y)
        self.captura_site(site)

    def captura_site(self, site):
        self.site = site or Site()
        self.url.set(self.site.url or "")
        self.data.set(self.site.data)
        self.categoria.set(self.site.categoria or "")
        self.t_notas.delete("1.0", tk.END)
        self.t_notas.insert("1.0", self.site.notas or "")

    def cria_controles(self):
        self.f_url = ttk.Frame(self)
        self.f_url.grid(row=0, column=0, columnspan=3, sticky=tk.EW, **self.padding)
        self.l_url = ttk.Label(self.f_url, text="URL")
        self.l_url.pack(anchor=tk.W)
        self.url = tk.StringVar()
        self.e_url = ttk.Entry(self.f_url, textvariable=self.url)
        self.e_url.pack(fill=tk.X, expand=True)
        self.f_categoria = ttk.Frame(self)
        self.f_categoria.grid(row=1, column=0, sticky=tk.W, **self.padding)
        self.l_categoria = ttk.Label(self.f_categoria, text="Categoria")
        self.l_categoria.pack(anchor=tk.W)
        self.categoria = tk.StringVar()
        self.e_categoria = ttk.Entry(self.f_categoria, textvariable=self.categoria)
        self.e_categoria.pack()
        self.f_data = ttk.Frame(self)
        self.f_data.grid(row=1, column=2, sticky=tk.E, **self.padding)
        self.l_data = ttk.Label(self.f_data, text="Data")
        self.l_data.pack(anchor=tk.W)
        self.data = Data(self.f_data)
        self.data.pack()
        self.f_notas = ttk.Frame(self)
        self.f_notas.grid(row=2, column=0, columnspan=3, sticky=tk.NSEW, **self.padding)
        self.l_notas = ttk.Label(self.f_notas, text="Notas")
        self.l_notas.pack(anchor=tk.W)
        self.t_notas = tk.Text(self.f_notas, height=3)
        self.t_notas.pack(expand=True, fill=tk.BOTH)
        self.grid_columnconfigure(0, weight=2)
        self.grid_columnconfigure(2, weight=1)
        self.grid_rowconfigure(2, weight=1)
        self.b_frame = ttk.Frame(self)
        self.b_frame.grid(row=3, column=0, columnspan=3, **self.padding)
        self.b_ok = ttk.Button(self.b_frame, text="Ok", command=self.ok)
        self.b_ok.pack(side=tk.LEFT)
        self.b_cancelar = ttk.Button(self.b_frame, text="Cancelar", command=self.fecha)
        self.b_cancelar.pack(side=tk.LEFT)

    def fecha(self):
        self.destroy()

    def ok(self):
        try:
            self.valida_url()
        except ValueError as e:
            showerror("Erro", f"URL inválida\n{e}")
            return
        try:
            self.validar_data()
        except ValueError:
            showerror("Erro", "Data inválida")
            return
        self.site.url = self.url.get()
        self.site.categoria = self.categoria.get()
        self.site.data = self.data.get()
        self.site.notas = self.t_notas.get("1.0", tk.END)
        if self.on_change:
            self.on_change(self.site)
        self.fecha()

    def valida_url(self):
        url = self.url.get()
        if not url:
            raise ValueError("URL não pode ser vazia")
        if not (url.startswith("http://") or url.startswith("https://")):
            raise ValueError("URL deve começar com http:// ou https://")

    def validar_data(self):
        dia, mês, ano = self.data.get().split("-")
        data = date(int(ano), int(mês), int(dia))
        return data


class App(tk.Tk):
    MIN_X = 800
    MIN_Y = 200

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.title("Controle de sites interessantes")
        self.geometry(f"{self.MIN_X}x{self.MIN_Y}")
        self.cria_controles()
        self.gerente = GerenteDeSitesDB()
        self.gerente.carrega()
        self.mostra_dados()
        self.minsize(self.MIN_X, self.MIN_Y)

    def cria_controles(self):
        self.quadro = ttk.Frame(self)
        self.quadro.grid(
            row=0, column=0, columnspan=2, padx=10, pady=10, sticky=tk.NSEW
        )
        self.grid_rowconfigure(0, weight=1)
        self.tabela = ttk.Treeview(
            self.quadro, columns=["url", "categoria", "data", "notas"], show="headings"
        )
        self.tabela.heading("url", text="URL")
        self.tabela.heading("categoria", text="Categoria")
        self.tabela.column("categoria", anchor=tk.CENTER)
        self.tabela.heading("data", text="Data")
        self.tabela.column("data", anchor=tk.CENTER)
        self.tabela.heading("notas", text="Notas")
        self.tabela.grid(row=0, column=0, sticky=tk.NSEW)
        self.tabela.config(selectmode="browse")
        scrollbar = ttk.Scrollbar(
            self.quadro, orient=tk.VERTICAL, command=self.tabela.yview
        )
        self.tabela.configure(yscroll=scrollbar.set)
        self.tabela.bind("<Double-Button-1>", self.abre_janela)
        scrollbar.grid(row=0, column=1, sticky=tk.NS)
        self.quadro.grid_columnconfigure(0, weight=1)
        self.quadro.grid_rowconfigure(0, weight=1)
        self.quadro.pack(expand=True, fill=tk.BOTH, padx=10, pady=10)
        self.menu = tk.Menu(self)
        # self.m_arquivo = tk.Menu(self.menu, tearoff=0)
        # self.m_arquivo.add_command(label="Ler", command=self.lê)
        # self.m_arquivo.add_command(label="Gravar", command=self.grava)
        self.m_sites = tk.Menu(self.menu, tearoff=0)
        self.m_sites.add_command(label="Adiciona", command=self.adiciona)
        self.m_sites.add_command(label="Apaga", command=self.apaga)
        self.m_sites.add_separator()
        self.m_sites.add_command(label="Apaga todos", command=self.apaga_todos)
        # self.menu.add_cascade(label="Arquivo", menu=self.m_arquivo)
        self.menu.add_cascade(label="Sites", menu=self.m_sites)
        self.menu.add_command(label="Sobre", command=self.sobre)
        self.config(menu=self.menu)

    def adiciona(self):
        self.mostra_site(None)

    def apaga(self):
        if id_selecionado := self.pega_selecionado():
            self.gerente.apaga(id_selecionado)
            self.tabela.delete(id_selecionado)

    def apaga_todos(self):
        if (
            askquestion(
                title="Apagar todos os sites", message="Confirma apagar todos os sites?"
            )
            == "yes"
        ):
            self.limpa()

    def limpa(self):
        for id in self.gerente.sites.keys():
            self.gerente.apaga(id)
        self.gerente.sites.clear()
        self.tabela.delete(*self.tabela.get_children())

    def sobre(self):
        showinfo(
            title="Sobre",
            message="Introdução à Programação com Python.\nhttps://python.nilo.pro.br",
        )

    def adiciona_site_a_tabela(self, site):
        self.gerente.salva(site)
        self.tabela.insert(
            "",
            tk.END,
            values=(site.url, site.categoria, site.data, site.notas),
            iid=site.id,
        )

    def mostra_dados(self):
        for site in self.gerente.sites.values():
            self.adiciona_site_a_tabela(site)

    def pega_selecionado(self):
        if item_selecionado := self.tabela.selection():
            id_selecionado = item_selecionado[0]
            return id_selecionado
        return None

    def abre_janela(self, event):
        if id_selecionado := self.pega_selecionado():
            site = self.gerente.sites[id_selecionado]
        else:
            site = None
        self.mostra_site(site)

    def mostra_site(self, site):
        self.janela = Janela(self, site, on_change=self.atualiza)
        self.janela.grab_set()

    def atualiza(self, site):
        if self.tabela.exists(site.id):
            self.tabela.item(
                site.id,
                text="",
                values=(site.url, site.categoria, site.data, site.notas),
            )
        else:
            self.adiciona_site_a_tabela(site)
        self.gerente.salva(site)


App().mainloop()
Clique aqui para baixar o arquivo