Skip to content

Commit

Permalink
feature add ids to works
Browse files Browse the repository at this point in the history
fixes #3430 and #1797

Not fully ready to merge, for at least two reasons:

Displays the ids on the work/edition page though #3430 currently
suggests this is optional, so may need removed until a design is
settled.
  • Loading branch information
davidscotson committed Oct 2, 2023
1 parent 025fb4e commit 346f8dc
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 23 deletions.
92 changes: 70 additions & 22 deletions openlibrary/plugins/openlibrary/js/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,38 @@ function validateLccn(data, dataConfig, label) {
return true;
}

export function initIdentifierValidation() {
$('#identifiers').repeat({
vars: {prefix: 'edition--'},
validate: function(data) {return validateIdentifiers(data)},
});
}

export function initWorkIdentifierValidation() {
$('#workidentifiers').repeat({
vars: {prefix: 'work--'},
validate: function(data) {return validateWorkIdentifiers(data)},
});
}

export function initClassificationValidation() {
const dataConfig = JSON.parse(document.querySelector('#classifications').dataset.config);
$('#classifications').repeat({
vars: {prefix: 'edition--'},
validate: function (data) {
if (data.name === '' || data.name === '---') {
return error('#classification-errors', '#select-classification', dataConfig['Please select a classification.']);
}
if (data.value === '') {
const label = $('#select-classification').find(`option[value='${data.name}']`).html();
return error('#classification-errors', '#classification-value', dataConfig['You need to give a value to CLASS.'].replace(/CLASS/, label));
}
$('#classification-errors').hide();
return true;
}
});
}

/**
* Called by initIdentifierValidation(), along with tests in
* tests/unit/js/editEditionsPage.test.js, to validate the addition of new
Expand Down Expand Up @@ -223,29 +255,45 @@ export function validateIdentifiers(data) {
return true;
}

export function initIdentifierValidation() {
$('#identifiers').repeat({
vars: {prefix: 'edition--'},
validate: function(data) {return validateIdentifiers(data)},
});
}
/**
* Called by initWorkIdentifierValidation(), along with tests in
* tests/unit/js/editEditionsPage.test.js, to validate the addition of new
* identifiers (ISBN, LCCN) to an edition.
* @param {Object} data data from the input form
* @returns {boolean} true if identifier passes validation
*/
export function validateWorkIdentifiers(data) {
const dataConfig = JSON.parse(document.querySelector('#workidentifiers').dataset.config);

export function initClassificationValidation() {
const dataConfig = JSON.parse(document.querySelector('#classifications').dataset.config);
$('#classifications').repeat({
vars: {prefix: 'edition--'},
validate: function (data) {
if (data.name === '' || data.name === '---') {
return error('#classification-errors', '#select-classification', dataConfig['Please select a classification.']);
}
if (data.value === '') {
const label = $('#select-classification').find(`option[value='${data.name}']`).html();
return error('#classification-errors', '#classification-value', dataConfig['You need to give a value to CLASS.'].replace(/CLASS/, label));
}
$('#classification-errors').hide();
return true;
}
});
if (data.name === '' || data.name === '---') {
return error('#workid-errors', 'workselect-id', dataConfig['Please select an identifier.'])
}
const label = $('#workselect-id').find(`option[value='${data.name}']`).html();
if (data.value === '') {
return error('#workid-errors', 'workid-value', dataConfig['You need to give a value to ID.'].replace(/ID/, label));
}
if (['ocaid'].includes(data.name) && /\s/g.test(data.value)) {
return error('#workid-errors', 'workid-value', dataConfig['ID ids cannot contain whitespace.'].replace(/ID/, label));
}

let validId = true;
if (data.name === 'lccn') {
validId = validateLccn(data, dataConfig, label);
}

// checking for duplicate identifier entry on all identifier types
// expects parsed ids so placed after validate
const entries = document.querySelectorAll(`.${data.name}`);
if (isIdDupe(entries, data.value) === true) {
// isbnOverride being set will override the dupe checker, so clear isbnOverride if there's a dupe.
if (isbnOverride.get()) {isbnOverride.clear()}
return error('#id-errors', 'id-value', dataConfig['That ID already exists for this edition.'].replace(/ID/, label));
}

if (validId === false) return false;

$('#id-errors').hide();
return true;
}

export function initLanguageMultiInputAutocomplete() {
Expand Down
4 changes: 4 additions & 0 deletions openlibrary/plugins/openlibrary/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ jQuery(function () {
const addRowButton = document.getElementById('add_row_button');
const roles = document.querySelector('#roles');
const identifiers = document.querySelector('#identifiers');
const workIdentifiers = document.querySelector('#workidentifiers');
const classifications = document.querySelector('#classifications');
const excerpts = document.getElementById('excerpts');
const links = document.getElementById('links');
Expand Down Expand Up @@ -164,6 +165,9 @@ jQuery(function () {
if (identifiers) {
module.initIdentifierValidation();
}
if (workIdentifiers) {
module.initWorkIdentifierValidation();
}
if (classifications) {
module.initClassificationValidation();
}
Expand Down
3 changes: 3 additions & 0 deletions openlibrary/plugins/upstream/addbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,9 @@ def save(self, formdata: web.Storage) -> None:
edition_data.works = [{'key': self.work.key}]

if self.work is not None:
identifiers = work_data.pop('identifiers', [])
self.work.set_identifiers(identifiers)

self.work.update(work_data)
saveutil.save(self.work)

Expand Down
83 changes: 82 additions & 1 deletion openlibrary/plugins/upstream/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
from openlibrary.core.models import Image
from openlibrary.core import lending

from openlibrary.plugins.upstream.utils import MultiDict, parse_toc, get_edition_config
from openlibrary.plugins.upstream.utils import (
MultiDict,
parse_toc,
get_edition_config,
get_work_config,
)
from openlibrary.plugins.upstream import account
from openlibrary.plugins.upstream import borrow
from openlibrary.plugins.worksearch.code import works_by_author
Expand Down Expand Up @@ -561,6 +566,82 @@ def get_covers(self, use_solr=True):
else:
return []

def get_identifiers(self):
"""Returns (name, value) pairs of all available identifiers."""
# identifierss stored directly on the work object, rather than in work.identifiers
# none currently
names = []
return self._process_identifiers(
get_work_config().identifiers, names, self.identifiers
)

def set_identifiers(self, identifiers):
"""Updates the edition from identifiers specified as (name, value) pairs."""
names = (
# identifierss stored directly on the work object, rather than in work.identifiers
# none currently
)

d = {}
for id in identifiers:
# ignore bad values
if 'name' not in id or 'value' not in id:
continue
name, value = id['name'], id['value']
if name == 'lccn':
value = normalize_lccn(value)
# `None` in this field causes errors. See #7999.
if value is not None:
d.setdefault(name, []).append(value)

# clear existing value first
for name in names:
self._getdata().pop(name, None)

self.identifiers = {}

for name, value in d.items():
# ocaid is not a list
if name == 'ocaid':
self.ocaid = value[0]
elif name in names:
self[name] = value
else:
self.identifiers[name] = value

def _process_identifiers(self, config_, names, values):
id_map = {}
for id in config_:
id_map[id.name] = id
id.setdefault("label", id.name)
id.setdefault("url_format", None)

d = MultiDict()

def process(name, value):
if value:
if not isinstance(value, list):
value = [value]

id = id_map.get(name) or web.storage(
name=name, label=name, url_format=None
)
for v in value:
d[id.name] = web.storage(
name=id.name,
label=id.label,
value=v,
url=id.get('url') and id.url.replace('@@@', v.replace(' ', '')),
)

for name in names:
process(name, self[name])

for name in values:
process(name, values[name])

return d

def get_covers_from_solr(self):
try:
w = self._solr_data
Expand Down
21 changes: 21 additions & 0 deletions openlibrary/plugins/upstream/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,27 @@ def _get_author_config():
return Storage(identifiers=identifiers)


@public
def get_work_config() -> Storage:
return _get_work_config()


@web.memoize
def _get_work_config():
"""Returns the work config.
The results are cached on the first invocation. Any changes to /config/work page require restarting the app.
This is is cached because fetching and creating the Thing object was taking about 20ms of time for each book request.
"""
thing = web.ctx.site.get('/config/work')
if hasattr(thing, "identifiers"):
identifiers = [Storage(t.dict()) for t in thing.identifiers if 'name' in t]
else:
identifiers = {}
return Storage(identifiers=identifiers)


@public
def get_edition_config() -> Storage:
return _get_edition_config()
Expand Down
78 changes: 78 additions & 0 deletions openlibrary/templates/books/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

$ this_title = work.title + ': ' + work.subtitle if work.get('subtitle', None) else work.title

$ work_config = get_work_config()

$var title: $this_title
$putctx("robots", "noindex,nofollow")

Expand Down Expand Up @@ -105,6 +107,82 @@ <h3 class="editFormBookAuthors">
</div>
</div>
</fieldset>
$ config = ({
$ 'Please select an identifier.': _('Please select an identifier.'),
$ 'You need to give a value to ID.': _('You need to give a value to ID.'),
$ 'ID ids cannot contain whitespace.': _('ID ids cannot contain whitespace.'),
$ 'That ID already exists for this work.': _('That ID already exists for this work.'),
$ 'Invalid ID format': _('Invalid ID format')
$ })
<fieldset class="major" id="workidentifiers" data-config="$dumps(config)">
<legend>$_("ID Numbers")</legend>
<div class="formBack">

<div id="id-errors" class="note" style="display: none"></div>
<div class="formElement">
<div class="label">
<label for="workselect-id">$_("Do you know any identifiers for this work?")</label>
<span class="tip">$_("Like, VIAF?")</span>
</div>
<div class="input">
<table class="identifiers">
<tr id="workidentifiers-form">
<td align="right">
<select name="name" id="workselect-id">
$ id_labels = dict((d.name, d.label) for d in work_config.identifiers)
$ id_dict = dict((id.name, id) for id in work_config.identifiers)
$ popular = ["wikidata", "viaf", "goodreads", "librarything"]

$ set1 = [id_dict[name] for name in popular if name in id_dict]
$ set2 = sorted([id for id in work_config.identifiers if id.name not in popular], key=lambda id:id.label)

<option value="">$_('Select one of many...')</option>
$for id in set1:
<option value="$id.name">$id.label</option>
<optgroup label="----------"></optgroup>
$for id in set2:
<option value="$id.name">$id.label</option>

<!-- <option>---</option> -->
<!-- <option value="__add__">Add a new ID type</option> -->
</select>
</td>
<td>
<input type="text" name="value" id="workid-value"/>
</td>
<td>
<button type="button" name="add" class="repeat-add larger">$_("Add")</button>
</td>
</tr>
<tbody id="workidentifiers-display">
<tr id="workidentifiers-template" style="display: none;" class="repeat-item">
<td align="right"><strong>{{\$("#workselect-id").find("option[value='" + name + "']").html()}}</strong></td>
<td>{{value}}
<input type="hidden" name="{{prefix}}identifiers--{{index}}--name" value="{{name}}"/>
<input type="hidden" name="{{prefix}}identifiers--{{index}}--value" value="{{value}}" class="{{name}}"/>
</td>
<td><a href="javascript:;" class="repeat-remove red plain" title="Remove this identifier">[x]</a></td>
</tr>
<tr>
<td align="right">Open Library</td>
<td>$work.key.split("/")[-1]</td>
<td></td>
</tr>
$for i, id in enumerate(work.get_identifiers().values()):
<tr id="workidentifiers--$i" class="repeat-item">
<td align="right"><strong>$id_labels.get(id.name, id.name)</strong></td>
<td>$id.value
<input type="hidden" name="work--identifiers--${i}--name" value="$id.name"/>
<input type="hidden" name="work--identifiers--${i}--value" value="$id.value" class="$id.name"/>
</td>
<td><a href="javascript:;" class="repeat-remove red plain" title="Remove this identifier">[x]</a></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</fieldset>
<fieldset class="major">
<legend>$_("Add Excerpts")</legend>
<div class="formBack" id="excerpts">
Expand Down
14 changes: 14 additions & 0 deletions openlibrary/templates/type/edition/view.html
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,20 @@ <h4>$:_('Links <span class="gray small sansserif">outside Open Library</span>')<
<li><a href="$link.url">$link.title</a></li>
</ul>
</div>

<div class="section">
<h3 class="list-header collapse">$_("Work ID Numbers")</h3>
<dl class="meta">
$:display_identifiers("Open Library", [storage(url=None, value=work.key.split("/")[-1])])
$for name, values in work.get_identifiers().multi_items():
$ identifier_label = values[0].label
$:display_identifiers(identifier_label, values)

$:display_goodreads("Open Library", [storage(url=None, value=work.key.split("/")[-1])])
$for name, values in work.get_identifiers().multi_items():
$:display_goodreads(values[0].label, values)
</dl>
</div>
</div>

$if show_observations:
Expand Down

0 comments on commit 346f8dc

Please sign in to comment.