├── README.md ├── index.html ├── index.js └── server.py /README.md: -------------------------------------------------------------------------------- 1 | # CopyAnnotations 2 | Copy top-level annotations from one URL (and/or group) to another 3 | 4 | https://jonudell.info/h/CopyAnnotations/ 5 | 6 | More Hypothesis tools 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 47 | 48 | 49 | 50 | 51 |
52 | 53 |

54 | Copy Hypothesis annotations

55 | 56 | 57 |
58 |
59 |
60 |
Tick this box to copy only the specified user's notes
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 69 |

How to do it

70 | 71 |
72 | 73 |
74 |
75 |
fetching annotations
76 |
copying annotations 0 of 0 errors 0
77 |
78 | 79 |
80 | 81 |
82 | 83 |

84 | reset API token 85 |

86 | 87 |
88 |
89 |

90 | This tool copies annotations from one group to another, and optionally also from one domain to another. 91 | Only top-level annotations will be copied, replies are ignored. 92 | Only maxAnnotations will be copied, the default is 2 as a safety check. 93 | The Hypothesis username you provide will be the creator of copied annotations. 94 | If the source or destination group is not Public, the Hypothesis user must be a member to 95 | read from the source or write to the destination. Tick lmsMode if you're a teacher copying your 96 | own prompts from one course group to another. 97 | 98 | More Hypothesis tools. 99 |

100 |
101 | 102 |
103 | 104 | 105 | 106 | 107 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | let maxAnnotations = 2 2 | hlib.createLmsModeCheckbox(hlib.getById('lmsModeContainer')) 3 | hlib.createUserInputForm(hlib.getById('userContainer'), 'Hypothesis user who will fetch and create annotations') 4 | hlib.getById('userForm').setAttribute('autocomplete', 'off') 5 | const tokenContainer = hlib.getById('tokenContainer') 6 | hlib.createApiTokenInputForm(tokenContainer) 7 | const sourceDomainContainer = hlib.getById('sourceDomainContainer') 8 | hlib.createFacetInputForm(sourceDomainContainer, 'sourceDomain', 'e.g. https://www.example.com') 9 | const destinationDomainContainer = hlib.getById('destinationDomainContainer') 10 | hlib.createFacetInputForm(destinationDomainContainer, 'destinationDomain', 'e.g. https://elsewhere.com, optional, defaults to sourceDomain') 11 | hlib.createFacetInputForm(hlib.getById('limitContainer'), 'maxAnnotations', 'max annotations to copy') 12 | 13 | async function createSourceGroupPicker() { 14 | await hlib.createGroupInputForm(hlib.getById('sourceGroupContainer'), 'sourceGroupsList') 15 | adjustGroupPicker('#sourceGroupContainer', 'sourceGroup', 'sourceGroupsList', 'group from which to copy annotations') 16 | } 17 | 18 | createSourceGroupPicker(); 19 | 20 | async function createDestinationGroupPicker() { 21 | await hlib.createGroupInputForm(hlib.getById('destinationGroupContainer'), 'destinationGroupsList'); 22 | adjustGroupPicker('#destinationGroupContainer', 'destinationGroup', 'destinationGroupsList', 'group to which to copy annotations') 23 | } 24 | 25 | createDestinationGroupPicker(); 26 | 27 | let maxAnnotationsForm = hlib.getById('maxAnnotationsForm') 28 | 29 | maxAnnotationsForm.value = maxAnnotations.toString() 30 | 31 | function lmsMode() { 32 | const checkbox = hlib.getById('lmsModeForm') 33 | return checkbox.checked; 34 | } 35 | 36 | function checkSettings() { 37 | if (!validInput()) { 38 | return 39 | } 40 | 41 | const { user, sourceDomain, sourceGroup, maxAnnotations } = gatherInput() 42 | let facetLink = `https://jonudell.info/h/facet/?group=${sourceGroup}&max=${maxAnnotations}&expanded=true` 43 | 44 | if (lmsMode()) { 45 | facetLink += `&user=${user}` 46 | } else { 47 | facetLink += `&wildcard_uri=${wildcardify(sourceDomain)}` 48 | } 49 | const facetSettingsLink = hlib.getById('facetSettingsLink') 50 | facetSettingsLink.innerHTML = `click to review selected annotations` 51 | } 52 | 53 | function checkResults() { 54 | async function delayedClick() { 55 | await hlib.delaySeconds(3) 56 | const anchor = hlib.getById("checkSettingsAnchor") 57 | anchor.href = facetLink 58 | anchor.click() 59 | } 60 | 61 | const { user, sourceDomain, destinationDomain, destinationGroup } = gatherInput() 62 | const domain = destinationDomain ? destinationDomain : sourceDomain 63 | let facetLink = `https://jonudell.info/h/facet/?group=${destinationGroup}&max=${maxAnnotations}&expanded=true` 64 | 65 | if (lmsMode()) { 66 | facetLink += `&user=${user}` 67 | } else { 68 | facetLink += `&wildcard_uri=${wildcardify(domain)}` 69 | } 70 | 71 | hlib.getById('facetResultsLink').innerHTML = `click to review copied annotations` 72 | const facetSettingsLink = hlib.getById('facetSettingsLink') 73 | const anchor = facetSettingsLink.querySelector('a') 74 | anchor.onclick = delayedClick 75 | } 76 | 77 | // main entry point, wired to copy button 78 | async function copy() { 79 | const { user, sourceDomain, sourceGroup, maxAnnotations } = gatherInput() 80 | hlib.getById('fetchProgress').style.display = 'block' 81 | let params = { 82 | wildcard_uri: wildcardify(sourceDomain), 83 | group: sourceGroup, 84 | max: maxAnnotations, 85 | _separate_replies: 'true' 86 | } 87 | 88 | if (lmsMode()) { 89 | delete params.wildcard_uri 90 | params.user = user 91 | } 92 | 93 | const [annoRows, replyRows] = await hlib.search(params, 'fetchProgress') 94 | _copy(annoRows) 95 | } 96 | 97 | async function _copy(rows) { 98 | function maybeSwapDomain(uri, sourceDomain, destinationDomain) { 99 | if (destinationDomain) { 100 | uri = uri.replace(sourceDomain, destinationDomain) 101 | } 102 | return uri; 103 | } 104 | 105 | const progressElement = document.querySelector('#postProgress') 106 | progressElement.style.display = 'block' 107 | const totalElement = progressElement.querySelector('.total') 108 | totalElement.innerText = rows.length.toString() 109 | let copyCount = 0 110 | let errorCount = 0 111 | const { user, sourceDomain, destinationDomain, destinationGroup } = gatherInput() 112 | const copyCounterElement = progressElement.querySelector('.copyCounter') 113 | const errorCounterElement = progressElement.querySelector('.errorCounter') 114 | for (let i = 0; i < rows.length; i++) { 115 | const anno = hlib.parseAnnotation(rows[i]) 116 | const uri = maybeSwapDomain(anno.url, sourceDomain, destinationDomain) 117 | const payload = { 118 | user: `${user}@hypothes.is`, 119 | uri: uri, 120 | tags: anno.tags, 121 | text: anno.text, 122 | target: anno.target, 123 | group: destinationGroup, 124 | permissions: hlib.createPermissions(user, destinationGroup), 125 | document: anno.document 126 | } 127 | await hlib.delaySeconds(.2) 128 | hlib.postAnnotation(JSON.stringify(payload), hlib.getToken()) 129 | .then(_ => { 130 | copyCount += 1 131 | copyCounterElement.innerText = copyCount.toString() 132 | }) 133 | .catch(e => { 134 | errorCounterElement.style.display = 'inline' 135 | errorCount += 1 136 | errorCounterElement.innerText = `errors ${errorCount.toString()}` 137 | console.log(e) 138 | }) 139 | } 140 | } 141 | 142 | function adjustGroupPicker(groupContainer, label, id, message) { 143 | const picker = document.querySelector(groupContainer) 144 | picker.querySelector('.formLabel').innerHTML = label 145 | const select = picker.querySelector('select') 146 | select.id = id 147 | select.onchange = null 148 | select.selectedIndex = 0 149 | picker.querySelector('.formMessage').innerHTML = message 150 | } 151 | 152 | function gatherInput() { 153 | // these are common to non-lmsMode and and lmsMode 154 | const sourceGroup = hlib.getSelectedGroup('sourceGroupsList') 155 | const destinationGroup = hlib.getSelectedGroup('destinationGroupsList') 156 | const maxAnnotationsForm = hlib.getById('maxAnnotationsForm') 157 | const maxAnnotations = parseInt(maxAnnotationsForm.value) 158 | const userForm = hlib.getById('userForm') 159 | const user = userForm.value 160 | // these are only for non-lmsMode 161 | let sourceDomain = '' 162 | let destinationDomain = '' 163 | if (!lmsMode()) { 164 | const sourceDomainElement = hlib.getById('sourceDomainForm') 165 | sourceDomain = sourceDomainElement.value 166 | const destinationDomainElement = hlib.getById('destinationDomainForm') 167 | destinationDomain = destinationDomainElement.value 168 | } 169 | return { 170 | user: user, 171 | sourceDomain: sourceDomain, 172 | destinationDomain: destinationDomain, 173 | sourceGroup: sourceGroup, 174 | destinationGroup: destinationGroup, 175 | maxAnnotations: maxAnnotations 176 | } 177 | } 178 | 179 | function wildcardify(domain) { 180 | return httpsify(slashstarify(domain)) 181 | } 182 | 183 | function httpsify(domain) { 184 | if (domain && !domain.startsWith('https://')) { 185 | domain = 'https://' + domain 186 | } 187 | return domain 188 | } 189 | 190 | function slashstarify(domain) { 191 | if (!domain.endsWith('/*')) { 192 | domain += '/*' 193 | } 194 | return domain 195 | } 196 | 197 | function validInput() { 198 | const { user, sourceDomain, destinationDomain, sourceGroup, destinationGroup } = gatherInput() 199 | 200 | if (!user) { 201 | alert('Please provide the Hypothesis username associated with this API token.') 202 | return false 203 | } 204 | 205 | if (!lmsMode() && !sourceDomain) { 206 | alert('Please provide a source domain.') 207 | return false 208 | } 209 | 210 | if ((sourceGroup === destinationGroup) && !destinationDomain) { 211 | alert('Please choose a destination group different from the source group.') 212 | return false 213 | } 214 | 215 | return true 216 | } 217 | 218 | function lmsModeHandler() { 219 | const sourceDomain = hlib.getById('sourceDomainContainer') 220 | const destDomain = hlib.getById('destinationDomainContainer') 221 | if (this.checked) { 222 | sourceDomain.style.display = 'none' 223 | destDomain.style.display = 'none' 224 | } else { 225 | sourceDomain.style.display = 'block' 226 | destDomain.style.display = 'block' 227 | } 228 | } 229 | 230 | hlib.getById('lmsModeForm').onclick = lmsModeHandler -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | import http.server 4 | from http.server import SimpleHTTPRequestHandler 5 | 6 | class CORSRequestHandler (SimpleHTTPRequestHandler): 7 | def end_headers (self): 8 | self.send_header('Access-Control-Allow-Origin', '*') 9 | SimpleHTTPRequestHandler.end_headers(self) 10 | 11 | def run(server_class=http.server.HTTPServer, 12 | handler_class=CORSRequestHandler): 13 | server_address = ('', 8001) 14 | httpd = server_class(server_address, handler_class) 15 | httpd.serve_forever() 16 | 17 | if __name__ == '__main__': 18 | run() --------------------------------------------------------------------------------