diff --git a/ejercicios/beautifulsoup/ej4/data/recipes.bd b/ejercicios/beautifulsoup/ej4/data/recipes.bd index 861aaa4..dbe7619 100644 Binary files a/ejercicios/beautifulsoup/ej4/data/recipes.bd and b/ejercicios/beautifulsoup/ej4/data/recipes.bd differ diff --git a/ejercicios/beautifulsoup/ej4/src/main.py b/ejercicios/beautifulsoup/ej4/src/main.py index b9f10f4..3d0373c 100644 --- a/ejercicios/beautifulsoup/ej4/src/main.py +++ b/ejercicios/beautifulsoup/ej4/src/main.py @@ -7,7 +7,7 @@ from datetime import datetime import locale from db import DBManager, DBAttr -#from ui import RecipesUI +from ui import RecipesUI from __ssl import init_ssl from config import * @@ -28,53 +28,105 @@ def create_tables(): dbm.create_table("recipes", recipes_attr) +def parse_duration(duration): + if not duration: + return None + + duration = duration.strip().lower() + + hours = 0 + minutes = 0 + + h_match = re.search(r"(\d+)h", duration) + m_match = re.search(r"(\d+)m", duration) + + if h_match: + hours = int(h_match.group(1)) + + if m_match: + minutes = int(m_match.group(1)) + + return hours * 60 + minutes + +def parse_duration_inverse(minutes): + if minutes is None: + return None + m = minutes % 60 + h = (minutes - m) // 60 + return f"{h}h {m}m" if h != 0 else f"{m}m" + def persist_recipes(): f = urllib.request.urlopen(RECIPES_URL) bs = BeautifulSoup(f, "lxml") results = bs.find_all("div", attrs={"data-js-selector": "resultado"}) for div in results: - print(div) title_a = div.a title = title_a.string.strip() info_div = div.find("div", class_="info_snippet") difficulty = info_div.find("span").get_text(strip=True) if info_div and info_div.find("span") else None properties = div.find("div", class_="properties") - duration = properties.find("span", class_=["property", "duracion"]).string.strip() if properties and properties.find("span", class_=["property", "duracion"]) else None - units = properties.find("span", class_=["property", "unidades"]).string.strip() if properties and properties.find("span", class_=["property", "unidades"]) else None + duration = properties.find("span", class_="duracion").string.strip() if properties and properties.find("span", class_="duracion") else None + units = properties.find("span", class_="unidades").string.strip() if properties and properties.find("span", class_="unidades") else None details_link = title_a["href"] f2 = urllib.request.urlopen(details_link) bs2 = BeautifulSoup(f2, "lxml") details = bs2.find("div", class_="autor").find("div", class_="nombre_autor") author = details.find("a").string - date_str = details.find("span").string + date_str = details.find("span").string.replace("Actualizado: ", "") updated_at = datetime.strptime(date_str, "%d %B %Y") - + dbm.insert("recipes", { "title": title, "difficulty": difficulty, "units": units, - "duration": duration, + "duration": parse_duration(duration), "author": author, "updated_at": updated_at }) - return dbm.count("recipes") + return dbm.count("recipes") - def main(): create_tables() - recipes_count = persist_recipes() - print(recipes_count) - #root = Tk() - #ui = RecipesUI(root) - - # def handle_action(action): - - #ui.callback = handle_action - #root.mainloop() - #dbm.close() - - print(dbm.get_all("recipes")) + root = Tk() + ui = RecipesUI(root) + + def handle_action(action): + match(action): + case "cargar": + resp = messagebox.askyesno(title="Cargar", message="Quieres cargar todos los datos de nuevo?") + if resp: + dbm.clear("recipes") + recipes_count = persist_recipes() + ui.info(f"Hay {recipes_count} recetas") + case "listar_recetas": + recipes = dbm.get_all("recipes") + for r in recipes: + r["units"] = str(r["units"]) + " personas" if r["units"] is not None else "Unknown personas" + r["duration"] = parse_duration_inverse(r["duration"]) + ui.show_list(recipes, ["title", "difficulty", "units", "duration"]) + case "buscar_autor": + def search_author(author): + recipes = [recipe for recipe in dbm.get_all("recipes") if author.lower() in recipe["author"].lower()] + for r in recipes: + r["units"] = str(r["units"]) + " personas" if r["units"] is not None else "Unknown personas" + r["duration"] = parse_duration_inverse(r["duration"]) + ui.show_list(recipes, ["title", "difficulty", "units", "duration", "author"]) + ui.ask_text("Buscar por autor: ", search_author) + case "buscar_fecha": + def search_date(date): + d = datetime.strptime(date, "%d/%m/%Y") + recipes = [recipe for recipe in dbm.get_all("recipes") + if d > datetime.strptime(recipe["updated_at"], "%Y-%m-%d %H:%M:%S")] + for r in recipes: + r["units"] = str(r["units"]) + " personas" if r["units"] is not None else "Unknown personas" + r["duration"] = parse_duration_inverse(r["duration"]) + ui.show_list(recipes, ["title", "difficulty", "units", "duration", "updated_at"]) + ui.ask_text("Buscar por fecha: ", search_date) + + ui.callback = handle_action + root.mainloop() + dbm.close() if __name__ == "__main__": main() \ No newline at end of file diff --git a/ejercicios/beautifulsoup/ej4/src/ui.py b/ejercicios/beautifulsoup/ej4/src/ui.py new file mode 100644 index 0000000..423e8c1 --- /dev/null +++ b/ejercicios/beautifulsoup/ej4/src/ui.py @@ -0,0 +1,79 @@ +import tkinter as tk +from tkinter import ttk, messagebox +from tkinter.scrolledtext import ScrolledText + +class RecipesUI(): + def __init__(self, root, title = "AII"): + self.root = root + self.root.title(title) + self.root.geometry("900x600") + + # Menu Principal + self.menu = tk.Menu(self.root) + self.root.config(menu=self.menu) + + # Menu Datos + datos_menu = tk.Menu(self.menu, tearoff=0) + datos_menu.add_command(label="Cargar", command=lambda: self.callback("cargar")) + datos_menu.add_separator() + datos_menu.add_command(label="Salir", command=self.root.quit) + self.menu.add_cascade(label="Datos", menu=datos_menu) + + # Menu Listar + listar_menu = tk.Menu(self.menu, tearoff=0) + listar_menu.add_command(label= "Recetas", command = lambda: self.callback("listar_recetas")) + self.menu.add_cascade(label="Listar", menu=listar_menu) + + # Menu Buscar + buscar_menu = tk.Menu(self.menu, tearoff=0) + buscar_menu.add_command(label="Receta por autor", command=lambda: self.callback("buscar_autor")) + buscar_menu.add_command(label="Receta por fecha", command=lambda: self.callback("buscar_fecha")) + self.menu.add_cascade(label="Buscar", menu=buscar_menu) + + # Callback externo desde el punto de entrada + self.callback = None + + def show_list(self, items, fields, title="Listado"): + mw = tk.Toplevel(self.root) + mw.title(title) + listbox = tk.Listbox(mw, width=80, height=20) + listbox.pack(side="left", fill="both", expand=True) + scrollbar = tk.Scrollbar(mw) + scrollbar.pack(side="right", fill="y") + listbox.config(yscrollcommand=scrollbar.set) + scrollbar.config(command=listbox.yview) + + for item in items: + row = " | ".join(str(item[field]) for field in fields) + listbox.insert("end", row) + + def ask_text(self, label, callback): + mw = tk.Toplevel(self.root) + mw.title(label) + tk.Label(mw, text=label).pack(pady=5) + entry = ttk.Entry(mw) + entry.pack(pady=5) + ttk.Button(mw, text="Aceptar", command= + lambda: [callback(entry.get()), mw.destroy()]).pack(pady=10) + + def ask_spinbox(self, label, options, callback): + mw = tk.Toplevel(self.root) + mw.title(label) + tk.Label(mw, text=label).pack(pady=5) + spinbox = ttk.Spinbox(mw, values=options, state="readonly", width=40) + spinbox.pack(pady=5) + ttk.Button(mw, text="Aceptar", command= + lambda: [callback(spinbox.get()), mw.destroy()]).pack(pady=10) + + def ask_radiobutton(self, label, options, callback): + mw = tk.Toplevel(self.root) + mw.title(label) + tk.Label(mw, text=label).pack(pady=5) + sv = tk.StringVar(value=options[0]) + for option in options: + tk.Radiobutton(mw, text=option, variable=sv, value=option).pack(anchor="w") + ttk.Button(mw, text="Aceptar", command= + lambda: [callback(sv.get()), mw.destroy()]).pack(pady=10) + + def info(slef, message): + messagebox.showinfo("Información", message) \ No newline at end of file