Skip to content
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
2 changes: 2 additions & 0 deletions openlibrary/asgi_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ def health() -> dict[str, str]:
from openlibrary.fastapi.internal.api import router as internal_router
from openlibrary.fastapi.languages import router as languages_router
from openlibrary.fastapi.lists import router as lists_router
from openlibrary.fastapi.merge_authors import router as merge_authors_router
from openlibrary.fastapi.partials import router as partials_router
from openlibrary.fastapi.public_my_books import router as public_my_books_router
from openlibrary.fastapi.publishers import router as publishers_router
Expand All @@ -240,6 +241,7 @@ def health() -> dict[str, str]:
app.include_router(internal_router)
app.include_router(languages_router)
app.include_router(lists_router)
app.include_router(merge_authors_router)
app.include_router(partials_router)
app.include_router(public_my_books_router)
app.include_router(publishers_router)
Expand Down
55 changes: 55 additions & 0 deletions openlibrary/fastapi/merge_authors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""FastAPI endpoint for merging authors.

Migrated from openlibrary.plugins.upstream.merge_authors.merge_authors_json.
"""

from __future__ import annotations

import json
from typing import Any

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

from infogami.infobase.client import ClientException
from openlibrary.fastapi.auth import LibrarianDep # noqa: TC001
from openlibrary.plugins.upstream.edits import perform_merge_update
from openlibrary.plugins.upstream.merge_authors import (
AuthorMergeEngine,
AuthorRedirectEngine,
)
from openlibrary.utils.request_context import req_context, web_ctx_ip
from openlibrary.utils.retry import MaxRetriesExceeded

router = APIRouter(tags=["merge-authors"])


class MergeAuthorsBody(BaseModel):
master: str
duplicates: list[str]
mrid: str | None = None
comment: str | None = None
olids: str = ""


@router.post("/authors/merge.json")
def merge_authors_json(_: LibrarianDep, data: MergeAuthorsBody) -> Any:
with web_ctx_ip(req_context.get().x_forwarded_for or "127.0.0.1"):
try:
engine = AuthorMergeEngine(AuthorRedirectEngine())
merge_result = engine.merge(data.master, data.duplicates)
except ClientException as e:
raise HTTPException(
status_code=400,
detail=json.loads(e.json),
)

try:
perform_merge_update(mrid=data.mrid, olids=data.olids, comment=data.comment)
except MaxRetriesExceeded as e:
raise HTTPException(
status_code=400,
detail=str(e.last_exception),
)

return merge_result
1 change: 1 addition & 0 deletions openlibrary/plugins/openlibrary/js/merge.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function initAuthorView() {
$.ajax({
url: '/authors/merge.json',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
error: function() {
$('#preMerge').fadeOut();
Expand Down
46 changes: 44 additions & 2 deletions openlibrary/plugins/upstream/edits.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

import web

from infogami.infobase.client import ClientException
from infogami.utils import delegate
from infogami.utils.view import render_template
from openlibrary import accounts
from openlibrary.core.edits import ApiMode, CommunityEditsQueue, get_status_for_view
from openlibrary.utils.request_context import site
from openlibrary.utils.retry import RetryStrategy

# Usergroups that may use create or update merge requests
ALLOWED_USERGROUPS: list[str] = [
Expand All @@ -35,6 +38,45 @@ def process_merge_request(rtype, data):
return resp


def perform_merge_update(mrid: str | None, olids: str, comment: str | None) -> None:
"""Orchestrate the merge request update with retries.

Builds the appropriate request type and data, then attempts to process
the merge request with retry logic.

Args:
mrid: Merge request ID to approve, or None to create a new request.
olids: Comma-separated author keys (used when creating a new request).
comment: Optional comment for the merge request.

Raises:
MaxRetriesExceeded: If all retry attempts to process the request fail.
"""

def update_request() -> None:
if mrid:
process_merge_request(
"update-request",
{
"action": "approve",
"mrid": mrid,
**({"comment": comment} if comment else {}),
},
)
else:
process_merge_request(
"create-request",
{
"mr_type": 2,
"olids": olids,
"action": "create-merged",
**({"comment": comment} if comment else {}),
},
)

RetryStrategy([ClientException], max_retries=5, delay=2)(update_request)


class community_edits_queue(delegate.page):
path = "/merges"

Expand Down Expand Up @@ -203,12 +245,12 @@ def create_url(mr_type: int, olids: list[str], primary: str | None = None) -> st
def create_title(mr_type: int, olids: list[str]) -> str:
if mr_type == CommunityEditsQueue.TYPE["WORK_MERGE"]:
for olid in olids:
book = web.ctx.site.get(f"/works/{olid}")
book = site.get().get(f"/works/{olid}")
if book and book.title:
return book.title
elif mr_type == CommunityEditsQueue.TYPE["AUTHOR_MERGE"]:
for olid in olids:
author = web.ctx.site.get(f"/authors/{olid}")
author = site.get().get(f"/authors/{olid}")
if author and author.name:
return author.name
return "Unknown record"
Expand Down
43 changes: 14 additions & 29 deletions openlibrary/plugins/upstream/merge_authors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
import json
import re
from typing import Any
from warnings import deprecated

import web

from infogami.infobase.client import ClientException
from infogami.utils import delegate
from infogami.utils.view import render_template, safeint
from openlibrary.accounts import get_current_user
from openlibrary.plugins.upstream.edits import process_merge_request
from openlibrary.plugins.upstream.edits import perform_merge_update, process_merge_request
from openlibrary.plugins.worksearch.code import top_books_from_author
from openlibrary.utils import dicthash, uniq
from openlibrary.utils.retry import MaxRetriesExceeded, RetryStrategy
from openlibrary.utils.request_context import site
from openlibrary.utils.retry import MaxRetriesExceeded


class BasicRedirectEngine:
Expand Down Expand Up @@ -87,7 +89,7 @@ def do_merge(self, master: str, duplicates: list[str]) -> list:
docs_to_save.extend(self.redirect_engine.make_redirects(master, duplicates))

# Merge all the duplicates into the master.
master_doc = web.ctx.site.get(master).dict()
master_doc = site.get().get(master).dict()
dups = get_many(duplicates)
for d in dups:
master_doc = self.merge_docs(master_doc, d)
Expand Down Expand Up @@ -125,12 +127,12 @@ def merge_property(self, a, b):
class AuthorRedirectEngine(BasicRedirectEngine):
def find_references(self, key):
q = {"type": "/type/edition", "authors": key, "limit": 10000}
edition_keys = web.ctx.site.things(q)
edition_keys = site.get().things(q)
editions = get_many(edition_keys)
work_keys_1 = [w["key"] for e in editions for w in e.get("works", [])]

q = {"type": "/type/work", "authors": {"author": {"key": key}}, "limit": 10000}
work_keys_2 = web.ctx.site.things(q)
work_keys_2 = site.get().things(q)
return edition_keys + work_keys_1 + work_keys_2


Expand All @@ -146,7 +148,7 @@ def merge_docs(self, master, dup):
return master

def save(self, docs, master, duplicates):
return web.ctx.site.save_many(
return site.get().save_many(
docs,
comment="merge authors",
action="merge-authors",
Expand Down Expand Up @@ -200,7 +202,7 @@ def process(doc):
doc["table_of_contents"] = fix_table_of_contents(doc["table_of_contents"])
return doc

return [process(thing.dict()) for thing in web.ctx.site.get_many(list(keys))]
return [process(thing.dict()) for thing in site.get().get_many(list(keys))]


def make_redirect_doc(key, redirect):
Expand All @@ -211,11 +213,11 @@ class merge_authors(delegate.page):
path = "/authors/merge"

def is_enabled(self):
user = web.ctx.site.get_user()
user = site.get().get_user()
return "merge-authors" in web.ctx.features or (user and user.is_admin())

def filter_authors(self, keys):
docs = web.ctx.site.get_many(["/authors/" + k for k in keys])
docs = site.get().get_many(["/authors/" + k for k in keys])
d = {doc.key: doc.type.key for doc in docs}
return [k for k in keys if d.get("/authors/" + k) == "/type/author"]

Expand Down Expand Up @@ -305,6 +307,7 @@ def POST(self):
raise web.seeother(redir_url)


@deprecated("migrated to fastapi")
class merge_authors_json(delegate.page):
"""JSON API for merge authors.

Expand All @@ -315,7 +318,7 @@ class merge_authors_json(delegate.page):
encoding = "json"

def is_enabled(self):
user = web.ctx.site.get_user()
user = site.get().get_user()
return "merge-authors" in web.ctx.features or (user and user.is_admin())

def POST(self):
Expand All @@ -333,29 +336,11 @@ def merge_records() -> Any:
except ClientException as e:
raise web.badrequest(json.loads(e.json))

def update_request() -> None:
data = {}
if mrid:
# Update the request
rtype = "update-request"
data = {"action": "approve", "mrid": mrid}
else:
# Create new request
rtype = "create-request"
data = {"mr_type": 2, "olids": olids, "action": "create-merged"}
if comment:
data["comment"] = comment
process_merge_request(rtype, data)

# actually perform merge and save affected records to db
merge_result = merge_records()
# attempt to update the merge request status with retries
try:
RetryStrategy(
[ClientException],
max_retries=5,
delay=2,
)(update_request)
perform_merge_update(mrid=mrid, olids=olids, comment=comment)
except MaxRetriesExceeded as e:
raise web.badrequest(str(e.last_exception))
return delegate.RawText(json.dumps(merge_result), content_type="application/json")
Expand Down
4 changes: 4 additions & 0 deletions openlibrary/plugins/upstream/tests/test_merge_authors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
space_squash_and_strip,
)
from openlibrary.utils import dicthash
from openlibrary.utils.request_context import site


def setup_module(mod):
Expand Down Expand Up @@ -137,6 +138,7 @@ def test_merge_property(self):

def test_get_many():
web.ctx.site = MockSite()
site.set(web.ctx.site)
# get_many should handle bad table_of_contents in the edition.
edition = {
"key": "/books/OL1M",
Expand All @@ -158,6 +160,7 @@ def test_get_many():
class TestAuthorRedirectEngine:
def setup_method(self, method):
web.ctx.site = MockSite()
site.set(web.ctx.site)

def test_fix_edition(self):
update_references = AuthorRedirectEngine().update_references
Expand Down Expand Up @@ -223,6 +226,7 @@ class TestAuthorMergeEngine:
def setup_method(self, method):
self.engine = AuthorMergeEngine(AuthorRedirectEngine())
web.ctx.site = MockSite()
site.set(web.ctx.site)

def test_redirection(self):
web.ctx.site.add([TEST_AUTHORS.a, TEST_AUTHORS.b, TEST_AUTHORS.c])
Expand Down
Loading