Study Name Validator

 


import os
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import pandas as pd
from datetime import datetime
import re
from openpyxl.styles import Border, Side, Alignment

class NameValidatorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Name Validator")
        w, h = 900, 550
        self.root.geometry(f"{w}x{h}+{int((root.winfo_screenwidth()-w)/2)}+{int((root.winfo_screenheight()-h)/2)}")
       
        # UI Elements
        frame_top = tk.Frame(root)
        frame_top.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)
       
        # Template selection
        self.template_path = tk.StringVar()
        tk.Label(frame_top, text="Default Template:").grid(row=0, column=0, sticky="w")
        tk.Entry(frame_top, textvariable=self.template_path, width=75).grid(row=0, column=1, padx=5)
        tk.Button(frame_top, text="Browse", command=self.browse_template).grid(row=0, column=2, padx=2)
       
        # Buttons (Unified size and alignment)
        btn_width = 18
        self.exec_btn = tk.Button(frame_top, text="Execute", command=self.start_execution, width=btn_width, pady=2, bg="#d4edda")
        self.exec_btn.grid(row=0, column=3, padx=10, sticky="e")

        # Study folder selection
        self.study_folder = tk.StringVar()
        tk.Label(frame_top, text="Study Folder:").grid(row=1, column=0, sticky="w", pady=5)
        tk.Entry(frame_top, textvariable=self.study_folder, width=75).grid(row=1, column=1, padx=5, pady=5)
        tk.Button(frame_top, text="Browse", command=self.browse_study_folder).grid(row=1, column=2, pady=5, padx=2)
       
        # Cleanup button (same size as Execute)
        tk.Button(frame_top, text="Cleanup Old Results", command=self.cleanup_old, width=btn_width, pady=2, bg="#f8d7da").grid(row=1, column=3, padx=10, sticky="e")
       
        self.status_var = tk.StringVar(value="Ready")
        tk.Label(frame_top, textvariable=self.status_var, fg="blue").grid(row=2, column=0, columnspan=4, pady=5)

        # Table for results
        frame_mid = tk.Frame(root)
        frame_mid.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=5)
       
        # UI Styling for Treeview Borders/Lines
        style = ttk.Style()
        # Ensure grid lines are visible if the theme supports it
        style.theme_use('clam') # 'clam' usually supports grid lines better than others
        style.configure("Treeview",
                        background="#ffffff",
                        foreground="black",
                        rowheight=25,
                        fieldbackground="#ffffff",
                        borderwidth=1)
        style.map("Treeview", background=[('selected', '#347083')])
       
        # Configure the headings
        style.configure("Treeview.Heading", font=('Calibri', 10, 'bold'))
       
        self.cols = ("Parent_Folder", "Folder path", "page_Name", "Page Order")
        self.tree = ttk.Treeview(frame_mid, columns=self.cols, show="headings", selectmode="browse")
       
        for c in self.cols:
            display_name = c.replace("_", " ").title()
            self.tree.heading(c, text=display_name)
            # Default widths
            if c == "Page Order":
                self.tree.column(c, width=70, stretch=False, anchor="center")
            elif c == "Folder path":
                self.tree.column(c, width=350, stretch=True)
            else:
                self.tree.column(c, width=150, stretch=False)
       
        sb = ttk.Scrollbar(frame_mid, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=sb.set)
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        sb.pack(side=tk.RIGHT, fill=tk.Y)

        # Activity Log
        frame_bot = tk.Frame(root)
        frame_bot.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=5)
        tk.Label(frame_bot, text="Activity Log:").pack(anchor="w")
        self.log_text = tk.Text(frame_bot, height=8, state='disabled')
        self.log_text.pack(fill=tk.X)

    def log(self, msg):
        self.log_text.config(state='normal')
        self.log_text.insert(tk.END, f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n")
        self.log_text.see(tk.END)
        self.log_text.config(state='disabled')

    def browse_template(self):
        filename = filedialog.askopenfilename(filetypes=[("Text files", "*Pages.txt"), ("All files", "*.*")])
        if filename:
            self.template_path.set(filename)

    def browse_study_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            self.study_folder.set(folder)

    def cleanup_old(self):
        count = 0
        try:
            curr_dir = os.path.dirname(os.path.abspath(__file__))
            for f in os.listdir(curr_dir):
                if f.startswith("Validation_Results_") and f.endswith(".xlsx"):
                    os.remove(os.path.join(curr_dir, f))
                    count += 1
            self.log(f"Deleted {count} old result files.")
            messagebox.showinfo("Cleanup", f"Deleted {count} old result files.")
        except Exception as e:
            self.log(f"Cleanup Error: {e}")
            messagebox.showerror("Error", str(e))

    def start_execution(self):
        if not self.template_path.get() or not self.study_folder.get():
            messagebox.showwarning("Warning", "Please select both template file and study folder.")
            return
       
        [self.tree.delete(i) for i in self.tree.get_children()]
        threading.Thread(target=self.execute_logic, daemon=True).start()

    def extract_page_info(self, line):
        # Example: Page:  Core Setup                                           Order:    0 ...
        try:
            if "Page:" in line and "Order:" in line:
                page_name = line.split("Page:")[1].split("Order:")[0].strip()
                # Order can be followed by spaces and numbers, and then "Visible:"
                order_part = line.split("Order:")[1].strip()
                order_match = re.search(r'^(\d+)', order_part)
                page_order = order_match.group(1) if order_match else "N/A"
                return page_name, page_order
        except Exception:
            pass
        return None, None

    def execute_logic(self):
        self.exec_btn.config(state=tk.DISABLED)
        self.status_var.set("Processing...")
        self.log("Starting validation...")
       
        standard_Names = set()
        results = []
       
        try:
            # 1. Read Template
            self.log(f"Reading template: {os.path.basename(self.template_path.get())}")
            with open(self.template_path.get(), 'r', encoding='utf-8', errors='ignore') as f:
                for line in f:
                    p_name, _ = self.extract_page_info(line)
                    if p_name:
                        standard_Names.add(p_name)
           
            self.log(f"Found {len(standard_Names)} standard page names.")
           
            # 2. Search Study Folder
            study_root = self.study_folder.get()
            for root, _, files in os.walk(study_root):
                for file in files:
                    if file.lower().endswith("pages.txt"):
                        fpath = os.path.join(root, file)
                        parent_folder = os.path.basename(root)
                        self.log(f"Processing study file: {fpath}")
                       
                        try:
                            with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                                for line in f:
                                    p_name, p_order = self.extract_page_info(line)
                                    if p_name and p_name not in standard_Names:
                                        row = (parent_folder, fpath, p_name, p_order)
                                        results.append(row)
                                        self.root.after(0, lambda r=row: self.tree.insert("", tk.END, values=r))
                        except Exception as e:
                            self.log(f"Error reading {file}: {e}")

            if results:
                self.root.after(0, self.auto_resize_columns)
                self.save_excel(results)
                self.status_var.set("Validation Complete - Results Saved")
            else:
                self.log("No validation mismatches found.")
                self.status_var.set("Validation Complete - All names valid")
                messagebox.showinfo("Complete", "Validation finished. No mismatches found.")
               
        except Exception as e:
            self.log(f"Execution Error: {e}")
            messagebox.showerror("Error", str(e))
            self.status_var.set("Error")
        finally:
            self.exec_btn.config(state=tk.NORMAL)

    def auto_resize_columns(self):
        # Dictionary to store max length for each column
        # Initialize with header lengths
        max_lens = {c: len(c.replace("_", " ")) for c in self.cols}
       
        # Iterate over all rows in the treeview
        for child in self.tree.get_children():
            values = self.tree.item(child)["values"]
            for i, c in enumerate(self.cols):
                val_len = len(str(values[i]))
                if val_len > max_lens[c]:
                    max_lens[c] = val_len

        # Adjust column widths
        for c in self.cols:
            if c == "Page Order":
                # Fixed 80 px for ~8 digits
                self.tree.column(c, width=80, stretch=False)
            elif c == "Folder path":
                # Let Folder Path expand to fill space
                self.tree.column(c, width=200, stretch=True)
            else:
                # Calculate width in pixels roughly (len * 8px + safety margin)
                new_width = max_lens[c] * 8 + 20
                self.tree.column(c, width=new_width, stretch=False)

    def save_excel(self, data):
        try:
            self.log("Exporting to Excel...")
            df = pd.DataFrame(data, columns=["Parent_Folder", "Folder path", "page_Name", "Page Order"])
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            out_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"Validation_Results_{timestamp}.xlsx")
           
            with pd.ExcelWriter(out_file, engine='openpyxl') as writer:
                df.to_excel(writer, sheet_name='Validation Results', index=False)
                ws = writer.sheets['Validation Results']
               
                thin = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
               
                # Formatting
                for col in ws.columns:
                    # Calculate max length for column width
                    max_len = 0
                    column = col[0].column_letter
                    for cell in col:
                        try:
                            if cell.value:
                                max_len = max(max_len, len(str(cell.value)))
                        except:
                            pass
                    ws.column_dimensions[column].width = min(max_len + 5, 100) # Cap width at 100
                   
                    for cell in col:
                        cell.border = thin
                        if cell.row == 1:
                            cell.alignment = Alignment(horizontal='center', vertical='center')
                            # Make header bold (optional, let's keep it consistent with VersionScanner)
               
            self.log(f"Results saved to: {out_file}")
            messagebox.showinfo("Success", f"Validation complete. Results exported to:\n{out_file}")
           
        except Exception as e:
            self.log(f"Export Error: {e}")
            messagebox.showerror("Export Error", str(e))

if __name__ == "__main__":
    root = tk.Tk()
    app = NameValidatorApp(root)
    root.mainloop()