This commit is contained in:
+41
-19
@@ -269,9 +269,12 @@ class ArticleRepository:
|
||||
raise ArticleEditorError(f"malformed controlled tag record: {record_path}")
|
||||
tags.append(Tag(slug, document.get("title").strip()))
|
||||
if not tags:
|
||||
raise ArticleEditorError("content/tags contains no controlled tag records")
|
||||
raise ArticleEditorError("content/tags contains no tracked tag records")
|
||||
return tags
|
||||
|
||||
def refresh_tags(self) -> None:
|
||||
self.tags = self._load_tags()
|
||||
|
||||
@staticmethod
|
||||
def _ensure_within(path: Path, parent: Path) -> Path:
|
||||
resolved = path.resolve()
|
||||
@@ -357,18 +360,16 @@ class ArticleRepository:
|
||||
if cleaned["kicker"] != "Article":
|
||||
raise ArticleEditorError("Kicker must be Article")
|
||||
|
||||
approved_tags = {tag.slug for tag in self.tags}
|
||||
selected_tags = [
|
||||
tag.strip()
|
||||
for tag in re.split(r"[,\n]", cleaned.get("tags", ""))
|
||||
if tag.strip()
|
||||
]
|
||||
unknown_tags = sorted(set(selected_tags) - approved_tags)
|
||||
if unknown_tags:
|
||||
raise ArticleEditorError(
|
||||
"unapproved tag slug(s): " + ", ".join(unknown_tags)
|
||||
)
|
||||
cleaned["tags"] = ", ".join(dict.fromkeys(selected_tags))
|
||||
selected_tags: list[str] = []
|
||||
for value in re.split(r"[,\n]", cleaned.get("tags", "")):
|
||||
if not value.strip():
|
||||
continue
|
||||
slug = suggest_slug(value)
|
||||
if not slug:
|
||||
raise ArticleEditorError(f"tag cannot be normalized to a slug: {value!r}")
|
||||
if slug not in selected_tags:
|
||||
selected_tags.append(slug)
|
||||
cleaned["tags"] = ", ".join(selected_tags)
|
||||
cleaned["external_url"] = self._validate_url(
|
||||
cleaned.get("external_url", ""), "External URL"
|
||||
)
|
||||
@@ -706,7 +707,7 @@ def run_gui(repository: ArticleRepository) -> None:
|
||||
|
||||
form.columnconfigure(1, weight=1)
|
||||
form.columnconfigure(3, weight=1)
|
||||
form.rowconfigure(10, weight=1)
|
||||
form.rowconfigure(11, weight=1)
|
||||
ttk.Label(form, text="Title").grid(row=0, column=0, sticky="w")
|
||||
ttk.Entry(form, textvariable=self.title_var).grid(
|
||||
row=0, column=1, columnspan=3, sticky="ew", pady=3
|
||||
@@ -774,7 +775,7 @@ def run_gui(repository: ArticleRepository) -> None:
|
||||
)
|
||||
self.published_text = tk.Text(form, height=3, wrap="none", undo=True)
|
||||
self.published_text.grid(row=8, column=1, columnspan=3, sticky="nsew", pady=3)
|
||||
ttk.Label(form, text="Controlled tags").grid(
|
||||
ttk.Label(form, text="Tracked tags").grid(
|
||||
row=9, column=0, sticky="nw", pady=(5, 0)
|
||||
)
|
||||
tag_frame = ttk.Frame(form)
|
||||
@@ -790,11 +791,17 @@ def run_gui(repository: ArticleRepository) -> None:
|
||||
for tag in repository.tags:
|
||||
self.tag_list.insert(tk.END, f"{tag.title} ({tag.slug})")
|
||||
|
||||
ttk.Label(form, text="Additional tags").grid(row=10, column=0, sticky="w")
|
||||
self.additional_tags_var = tk.StringVar()
|
||||
ttk.Entry(form, textvariable=self.additional_tags_var).grid(
|
||||
row=10, column=1, columnspan=3, sticky="ew", pady=3
|
||||
)
|
||||
|
||||
ttk.Label(form, text="Body (Markdown)").grid(
|
||||
row=10, column=0, sticky="nw", pady=(5, 0)
|
||||
row=11, column=0, sticky="nw", pady=(5, 0)
|
||||
)
|
||||
body_frame = ttk.Frame(form)
|
||||
body_frame.grid(row=10, column=1, columnspan=3, sticky="nsew", pady=3)
|
||||
body_frame.grid(row=11, column=1, columnspan=3, sticky="nsew", pady=3)
|
||||
body_frame.rowconfigure(0, weight=1)
|
||||
body_frame.columnconfigure(0, weight=1)
|
||||
self.body_text = tk.Text(body_frame, wrap="word", undo=True)
|
||||
@@ -804,7 +811,7 @@ def run_gui(repository: ArticleRepository) -> None:
|
||||
body_scroll.grid(row=0, column=1, sticky="ns")
|
||||
|
||||
footer = ttk.Frame(form)
|
||||
footer.grid(row=11, column=0, columnspan=4, sticky="ew", pady=(8, 0))
|
||||
footer.grid(row=12, column=0, columnspan=4, sticky="ew", pady=(8, 0))
|
||||
footer.columnconfigure(0, weight=1)
|
||||
ttk.Label(footer, textvariable=self.status_var).grid(row=0, column=0, sticky="w")
|
||||
self.delete_button = ttk.Button(
|
||||
@@ -822,12 +829,13 @@ def run_gui(repository: ArticleRepository) -> None:
|
||||
|
||||
def collect_values(self) -> dict[str, str]:
|
||||
selected = [repository.tags[index].slug for index in self.tag_list.curselection()]
|
||||
tags = selected + [self.additional_tags_var.get()]
|
||||
return {
|
||||
"title": self.title_var.get(),
|
||||
"date": self.date_var.get(),
|
||||
"updated": self.updated_var.get(),
|
||||
"author": self.author_var.get(),
|
||||
"tags": ", ".join(selected),
|
||||
"tags": ", ".join(value for value in tags if value.strip()),
|
||||
"kicker": self.kicker_var.get(),
|
||||
"summary": self.summary_text.get("1.0", "end-1c"),
|
||||
"body": self.body_text.get("1.0", "end-1c"),
|
||||
@@ -852,11 +860,19 @@ def run_gui(repository: ArticleRepository) -> None:
|
||||
selected = {
|
||||
part.strip() for part in values.get("tags", "").split(",") if part.strip()
|
||||
}
|
||||
tracked = {tag.slug for tag in repository.tags}
|
||||
self.additional_tags_var.set(", ".join(tag for tag in selected if tag not in tracked))
|
||||
self.tag_list.selection_clear(0, tk.END)
|
||||
for index, tag in enumerate(repository.tags):
|
||||
if tag.slug in selected:
|
||||
self.tag_list.selection_set(index)
|
||||
|
||||
def refresh_tag_choices(self) -> None:
|
||||
repository.refresh_tags()
|
||||
self.tag_list.delete(0, tk.END)
|
||||
for tag in repository.tags:
|
||||
self.tag_list.insert(tk.END, f"{tag.title} ({tag.slug})")
|
||||
|
||||
def has_unsaved_changes(self) -> bool:
|
||||
return self.snapshot is not None and self.collect_values() != self.snapshot
|
||||
|
||||
@@ -889,6 +905,11 @@ def run_gui(repository: ArticleRepository) -> None:
|
||||
self.delete_button.configure(state="disabled")
|
||||
self.slug_entry.configure(state="normal")
|
||||
self.slug_var.set("")
|
||||
try:
|
||||
self.refresh_tag_choices()
|
||||
except ArticleEditorError as exc:
|
||||
messagebox.showerror("Cannot load tracked tags", str(exc))
|
||||
return
|
||||
self.set_values(
|
||||
{
|
||||
"date": current_date.today().isoformat(),
|
||||
@@ -914,6 +935,7 @@ def run_gui(repository: ArticleRepository) -> None:
|
||||
if not self.may_discard():
|
||||
return
|
||||
try:
|
||||
self.refresh_tag_choices()
|
||||
entry = repository.load_entry(selection[0])
|
||||
except ArticleEditorError as exc:
|
||||
messagebox.showerror("Cannot load entry", str(exc))
|
||||
|
||||
Reference in New Issue
Block a user