-
Notifications
You must be signed in to change notification settings - Fork 144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement controlled list manager and reference datatype #10541 #10569
Changes from 250 commits
f4492b7
3eebbe0
2192acf
b9a4bd0
68d271a
36cac54
77b4700
9e5122e
5340333
eb41755
19b499a
a7fbbc8
528ca37
a5314b5
2572147
3847aac
2d46998
02f5088
cee7755
564ef94
9cce931
61c4178
74231ab
7718ef9
7251808
8c8f5de
71a0d8c
cc2d3c0
6def92f
6870c4a
d72c719
25379a4
ab3437f
a68b7aa
d36589f
74bd851
e2a2c3e
36c7b80
e7465f6
eca6de5
b6d2b51
4686fe1
d463622
d6c50ed
a49ec9a
34617b7
864e13a
93ee214
0a24b5c
fa6a1e2
bba6f9f
8160426
2dc1199
8b79c1e
9e319c0
ad0ef77
8979f0a
74e5da7
1abcade
8601555
cbf86ed
cf2bb2e
49cce22
6b6afac
b45b171
eb9d05d
3f36f4f
2970fba
c0c0710
f221b4f
e4eae4b
4e5280d
5e28155
dd3f759
98adbb8
f652b36
5f86c08
69fc9ea
c8b3d0b
41dde2a
36505a2
99f3a08
ff55f49
df3821a
9937005
19bf861
6480606
528eabd
c40992f
6a03157
b0e7180
ce8b65e
6ab902e
93aeb5a
ccea2cc
b02270b
6af3e49
92994cc
360eb41
3bd6edb
9c10b3d
8044256
bac06e8
f98bf81
fb740ae
ce0e85c
0470d15
1b05c62
d930606
6d6fbd2
79f6e94
0552f70
a21cb6b
124c3f0
467ec48
a58f24d
c286907
3634099
c91fb37
5b46b68
a201ff1
9c2a4fb
cb34e46
db2b710
1517cd9
7478373
b1b01ea
bc19e9e
296a002
d0b0d78
e67d808
026fd41
496880e
87cb538
5de1f21
3447e79
fbefa91
f96a22c
b35623b
9b5408d
99d9795
91de3ce
413c57c
3ac7b4c
8b7808d
b4e16e5
1302c2a
df10979
c6f8cd8
e9907e1
2c6d296
6cef57b
ac900e3
0ba0217
e237727
a383a34
8854fb9
3244e38
b06a75b
012ce76
1ecd939
0a4e7a5
1d7d411
3cfcba8
4ddccb1
ed283a2
fa50e46
1f71456
5bb543b
779a4fd
b6ecd95
cb36cc4
1890e50
a8e450d
1f3bd47
439216c
2ec831b
249ae8a
9334930
7188db5
7af4440
24871ba
460e01a
471a22c
adb0daa
2567239
004f543
5ff6b98
9599a80
f134858
790dbcb
a436449
96cb9ec
1315367
2842230
e7928ac
f584e89
3c0ef0e
12ce9df
c5ab303
a73a264
9e9beb1
dfb7ae0
ae8981c
aabc4d6
4251152
9896cde
1e40ec2
81ca9d0
aa8b8d4
74c055e
22a8ab3
0df502a
18696a8
720dd16
f4eb600
5fe1ae0
efecc3d
e7c57e4
359517a
53896cc
60e96de
a78bd6e
0c7babc
d5d7608
a8bb74f
57efe61
a2f3315
2b2b036
6726096
6955250
a136371
19259b7
345f754
341b5db
4639af6
a85f3f3
8c5ee69
1a30b62
5ad25c5
419d409
fc14e27
ac48661
fa06249
da2b974
ea60f9b
9fa11b7
72aa381
a26e142
4f25c97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -895,7 +895,7 @@ def add_date_to_doc(query, edtf): | |
if edtf.lower is None and edtf.upper is None: | ||
raise Exception(_("Invalid date specified.")) | ||
|
||
if not value.get('op'): | ||
if not value.get("op"): | ||
pass | ||
elif value["op"] == "null" or value["op"] == "not_null": | ||
self.append_null_search_filters(value, node, query, request) | ||
|
@@ -1962,7 +1962,7 @@ def get_localized_option_text(self, node, option_id, return_lang=False): | |
for option in node.config["options"]: | ||
if option["id"] == option_id: | ||
return get_localized_value(option["text"], return_lang=return_lang) | ||
raise Exception(_("No domain option found for option id {0}, in node conifg: {1}".format(option_id, node.config["options"]))) | ||
raise Exception(_("No domain option found for option id {0}, in node config: {1}".format(option_id, node.config["options"]))) | ||
|
||
def get_option_id_from_text(self, value): | ||
# this could be better written with most of the logic in SQL tbh | ||
|
@@ -2641,3 +2641,103 @@ def get_value_from_jsonld(json_ld_node): | |
return | ||
except IndexError as e: | ||
return | ||
|
||
|
||
class ReferenceDataType(BaseDataType): | ||
def validate(self, value, row_number=None, source="", node=None, nodeid=None, strict=False, **kwargs): | ||
errors = [] | ||
title = _("Invalid Reference Datatype Value") | ||
if value is None: | ||
chrabyrd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return errors | ||
|
||
if type(value) == list and len(value): | ||
for reference in value: | ||
if "uri" in reference and len(reference["uri"]): | ||
pass | ||
else: | ||
errors.append( | ||
{ | ||
"type": "ERROR", | ||
"message": _("Reference objects require a 'uri' property and corresponding value"), | ||
"title": title, | ||
} | ||
) | ||
if "labels" in reference: | ||
pref_label_languages = [] | ||
for label in reference["labels"]: | ||
if not all(key in label for key in ("id", "value", "language_id", "valuetype_id")): | ||
errors.append( | ||
{ | ||
"type": "ERROR", | ||
"message": _( | ||
"Reference labels require properties: id(uuid), value(string), language_id(e.g. 'en'), and valuetype_id(e.g. 'prefLabel')" | ||
), | ||
"title": title, | ||
} | ||
) | ||
if label["valuetype_id"] == "prefLabel": | ||
pref_label_languages.append(label["language_id"]) | ||
|
||
if len(set(pref_label_languages)) < len(pref_label_languages): | ||
errors.append( | ||
{ | ||
"type": "ERROR", | ||
"message": _("A reference can have only one prefLabel per language"), | ||
"title": title, | ||
} | ||
) | ||
else: | ||
errors.append({"type": "ERROR", "message": _("Reference value must be a list of reference objects"), "title": title}) | ||
return errors | ||
|
||
def transform_value_for_tile(self, value, **kwargs): | ||
ret = value | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
return ret | ||
|
||
def clean(self, tile, nodeid): | ||
super().clean(tile, nodeid) | ||
if tile.data[nodeid] == []: | ||
tile.data[nodeid] = None | ||
|
||
def transform_export_values(self, value, *args, **kwargs): | ||
new_values = value | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
return ",".join(new_values) | ||
|
||
def get_display_value(self, tile, node, **kwargs): | ||
labels = [] | ||
requested_language = kwargs.pop("language", None) | ||
current_language = requested_language or get_language() | ||
for item in self.get_tile_data(tile)[str(node.nodeid)]: | ||
for label in item["labels"]: | ||
if label["language_id"] == current_language and label["valuetype_id"] == "prefLabel": | ||
labels.append(label.get("value", "")) | ||
return ", ".join(labels) | ||
|
||
def collects_multiple_values(self): | ||
return True | ||
|
||
def default_es_mapping(self): | ||
mapping = { | ||
"properties": { | ||
"uri": {"type": "keyword"}, | ||
"id": {"type": "keyword"}, | ||
"labels": { | ||
"properties": {}, | ||
}, | ||
} | ||
} | ||
return mapping | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: return JSON There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All datatypes are returning a dictionary to save the ES mapping of a node in graph.py. This is just overriding the default_es_mapping in base.py. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh my bad, I mean don't declare |
||
|
||
def validate_node(self, node): | ||
valid = False | ||
message = "" | ||
title = "" | ||
try: | ||
uuid.UUID(node.config["controlledList"]) | ||
valid = True | ||
except (TypeError, KeyError): | ||
chrabyrd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
message = _("A reference datatype node must be configured with a controlled list") | ||
title = _("Invalid Node Configuration") | ||
logger.error(message) | ||
|
||
return {"success": valid, "message": message, "title": title} |
jacobtylerwalls marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
define([ | ||
'jquery', | ||
'knockout', | ||
'knockout-mapping', | ||
'arches', | ||
'viewmodels/widget', | ||
], function($, ko, koMapping, arches, WidgetViewModel) { | ||
var NAME_LOOKUP = {}; | ||
var ReferenceSelectViewModel = function(params) { | ||
var self = this; | ||
|
||
params.configKeys = ['placeholder']; | ||
this.multiple = !!ko.unwrap(params.node.config.multiValue); | ||
this.displayName = ko.observable(''); | ||
this.selectionValue = ko.observable([]); // formatted version of this.value that select2 can use | ||
this.activeLanguage = arches.activeLanguage; | ||
|
||
WidgetViewModel.apply(this, [params]); | ||
|
||
this.getPrefLabel = function(labels){ | ||
return koMapping.toJS(labels)?.find( | ||
label => label.language_id === arches.activeLanguage && label.valuetype_id === 'prefLabel' | ||
)?.value || arches.translations.unlabeledItem; | ||
}; | ||
|
||
this.isLabel = function (value) { | ||
return ['prefLabel', 'altLabel'].includes(value.valuetype_id); | ||
}; | ||
|
||
this.displayValue = ko.computed(function() { | ||
const val = self.value(); | ||
let name = ''; | ||
if (val) { | ||
name = val.map(item=>self.getPrefLabel(item.values)).join(", "); | ||
} | ||
return val ? name : null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: on L29 control flow logic involving |
||
}); | ||
|
||
this.valueAndSelectionDiffer = function(value, selection) { | ||
if (!(ko.unwrap(value) instanceof Array)) { | ||
return true; | ||
} | ||
const valueUris = ko.unwrap(value).map(val=>ko.unwrap(val.uri)); | ||
return JSON.stringify(selection) !== JSON.stringify(valueUris); | ||
}; | ||
|
||
this.selectionValue.subscribe(selection => { | ||
if (selection) { | ||
if (!(selection instanceof Array)) { selection = [selection]; } | ||
if (self.valueAndSelectionDiffer(self.value, selection)) { | ||
const newItem = selection.map(uri => { | ||
return { | ||
"labels": NAME_LOOKUP[uri].values.filter(val => this.isLabel(val)), | ||
"listid": NAME_LOOKUP[uri]["listid"], | ||
"uri": uri | ||
}; | ||
}); | ||
self.value(newItem); | ||
} | ||
} else { | ||
self.value(null); | ||
} | ||
}); | ||
|
||
this.value.subscribe(val => { | ||
if (val?.length) { | ||
self.selectionValue(val.map(item=>ko.unwrap(item.uri))); | ||
} else { | ||
self.selectionValue(null); | ||
} | ||
}); | ||
|
||
this.select2Config = { | ||
value: self.selectionValue, | ||
clickBubble: true, | ||
multiple: this.multiple, | ||
closeOnSelect: true, | ||
placeholder: self.placeholder, | ||
allowClear: true, | ||
ajax: { | ||
url: arches.urls.controlled_list(ko.unwrap(params.node.config.controlledList)), | ||
dataType: 'json', | ||
quietMillis: 250, | ||
data: function(requestParams) { | ||
|
||
return { | ||
flat: true | ||
}; | ||
}, | ||
processResults: function(data) { | ||
const items = data.items; | ||
items.forEach(item => { | ||
item["listid"] = item.id; | ||
item.id = item.uri; | ||
}); | ||
return { | ||
"results": items, | ||
"pagination": { | ||
"more": false | ||
} | ||
}; | ||
} | ||
}, | ||
templateResult: function(item) { | ||
let indentation = ''; | ||
for (let i = 0; i < item.depth; i++) { | ||
indentation += ' '; | ||
} | ||
|
||
if (item.uri) { | ||
let text = self.getPrefLabel(item.values) || arches.translations.searching + '...'; | ||
NAME_LOOKUP[item.uri] = {"prefLabel": text, "labels": item.values.filter(val => this.isLabel(val)), "listid": item.controlled_list_id}; | ||
text = indentation + text; | ||
return text; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
} | ||
}, | ||
templateSelection: function(item) { | ||
if (!item.uri) { // option has a different shape when coming from initSelection vs templateResult | ||
return item.text; | ||
} else { | ||
return NAME_LOOKUP[item.uri]["prefLabel"]; | ||
} | ||
}, | ||
escapeMarkup: function(m) { return m; }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I'm personally against single-letter variable names ( outside of old school for-loops ) |
||
initComplete: false, | ||
initSelection: function(el, callback) { | ||
|
||
const setSelectionData = function(data) { | ||
const valueData = koMapping.toJS(self.value()); | ||
valueData.forEach(function(value) { | ||
NAME_LOOKUP[value.uri] = { | ||
"prefLabel": self.getPrefLabel(value.values), | ||
"labels": value.values.filter(val => this.isLabel(val)), | ||
"listid": value.listid | ||
}; | ||
}); | ||
|
||
if(!self.select2Config.initComplete){ | ||
valueData.forEach(function(data) { | ||
const option = new Option( | ||
self.getPrefLabel(data.values), | ||
data.uri, | ||
true, | ||
true | ||
); | ||
$(el).append(option); | ||
self.selectionValue().push(data.uri); | ||
}); | ||
self.select2Config.initComplete = true; | ||
} | ||
callback(valueData); | ||
}; | ||
|
||
if (self.value()?.length) { | ||
setSelectionData(); | ||
} else { | ||
callback([]); | ||
} | ||
|
||
} | ||
}; | ||
|
||
}; | ||
|
||
return ReferenceSelectViewModel; | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
define([ | ||
'knockout', | ||
'arches', | ||
'js-cookie', | ||
'templates/views/components/datatypes/reference.htm', | ||
'views/components/simple-switch', | ||
], function(ko, arches, Cookies, referenceDatatypeTemplate) { | ||
|
||
const viewModel = function(params) { | ||
const self = this; | ||
this.search = params.search; | ||
|
||
this.search = params.search; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dupes 😄 |
||
if (this.search) { | ||
params.config = ko.observable({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this mutates its inputs? |
||
controlledList:[], | ||
placeholder: arches.translations.selectAnOption, | ||
multiValue: true | ||
}); | ||
} | ||
|
||
this.controlledList = params.config.controlledList; | ||
this.multiValue = params.config.multiValue; | ||
this.controlledLists = ko.observable(); | ||
this.getControlledLists = async function() { | ||
const response = await fetch(arches.urls.controlled_lists, { | ||
method: 'GET', | ||
credentials: 'include', | ||
headers: { | ||
"X-CSRFToken": Cookies.get('csrftoken') | ||
}, | ||
}); | ||
if (response.ok) { | ||
return await response.json(); | ||
} else { | ||
console.error('Failed to fetch controlled lists'); | ||
} | ||
}; | ||
|
||
this.init = async function() { | ||
const lists = await this.getControlledLists(); | ||
this.controlledLists(lists.controlled_lists); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: does this need error handling around and empty return from |
||
}; | ||
|
||
this.init(); | ||
}; | ||
|
||
|
||
ko.components.register('reference-datatype-config', { | ||
viewModel: viewModel, | ||
template: referenceDatatypeTemplate, | ||
}); | ||
|
||
return name; | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import ko from 'knockout'; | ||
import ControlledListManager from '@/plugins/ControlledListManager.vue'; | ||
import createVueApp from 'utils/create-vue-application'; | ||
import ControlledListManagerTemplate from 'templates/views/components/plugins/controlled-list-manager.htm'; | ||
|
||
|
||
ko.components.register('controlled-list-manager', { | ||
viewModel: function() { | ||
createVueApp(ControlledListManager).then((vueApp) => { | ||
vueApp.mount('#controlled-list-manager-mounting-point'); | ||
}) | ||
}, | ||
template: ControlledListManagerTemplate, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
define([ | ||
'knockout', | ||
'viewmodels/reference-select', | ||
'templates/views/components/widgets/reference-select.htm', | ||
'bindings/select2-query', | ||
], function(ko, ReferenceSelectViewModel, referenceSelectTemplate) { | ||
const viewModel = function(params) { | ||
ReferenceSelectViewModel.apply(this, [params]); | ||
}; | ||
|
||
return ko.components.register('reference-select-widget', { | ||
viewModel: viewModel, | ||
template: referenceSelectTemplate, | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this be a bool instead of JSON?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be cleaner, but this is JSON because we need to send a value on the 'message' property to
GraphValidationError
. All other datatypes are using this to send back success so that theGraphValidationError
isn't thrown because we aren't actually validating their config in _validate_node_config. We could add some conditional logic in_validate_node_config
to sometimes accept a boolean and sometimes JSON, but that seems a bit messy.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I still don't love the idea. How do you feel about throwing an exception ( generic or creating a DatatypeException ) , and wrapping the callsite in a try/catch to harvest the message? That would still avoid this returning JSON but also allow an error message to be passed to the parent.