Repository: internetarchive/openlibrary

Issue:
Provide beta UI for record merging
### Summary of Requirements
- [x] GETing /libtools/merge?olids=OL1W,OL2W displays the beta merging UI
    - Used /works/merge instead
- [ ] UI will not merge works list backreferences (but allow the merge to continue for the other selected items)
- [ ] UI will not merge works if works are on reading logs (but will allow the merge to continue for the other selected items)
- [x] Should only be available to librarians
- [ ] Log to stats.ol.edits.action.merge-works when a merge occurs
- [x] There is a means of selecting items from the search page
- [ ] Batch edit is created for the merge, with action = merge-works
- [x] Pressing "Do merge" does a merge

### Relationships
- Part of #805
- Depends on #2963


Discussion:

Resolving patch:
diff --git a/openlibrary/plugins/upstream/code.py b/openlibrary/plugins/upstream/code.py
--- a/openlibrary/plugins/upstream/code.py
+++ b/openlibrary/plugins/upstream/code.py
@@ -94,13 +94,17 @@ def GET(self):
 
 
 class merge_work(delegate.page):
-    path = r"(/works/OL\d+W)/merge"
+    path = "/works/merge"
 
-    def GET(self, key):
-        return "This looks like a good place for a merge UI!"
-
-    def POST(self, key):
-        pass
+    def GET(self):
+        user = web.ctx.site.get_user()
+        has_access = user and (
+            (user.is_admin() or user.is_librarian())
+            and user.is_usergroup_member('/usergroup/librarian-work-merge')
+        )
+        if not has_access:
+            raise web.HTTPError('403 Forbidden')
+        return render_template('merge/works')
 
 
 @web.memoize


Test patch:
diff --git a/tests/unit/js/SelectionManager.test.js b/tests/unit/js/SelectionManager.test.js
new file mode 100644
--- /dev/null
+++ b/tests/unit/js/SelectionManager.test.js
@@ -0,0 +1,36 @@
+import SelectionManager from '../../../openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js';
+
+describe('SelectionManager', () => {
+    describe('getSelectedItems', () => {
+        test('empty', () => {
+            document.body.innerHTML = `
+            <ul>
+                <li class="searchResultItem ile-selectable" itemscope="" itemtype="https://schema.org/Book" draggable="true">
+                    <h3 itemprop="name" class="booktitle">
+                        <a itemprop="url" href="/works/OL15000194W?edition=ia%3Awintersongpoem00shak" class="results">The Long Run</a>
+                    </h3>
+                </li>
+            </ul>`;
+            const sm = new SelectionManager();
+            expect(sm.getSelectedItems()).toEqual([]);
+        });
+
+        test('has selected', () => {
+            document.body.innerHTML = `
+            <ul>
+                <li class="searchResultItem ile-selectable ile-selected" itemscope="" itemtype="https://schema.org/Book" draggable="true">
+                    <h3 itemprop="name" class="booktitle">
+                        <a itemprop="url" href="/works/OL15000194W?edition=ia%3Awintersongpoem00shak" class="results">The Long Run</a>
+                    </h3>
+                </li>
+                <li class="searchResultItem ile-selectable" itemscope="" itemtype="https://schema.org/Book" draggable="true">
+                    <h3 itemprop="name" class="booktitle">
+                        <a itemprop="url" href="/works/OL15000194W?edition=ia%3Awintersongpoem00shak" class="results">The Long Run</a>
+                    </h3>
+                </li>
+            </ul>`;
+            const sm = new SelectionManager(null, '/search');
+            expect(sm.getSelectedItems()).toEqual(['OL15000194W']);
+        });
+    });
+});
diff --git a/tests/unit/js/styleMock.js b/tests/unit/js/styleMock.js
new file mode 100644
--- /dev/null
+++ b/tests/unit/js/styleMock.js
@@ -0,0 +1,8 @@
+// Jest transformer to handle .less imports
+// See https://stackoverflow.com/a/46184841/2317712
+// eslint-disable-next-line no-undef
+module.exports = {
+    process() {
+        return '';
+    },
+};