Skip to content

Commit 37d059b

Browse files
authored
Merge pull request #10032 from schu96/3430/feature/add-work-ids-ui
Use IdentifiersInput Vue component for work identifier UI
2 parents 70fc10d + 703e2a1 commit 37d059b

File tree

12 files changed

+213
-79
lines changed

12 files changed

+213
-79
lines changed

openlibrary/components/IdentifiersInput.vue

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@
2828
</th>
2929
</tr>
3030
<template v-for="(value, name) in assignedIdentifiers">
31-
<tr :key="name" v-if="value && !isEdition">
31+
<tr :key="name" v-if="value && !saveIdentifiersAsList">
3232
<td>{{ identifierConfigsByKey[name].label }}</td>
3333
<td>{{ value }}</td>
3434
<td>
3535
<button class="form-control" @click="removeIdentifier(name)">Remove</button>
3636
</td>
3737
</tr>
38-
<template v-else-if="value && isEdition">
38+
<template v-else-if="value && saveIdentifiersAsList">
3939
<tr v-for="(item, idx) in value" :key="name + idx">
4040
<td>{{ identifierConfigsByKey[name].label }}</td>
4141
<td>{{ item }}</td>
@@ -52,7 +52,7 @@
5252
</template>
5353

5454
<script>
55-
import { errorDisplay, validateEditionIdentifiers } from './IdentifiersInput/utils/utils.js';
55+
import { errorDisplay, validateIdentifiers } from './IdentifiersInput/utils/utils.js';
5656
const identifierPatterns = {
5757
wikidata: /^Q[1-9]\d*$/i,
5858
isni: /^[0]{4} ?[0-9]{4} ?[0-9]{4} ?[0-9]{3}[0-9X]$/i,
@@ -76,7 +76,9 @@ export default {
7676
id_config_string: {
7777
type: String
7878
},
79-
/** see createHiddenInputs function for usage */
79+
/** see createHiddenInputs function for usage
80+
* #hiddenEditionIdentifiers, #hiddenWorkIdentifiers
81+
*/
8082
output_selector: {
8183
type: String
8284
},
@@ -108,20 +110,20 @@ export default {
108110
return {
109111
selectedIdentifier: '', // Which identifier is selected in dropdown
110112
inputValue: '', // What user put into input
111-
assignedIdentifiers: {}, // IDs assigned to the entity Ex: {'viaf': '12632978'}
113+
assignedIdentifiers: {}, // IDs assigned to the entity Ex: {'viaf': '12632978'} or {'abaa': ['123456','789012']}
112114
}
113115
},
114116
115117
computed: {
116118
popularEditionConfigs: function() {
117-
if (this.isEdition) {
119+
if (this.edition_popular) {
118120
const popularConfigs = JSON.parse(decodeURIComponent(this.edition_popular));
119121
return Object.fromEntries(popularConfigs.map(e => [e.name, e]));
120122
}
121123
return {};
122124
},
123125
secondaryEditionConfigs: function() {
124-
if (this.isEdition) {
126+
if (this.secondary_identifiers) {
125127
const secondConfigs = JSON.parse(decodeURIComponent(this.secondary_identifiers));
126128
return Object.fromEntries(secondConfigs.map(e => [e.name, e]));
127129
}
@@ -139,6 +141,9 @@ export default {
139141
return this.admin.toLowerCase() === 'true';
140142
},
141143
isEdition() {
144+
return this.multiple.toLowerCase() === 'true' && this.edition_popular;
145+
},
146+
saveIdentifiersAsList() {
142147
return this.multiple.toLowerCase() === 'true';
143148
},
144149
setButtonEnabled: function(){
@@ -154,10 +159,10 @@ export default {
154159
if (this.selectedIdentifier === 'isni') {
155160
this.inputValue = this.inputValue.replace(/\s/g, '')
156161
}
157-
if (this.isEdition) {
162+
if (this.saveIdentifiersAsList) {
158163
// collect id values of matching type, or empty array if none present
159164
const existingIds = this.assignedIdentifiers[this.selectedIdentifier] ?? [];
160-
const validEditionId = validateEditionIdentifiers(this.selectedIdentifier, this.inputValue, existingIds);
165+
const validEditionId = validateIdentifiers(this.selectedIdentifier, this.inputValue, existingIds, this.output_selector);
161166
if (validEditionId) {
162167
if (!this.assignedIdentifiers[this.selectedIdentifier]) {
163168
this.inputValue = [this.inputValue];
@@ -170,18 +175,18 @@ export default {
170175
return;
171176
}
172177
} else if (this.assignedIdentifiers[this.selectedIdentifier]) {
173-
errorDisplay(`An author identifier for ${this.identifierConfigsByKey[this.selectedIdentifier].label} already exists.`)
178+
errorDisplay(`An identifier for ${this.identifierConfigsByKey[this.selectedIdentifier].label} already exists.`, this.output_selector)
174179
return;
175-
} else { errorDisplay() }
180+
} else { errorDisplay('', this.output_selector) }
176181
// We use $set otherwise we wouldn't get the reactivity desired
177182
// See https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
178183
this.$set(this.assignedIdentifiers, this.selectedIdentifier, this.inputValue);
179184
this.inputValue = '';
180185
this.selectedIdentifier = '';
181186
},
182187
/** Removes an identifier with value from memory and it will be deleted from database on save */
183-
removeIdentifier: function(identifierName, idx = 0){
184-
if (this.isEdition) {
188+
removeIdentifier: function(identifierName, idx = 0) {
189+
if (this.saveIdentifiersAsList) {
185190
this.assignedIdentifiers[identifierName].splice(idx, 1);
186191
} else {
187192
this.$set(this.assignedIdentifiers, identifierName, '');
@@ -194,7 +199,8 @@ export default {
194199
* So for now this just drops the hidden inputs into the the parent form anytime there is a change
195200
*/
196201
let html = '';
197-
if (this.isEdition) {
202+
// should save a list of ids for work + edition identifiers
203+
if (this.saveIdentifiersAsList) {
198204
let num = 0;
199205
for (const [key, value] of Object.entries(this.assignedIdentifiers)) {
200206
for (const idx in value) {
@@ -227,7 +233,11 @@ export default {
227233
},
228234
created: function(){
229235
this.assignedIdentifiers = JSON.parse(decodeURIComponent(this.assigned_ids_string));
230-
if (this.isEdition) {
236+
if (this.assignedIdentifiers.length === 0) {
237+
this.assignedIdentifiers = {}
238+
return;
239+
}
240+
if (this.saveIdentifiersAsList) {
231241
const edition_identifiers = {};
232242
this.assignedIdentifiers.forEach(entry => {
233243
if (!edition_identifiers[entry.name]) {

openlibrary/components/IdentifiersInput/utils/utils.js

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ import {
88
isValidLccn,
99
} from '../../../plugins/openlibrary/js/idValidation.js';
1010

11-
export function errorDisplay(message) {
12-
const errorSelector = document.querySelector('#id-errors');
11+
export function errorDisplay(message, error_output) {
12+
let errorSelector;
13+
if (error_output === '#hiddenAuthorIdentifiers') {
14+
errorSelector = document.querySelector('#id-errors-author')
15+
} else if (error_output === '#hiddenWorkIdentifiers') {
16+
errorSelector = document.querySelector('#id-errors-work')
17+
} else if (error_output === '#hiddenEditionIdentifiers') {
18+
errorSelector = document.querySelector('#id-errors-edition')
19+
}
1320
if (message) {
1421
errorSelector.style.display = '';
1522
errorSelector.innerHTML = `<div>${message}</div>`;
@@ -23,10 +30,12 @@ export function errorDisplay(message) {
2330
function validateIsbn10(value) {
2431
const isbn10_value = parseIsbn(value);
2532
if (!isFormatValidIsbn10(isbn10_value)) {
26-
errorDisplay('ID must be exactly 10 characters [0-9] or X.');
33+
errorDisplay('ID must be exactly 10 characters [0-9] or X.', '#hiddenEditionIdentifiers');
2734
return false;
28-
} else if (isFormatValidIsbn10(isbn10_value) === true && isChecksumValidIsbn10(isbn10_value) === false) {
29-
errorDisplay(`ISBN ${isbn10_value} may be invalid. Please confirm if you'd like to add it before saving all changes`);
35+
} else if (
36+
isFormatValidIsbn10(isbn10_value) && !isChecksumValidIsbn10(isbn10_value)
37+
) {
38+
errorDisplay(`ISBN ${isbn10_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, '#hiddenEditionIdentifiers');
3039
}
3140
return true;
3241
}
@@ -35,10 +44,12 @@ function validateIsbn13(value) {
3544
const isbn13_value = parseIsbn(value);
3645

3746
if (!isFormatValidIsbn13(isbn13_value)) {
38-
errorDisplay('ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4');
47+
errorDisplay('ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4', '#hiddenEditionIdentifiers');
3948
return false;
40-
} else if (isFormatValidIsbn13(isbn13_value) === true && isChecksumValidIsbn13(isbn13_value) === false) {
41-
errorDisplay(`ISBN ${isbn13_value} may be invalid. Please confirm if you'd like to add it before saving all changes`);
49+
} else if (
50+
isFormatValidIsbn13(isbn13_value) && !isChecksumValidIsbn13(isbn13_value)
51+
) {
52+
errorDisplay(`ISBN ${isbn13_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, '#hiddenEditionIdentifiers');
4253
}
4354
return true;
4455
}
@@ -47,18 +58,18 @@ function validateLccn(value) {
4758
const lccn_value = parseLccn(value);
4859

4960
if (!isValidLccn(lccn_value)) {
50-
errorDisplay('Invalid ID format');
61+
errorDisplay('Invalid ID format', '#hiddenEditionIdentifiers');
5162
return false;
5263
}
5364
return true;
5465
}
5566

56-
export function validateEditionIdentifiers(name, value, entries) {
67+
export function validateIdentifiers(name, value, entries, error_output) {
5768
let validId = true;
58-
errorDisplay('');
69+
errorDisplay('', error_output);
5970
if (name === '' || name === '---') {
6071
// if somehow an invalid identifier is passed through
61-
errorDisplay('Invalid identifier');
72+
errorDisplay('Invalid identifier', error_output);
6273
return false;
6374
}
6475
if (name === 'isbn_10') {
@@ -70,7 +81,7 @@ export function validateEditionIdentifiers(name, value, entries) {
7081
}
7182
if (Array.from(entries).some(entry => entry === value) === true) {
7283
validId = false;
73-
errorDisplay('That ID already exists for this edition');
84+
errorDisplay('That ID already exists for an identifier.', error_output);
7485
}
7586
return validId;
7687
}

openlibrary/i18n/messages.pot

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3604,6 +3604,21 @@ msgstr ""
36043604
msgid "Links"
36053605
msgstr ""
36063606

3607+
#: books/edit.html
3608+
msgid "Work Identifiers"
3609+
msgstr ""
3610+
3611+
#: books/edit.html
3612+
msgid "Do you know any identifiers for this work?"
3613+
msgstr ""
3614+
3615+
#: books/edit.html
3616+
msgid ""
3617+
"These identifiers apply to all editions of this work, for example "
3618+
"Wikidata work identifiers. For edition-specific identifiers, like ISBN or"
3619+
" LCCN, go to the edition tab."
3620+
msgstr ""
3621+
36073622
#: FulltextSearchSuggestionItem.html IABook.html SearchResultsWork.html
36083623
#: books/edition-sort.html jsdef/LazyWorkPreview.html lists/list_overview.html
36093624
#: lists/preview.html lists/snippet.html lists/widget.html
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
identifiers:
2+
- label: BookBrainz
3+
name: bookbrainz
4+
notes: ''
5+
url: https://bookbrainz.org/work/@@@
6+
website: https://bookbrainz.org
7+
- label: Книга Файнфиков
8+
name: ficbook
9+
notes: ''
10+
url: https://ficbook.net/readfic/@@@
11+
- label: MusicBrainz
12+
name: musicbrainz
13+
url: https://musicbrainz.org/artist/@@@
14+
website: https://musicbrainz.org
15+
- label: MyAnimeList
16+
name: myanimelist
17+
notes: ''
18+
url: https://myanimelist.net/manga/@@@
19+
- label: Wikidata
20+
name: wikidata
21+
notes: ''
22+
url: https://www.wikidata.org/wiki/@@@
23+
website: https://wikidata.org
24+
- label: SBN/ICCU (National Library Service of Italy)
25+
name: opac_sbn
26+
notes: format is /^\D{2}[A-Z0-3]V\d{6}$/
27+
url: https://opac.sbn.it/risultati-autori/-/opac-autori/detail/@@@
28+
website: https://opac.sbn.it/

openlibrary/plugins/upstream/addbook.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,9 @@ def save(self, formdata: web.Storage) -> None:
595595
elif self.work is not None and new_work_key is None:
596596
# we're trying to create an orphan; let's not do that
597597
edition_data.works = [{'key': self.work.key}]
598-
599598
if self.work is not None:
599+
work_identifiers = work_data.pop('identifiers', {})
600+
self.work.set_identifiers(work_identifiers)
600601
self.work.update(work_data)
601602
saveutil.save(self.work)
602603

@@ -782,8 +783,8 @@ def read_subject(subjects):
782783
else:
783784
work.subtitle = None
784785

785-
for k in ('excerpts', 'links'):
786-
work[k] = work.get(k) or []
786+
for k in ['excerpts', 'links', 'identifiers']:
787+
work[k] = work.get(k, {})
787788

788789
# ignore empty authors
789790
work.authors = [

openlibrary/plugins/upstream/models.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
borrow,
1919
)
2020
from openlibrary.plugins.upstream.table_of_contents import TableOfContents
21-
from openlibrary.plugins.upstream.utils import MultiDict, get_edition_config
21+
from openlibrary.plugins.upstream.utils import MultiDict, get_identifier_config
2222
from openlibrary.plugins.worksearch.code import works_by_author
2323
from openlibrary.plugins.worksearch.search import get_solr
2424
from openlibrary.utils import dateutil # noqa: F401 side effects may be needed
@@ -124,7 +124,7 @@ def get_identifiers(self):
124124
"""Returns (name, value) pairs of all available identifiers."""
125125
names = ['ocaid', 'isbn_10', 'isbn_13', 'lccn', 'oclc_numbers']
126126
return self._process_identifiers(
127-
get_edition_config().identifiers, names, self.identifiers
127+
get_identifier_config('edition').identifiers, names, self.identifiers
128128
)
129129

130130
def get_ia_meta_fields(self):
@@ -358,10 +358,15 @@ def set_identifiers(self, identifiers):
358358
else:
359359
self.identifiers[name] = value
360360

361+
if not d.items():
362+
self.identifiers = None
363+
361364
def get_classifications(self):
362365
names = ["dewey_decimal_class", "lc_classifications"]
363366
return self._process_identifiers(
364-
get_edition_config().classifications, names, self.classifications
367+
get_identifier_config('edition').classifications,
368+
names,
369+
self.classifications,
365370
)
366371

367372
def set_classifications(self, classifications):
@@ -386,6 +391,9 @@ def set_classifications(self, classifications):
386391
else:
387392
self.classifications[name] = value
388393

394+
if not self.classifications.items():
395+
self.classifications = None
396+
389397
def get_weight(self):
390398
"""returns weight as a storage object with value and units fields."""
391399
w = self.weight
@@ -768,6 +776,66 @@ def as_fake_solr_record(self):
768776
record['subtitle'] = self.subtitle
769777
return record
770778

779+
def get_identifiers(self):
780+
"""Returns (name, value) pairs of all available identifiers."""
781+
names = []
782+
return self._process_identifiers(
783+
get_identifier_config('work').identifiers, names, self.identifiers
784+
)
785+
786+
def set_identifiers(self, identifiers):
787+
"""Updates the work from identifiers specified as (name, value) pairs."""
788+
789+
d = {}
790+
if identifiers:
791+
for id in identifiers:
792+
if 'name' not in id or 'value' not in id:
793+
continue
794+
name, value = id['name'], id['value']
795+
if value is not None:
796+
d.setdefault(name, []).append(value)
797+
798+
self.identifiers = {}
799+
800+
for name, value in d.items():
801+
self.identifiers[name] = value
802+
803+
if not d.items():
804+
self.identifiers = None
805+
806+
def _process_identifiers(self, config_, names, values):
807+
id_map = {}
808+
for id in config_:
809+
id_map[id.name] = id
810+
id.setdefault("label", id.name)
811+
id.setdefault("url_format", None)
812+
813+
d = MultiDict()
814+
815+
def process(name, value):
816+
if value:
817+
if not isinstance(value, list):
818+
value = [value]
819+
820+
id = id_map.get(name) or web.storage(
821+
name=name, label=name, url_format=None
822+
)
823+
for v in value:
824+
d[id.name] = web.storage(
825+
name=id.name,
826+
label=id.label,
827+
value=v,
828+
url=id.get('url') and id.url.replace('@@@', v.replace(' ', '')),
829+
)
830+
831+
for name in names:
832+
process(name, self[name])
833+
834+
for name in values:
835+
process(name, values[name])
836+
837+
return d
838+
771839

772840
class Subject(client.Thing):
773841
pass

0 commit comments

Comments
 (0)