Skip to content

Commit

Permalink
Nursing tags (#3891)
Browse files Browse the repository at this point in the history
* Added AACN field

* Improved AACN field behavior, added NCLEX field

* Shortened AACN error message
  • Loading branch information
Dantemss authored Jul 17, 2023
1 parent af1e3c1 commit 0ab58dd
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6607,6 +6607,8 @@ exports[`Exercises component resets fields when model is new 1`] = `
</TagWrapper>
</LoTags>
<ApLoTags exercise={{...}} />
<AACNTags exercise={{...}} />
<NCLEXTag exercise={{...}} />
<QuestionTypeTag exercise={{...}}>
<MultiSelect exercise={{...}} readonly={false} tagType=\\"assignment-type\\" label=\\"Assignment Type\\" options={{...}}>
<TagWrapper label=\\"Assignment Type\\">
Expand Down
4 changes: 4 additions & 0 deletions exercises/src/components/exercise/tags.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import Blooms from '../tags/blooms';
import Time from '../tags/time';
import HistoricalThinking from '../tags/historical-thinking';
import ReasoningProcess from '../tags/reasoning-process';
import Aacn from '../tags/aacn';
import ApLo from '../tags/aplo';
import Nclex from '../tags/nclex';
import SciencePractice from '../tags/science-practice';
import PublicSolutions from '../tags/public-solutions';
import Exercise from '../../models/exercises/exercise';
Expand All @@ -29,6 +31,8 @@ function ExerciseTags({ exercise }) {
<SciencePractice {...tagProps} />
<Lo {...tagProps} />
<ApLo {...tagProps} />
<Aacn {...tagProps} />
<Nclex {...tagProps} />
<AssignmentType {...tagProps} />
<HistoricalThinking {...tagProps} />
<ReasoningProcess {...tagProps} />
Expand Down
107 changes: 107 additions & 0 deletions exercises/src/components/tags/aacn.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import PropTypes from 'prop-types';
import React from 'react';
import classnames from 'classnames';
import Exercise from '../../models/exercises/exercise';
import { observer } from 'mobx-react';
import { action, computed, observable, modelize } from 'shared/model';
import TagModel from 'shared/model/exercise/tag';
import { Icon } from 'shared';
import Error from './error';
import { nursingBooks } from './nursingBooks';
import Wrapper from './wrapper';

const pattern = '##.#[a-z]';

@observer
class Input extends React.Component {
static propTypes = {
exercise: PropTypes.instanceOf(Exercise).isRequired,
tag: PropTypes.instanceOf(TagModel).isRequired,
};

@observable aacn = this.props.tag.value;

isValid(aacn) {
return aacn && aacn.match(/^\d{1,2}\.\d[a-z]$/);
}

@computed get errorMsg() {
if (this.isValid(this.aacn)) {
return null;
} else {
return `Must match AACN pattern of ${pattern}`;
}
}

constructor(props) {
super(props)
modelize(this);
}

@action.bound onTextChange(ev) {
this.aacn = ev.target.value.toLowerCase().replace(/[^0-9a-z.]+/, '');
}

@action.bound onTextBlur() {
this.props.tag.value = this.isValid(this.aacn) ? this.aacn : '';
}

@action.bound onDelete() {
this.props.exercise.tags.remove(this.props.tag);
}

render() {
return (
<div className={classnames('tag', { 'has-error': this.errorMsg })}>
<input
className="form-control"
type="text"
onChange={this.onTextChange}
onBlur={this.onTextBlur}
value={this.aacn}
placeholder={pattern}
/>
<Error error={this.errorMsg} />
<span className="controls">
<Icon type="trash" onClick={this.onDelete} />
</span>
</div>
);
}
}

@observer
class AACNTags extends React.Component {
static propTypes = {
exercise: PropTypes.instanceOf(Exercise).isRequired,
};

constructor(props) {
super(props)
modelize(this);
}

@action.bound onAdd() {
this.props.exercise.tags.push({ type: 'nursing', specifier: 'aacn', value: '' });
}

render() {
const { exercise } = this.props;

const bookTags = exercise.tags.withType('book', { multiple: true });
if (!bookTags.find(tag => nursingBooks.includes(tag.value))) {
return null;
}

const nursingTags = exercise.tags.withType('nursing', { multiple: true });
const aacnTags = nursingTags.filter(tag => tag.specifier === 'aacn');

return (
<Wrapper label="AACN" onAdd={this.onAdd}>
{aacnTags.map((tag, i) => <Input key={i} {...this.props} tag={tag} />)}
</Wrapper>
);
}
}

export default AACNTags;
25 changes: 25 additions & 0 deletions exercises/src/components/tags/nclex.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import React from 'react';
import { observer } from 'mobx-react';
import Exercise from '../../models/exercises/exercise';
import { nursingBooks } from './nursingBooks';
import SingleDropdown from './single-dropdown';

const CHOICES = { 'y': 'Yes', 'n': 'No' };

function NCLEXTag(props) {
const bookTags = props.exercise.tags.withType('book', { multiple: true });
if (!bookTags.find(tag => nursingBooks.includes(tag.value))) {
return null;
}

return (
<SingleDropdown {...props} label="NCLEX" type="nursing" specifier="nclex" choices={CHOICES} />
);
}

NCLEXTag.propTypes = {
exercise: PropTypes.instanceOf(Exercise).isRequired,
};

export default observer(NCLEXTag);
10 changes: 10 additions & 0 deletions exercises/src/components/tags/nursingBooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const nursingBooks = [
'stax-matnewborn',
'stax-medsurg',
'stax-nursingfundamentals',
'stax-nursingskills',
'stax-nutrition',
'stax-pharmacology',
'stax-pophealth',
'stax-psychnursing',
];
16 changes: 13 additions & 3 deletions exercises/src/components/tags/single-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface SingleDropdownProps {
exercise: Exercise
label: string
type: string
specifier?: string
readonly: boolean
icon: React.ReactNode
choices: Record<string, string>
Expand All @@ -35,6 +36,7 @@ class SingleDropdown extends React.Component<SingleDropdownProps> {
exercise: PropTypes.instanceOf(Exercise).isRequired,
label: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
specifier: PropTypes.string,
readonly: PropTypes.bool,
icon: PropTypes.node,
choices: PropTypes.object.isRequired,
Expand All @@ -51,15 +53,23 @@ class SingleDropdown extends React.Component<SingleDropdownProps> {

@action.bound onChange(option?: SelectOption) {
if (option) {
const tag = this.props.exercise.tags.findOrAddWithType(this.props.type)
const tag = this.props.specifier ?
this.props.exercise.tags.findOrAddWithTypeAndSpecifier(this.props.type, this.props.specifier) :
this.props.exercise.tags.findOrAddWithType(this.props.type)
tag.value = option.value
} else {
this.props.exercise.tags.removeType(this.props.type)
if (this.props.specifier) {
this.props.exercise.tags.removeTypeAndSpecifier(this.props.type, this.props.specifier)
} else {
this.props.exercise.tags.removeType(this.props.type)
}
}
}

@computed get selectedOption() {
const tag = this.props.exercise.tags.withType(this.props.type);
const tag = this.props.specifier ?
this.props.exercise.tags.withTypeAndSpecifier(this.props.type, this.props.specifier) :
this.props.exercise.tags.withType(this.props.type)
const value = get(tag, 'value', '')
return this.options.find(opt => opt.value == value)
}
Expand Down
2 changes: 1 addition & 1 deletion shared/src/model/exercise/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
import { isString, isObject, first, last, filter, extend, values, pick, isNil } from 'lodash';

const TYPES = {
IMPORTANT: ['lo', 'aplo', 'blooms', 'dok', 'length', 'time', 'hts', 'rp', 'difficulty'],
IMPORTANT: ['lo', 'aplo', 'blooms', 'dok', 'length', 'time', 'hts', 'rp', 'difficulty', 'nursing'],
};
const TITLE_SUBSTITUTIONS = [
['hts:1', 'HTS-1 Developments and Processes'],
Expand Down
20 changes: 17 additions & 3 deletions shared/src/model/exercise/tags-association.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,27 @@ export default class TagsAssociation {
clear() { this.all.splice(0, this.all.length) }
get length() { return this.all.length }

withType(type: string): Tag // eslint-disable-line
withType(type: string, multiple:false): Tag // eslint-disable-line
withType(type: string, multiple:true): Tag[] // eslint-disable-line
withType(type: string, multiple?: false): Tag // eslint-disable-line
withType(type: string, multiple: true): Tag[] // eslint-disable-line
withType(type: string, multiple = false) { // eslint-disable-line no-dupe-class-members
return multiple ? filter(this.all, { type }) : find(this.all, { type });
}

withTypeAndSpecifier(type: string, specifier: string, multiple?: false): Tag | undefined // eslint-disable-line
withTypeAndSpecifier(type: string, specifier: string, multiple: true): Tag[] // eslint-disable-line
withTypeAndSpecifier(type: string, specifier: string, multiple = false) { // eslint-disable-line no-dupe-class-members
return multiple ? filter(this.all, { type, specifier }) : find(this.all, { type, specifier });
}

@action findOrAddWithType(type: string) {
return this.withType(type) || this.all[ this.all.push(new Tag(`${type}:`)) - 1 ];
}

@action findOrAddWithTypeAndSpecifier(type: string, specifier: string) {
return this.withType(type, true).find(tag => tag.specifier === specifier) ||
this.all[ this.all.push(new Tag(`${type}:${specifier}:`)) - 1 ];
}

@action replaceType(type: string, tags: Tag[]) {
this.removeType(type)
tags.forEach((tag) => {
Expand All @@ -36,6 +46,10 @@ export default class TagsAssociation {
remove(this.all, { type })
}

@action removeTypeAndSpecifier(type: string, specifier: string) {
remove(this.all, { type, specifier })
}

@action setUniqueValue(tag:Tag, value: string) {
const existing = find(this.all, { type: tag.type, value: value });
if (existing) this.remove(tag);
Expand Down

0 comments on commit 0ab58dd

Please sign in to comment.