Repository: internetarchive/openlibrary

Issue:
Make it possible to undo work merge
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
When authors are merged, it is (usually) possible to "undo all" with one click. See https://openlibrary.org/recentchanges/2022/07/22/merge-authors/95371217, for example. However, the same is not true for a work merge. Instead, the IDs must be found in the edition or user's history and reverted individually. This also makes it difficult to review a merge for accuracy.

### Describe the problem that you'd like solved
<!-- A clear and concise description of what you want to happen. -->
Make it possible to review and/or undo a work merge similar to the way one can do with author merges.

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->
Author merges. 

<!-- Which suggestions or requirements should be considered for how feature needs to appear or be implemented? -->

### Additional context
<!-- Add any other context or screenshots about the feature request here. -->

### Stakeholders
<!-- @ tag stakeholders of this bug -->
@mekarpeles @cdrini @jimchamp @dcapillae @mheiman 




Discussion:
This would be fantastic! Infogami does provide hooks to make bulk edits of a certain `kind`; that's how `merge-authors` works. We should do the same thing for merging works, then we can have a custom UI (just like we do for merge authors) to view a historical merge.

Here are some relevant files/classes:

- Defining a custom type for a changeset: https://github.com/internetarchive/openlibrary/blob/7bb4b5345718fe634559100c655a42ede720e021/openlibrary/plugins/upstream/models.py#L936-L952
- How to `save_many` with extra fields/arbitrary data: https://github.com/internetarchive/openlibrary/blob/b897c8c51a79308e38f9825fac82864a5cc7d3ae/openlibrary/plugins/upstream/merge_authors.py#L172-L177
- Where the "Undo All" button is rendered from that appears in the historical author merge changeset UI: https://github.com/internetarchive/openlibrary/blob/b5082d64f0d89812b7ee172a8f8954266ac8b604/openlibrary/templates/recentchanges/header.html#L6-L11
- Full UI for merge-authors page @seabelis linked: https://github.com/internetarchive/openlibrary/blob/492a20322524f91891cb6caad0cf36c99061cf6b/openlibrary/templates/recentchanges/merge-authors/view.html

So it seems like we'd need to create a directory adjacent to openlibrary/templates/recentchanges/merge-authors called `merge-works` and have that have a UI similar to `merge-authors`. Then add a class adjacent to `MergeAuthors` for `MergeWorks` and have that implement `can_undo`. And then update the `save_many` call in the UI to also set `action` to be `merge-works`.

Something like this should do the trick! CC @mheiman for thoughts or if this is of interest.
I'm happy to take a look that this, but it appears I don't have access to the "Undo all" feature, so it's a little tricky to get started...
Awesome! I assigned you. I created an issue for giving super librarians permission to undo author merges: #6885 . If you comment on it I'll assign it to you (if you don't have perms to assign yourself yet :P).

Resolving patch:
diff --git a/openlibrary/plugins/upstream/models.py b/openlibrary/plugins/upstream/models.py
--- a/openlibrary/plugins/upstream/models.py
+++ b/openlibrary/plugins/upstream/models.py
@@ -936,6 +936,25 @@ def get_duplicates(self):
         ]
 
 
+class MergeWorks(Changeset):
+    def can_undo(self):
+        return self.get_undo_changeset() is None
+
+    def get_master(self):
+        master = self.data.get("master")
+        return master and web.ctx.site.get(master, lazy=True)
+
+    def get_duplicates(self):
+        duplicates = self.data.get("duplicates")
+        changes = {c['key']: c['revision'] for c in self.changes}
+
+        return duplicates and [
+            web.ctx.site.get(key, revision=changes[key] - 1, lazy=True)
+            for key in duplicates
+            if key in changes
+        ]
+
+
 class Undo(Changeset):
     def can_undo(self):
         return False
@@ -1000,6 +1019,7 @@ def setup():
 
     client.register_changeset_class(None, Changeset)  # set the default class
     client.register_changeset_class('merge-authors', MergeAuthors)
+    client.register_changeset_class('merge-works', MergeWorks)
     client.register_changeset_class('undo', Undo)
 
     client.register_changeset_class('add-book', AddBookChangeset)
diff --git a/openlibrary/plugins/upstream/recentchanges.py b/openlibrary/plugins/upstream/recentchanges.py
--- a/openlibrary/plugins/upstream/recentchanges.py
+++ b/openlibrary/plugins/upstream/recentchanges.py
@@ -161,7 +161,8 @@ def GET(self, id):
         if path != web.ctx.path:
             raise web.redirect(path)
         else:
-            tname = "recentchanges/" + change.kind + "/view"
+            kind = "merge" if change.kind.startswith("merge-") else change.kind
+            tname = "recentchanges/" + kind + "/view"
             if tname in render:
                 return render_template(tname, change)
             else:


Test patch: