Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

One step closer to submitting changes via web editor #56

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion genweb/data/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"contents"
];
const visible_fields = {
id:["inline","href", "picture"],
title:["inline", "href", "picture"],
path:["inline", "href", "picture"],
file:["inline", "href", "picture"],
Expand Down Expand Up @@ -280,6 +281,37 @@
load_person_into(element.value, person, parents, spouses, children);
}

function get_form_json() {
var results = {};

for (var field in visible_fields) {
var value = get_row_input(field);

console.log("field = " + field + " value = " + value[0].value);
results[field] = value[0].value;
}

return JSON.stringify(results);
}

function save_form() {
var request = new XMLHttpRequest();
var form_info = get_form_json();
// disable button

request.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
// update server state
// enable button
// clear form
}
}

request.open("POST", "/api/v1/metadata/" + form_info.id, true);
request.setRequestHeader("Content-Type", "text/json;charset=UTF-8");
request.send(form_info);
}

</script>
</head>
<body class="notranslate" onload="load_values();type_change('type-selector')">
Expand Down Expand Up @@ -349,7 +381,7 @@ <h1>genweb editor</h1>
</tr>
<tr id="submit">
<td></td>
<td style="text-align: center;"><input value="Add" type="submit"/></td></td>
<td style="text-align: center;"><input value="Add" type="submit" onclick="save_form()"/></td></td>
</tr>
</table>
</div>
Expand Down
4 changes: 2 additions & 2 deletions genweb/genweb.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from genweb.relationships import load_gedcom
from genweb.people import People
from genweb.metadata import load_yaml
from genweb.metadata import Metadata
from genweb.template import render_to_file
from genweb.inventory import Artifacts

Expand Down Expand Up @@ -247,7 +247,7 @@ def main() -> None:
people = People(
load_gedcom(settings["gedcom_path"]), settings.get("alias_path", None)
)
metadata = load_yaml(settings["metadata_yaml"])
metadata = Metadata(settings["metadata_yaml"])
link_people_to_metadata(people, metadata)
copy_static_files(settings["copy files"], settings["site_dir"])
copy_metadata_files(
Expand Down
126 changes: 106 additions & 20 deletions genweb/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,10 @@
""" Hanldes loading and saving of metadta """


from yaml import safe_load


def validate(metadata: dict[str, dict]) -> dict[str, dict]:
"""Validates that the metadata is correct

Args:
metadata (dict[str, dict]): The loaded metadata
from glob import glob
from copy import deepcopy

Returns:
dict[str, dict]: Just returns the metadata so you can chain calls
"""
return metadata
from yaml import safe_load


def load_yaml(path: str) -> dict[str, dict]:
Expand All @@ -32,13 +23,108 @@ def load_yaml(path: str) -> dict[str, dict]:
return safe_load(file)


def load(path: str) -> dict[str, dict]:
"""Loads a metadata yaml file
class Metadata:
"""read-only-dict-like object"""

Args:
path (str): The path to the metadata yaml file
def __init__(self, path_pattern: str):
self.path = path_pattern
self.original = Metadata.__load(path_pattern)
self.updated = {}

Returns:
dict[str, dict]: The metadata
"""
return validate(load_yaml(path))
@staticmethod
def __load(path_pattern: str) -> dict[str:dict]:
result = {}

for path in sorted(glob(path_pattern)):
result.update(Metadata.__validate(load_yaml(path)))

return result

@staticmethod
def __validate(metadata: dict[str:dict]) -> dict[str:dict]:
return metadata

def __combined(self, deep=False) -> dict[str:dict]:
if deep:
combined = deepcopy(self.original)
combined.update(deepcopy(self.updated))
else:
combined = dict(self.original)
combined.update(self.updated)

return combined

def __getitem__(self, key: str) -> dict:
if key in self.updated:
return deepcopy(self.updated[key])

return deepcopy(self.original[key])

def __repr__(self) -> str:
return repr(self.__combined())

def __len__(self) -> int:
return len(self.__combined())

def has_key(self, key: str) -> bool:
"""Is the given identifier available

Args:
key (str): The identifier

Returns:
bool: True if found
"""
return key in self.original or key in self.updated

def keys(self) -> list[str]:
"""A list of the identifiers

Returns:
list[str]: The identifiers
"""
return self.__combined().keys()

def values(self) -> list[dict]:
"""A list of the people

Returns:
list[dict]: The people
"""
return [deepcopy(v) for v in self.__combined().values()]

def items(self) -> list[tuple[str, dict]]:
"""Get a list of pairs of identifiers and people

Returns:
list[tuple(str, dict)]: The identifiers and people
"""
return [(k, deepcopy(v)) for k, v in self.__combined().items()]

def __contains__(self, key: str) -> bool:
return key in self.original or key in self.updated

def __iter__(self):
return iter(self.__combined(deep=True))

def get(self, key: str, default: dict | None = None) -> dict | None:
"""Gets the person for a given id, or a default person if the id is not found

Args:
key (str): The person's canonical identifier
default (dict | None, optional): The person to return if the
id is not found. Defaults to None.

Returns:
dict | None: _description_
"""
if key in self.updated:
return deepcopy(self.updated[key])

if key in self.original:
return deepcopy(self.original[key])

return default

def __setitem__(self, key, item):
self.updated[key] = item
72 changes: 53 additions & 19 deletions genweb/webserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
from http.server import HTTPServer, BaseHTTPRequestHandler
from types import SimpleNamespace
from os.path import join, dirname
from json import dumps
from json import dumps, loads
from re import compile as regex
from traceback import format_exc

from devopsdriver.settings import Settings

from genweb.relationships import load_gedcom, person_json
from genweb.people import People
from genweb.metadata import load_yaml
from genweb.metadata import Metadata
from genweb.genweb import link_people_to_metadata


Expand All @@ -24,6 +24,7 @@
STATIC_FILES = {"/": "editor.html"}
API_V1 = "/api/v1/"
V1_CALL = regex(rf"^{API_V1}(people|metadata)(/([^/]+))?$")
SEPARATOR_PATTERN = regex(r"[\s:;,]+")


class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
Expand Down Expand Up @@ -63,17 +64,16 @@ def send_file(self, path: str, mimetype: str = "text/html") -> None:
code=404,
)

def handle_api_v1(self):
def handle_api_v1(self, method: str):
"""Handle any API calls"""
api_call = V1_CALL.match(self.path)
assert api_call, self.path

if api_call.group(1) == "people" and not api_call.group(2):
if api_call.group(1) == "people" and not api_call.group(2) and method == "GET":
people = dumps([str(i) for i in GLOBALS.people]).encode("utf-8")
self.respond(people, mimetype="text/json")
return

if api_call.group(1) == "people" and api_call.group(3):
elif api_call.group(1) == "people" and api_call.group(3) and method == "GET":
person = GLOBALS.people.get(api_call.group(3), None)

if not person:
Expand All @@ -82,16 +82,18 @@ def handle_api_v1(self):
)
return

info = dumps(person_json(person)).encode("utf-8")
self.respond(info, mimetype="text/json")
return
metadata = dumps(person_json(person)).encode("utf-8")
self.respond(metadata, mimetype="text/json")

if api_call.group(1) == "metadata" and not api_call.group(2):
elif (
api_call.group(1) == "metadata"
and not api_call.group(2)
and method == "GET"
):
people = dumps(list(GLOBALS.metadata)).encode("utf-8")
self.respond(people, mimetype="text/json")
return

if api_call.group(1) == "metadata" and api_call.group(3):
elif api_call.group(1) == "metadata" and api_call.group(3) and method == "GET":
if api_call.group(3) not in GLOBALS.metadata:
self.respond(
f"Metadata not found: {api_call.group(3)}".encode("utf-8"), code=404
Expand All @@ -100,12 +102,24 @@ def handle_api_v1(self):

metadata = dumps(GLOBALS.metadata[api_call.group(3)]).encode("utf-8")
self.respond(metadata, mimetype="text/json")
return

self.respond(
f"API not found: {self.path}".encode("utf-8"),
code=404,
)
elif api_call.group(1) == "metadata" and api_call.group(3) and method == "POST":
metadata = loads(self.rfile.read(int(self.headers["Content-Length"])))

for key in [k for k in metadata if not metadata[k]]:
del metadata[key]

if "people" in metadata:
metadata["people"] = SEPARATOR_PATTERN.split(metadata["people"])

print(dumps(metadata, indent=2))
self.respond(dumps(metadata, indent=2).encode("utf-8"))

else:
self.respond(
f"API not found: {self.path}".encode("utf-8"),
code=404,
)

def do_GET(self): # pylint: disable=invalid-name
"""Handle GET requests"""
Expand All @@ -115,7 +129,7 @@ def do_GET(self): # pylint: disable=invalid-name
return

if self.path.startswith(API_V1):
self.handle_api_v1()
self.handle_api_v1("GET")
return

self.respond(
Expand All @@ -130,6 +144,26 @@ def do_GET(self): # pylint: disable=invalid-name
code=500,
)

def do_POST(self): # pylint: disable=invalid-name
"""Handle POSTing of data"""
try:
if self.path.startswith(API_V1):
self.handle_api_v1("POST")
return

self.respond(
f"File not found: {self.path}".encode("utf-8"),
code=404,
)

except Exception as error: # pylint: disable=broad-exception-caught
self.respond(
f"Internal server error {error}<br/><pre>{format_exc()}</pre>".encode(
"utf-8"
),
code=500,
)


def start_webserver(port=8000, host="") -> None:
"""Start the editor web server
Expand All @@ -143,7 +177,7 @@ def start_webserver(port=8000, host="") -> None:
load_gedcom(GLOBALS.settings["gedcom_path"]),
GLOBALS.settings.get("alias_path", None),
)
GLOBALS.metadata = load_yaml(GLOBALS.settings["metadata_yaml"])
GLOBALS.metadata = Metadata(GLOBALS.settings["metadata_yaml"])
link_people_to_metadata(GLOBALS.people, GLOBALS.metadata)
httpd = HTTPServer((host, port), SimpleHTTPRequestHandler)
print("Starting web server")
Expand Down
29 changes: 29 additions & 0 deletions tests/data/layered.2024-01-01.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
0000000000SmithCaleb1765JonesMary1724:
file: 0000000000SmithCaleb1765JonesMary1724.src
mod_date: '2015-03-21'
path: SmithCaleb1765JonesMary1724
people:
- SmithMargaret1804BrownElisabeth1772
- SmithMargaret1802BrownElisabeth1772
- SmithGideon1802BrownElisabeth1772
- SmithMary1798BrownElisabeth1772
- SmithVarnum1796BrownElisabeth1772
- SmithJobS1794BrownElisabeth1772
- SmithOlive1792BrownElisabeth1772
- SmithCalebA1814BrownElisabeth1772
- BrownElisabeth1772-
- SmithCaleb1765JonesMary1724
- StoriesPersonal0000-
title: Caleb Smith (1765-1840) Bio
type: inline

0000000000JohnsonSamI1892MillerJane1860:
file: 0000000000JohnsonSamI1892MillerJane1860.html
folder: 0000000000JohnsonSamI1892MillerJane1860
mod_date: '2015-03-28'
path: JohnsonSamI1892MillerJane1860
people:
- JohnsonSamI1892MillerJane1860
- StoriesPersonal0000-
title: Sam I Johnson (1892-1984) Bio
type: href
Loading
Loading