We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `openlibrary/plugins/upstream/code.py`
Content:
```
1 """Upstream customizations."""
2 
3 import datetime
4 import hashlib
5 import io
6 import os.path
7 import random
8 
9 import web
10 
11 from infogami import config
12 from infogami.core import code as core
13 from infogami.infobase import client
14 from infogami.utils import delegate, app, types
15 from infogami.utils.view import public, safeint, render
16 from infogami.utils.view import render_template  # noqa: F401 used for its side effects
17 from infogami.utils.context import context
18 
19 from openlibrary import accounts
20 
21 from openlibrary.plugins.upstream import addbook, covers, merge_authors, models, utils
22 from openlibrary.plugins.upstream import spamcheck
23 from openlibrary.plugins.upstream import borrow, recentchanges  # TODO: unused imports?
24 from openlibrary.plugins.upstream.utils import render_component
25 
26 if not config.get('coverstore_url'):
27     config.coverstore_url = "https://covers.openlibrary.org"
28 
29 
30 class static(delegate.page):
31     path = "/images/.*"
32 
33     def GET(self):
34         host = 'https://%s' % web.ctx.host if 'openlibrary.org' in web.ctx.host else ''
35         raise web.seeother(host + '/static' + web.ctx.path)
36 
37 
38 class edit(core.edit):
39     """Overwrite ?m=edit behaviour for author, book, work, and people pages."""
40 
41     def GET(self, key):
42         page = web.ctx.site.get(key)
43 
44         if web.re_compile('/(authors|books|works)/OL.*').match(key):
45             if page is None:
46                 raise web.seeother(key)
47             else:
48                 raise web.seeother(page.url(suffix="/edit"))
49         else:
50             return core.edit.GET(self, key)
51 
52     def POST(self, key):
53         if web.re_compile('/(people/[^/]+)').match(key) and spamcheck.is_spam():
54             return render_template(
55                 'message.html', 'Oops', 'Something went wrong. Please try again later.'
56             )
57         return core.edit.POST(self, key)
58 
59 
60 # handlers for change photo and change cover
61 
62 
63 class change_cover(delegate.mode):
64     path = r"(/books/OL\d+M)/cover"
65 
66     def GET(self, key):
67         page = web.ctx.site.get(key)
68         if page is None or page.type.key not in ['/type/edition', '/type/author']:
69             raise web.seeother(key)
70         return render.change_cover(page)
71 
72 
73 class change_photo(change_cover):
74     path = r"(/authors/OL\d+A)/photo"
75 
76 
77 del delegate.modes[
78     'change_cover'
79 ]  # delete change_cover mode added by openlibrary plugin
80 
81 
82 class components_test(delegate.page):
83     path = "/_dev/components/HelloWorld"
84 
85     def GET(self):
86         return render_component('HelloWorld') + render_component('HelloWorld')
87 
88 
89 class library_explorer(delegate.page):
90     path = "/explore"
91 
92     def GET(self):
93         return render_template('library_explorer')
94 
95 
96 class merge_work(delegate.page):
97     path = r"(/works/OL\d+W)/merge"
98 
99     def GET(self, key):
100         return "This looks like a good place for a merge UI!"
101 
102     def POST(self, key):
103         pass
104 
105 
106 @web.memoize
107 @public
108 def vendor_js():
109     pardir = os.path.pardir
110     path = os.path.abspath(
111         os.path.join(
112             __file__,
113             pardir,
114             pardir,
115             pardir,
116             pardir,
117             'static',
118             'upstream',
119             'js',
120             'vendor.js',
121         )
122     )
123     with open(path, 'rb') as in_file:
124         digest = hashlib.md5(in_file.read()).hexdigest()
125     return '/static/upstream/js/vendor.js?v=' + digest
126 
127 
128 @web.memoize
129 @public
130 def static_url(path):
131     """Takes path relative to static/ and constructs url to that resource with hash."""
132     pardir = os.path.pardir
133     fullpath = os.path.abspath(
134         os.path.join(__file__, pardir, pardir, pardir, pardir, "static", path)
135     )
136     with open(fullpath, 'rb') as in_file:
137         digest = hashlib.md5(in_file.read()).hexdigest()
138     return f"/static/{path}?v={digest}"
139 
140 
141 class DynamicDocument:
142     """Dynamic document is created by concatinating various rawtext documents in the DB.
143     Used to generate combined js/css using multiple js/css files in the system.
144     """
145 
146     def __init__(self, root):
147         self.root = web.rstrips(root, '/')
148         self.docs = None
149         self._text = None
150         self.last_modified = None
151 
152     def update(self):
153         keys = web.ctx.site.things({'type': '/type/rawtext', 'key~': self.root + '/*'})
154         docs = sorted(web.ctx.site.get_many(keys), key=lambda doc: doc.key)
155         if docs:
156             self.last_modified = min(doc.last_modified for doc in docs)
157             self._text = "\n\n".join(doc.get('body', '') for doc in docs)
158         else:
159             self.last_modified = datetime.datetime.utcnow()
160             self._text = ""
161 
162     def get_text(self):
163         """Returns text of the combined documents"""
164         if self._text is None:
165             self.update()
166         return self._text
167 
168     def md5(self):
169         """Returns md5 checksum of the combined documents"""
170         return hashlib.md5(self.get_text().encode('utf-8')).hexdigest()
171 
172 
173 def create_dynamic_document(url, prefix):
174     """Creates a handler for `url` for servering combined js/css for `prefix/*` pages"""
175     doc = DynamicDocument(prefix)
176 
177     if url.endswith('.js'):
178         content_type = "text/javascript"
179     elif url.endswith(".css"):
180         content_type = "text/css"
181     else:
182         content_type = "text/plain"
183 
184     class page(delegate.page):
185         """Handler for serving the combined content."""
186 
187         path = "__registered_later_without_using_this__"
188 
189         def GET(self):
190             i = web.input(v=None)
191             v = doc.md5()
192             if v != i.v:
193                 raise web.seeother(web.changequery(v=v))
194 
195             if web.modified(etag=v):
196                 oneyear = 365 * 24 * 3600
197                 web.header("Content-Type", content_type)
198                 web.header("Cache-Control", "Public, max-age=%d" % oneyear)
199                 web.lastmodified(doc.last_modified)
200                 web.expires(oneyear)
201                 return delegate.RawText(doc.get_text())
202 
203         def url(self):
204             return url + "?v=" + doc.md5()
205 
206         def reload(self):
207             doc.update()
208 
209     class hook(client.hook):
210         """Hook to update the DynamicDocument when any of the source pages is updated."""
211 
212         def on_new_version(self, page):
213             if page.key.startswith(doc.root):
214                 doc.update()
215 
216     # register the special page
217     delegate.pages[url] = {}
218     delegate.pages[url][None] = page
219     return page
220 
221 
222 all_js = create_dynamic_document("/js/all.js", config.get("js_root", "/js"))
223 web.template.Template.globals['all_js'] = all_js()
224 
225 all_css = create_dynamic_document("/css/all.css", config.get("css_root", "/css"))
226 web.template.Template.globals['all_css'] = all_css()
227 
228 
229 def reload():
230     """Reload all.css and all.js"""
231     all_css().reload()
232     all_js().reload()
233 
234 
235 def setup_jquery_urls():
236     if config.get('use_google_cdn', True):
237         jquery_url = "http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"
238         jqueryui_url = (
239             "http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js"
240         )
241     else:
242         jquery_url = "/static/upstream/js/jquery-1.3.2.min.js"
243         jqueryui_url = "/static/upstream/js/jquery-ui-1.7.2.min.js"
244 
245     web.template.Template.globals['jquery_url'] = jquery_url
246     web.template.Template.globals['jqueryui_url'] = jqueryui_url
247     web.template.Template.globals['use_google_cdn'] = config.get('use_google_cdn', True)
248 
249 
250 def user_is_admin_or_librarian():
251     user = web.ctx.site.get_user()
252     return user and (user.is_admin() or user.is_librarian())
253 
254 
255 @public
256 def get_document(key, limit_redirs=5):
257     doc = None
258     for i in range(limit_redirs):
259         doc = web.ctx.site.get(key)
260         if doc is None:
261             return None
262         if doc.type.key == "/type/redirect":
263             key = doc.location
264         else:
265             return doc
266     return doc
267 
268 
269 class revert(delegate.mode):
270     def GET(self, key):
271         raise web.seeother(web.changequery(m=None))
272 
273     def POST(self, key):
274         i = web.input("v", _comment=None)
275         v = i.v and safeint(i.v, None)
276 
277         if v is None:
278             raise web.seeother(web.changequery({}))
279 
280         if not web.ctx.site.can_write(key) or not user_is_admin_or_librarian():
281             return render.permission_denied(
282                 web.ctx.fullpath, "Permission denied to edit " + key + "."
283             )
284 
285         thing = web.ctx.site.get(key, i.v)
286 
287         if not thing:
288             raise web.notfound()
289 
290         def revert(thing):
291             if thing.type.key == "/type/delete" and thing.revision > 1:
292                 prev = web.ctx.site.get(thing.key, thing.revision - 1)
293                 if prev.type.key in ["/type/delete", "/type/redirect"]:
294                     return revert(prev)
295                 else:
296                     prev._save("revert to revision %d" % prev.revision)
297                     return prev
298             elif thing.type.key == "/type/redirect":
299                 redirect = web.ctx.site.get(thing.location)
300                 if redirect and redirect.type.key not in [
301                     "/type/delete",
302                     "/type/redirect",
303                 ]:
304                     return redirect
305                 else:
306                     # bad redirect. Try the previous revision
307                     prev = web.ctx.site.get(thing.key, thing.revision - 1)
308                     return revert(prev)
309             else:
310                 return thing
311 
312         def process(value):
313             if isinstance(value, list):
314                 return [process(v) for v in value]
315             elif isinstance(value, client.Thing):
316                 if value.key:
317                     if value.type.key in ['/type/delete', '/type/revert']:
318                         return revert(value)
319                     else:
320                         return value
321                 else:
322                     for k in value:
323                         value[k] = process(value[k])
324                     return value
325             else:
326                 return value
327 
328         for k in thing:
329             thing[k] = process(thing[k])
330 
331         comment = i._comment or "reverted to revision %d" % v
332         thing._save(comment)
333         raise web.seeother(key)
334 
335 
336 def setup():
337     """Setup for upstream plugin"""
338     models.setup()
339     utils.setup()
340     addbook.setup()
341     covers.setup()
342     merge_authors.setup()
343 
344     from openlibrary.plugins.upstream import data, jsdef
345 
346     data.setup()
347 
348     # setup template globals
349     from openlibrary.i18n import ugettext, ungettext, gettext_territory
350 
351     web.template.Template.globals.update(
352         {
353             "gettext": ugettext,
354             "ugettext": ugettext,
355             "_": ugettext,
356             "ungettext": ungettext,
357             "gettext_territory": gettext_territory,
358             "random": random.Random(),
359             "commify": web.commify,
360             "group": web.group,
361             "storage": web.storage,
362             "all": all,
363             "any": any,
364             "locals": locals,
365         }
366     )
367 
368     web.template.STATEMENT_NODES["jsdef"] = jsdef.JSDefNode
369 
370     setup_jquery_urls()
371 
372 
373 setup()
374
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
 
 
 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

{"golden_diff": "diff --git a/openlibrary/plugins/upstream/code.py b/openlibrary/plugins/upstream/code.py\n--- a/openlibrary/plugins/upstream/code.py\n+++ b/openlibrary/plugins/upstream/code.py\n@@ -94,13 +94,17 @@\n \n \n class merge_work(delegate.page):\n-    path = r\"(/works/OL\\d+W)/merge\"\n+    path = \"/works/merge\"\n \n-    def GET(self, key):\n-        return \"This looks like a good place for a merge UI!\"\n-\n-    def POST(self, key):\n-        pass\n+    def GET(self):\n+        user = web.ctx.site.get_user()\n+        has_access = user and (\n+            (user.is_admin() or user.is_librarian())\n+            and user.is_usergroup_member('/usergroup/librarian-work-merge')\n+        )\n+        if not has_access:\n+            raise web.HTTPError('403 Forbidden')\n+        return render_template('merge/works')\n \n \n @web.memoize\n", "issue": "Provide beta UI for record merging\n### Summary of Requirements\r\n- [x] GETing /libtools/merge?olids=OL1W,OL2W displays the beta merging UI\r\n    - Used /works/merge instead\r\n- [ ] UI will not merge works list backreferences (but allow the merge to continue for the other selected items)\r\n- [ ] UI will not merge works if works are on reading logs (but will allow the merge to continue for the other selected items)\r\n- [x] Should only be available to librarians\r\n- [ ] Log to stats.ol.edits.action.merge-works when a merge occurs\r\n- [x] There is a means of selecting items from the search page\r\n- [ ] Batch edit is created for the merge, with action = merge-works\r\n- [x] Pressing \"Do merge\" does a merge\r\n\r\n### Relationships\r\n- Part of #805\r\n- Depends on #2963\n", "before_files": [{"content": "\"\"\"Upstream customizations.\"\"\"\n\nimport datetime\nimport hashlib\nimport io\nimport os.path\nimport random\n\nimport web\n\nfrom infogami import config\nfrom infogami.core import code as core\nfrom infogami.infobase import client\nfrom infogami.utils import delegate, app, types\nfrom infogami.utils.view import public, safeint, render\nfrom infogami.utils.view import render_template  # noqa: F401 used for its side effects\nfrom infogami.utils.context import context\n\nfrom openlibrary import accounts\n\nfrom openlibrary.plugins.upstream import addbook, covers, merge_authors, models, utils\nfrom openlibrary.plugins.upstream import spamcheck\nfrom openlibrary.plugins.upstream import borrow, recentchanges  # TODO: unused imports?\nfrom openlibrary.plugins.upstream.utils import render_component\n\nif not config.get('coverstore_url'):\n    config.coverstore_url = \"https://covers.openlibrary.org\"\n\n\nclass static(delegate.page):\n    path = \"/images/.*\"\n\n    def GET(self):\n        host = 'https://%s' % web.ctx.host if 'openlibrary.org' in web.ctx.host else ''\n        raise web.seeother(host + '/static' + web.ctx.path)\n\n\nclass edit(core.edit):\n    \"\"\"Overwrite ?m=edit behaviour for author, book, work, and people pages.\"\"\"\n\n    def GET(self, key):\n        page = web.ctx.site.get(key)\n\n        if web.re_compile('/(authors|books|works)/OL.*').match(key):\n            if page is None:\n                raise web.seeother(key)\n            else:\n                raise web.seeother(page.url(suffix=\"/edit\"))\n        else:\n            return core.edit.GET(self, key)\n\n    def POST(self, key):\n        if web.re_compile('/(people/[^/]+)').match(key) and spamcheck.is_spam():\n            return render_template(\n                'message.html', 'Oops', 'Something went wrong. Please try again later.'\n            )\n        return core.edit.POST(self, key)\n\n\n# handlers for change photo and change cover\n\n\nclass change_cover(delegate.mode):\n    path = r\"(/books/OL\\d+M)/cover\"\n\n    def GET(self, key):\n        page = web.ctx.site.get(key)\n        if page is None or page.type.key not in ['/type/edition', '/type/author']:\n            raise web.seeother(key)\n        return render.change_cover(page)\n\n\nclass change_photo(change_cover):\n    path = r\"(/authors/OL\\d+A)/photo\"\n\n\ndel delegate.modes[\n    'change_cover'\n]  # delete change_cover mode added by openlibrary plugin\n\n\nclass components_test(delegate.page):\n    path = \"/_dev/components/HelloWorld\"\n\n    def GET(self):\n        return render_component('HelloWorld') + render_component('HelloWorld')\n\n\nclass library_explorer(delegate.page):\n    path = \"/explore\"\n\n    def GET(self):\n        return render_template('library_explorer')\n\n\nclass merge_work(delegate.page):\n    path = r\"(/works/OL\\d+W)/merge\"\n\n    def GET(self, key):\n        return \"This looks like a good place for a merge UI!\"\n\n    def POST(self, key):\n        pass\n\n\n@web.memoize\n@public\ndef vendor_js():\n    pardir = os.path.pardir\n    path = os.path.abspath(\n        os.path.join(\n            __file__,\n            pardir,\n            pardir,\n            pardir,\n            pardir,\n            'static',\n            'upstream',\n            'js',\n            'vendor.js',\n        )\n    )\n    with open(path, 'rb') as in_file:\n        digest = hashlib.md5(in_file.read()).hexdigest()\n    return '/static/upstream/js/vendor.js?v=' + digest\n\n\n@web.memoize\n@public\ndef static_url(path):\n    \"\"\"Takes path relative to static/ and constructs url to that resource with hash.\"\"\"\n    pardir = os.path.pardir\n    fullpath = os.path.abspath(\n        os.path.join(__file__, pardir, pardir, pardir, pardir, \"static\", path)\n    )\n    with open(fullpath, 'rb') as in_file:\n        digest = hashlib.md5(in_file.read()).hexdigest()\n    return f\"/static/{path}?v={digest}\"\n\n\nclass DynamicDocument:\n    \"\"\"Dynamic document is created by concatinating various rawtext documents in the DB.\n    Used to generate combined js/css using multiple js/css files in the system.\n    \"\"\"\n\n    def __init__(self, root):\n        self.root = web.rstrips(root, '/')\n        self.docs = None\n        self._text = None\n        self.last_modified = None\n\n    def update(self):\n        keys = web.ctx.site.things({'type': '/type/rawtext', 'key~': self.root + '/*'})\n        docs = sorted(web.ctx.site.get_many(keys), key=lambda doc: doc.key)\n        if docs:\n            self.last_modified = min(doc.last_modified for doc in docs)\n            self._text = \"\\n\\n\".join(doc.get('body', '') for doc in docs)\n        else:\n            self.last_modified = datetime.datetime.utcnow()\n            self._text = \"\"\n\n    def get_text(self):\n        \"\"\"Returns text of the combined documents\"\"\"\n        if self._text is None:\n            self.update()\n        return self._text\n\n    def md5(self):\n        \"\"\"Returns md5 checksum of the combined documents\"\"\"\n        return hashlib.md5(self.get_text().encode('utf-8')).hexdigest()\n\n\ndef create_dynamic_document(url, prefix):\n    \"\"\"Creates a handler for `url` for servering combined js/css for `prefix/*` pages\"\"\"\n    doc = DynamicDocument(prefix)\n\n    if url.endswith('.js'):\n        content_type = \"text/javascript\"\n    elif url.endswith(\".css\"):\n        content_type = \"text/css\"\n    else:\n        content_type = \"text/plain\"\n\n    class page(delegate.page):\n        \"\"\"Handler for serving the combined content.\"\"\"\n\n        path = \"__registered_later_without_using_this__\"\n\n        def GET(self):\n            i = web.input(v=None)\n            v = doc.md5()\n            if v != i.v:\n                raise web.seeother(web.changequery(v=v))\n\n            if web.modified(etag=v):\n                oneyear = 365 * 24 * 3600\n                web.header(\"Content-Type\", content_type)\n                web.header(\"Cache-Control\", \"Public, max-age=%d\" % oneyear)\n                web.lastmodified(doc.last_modified)\n                web.expires(oneyear)\n                return delegate.RawText(doc.get_text())\n\n        def url(self):\n            return url + \"?v=\" + doc.md5()\n\n        def reload(self):\n            doc.update()\n\n    class hook(client.hook):\n        \"\"\"Hook to update the DynamicDocument when any of the source pages is updated.\"\"\"\n\n        def on_new_version(self, page):\n            if page.key.startswith(doc.root):\n                doc.update()\n\n    # register the special page\n    delegate.pages[url] = {}\n    delegate.pages[url][None] = page\n    return page\n\n\nall_js = create_dynamic_document(\"/js/all.js\", config.get(\"js_root\", \"/js\"))\nweb.template.Template.globals['all_js'] = all_js()\n\nall_css = create_dynamic_document(\"/css/all.css\", config.get(\"css_root\", \"/css\"))\nweb.template.Template.globals['all_css'] = all_css()\n\n\ndef reload():\n    \"\"\"Reload all.css and all.js\"\"\"\n    all_css().reload()\n    all_js().reload()\n\n\ndef setup_jquery_urls():\n    if config.get('use_google_cdn', True):\n        jquery_url = \"http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js\"\n        jqueryui_url = (\n            \"http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js\"\n        )\n    else:\n        jquery_url = \"/static/upstream/js/jquery-1.3.2.min.js\"\n        jqueryui_url = \"/static/upstream/js/jquery-ui-1.7.2.min.js\"\n\n    web.template.Template.globals['jquery_url'] = jquery_url\n    web.template.Template.globals['jqueryui_url'] = jqueryui_url\n    web.template.Template.globals['use_google_cdn'] = config.get('use_google_cdn', True)\n\n\ndef user_is_admin_or_librarian():\n    user = web.ctx.site.get_user()\n    return user and (user.is_admin() or user.is_librarian())\n\n\n@public\ndef get_document(key, limit_redirs=5):\n    doc = None\n    for i in range(limit_redirs):\n        doc = web.ctx.site.get(key)\n        if doc is None:\n            return None\n        if doc.type.key == \"/type/redirect\":\n            key = doc.location\n        else:\n            return doc\n    return doc\n\n\nclass revert(delegate.mode):\n    def GET(self, key):\n        raise web.seeother(web.changequery(m=None))\n\n    def POST(self, key):\n        i = web.input(\"v\", _comment=None)\n        v = i.v and safeint(i.v, None)\n\n        if v is None:\n            raise web.seeother(web.changequery({}))\n\n        if not web.ctx.site.can_write(key) or not user_is_admin_or_librarian():\n            return render.permission_denied(\n                web.ctx.fullpath, \"Permission denied to edit \" + key + \".\"\n            )\n\n        thing = web.ctx.site.get(key, i.v)\n\n        if not thing:\n            raise web.notfound()\n\n        def revert(thing):\n            if thing.type.key == \"/type/delete\" and thing.revision > 1:\n                prev = web.ctx.site.get(thing.key, thing.revision - 1)\n                if prev.type.key in [\"/type/delete\", \"/type/redirect\"]:\n                    return revert(prev)\n                else:\n                    prev._save(\"revert to revision %d\" % prev.revision)\n                    return prev\n            elif thing.type.key == \"/type/redirect\":\n                redirect = web.ctx.site.get(thing.location)\n                if redirect and redirect.type.key not in [\n                    \"/type/delete\",\n                    \"/type/redirect\",\n                ]:\n                    return redirect\n                else:\n                    # bad redirect. Try the previous revision\n                    prev = web.ctx.site.get(thing.key, thing.revision - 1)\n                    return revert(prev)\n            else:\n                return thing\n\n        def process(value):\n            if isinstance(value, list):\n                return [process(v) for v in value]\n            elif isinstance(value, client.Thing):\n                if value.key:\n                    if value.type.key in ['/type/delete', '/type/revert']:\n                        return revert(value)\n                    else:\n                        return value\n                else:\n                    for k in value:\n                        value[k] = process(value[k])\n                    return value\n            else:\n                return value\n\n        for k in thing:\n            thing[k] = process(thing[k])\n\n        comment = i._comment or \"reverted to revision %d\" % v\n        thing._save(comment)\n        raise web.seeother(key)\n\n\ndef setup():\n    \"\"\"Setup for upstream plugin\"\"\"\n    models.setup()\n    utils.setup()\n    addbook.setup()\n    covers.setup()\n    merge_authors.setup()\n\n    from openlibrary.plugins.upstream import data, jsdef\n\n    data.setup()\n\n    # setup template globals\n    from openlibrary.i18n import ugettext, ungettext, gettext_territory\n\n    web.template.Template.globals.update(\n        {\n            \"gettext\": ugettext,\n            \"ugettext\": ugettext,\n            \"_\": ugettext,\n            \"ungettext\": ungettext,\n            \"gettext_territory\": gettext_territory,\n            \"random\": random.Random(),\n            \"commify\": web.commify,\n            \"group\": web.group,\n            \"storage\": web.storage,\n            \"all\": all,\n            \"any\": any,\n            \"locals\": locals,\n        }\n    )\n\n    web.template.STATEMENT_NODES[\"jsdef\"] = jsdef.JSDefNode\n\n    setup_jquery_urls()\n\n\nsetup()\n", "path": "openlibrary/plugins/upstream/code.py"}], "after_files": [{"content": "\"\"\"Upstream customizations.\"\"\"\n\nimport datetime\nimport hashlib\nimport io\nimport os.path\nimport random\n\nimport web\n\nfrom infogami import config\nfrom infogami.core import code as core\nfrom infogami.infobase import client\nfrom infogami.utils import delegate, app, types\nfrom infogami.utils.view import public, safeint, render\nfrom infogami.utils.view import render_template  # noqa: F401 used for its side effects\nfrom infogami.utils.context import context\n\nfrom openlibrary import accounts\n\nfrom openlibrary.plugins.upstream import addbook, covers, merge_authors, models, utils\nfrom openlibrary.plugins.upstream import spamcheck\nfrom openlibrary.plugins.upstream import borrow, recentchanges  # TODO: unused imports?\nfrom openlibrary.plugins.upstream.utils import render_component\n\nif not config.get('coverstore_url'):\n    config.coverstore_url = \"https://covers.openlibrary.org\"\n\n\nclass static(delegate.page):\n    path = \"/images/.*\"\n\n    def GET(self):\n        host = 'https://%s' % web.ctx.host if 'openlibrary.org' in web.ctx.host else ''\n        raise web.seeother(host + '/static' + web.ctx.path)\n\n\nclass edit(core.edit):\n    \"\"\"Overwrite ?m=edit behaviour for author, book, work, and people pages.\"\"\"\n\n    def GET(self, key):\n        page = web.ctx.site.get(key)\n\n        if web.re_compile('/(authors|books|works)/OL.*').match(key):\n            if page is None:\n                raise web.seeother(key)\n            else:\n                raise web.seeother(page.url(suffix=\"/edit\"))\n        else:\n            return core.edit.GET(self, key)\n\n    def POST(self, key):\n        if web.re_compile('/(people/[^/]+)').match(key) and spamcheck.is_spam():\n            return render_template(\n                'message.html', 'Oops', 'Something went wrong. Please try again later.'\n            )\n        return core.edit.POST(self, key)\n\n\n# handlers for change photo and change cover\n\n\nclass change_cover(delegate.mode):\n    path = r\"(/books/OL\\d+M)/cover\"\n\n    def GET(self, key):\n        page = web.ctx.site.get(key)\n        if page is None or page.type.key not in ['/type/edition', '/type/author']:\n            raise web.seeother(key)\n        return render.change_cover(page)\n\n\nclass change_photo(change_cover):\n    path = r\"(/authors/OL\\d+A)/photo\"\n\n\ndel delegate.modes[\n    'change_cover'\n]  # delete change_cover mode added by openlibrary plugin\n\n\nclass components_test(delegate.page):\n    path = \"/_dev/components/HelloWorld\"\n\n    def GET(self):\n        return render_component('HelloWorld') + render_component('HelloWorld')\n\n\nclass library_explorer(delegate.page):\n    path = \"/explore\"\n\n    def GET(self):\n        return render_template('library_explorer')\n\n\nclass merge_work(delegate.page):\n    path = \"/works/merge\"\n\n    def GET(self):\n        user = web.ctx.site.get_user()\n        has_access = user and (\n            (user.is_admin() or user.is_librarian())\n            and user.is_usergroup_member('/usergroup/librarian-work-merge')\n        )\n        if not has_access:\n            raise web.HTTPError('403 Forbidden')\n        return render_template('merge/works')\n\n\n@web.memoize\n@public\ndef vendor_js():\n    pardir = os.path.pardir\n    path = os.path.abspath(\n        os.path.join(\n            __file__,\n            pardir,\n            pardir,\n            pardir,\n            pardir,\n            'static',\n            'upstream',\n            'js',\n            'vendor.js',\n        )\n    )\n    with open(path, 'rb') as in_file:\n        digest = hashlib.md5(in_file.read()).hexdigest()\n    return '/static/upstream/js/vendor.js?v=' + digest\n\n\n@web.memoize\n@public\ndef static_url(path):\n    \"\"\"Takes path relative to static/ and constructs url to that resource with hash.\"\"\"\n    pardir = os.path.pardir\n    fullpath = os.path.abspath(\n        os.path.join(__file__, pardir, pardir, pardir, pardir, \"static\", path)\n    )\n    with open(fullpath, 'rb') as in_file:\n        digest = hashlib.md5(in_file.read()).hexdigest()\n    return f\"/static/{path}?v={digest}\"\n\n\nclass DynamicDocument:\n    \"\"\"Dynamic document is created by concatinating various rawtext documents in the DB.\n    Used to generate combined js/css using multiple js/css files in the system.\n    \"\"\"\n\n    def __init__(self, root):\n        self.root = web.rstrips(root, '/')\n        self.docs = None\n        self._text = None\n        self.last_modified = None\n\n    def update(self):\n        keys = web.ctx.site.things({'type': '/type/rawtext', 'key~': self.root + '/*'})\n        docs = sorted(web.ctx.site.get_many(keys), key=lambda doc: doc.key)\n        if docs:\n            self.last_modified = min(doc.last_modified for doc in docs)\n            self._text = \"\\n\\n\".join(doc.get('body', '') for doc in docs)\n        else:\n            self.last_modified = datetime.datetime.utcnow()\n            self._text = \"\"\n\n    def get_text(self):\n        \"\"\"Returns text of the combined documents\"\"\"\n        if self._text is None:\n            self.update()\n        return self._text\n\n    def md5(self):\n        \"\"\"Returns md5 checksum of the combined documents\"\"\"\n        return hashlib.md5(self.get_text().encode('utf-8')).hexdigest()\n\n\ndef create_dynamic_document(url, prefix):\n    \"\"\"Creates a handler for `url` for servering combined js/css for `prefix/*` pages\"\"\"\n    doc = DynamicDocument(prefix)\n\n    if url.endswith('.js'):\n        content_type = \"text/javascript\"\n    elif url.endswith(\".css\"):\n        content_type = \"text/css\"\n    else:\n        content_type = \"text/plain\"\n\n    class page(delegate.page):\n        \"\"\"Handler for serving the combined content.\"\"\"\n\n        path = \"__registered_later_without_using_this__\"\n\n        def GET(self):\n            i = web.input(v=None)\n            v = doc.md5()\n            if v != i.v:\n                raise web.seeother(web.changequery(v=v))\n\n            if web.modified(etag=v):\n                oneyear = 365 * 24 * 3600\n                web.header(\"Content-Type\", content_type)\n                web.header(\"Cache-Control\", \"Public, max-age=%d\" % oneyear)\n                web.lastmodified(doc.last_modified)\n                web.expires(oneyear)\n                return delegate.RawText(doc.get_text())\n\n        def url(self):\n            return url + \"?v=\" + doc.md5()\n\n        def reload(self):\n            doc.update()\n\n    class hook(client.hook):\n        \"\"\"Hook to update the DynamicDocument when any of the source pages is updated.\"\"\"\n\n        def on_new_version(self, page):\n            if page.key.startswith(doc.root):\n                doc.update()\n\n    # register the special page\n    delegate.pages[url] = {}\n    delegate.pages[url][None] = page\n    return page\n\n\nall_js = create_dynamic_document(\"/js/all.js\", config.get(\"js_root\", \"/js\"))\nweb.template.Template.globals['all_js'] = all_js()\n\nall_css = create_dynamic_document(\"/css/all.css\", config.get(\"css_root\", \"/css\"))\nweb.template.Template.globals['all_css'] = all_css()\n\n\ndef reload():\n    \"\"\"Reload all.css and all.js\"\"\"\n    all_css().reload()\n    all_js().reload()\n\n\ndef setup_jquery_urls():\n    if config.get('use_google_cdn', True):\n        jquery_url = \"http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js\"\n        jqueryui_url = (\n            \"http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js\"\n        )\n    else:\n        jquery_url = \"/static/upstream/js/jquery-1.3.2.min.js\"\n        jqueryui_url = \"/static/upstream/js/jquery-ui-1.7.2.min.js\"\n\n    web.template.Template.globals['jquery_url'] = jquery_url\n    web.template.Template.globals['jqueryui_url'] = jqueryui_url\n    web.template.Template.globals['use_google_cdn'] = config.get('use_google_cdn', True)\n\n\ndef user_is_admin_or_librarian():\n    user = web.ctx.site.get_user()\n    return user and (user.is_admin() or user.is_librarian())\n\n\n@public\ndef get_document(key, limit_redirs=5):\n    doc = None\n    for i in range(limit_redirs):\n        doc = web.ctx.site.get(key)\n        if doc is None:\n            return None\n        if doc.type.key == \"/type/redirect\":\n            key = doc.location\n        else:\n            return doc\n    return doc\n\n\nclass revert(delegate.mode):\n    def GET(self, key):\n        raise web.seeother(web.changequery(m=None))\n\n    def POST(self, key):\n        i = web.input(\"v\", _comment=None)\n        v = i.v and safeint(i.v, None)\n\n        if v is None:\n            raise web.seeother(web.changequery({}))\n\n        if not web.ctx.site.can_write(key) or not user_is_admin_or_librarian():\n            return render.permission_denied(\n                web.ctx.fullpath, \"Permission denied to edit \" + key + \".\"\n            )\n\n        thing = web.ctx.site.get(key, i.v)\n\n        if not thing:\n            raise web.notfound()\n\n        def revert(thing):\n            if thing.type.key == \"/type/delete\" and thing.revision > 1:\n                prev = web.ctx.site.get(thing.key, thing.revision - 1)\n                if prev.type.key in [\"/type/delete\", \"/type/redirect\"]:\n                    return revert(prev)\n                else:\n                    prev._save(\"revert to revision %d\" % prev.revision)\n                    return prev\n            elif thing.type.key == \"/type/redirect\":\n                redirect = web.ctx.site.get(thing.location)\n                if redirect and redirect.type.key not in [\n                    \"/type/delete\",\n                    \"/type/redirect\",\n                ]:\n                    return redirect\n                else:\n                    # bad redirect. Try the previous revision\n                    prev = web.ctx.site.get(thing.key, thing.revision - 1)\n                    return revert(prev)\n            else:\n                return thing\n\n        def process(value):\n            if isinstance(value, list):\n                return [process(v) for v in value]\n            elif isinstance(value, client.Thing):\n                if value.key:\n                    if value.type.key in ['/type/delete', '/type/revert']:\n                        return revert(value)\n                    else:\n                        return value\n                else:\n                    for k in value:\n                        value[k] = process(value[k])\n                    return value\n            else:\n                return value\n\n        for k in thing:\n            thing[k] = process(thing[k])\n\n        comment = i._comment or \"reverted to revision %d\" % v\n        thing._save(comment)\n        raise web.seeother(key)\n\n\ndef setup():\n    \"\"\"Setup for upstream plugin\"\"\"\n    models.setup()\n    utils.setup()\n    addbook.setup()\n    covers.setup()\n    merge_authors.setup()\n\n    from openlibrary.plugins.upstream import data, jsdef\n\n    data.setup()\n\n    # setup template globals\n    from openlibrary.i18n import ugettext, ungettext, gettext_territory\n\n    web.template.Template.globals.update(\n        {\n            \"gettext\": ugettext,\n            \"ugettext\": ugettext,\n            \"_\": ugettext,\n            \"ungettext\": ungettext,\n            \"gettext_territory\": gettext_territory,\n            \"random\": random.Random(),\n            \"commify\": web.commify,\n            \"group\": web.group,\n            \"storage\": web.storage,\n            \"all\": all,\n            \"any\": any,\n            \"locals\": locals,\n        }\n    )\n\n    web.template.STATEMENT_NODES[\"jsdef\"] = jsdef.JSDefNode\n\n    setup_jquery_urls()\n\n\nsetup()\n", "path": "openlibrary/plugins/upstream/code.py"}]}