Post by Uncle Buddy on Nov 25, 2022 17:46:51 GMT -8
<drive>:\treebard_gps\app\python\persons.py Last Changed 2022-12-07
# persons.py
import tkinter as tk
import sqlite3
from PIL import Image, ImageTk
from files import get_current_file, global_db_path, get_image_dir
from widgets import (
Frame, Label, Button, LabelMovable, LabelH3, Entry, Toplevel, Border,
Dialogue, Radiobutton, LabelHeader, configall, LabelFrame,
make_formats_dict, EntryAutoPerson, Combobox, Scrollbar,
open_message, EntryAutoPersonHilited, FrameHilited, LabelStay)
from right_click_menu import RightClickMenu, make_rc_menus
from scrolling import resize_scrolled_content, MousewheelScrolling
from toykinter_widgets import run_statusbar_tooltips
from error_messages import open_yes_no_message
from messages_context_help import person_add_help_msg
from messages import persons_msg
from images import get_all_pics
from query_strings import (
select_current_person, select_name_with_id, select_all_names_ids,
select_all_person_ids, select_image_id, select_max_person_id,
select_name_type_id, insert_name, insert_links_links_image_person,
select_all_images, select_all_name_types, insert_person_new,
select_person_gender, select_max_name_type_id, insert_name_type_new,
select_name_with_id_any, select_birth_names_ids, select_images_all,
insert_finding_birth_new_person, update_findings_roles_person,
select_current_person_id, delete_name_person, insert_image_new,
select_name_id_by_person_id, delete_links_links_person, delete_links_links_name,
update_finding_person_1_null, update_finding_person_2_null,
delete_finding_person, delete_person, update_assertion_delete_finding,
delete_links_links_person, select_name_sorter, update_current_person_to_1,
select_name_type_sorter_with_id, select_all_names, select_finding_person,
select_name_type_hierarchy, select_all_names_all_details_order_hierarchy)
import dev_tools as dt
from dev_tools import looky, seeline
formats = make_formats_dict()
GENDER_TYPES = ('unknown', 'female', 'male')
NAME_SUFFIXES = (
'jr.', 'sr.', 'jr', 'sr', 'junior', 'senior',
'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii',
'ix', 'x', 'xi', 'xii', 'xiii', 'xiv', 'xv')
def get_name_types():
conn = sqlite3.connect(global_db_path)
cur = conn.cursor()
cur.execute(select_all_name_types)
name_types = cur.fetchall()
cur.close()
conn.close()
name_types = [i[0] for i in name_types]
return name_types
all_name_types = get_name_types()
def validate_id(iD, entry):
""" Get a name to fill in when ID is input. Unlike name autofill, the best
name to fill in is determined by hierarchy column in name_type table so
names are put into the dict in the right order so the first name
found will be the most suitable one according to the name types hierarchy.
E.G. birth name trumps pseudonym and pseudonym trumps nickname.
"""
def err_done(entry, msg):
entry.delete(0, 'end')
msg[0].grab_release()
msg[0].destroy()
entry.focus_set()
if iD not in EntryAutoPerson.person_autofill_values:
msg = open_message(
entry,
persons_msg[2],
"Unknown Person ID",
"OK")
msg[0].grab_set()
msg[2].config(
command=lambda entry=entry, msg=msg: err_done(
entry, msg))
else:
name_from_id = get_name_from_id(iD)
return name_from_id
def get_name_from_id(iD):
name_from_id = None
for k,v in EntryAutoPerson.person_autofill_values.items():
if iD == k:
name_from_id = (v[0], k)
break
return name_from_id
def check_name(evt=None, ent=None, label=None):
""" Used in families.py, main.py, roles.py. """
iD = None
name_from_id = None
if evt:
ent = evt.widget
elif ent:
ent = ent
else:
return None
filled = ent.get().strip()
if filled.startswith("#"):
filled = filled
elif filled.startswith("+") or filled.endswith("+"):
filled = filled
else:
filled = ent.filled_name
ent.original = ""
if filled == ent.original or filled is None:
return None
elif filled.startswith("#"):
name_from_id = validate_id(int(filled.lstrip("#").strip()), ent)
if name_from_id is None:
return None
else:
ent.delete(0, 'end')
return name_from_id
elif filled.startswith("+") or filled.endswith("+"):
return "add_new_person"
dupes = []
for hit in ent.hits:
the_one = hit[0]
if the_one["dupe name"] is False or the_one["name"] != ent.filled_name:
continue
else:
dupes.append(hit)
if len(dupes) > 1:
right_dupe = ent.open_dupe_dialog(dupes)
return right_dupe
elif len(ent.hits) > 0:
pass
else:
ent.delete(0, 'end')
if len(ent.hits) != 0:
return ent.hits[0]
def get_original(evt):
widg=evt.widget
widg.original = widg.get()
def delete_current_person(evt):
""" Remove all references to a person. Due to import circles, the user will
have to change current person manually after deleting the current
person. That is not an emergency but what if he tries to do something
on the current person conclusions or families table?
"""
if len (EntryAutoPerson.person_autofill_values) < 2:
return
tree = get_current_file()[0]
conn = sqlite3.connect(tree)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
cur.execute(select_current_person_id)
current_person = cur.fetchone()[0]
if current_person == 1:
return
cur.execute(update_current_person_to_1)
cur.execute(select_name_id_by_person_id, (current_person,))
names = cur.fetchall()
for name_id in names:
cur.execute(delete_links_links_name, name_id)
cur.execute(delete_links_links_person, (current_person,))
cur.execute(delete_name_person, (current_person,))
cur.execute(update_finding_person_1_null, (current_person,))
cur.execute(update_finding_person_2_null, (current_person,))
cur.execute(select_finding_person, (current_person,))
findings = cur.fetchall()
for finding_id in findings:
cur.execute(update_assertion_delete_finding, finding_id)
cur.execute(delete_finding_person, (current_person,))
conn.commit()
cur.execute(delete_person, (current_person,))
conn.commit()
EntryAutoPerson.person_autofill_values = EntryAutoPerson.update_person_autofill_values()
cur.close()
conn.close()
# can't run change_person() here
msg = open_message(
None,
persons_msg[4],
"Current Person Deleted",
"OK")
msg[0].grab_set()
def open_new_person_dialog(
master, root, treebard, inwidg=None, inwidg2=None):
person_add = PersonAdd(
master, inwidg, root, treebard, inwidg2)
root.wait_window(person_add)
new_person_id = person_add.show()
return new_person_id
class PersonAdd(Toplevel):
def __init__(
self, master, inwidg, root, treebard, inwidg2,
*args, **kwargs):
Toplevel.__init__(self, master, *args, **kwargs)
self.master = master
self.inwidg = inwidg
self.root = root
self.treebard = treebard
self.inwidg2 = inwidg2
EntryAutoPerson.person_autofill_values = EntryAutoPerson.make_all_names_dict_for_person_select()
self.full_name = ""
self.sorted_name = ""
self.name_type = ""
self.xfr = ""
if self.inwidg:
self.xfr = self.inwidg.get()
if "+" in self.xfr:
self.xfr = self.xfr.strip().strip("+").strip()
self.new_images = []
self.existing_images = []
self.genvar = tk.IntVar(None, 3)
self.role_person_edited = False
self.rc_menu = RightClickMenu(self.root, treebard=self.treebard)
self.new_person_id = None
self.full_name = ""
self.name_type_id = None
self.make_dupe = False
tree = get_current_file()[0]
conn = sqlite3.connect(tree)
cur = conn.cursor()
self.names_only = self.check_for_dupes(cur)
self.make_widgets()
self.make_inputs()
cur.close()
conn.close()
def make_widgets(self):
self.geometry('+100+20')
self.columnconfigure(1, weight=1)
self.canvas = Border(self, self.root, formats)
self.canvas.title_1.config(text="Add Person Dialog")
self.canvas.title_2.config(text="")
self.window = Frame(self.canvas)
self.canvas.create_window(0, 0, anchor='nw', window=self.window)
scridth = 16
scridth_n = Frame(self.window, height=scridth)
scridth_w = Frame(self.window, width=scridth)
scridth_n.grid(column=0, row=0, sticky='ew')
scridth_w.grid(column=0, row=1, sticky='ns')
self.window.vsb = Scrollbar(
self,
hideable=True,
command=self.canvas.yview,
width=scridth)
self.window.hsb = Scrollbar(
self,
hideable=True,
width=scridth,
orient='horizontal',
command=self.canvas.xview)
self.canvas.config(
xscrollcommand=self.window.hsb.set,
yscrollcommand=self.window.vsb.set)
self.window.vsb.grid(column=2, row=4, sticky='ns')
self.window.hsb.grid(column=1, row=5, sticky='ew')
scridth_n.grid(column=0, row=0, sticky='ew')
scridth_w.grid(column=0, row=1, sticky='ns')
self.window.columnconfigure(2, weight=1)
self.window.rowconfigure(1, weight=1)
configall(self, formats)
# self.maxsize(
# int(self.winfo_screenwidth() * 0.90),
# int(self.winfo_screenheight() * 0.90))
self.grab_set()
def make_inputs(self):
def instruct_user():
msg1 = open_message(
dupe_exists,
persons_msg[3],
"Person Already Exists",
"OK")
msg1[0].grab_set()
namelab = Label(self.window, text="Full Name")
self.name_input = EntryAutoPersonHilited(
self.window, autofill=True,
values=EntryAutoPerson.person_autofill_values)
# rerun this on focus out of name input if content changes and again on OK:
self.name_input.bind("<FocusOut>", self.show_dupe_msg, add="+")
autosort = Button(self.window, text="AUTOSORT", command=self.sort_name)
self.autosort_input = Entry(self.window, width=32)
typelab = Label(self.window, text="Name Type")
self.type_input = Combobox(self.window, root=self.root, values=all_name_types)
self.type_input.insert(0, "birth name")
radframe = Frame(self.window)
rad_male = Radiobutton(
radframe, value=1, variable=self.genvar, text="male")
rad_female = Radiobutton(
radframe, value=2, variable=self.genvar, text="female")
rad_unknown = Radiobutton(
radframe, value=3, variable=self.genvar, text="unknown")
dupes = LabelFrame(
self.window, text="If this name already exists, a notice will appear.")
self.dupelab = LabelStay(
dupes, bg=formats["bg"], fg=formats["bg"],
text="Press OK to make another person with the existing name. The "
"persons can be merged later if desired.",
wraplength=270, justify="left")
image_select = Button(
self.window, text="SELECT IMAGE(S) FOR THIS PERSON",
command=self.open_image_dlg)
buttons = Frame(self.window)
self.ok_new_person = Button(
buttons, text="OK", width=8, command=self.ok_add_person)
cancel_new_person = Button(
buttons, text="CANCEL", width=8, command=self.cancel_add_person)
# children of self.window
namelab.grid(column=0, row=0, padx=(12,0), pady=(12,0), sticky="e")
self.name_input.grid(column=1, row=0, padx=(6,0), pady=(12,0), sticky="w")
autosort.grid(column=2, row=0, padx=(0,0), pady=(12,0))
self.autosort_input.grid(column=3, row=0, padx=(12,0), pady=(12,0))
typelab.grid(column=0, row=1, padx=(12,0), pady=(0,0), sticky="e")
self.type_input.grid(column=1, row=1, padx=(6,0), pady=(0,0), sticky="w")
radframe.grid(column=3, row=1, padx=(0,0), pady=(0,0), rowspan=3)
dupes.grid(column=0, row=2, padx=12, pady=12, columnspan=3)
image_select.grid(column=0, row=3, padx=12, pady=12, columnspan=3)
# pics.grid(column=0, row=5, padx=12, pady=12, sticky="news", columnspan=3)
buttons.grid(column=3, row=6, padx=12, pady=12, sticky="e")
# children of radframe
rad_male.grid(column=0, row=0, padx=(0,0), pady=(0,0), sticky="w")
rad_female.grid(column=0, row=1, padx=(0,0), pady=(0,0), sticky="w")
rad_unknown.grid(column=0, row=2, padx=(0,0), pady=(0,0), sticky="w")
# children of dupes
self.dupelab.grid(column=0, row=0, sticky="news", ipadx=12, ipady=12)
# children of buttons
self.ok_new_person.grid(column=0, row=0, padx=(0,0), pady=(0,0))
cancel_new_person.grid(column=1, row=0, padx=(0,0), pady=(0,0))
resize_scrolled_content(self, self.canvas, self.window)
self.name_input.insert(0, self.xfr)
self.name_input.focus_set()
def make_new_name_type(self, cur, conn):
cur.execute(insert_name_type_new, (self.name_type,))
conn.commit()
return cur.lastrowid
def save_new_person(self):
tree = get_current_file()[0]
conn = sqlite3.connect(global_db_path)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute("ATTACH ? as tree", (tree,))
cur.execute(insert_person_new, (self.gender,))
conn.commit()
self.new_person_id = cur.lastrowid
cur.execute(select_name_type_id, (self.name_type,))
result = cur.fetchone()
if result:
self.name_type_id = result[0]
else:
self.name_type_id = self.make_new_name_type(cur, conn)
cur.execute(
insert_name,
(self.new_person_id, self.full_name, self.name_type_id, self.sorted_name))
cur.execute(select_images_all)
all_images = {i[0]:i[1] for i in cur.fetchall()}
if len(self.new_images) == 0 and len(self.existing_images) == 0:
pass
elif len(self.new_images) == 0:
for imagetup in self.existing_images:
image_name, main_image = imagetup
image_name = ".".join(image_name)
image_id = all_images[image_name]
cur.execute(insert_links_links_image_person, (image_id, self.new_person_id, main_image))
conn.commit()
for imagetup in self.new_images:
filename, caption, main_image = imagetup
if filename not in all_images:
cur.execute(insert_image_new, (filename, caption))
conn.commit()
image_id = cur.lastrowid
cur.execute(insert_links_links_image_person, (image_id, self.new_person_id, main_image))
cur.execute(insert_finding_birth_new_person, (self.new_person_id,))
conn.commit()
cur.execute("DETACH tree")
cur.close()
conn.close()
if self.inwidg:
self.inwidg.delete(0, 'end')
self.inwidg.insert(0, self.full_name)
def sort_name(self):
namelist = self.name_input.get().strip().split()
surname = namelist.pop()
self.sorted_name = "{}, {}".format(surname, " ".join(namelist))
self.autosort_input.delete(0, "end")
self.autosort_input.insert(0, self.sorted_name)
def open_image_dlg(self):
image_dialog = SelectImageDialog(
self.root, self.treebard, self.ok_new_person,
self.new_images, self.existing_images)
def check_for_dupes(self, cur):
cur.execute(select_all_names)
names_only = [i[0] for i in cur.fetchall()]
return names_only
def show(self):
return self.new_person_id
def show_dupe_msg(self, evt):
if self.name_input.get() in self.names_only:
self.dupelab.config(fg=formats["fg"])
else:
self.dupelab.config(fg=formats["bg"])
def ok_add_person(self):
gendermap = {1: "male", 2: "female", 3: "unknown"}
self.full_name = self.name_input.get()
self.sorted_name = self.autosort_input.get()
self.name_type = self.type_input.get()
gender = self.genvar.get()
for k,v in gendermap.items():
if gender == k:
self.gender = v
break
for stg in (self.full_name, self.sorted_name, self.name_type):
if len(stg) == 0:
return
self.save_new_person()
self.destroy()
EntryAutoPerson.person_autofill_values = EntryAutoPerson.update_person_autofill_values()
def cancel_add_person(self):
self.destroy()
class SelectImageDialog(Toplevel):
def __init__(
self, master, treebard, ok_new_person, new_images, existing_images, *args, **kwargs):
Toplevel.__init__(self, master, *args, **kwargs)
self.root = master
self.treebard = treebard
self.ok_new_person = ok_new_person
self.new_images = new_images
self.existing_images = existing_images
self.rc_menu = RightClickMenu(self.root, treebard=treebard)
self.sizers = []
self.picvar = tk.IntVar()
self.make_widgets()
self.make_inputs()
self.get_user_input()
def make_widgets(self):
self.columnconfigure(1, weight=1)
self.canvas = Border(self, self.root, formats)
self.canvas.title_1.config(text="Select Images Dialog")
self.canvas.title_2.config(text="")
self.window = Frame(self.canvas)
self.canvas.create_window(0, 0, anchor='nw', window=self.window)
scridth = 16
scridth_n = Frame(self.window, height=scridth)
scridth_w = Frame(self.window, width=scridth)
scridth_n.grid(column=0, row=0, sticky='ew')
scridth_w.grid(column=0, row=1, sticky='ns')
self.window.vsb = Scrollbar(
self,
hideable=True,
command=self.canvas.yview,
width=scridth)
self.window.hsb = Scrollbar(
self,
hideable=True,
width=scridth,
orient='horizontal',
command=self.canvas.xview)
self.canvas.config(
xscrollcommand=self.window.hsb.set,
yscrollcommand=self.window.vsb.set)
self.window.vsb.grid(column=2, row=4, sticky='ns')
self.window.hsb.grid(column=1, row=5, sticky='ew')
scridth_n.grid(column=0, row=0, sticky='ew')
scridth_w.grid(column=0, row=1, sticky='ns')
self.window.columnconfigure(2, weight=1)
self.window.rowconfigure(1, weight=1)
configall(self, formats)
self.grab_set()
def make_inputs(self):
def ok_selected_images():
tree = get_current_file()[0]
save_path = "{}/images/".format("/".join(tree.split("/")[0:4]))
main_pic = self.picvar.get()
for idx, ent in enumerate(self.sizers):
main_image = 0
(resize_input, (orig_w, orig_h), name,
picorig, saveas_input, caption_input) = ent
ratio = resize_input.get()
if ratio.isnumeric():
ratio = int(ratio) / 100
else:
ratio = 1
new_width = int(orig_w * ratio)
new_height = int(orig_h * ratio)
ext = name.split(".")[-1]
new_file = "{}.{}".format(saveas_input.get(), ext)
filename = "{}{}".format(save_path, new_file)
source_path = filename.split('/')[0:5]
if idx == main_pic:
main_image = 1
if source_path != save_path.split('/')[0:5] or ratio != 1:
treepic = picorig.resize((new_width, new_height))
treepic.save(filename)
# if idx == main_pic:
# main_image = 1
self.new_images.append((new_file, caption_input.get(), main_image))
else:
self.existing_images.append((name.split(".")[-2:], main_image))
self.ok_new_person.focus_set()
self.destroy()
def cancel_selected_images():
self.destroy()
msg = "Choose one image to be the main image for this element.\n\nThe image(s) will be copied to the tree's `images` folder at a percentage of current size. The original image will not be moved or changed.\n\nThe size of this computer's screen is {} pixels wide by {} pixels high.".format(self.winfo_screenwidth(), self.winfo_screenheight())
lab = LabelHeader(self.window, text=msg, wraplength=650, justify="left")
self.picframe = Frame(self.window)
buttons = Frame(self.window)
ok_savepics = Button(
buttons, text="OK", width=8, command=ok_selected_images)
cancel_savepics = Button(
buttons, text="CANCEL", width=8, command=cancel_selected_images)
# children of self.window
lab.grid(column=0, row=0, ipadx=6, ipady=6)
self.picframe.grid(column=0, row=1, sticky="news")
buttons.grid(column=0, row=2, padx=12, pady=12, sticky="e")
# children of buttons
ok_savepics.grid(column=0, row=0, padx=(0,0), pady=(0,0))
cancel_savepics.grid(column=1, row=0, padx=(0,0), pady=(0,0))
def get_user_input(self):
imagedir = get_image_dir()
filetypes = [
("ALL Files", "*.*"),
('JPG Files', '*.jpg'),
('PNG Files', '*.png'),
('GIF Files', '*.gif'),
('BMP Files', '*.bmp')]
filenames = tk.filedialog.askopenfilename(
multiple=True, filetypes=filetypes, initialdir=imagedir)
for idx, file in enumerate(filenames):
rad = Radiobutton(self.picframe, value=idx, variable=self.picvar)
picorig = Image.open(file)
orig_w, orig_h = picorig.size
show = picorig.resize((100,100))
img1 = ImageTk.PhotoImage(show)
lab = Label(self.picframe)
lab.image = img1
lab['image'] = img1
sourcelab = Label(
self.picframe, text=file, anchor="w", wraplength=360, justify="left")
sizelab = Label(
self.picframe,
text="({} x {} pixels)".format(orig_w, orig_h), anchor="w")
resizelab = Label(self.picframe, text="resize %:")
resize_input = Entry(self.picframe, width=6)
resize_input.insert(0, "100")
filelab = Label(self.picframe, text="Save As:")
saveas_input = Entry(self.picframe, width=32)
captionlab = Label(self.picframe, text="Caption:")
caption_input = Entry(self.picframe, width=36)
name = file.split("/")[-1]
self.sizers.append(
(resize_input, (orig_w, orig_h), name,
picorig, saveas_input, caption_input))
# children of self.picframe
rad.grid(column=0, row=idx)
lab.grid(column=1, row=idx)
sourcelab.grid(column=2, row=idx, sticky="ew", padx=(9,0))
sizelab.grid(column=3, row=idx, sticky="ew")
resizelab.grid(column=4, row=idx, sticky="e", padx=(9,0))
resize_input.grid(column=5, row=idx, sticky="w")
filelab.grid(column=6, row=idx, sticky="e", padx=(9,0))
saveas_input.grid(column=7, row=idx, sticky="w")
captionlab.grid(column=8, row=idx, sticky="e", padx=(9,0))
caption_input.grid(column=9, row=idx, sticky="w")
resize_scrolled_content(self, self.canvas, self.window)