Skip to content

Commit

Permalink
feat: OC support
Browse files Browse the repository at this point in the history
  • Loading branch information
ieuans committed Nov 5, 2024
1 parent 3b11612 commit 0264722
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -840,8 +840,8 @@ class ONTOLOGY_TYPES(int, enum.Enum, metaclass=IterableMeta):
'output_type': 'string_inputlist',
},
'url_list': {
'input_type': 'string_inputlist',
'output_type': 'url_list',
'input_type': 'generic/url_list',
'output_type': 'generic/url_list',
},
'source_reference': {
'data_type': 'string',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ def validate_concept_form(form, errors):
component_code.get('attributes'), 'string_array'
)
if isinstance(code_attributes, list):
if len(set(code_attributes)) != len(code_attributes):
if len(set(attribute_headers)) != len(code_attributes):
errors.append(f'Invalid concept with ID {concept_id} - attribute headers must be unique.')
return None

Expand Down
24 changes: 24 additions & 0 deletions CodeListLibrary_project/clinicalcode/entity_utils/gen_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,30 @@ def try_value_as_type(field_value, field_type, validation=None, default=None):
if not isinstance(field_value, list):
return default
return field_value
elif field_type == 'url_list':
if not isinstance(field_value, list):
return default

if len(field_value) < 0:
return field_value

valid = True
for val in field_value:
if not isinstance(val, dict):
valid = False
break

title = val.get('title')
if not title or not isinstance(title, str) or is_empty_string(title):
valid = False
break

url = val.get('url')
if url is not None and not isinstance(url, str):
valid = False
break

return field_value if valid else default
elif field_type == 'publication':
if not isinstance(field_value, list):
return default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PublicationCreator from '../clinical/publicationCreator.js';
import TrialCreator from '../clinical/trialCreator.js';
import EndorsementCreator from '../clinical/endorsementCreator.js';
import StringInputListCreator from '../stringInputListCreator.js';
import UrlReferenceListCreator from '../generic/urlReferenceListCreator.js';
import OntologySelectionService from '../generic/ontologySelector/index.js';

import {
Expand Down Expand Up @@ -224,6 +225,21 @@ export const ENTITY_HANDLERS = {
return new StringInputListCreator(element, parsed)
},

// Generates a list component for an element
'url_list': (element) => {
const data = element.parentNode.querySelector(`script[type="application/json"][for="${element.getAttribute('data-field')}"]`);

let parsed;
try {
parsed = JSON.parse(data.innerText);
}
catch (e) {
parsed = [];
}

return new UrlReferenceListCreator(element, parsed)
},

// Generates a clinical publication list component for an element
'clinical-publication': (element) => {
const data = element.parentNode.querySelector(`script[type="application/json"][for="${element.getAttribute('data-field')}"]`);
Expand Down Expand Up @@ -659,6 +675,36 @@ export const ENTITY_FIELD_COLLECTOR = {
}
},

// Retrieves and validates list components
'url_list': (field, packet) => {
const handler = packet.handler;
const listItems = handler.getData();

if (isMandatoryField(packet)) {
if (isNullOrUndefined(listItems) || listItems.length < 1) {
return {
valid: false,
value: listItems,
message: (isNullOrUndefined(listItems) || listItems.length < 1) ? ENTITY_TEXT_PROMPTS.REQUIRED_FIELD : ENTITY_TEXT_PROMPTS.INVALID_FIELD
}
}
}

const parsedValue = parseAsFieldType(packet, listItems);
if (!parsedValue || !parsedValue?.success) {
return {
valid: false,
value: listItems,
message: ENTITY_TEXT_PROMPTS.INVALID_FIELD
}
}

return {
valid: true,
value: parsedValue?.value
}
},

// Retrieves and validates publication components
'clinical-publication': (field, packet) => {
const handler = packet.handler;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { PUBLICATION_MIN_MSG_DURATION } from '../entityFormConstants.js';
/**
* PUBLICATION_NOTIFICATIONS
* @desc notification text that is used to present information
* to the client, _e.g._ to inform them of a validation
* error, or to confirm them of a forced change _etc_
*
*/
const URL_REF_NOTIFICATIONS = {
// e.g. in the case of an invalid title input
InvalidTitle: 'Please provide a descriptive title',
// e.g. in the case of an invalid URL input
InvalidURL: 'Please provide a valid URL',
};

/**
* @class UrlReferenceListCreator
* @desc A class that can be used to control a URL reference; used initially
* for OpenCodelists
*/
export default class UrlReferenceListCreator {
constructor(element, data) {
this.data = data || [ ];
this.element = element;
this.dirty = false;

this.#setUp();
this.#redrawElements();
}

/*************************************
* *
* Getter *
* *
*************************************/
/**
* getData
* @returns {object} the list data
*/
getData() {
return this.data;
}

/**
* getElement
* @returns {node} the assoc. element
*/
getElement() {
return this.element;
}

/**
* isDirty
* @returns {bool} returns the dirty state of this component
*/
isDirty() {
return this.dirty;
}

/*************************************
* *
* Setter *
* *
*************************************/
/**
* makeDirty
* @desc informs the top-level parent that we're dirty
* and updates our internal dirty state
* @return {object} return this for chaining
*/
makeDirty() {
window.entityForm.makeDirty();
this.dirty = true;
return this;
}

/*************************************
* *
* Render *
* *
*************************************/
/**
* drawItem
* @param {integer} index the index of the list in our data
* @param {string} listItem the list item name
* @returns {string} html string representing the element
*/
#drawItem(index, listItem) {
return `
<div class="publication-list-group__list-item" data-target="${index}">
<div class="publication-list-group__list-item-url">
<a href="${listItem.url}">${listItem.title}</a>
</div>
<button class="publication-list-group__list-item-btn" data-target="${index}">
<span class="delete-icon"></span>
<span>Remove</span>
</button>
</div>`
}

/**
* redrawElements
* @desc redraws the entire list
*/
#redrawElements() {
this.dataResult.innerText = JSON.stringify(this.data);
this.renderables.list.innerHTML = '';

if (this.data.length > 0) {
this.renderables.group.classList.add('show');
this.renderables.none.classList.remove('show');

for (let i = 0; i < this.data.length; ++i) {
const node = this.#drawItem(i, this.data[i]);
this.renderables.list.insertAdjacentHTML('beforeend', node);
}

return;
}

this.renderables.none.classList.add('show');
this.renderables.group.classList.remove('show');
}

/**
* setUp
* @desc initialises the list component
*/
#setUp() {
this.textInput = this.element.querySelector('.publication-list-group__interface-children .text-input[x-content="title"]');
this.linkInput = this.element.querySelector('.publication-list-group__interface-children .text-input[x-content="url"]');

this.addButton = this.element.querySelector('#add-input-btn');
this.addButton.addEventListener('click', this.#handleInput.bind(this));
window.addEventListener('click', this.#handleClick.bind(this));

const noneAvailable = this.element.parentNode.querySelector('#no-available-publications');
const listGroup = this.element.parentNode.querySelector('#publication-group');
const list = this.element.parentNode.querySelector('#publication-list');
this.renderables = {
none: noneAvailable,
group: listGroup,
list: list,
}

this.dataResult = this.element.parentNode.querySelector(`[for="${this.element.getAttribute('data-field')}"]`);
}

/*************************************
* *
* Events *
* *
*************************************/
/**
* handleInput
* @desc bindable event handler for key up events of the list input box
* @param {event} e the event of the input
*/
#handleInput(e) {
e.preventDefault();
e.stopPropagation();

const textItem = strictSanitiseString(this.textInput.value);
if (!this.textInput.checkValidity() || isNullOrUndefined(textItem) || isStringEmpty(textItem)) {
window.ToastFactory.push({
type: 'danger',
message: URL_REF_NOTIFICATIONS.InvalidTitle,
duration: PUBLICATION_MIN_MSG_DURATION,
});
return;
}

const linkItem = strictSanitiseString(this.linkInput.value);
if (!this.linkInput.checkValidity() || isNullOrUndefined(linkItem) || isStringEmpty(linkItem)) {
window.ToastFactory.push({
type: 'danger',
message: URL_REF_NOTIFICATIONS.InvalidURL,
duration: PUBLICATION_MIN_MSG_DURATION,
});
return;
}

this.textInput.value = '';
this.linkInput.value = '';
this.data.push({ title: textItem, url: linkItem });
this.makeDirty();

this.#redrawElements();
}

/**
* handleClick
* @desc bindable event handler for click events of the list item's delete button
* @param {event} e the event of the input
*/
#handleClick(e) {
const target = e.target;
if (!target || !this.renderables.list.contains(target)) {
return;
}

if (target.nodeName != 'BUTTON') {
return;
}

const index = target.getAttribute('data-target');
if (isNullOrUndefined(index)) {
return;
}

this.data.splice(parseInt(index), 1);
this.#redrawElements();
this.makeDirty();
}
}
25 changes: 25 additions & 0 deletions CodeListLibrary_project/cll/static/scss/components/_inputs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

&__interface {
display: flex;
max-width: 100%;
flex-flow: column nowrap;
height: min-content;

Expand All @@ -41,7 +42,31 @@
font-weight: normal;
margin-bottom: 0.25rem;
}

&--references {
& > button {
margin-top: 2rem;

@include media('<tablet', 'screen') {
margin-top: 1rem;
}
}
}
}

&-group {
display: flex;
flex-grow: 1;
flex-flow: column nowrap;
height: min-content;
max-width: 100%;

input,
input:first-child {
margin-right: 2rem;
}
}

&__trial {
&-children {
display: flex;
Expand Down
Loading

0 comments on commit 0264722

Please sign in to comment.