/*
Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
This file is part of CamCOPS.
CamCOPS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CamCOPS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/
#include "icd10specpd.h"
#include "common/appstrings.h"
#include "common/textconst.h"
#include "lib/datetime.h"
#include "lib/stringfunc.h"
#include "lib/uifunc.h"
#include "maths/mathfunc.h"
#include "questionnairelib/commonoptions.h"
#include "questionnairelib/quboolean.h"
#include "questionnairelib/qudatetime.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/quheading.h"
#include "questionnairelib/qumcqgrid.h"
#include "questionnairelib/qutext.h"
#include "questionnairelib/qutextedit.h"
#include "tasklib/taskfactory.h"
#include "tasklib/taskregistrar.h"
using datetime::shortDate;
using mathfunc::allTrue;
using mathfunc::anyFalse;
using mathfunc::countTrue;
using mathfunc::noneNull;
using stringfunc::standardResult;
using stringfunc::strnum;
using stringfunc::strseq;
using uifunc::yesNoUnknown;
const QString Icd10SpecPD::ICD10SPECPD_TABLENAME("icd10specpd");
const int N_GENERAL = 6;
const int N_GENERAL_1 = 4;
const int N_PARANOID = 7;
const int N_SCHIZOID = 9;
const int N_DISSOCIAL = 6;
const int N_EU = 10;
const int N_EUPD_I = 5;
const int N_HISTRIONIC = 6;
const int N_ANANKASTIC = 8;
const int N_ANXIOUS = 5;
const int N_DEPENDENT = 6;
const QString G_PREFIX("g");
const QString G1_PREFIX("g1_");
const QString PARANOID_PREFIX("paranoid");
const QString SCHIZOID_PREFIX("schizoid");
const QString DISSOCIAL_PREFIX("dissocial");
const QString EU_PREFIX("eu");
const QString HISTRIONIC_PREFIX("histrionic");
const QString ANANKASTIC_PREFIX("anankastic");
const QString ANXIOUS_PREFIX("anxious");
const QString DEPENDENT_PREFIX("dependent");
const QString DATE_PERTAINS_TO("date_pertains_to");
const QString COMMENTS("comments");
const QString SKIP_PARANOID("skip_paranoid");
const QString SKIP_SCHIZOID("skip_schizoid");
const QString SKIP_DISSOCIAL("skip_dissocial");
const QString SKIP_EU("skip_eu");
const QString SKIP_HISTRIONIC("skip_histrionic");
const QString SKIP_ANANKASTIC("skip_anankastic");
const QString SKIP_ANXIOUS("skip_anxious");
const QString SKIP_DEPENDENT("skip_dependent");
const QString OTHER_PD_PRESENT("other_pd_present"); // new in v2.0.0
const QString VIGNETTE("vignette");
void initializeIcd10SpecPD(TaskFactory& factory)
{
static TaskRegistrar<Icd10SpecPD> registered(factory);
}
Icd10SpecPD::Icd10SpecPD(
CamcopsApp& app, DatabaseManager& db, const int load_pk
) :
Task(app, db, ICD10SPECPD_TABLENAME, false, true, false),
// ... anon, clin, resp
m_fr_has_pd(nullptr)
{
addFields(strseq(G_PREFIX, 1, N_GENERAL), QMetaType::fromType<bool>());
addFields(strseq(G1_PREFIX, 1, N_GENERAL_1), QMetaType::fromType<bool>());
addFields(
strseq(PARANOID_PREFIX, 1, N_PARANOID), QMetaType::fromType<bool>()
);
addFields(
strseq(SCHIZOID_PREFIX, 1, N_SCHIZOID), QMetaType::fromType<bool>()
);
addFields(
strseq(DISSOCIAL_PREFIX, 1, N_DISSOCIAL), QMetaType::fromType<bool>()
);
addFields(strseq(EU_PREFIX, 1, N_EU), QMetaType::fromType<bool>());
addFields(
strseq(HISTRIONIC_PREFIX, 1, N_HISTRIONIC), QMetaType::fromType<bool>()
);
addFields(
strseq(ANANKASTIC_PREFIX, 1, N_ANANKASTIC), QMetaType::fromType<bool>()
);
addFields(
strseq(ANXIOUS_PREFIX, 1, N_ANXIOUS), QMetaType::fromType<bool>()
);
addFields(
strseq(DEPENDENT_PREFIX, 1, N_DEPENDENT), QMetaType::fromType<bool>()
);
addField(DATE_PERTAINS_TO, QMetaType::fromType<QDate>());
addField(COMMENTS, QMetaType::fromType<QString>());
addField(SKIP_PARANOID, QMetaType::fromType<bool>());
addField(SKIP_SCHIZOID, QMetaType::fromType<bool>());
addField(SKIP_DISSOCIAL, QMetaType::fromType<bool>());
addField(SKIP_EU, QMetaType::fromType<bool>());
addField(SKIP_HISTRIONIC, QMetaType::fromType<bool>());
addField(SKIP_ANANKASTIC, QMetaType::fromType<bool>());
addField(SKIP_ANXIOUS, QMetaType::fromType<bool>());
addField(SKIP_DEPENDENT, QMetaType::fromType<bool>());
addField(OTHER_PD_PRESENT, QMetaType::fromType<bool>());
addField(VIGNETTE, QMetaType::fromType<QString>());
load(load_pk); // MUST ALWAYS CALL from derived Task constructor.
// Extra initialization:
if (load_pk == dbconst::NONEXISTENT_PK) {
setValue(DATE_PERTAINS_TO, datetime::nowDate(), false);
}
}
// ============================================================================
// Class info
// ============================================================================
QString Icd10SpecPD::shortname() const
{
return "ICD10-PD";
}
QString Icd10SpecPD::longname() const
{
return tr("ICD-10 criteria for specific personality disorders (F60)");
}
QString Icd10SpecPD::description() const
{
return TextConst::icd10();
}
QString Icd10SpecPD::infoFilenameStem() const
{
return "icd";
}
// ============================================================================
// Instance info
// ============================================================================
bool Icd10SpecPD::isComplete() const
{
return !valueIsNull(DATE_PERTAINS_TO)
&& (isPDExcluded()
|| (isCompleteGeneral()
&& (valueBool(SKIP_PARANOID) || isCompleteParanoid())
&& (valueBool(SKIP_SCHIZOID) || isCompleteSchizoid())
&& (valueBool(SKIP_DISSOCIAL) || isCompleteDissocial())
&& (valueBool(SKIP_EU) || isCompleteEU())
&& (valueBool(SKIP_HISTRIONIC) || isCompleteHistrionic())
&& (valueBool(SKIP_ANANKASTIC) || isCompleteAnankastic())
&& (valueBool(SKIP_ANXIOUS) || isCompleteAnxious())
&& (valueBool(SKIP_DEPENDENT) || isCompleteDependent())));
}
QStringList Icd10SpecPD::summary() const
{
return QStringList{
standardResult(
appstring(appstrings::DATE_PERTAINS_TO),
shortDate(value(DATE_PERTAINS_TO))
),
standardResult(
xstring("meets_general_criteria"), yesNoUnknown(hasPD())
),
};
}
QStringList Icd10SpecPD::detail() const
{
return completenessInfo()
+ QStringList{
standardResult(
appstring(appstrings::DATE_PERTAINS_TO),
shortDate(value(DATE_PERTAINS_TO))
),
fieldSummary(COMMENTS, TextConst::examinerComments()),
standardResult(
xstring("meets_general_criteria"), yesNoUnknown(hasPD())
),
standardResult(
xstring("paranoid_pd_title"), yesNoUnknown(hasParanoidPD())
),
standardResult(
xstring("schizoid_pd_title"), yesNoUnknown(hasSchizoidPD())
),
standardResult(
xstring("dissocial_pd_title"), yesNoUnknown(hasDissocialPD())
),
standardResult(
xstring("eu_pd_i_title"), yesNoUnknown(hasEUPD_I())
),
standardResult(
xstring("eu_pd_b_title"), yesNoUnknown(hasEUPD_B())
),
standardResult(
xstring("histrionic_pd_title"), yesNoUnknown(hasHistrionicPD())
),
standardResult(
xstring("anankastic_pd_title"), yesNoUnknown(hasAnankasticPD())
),
standardResult(
xstring("anxious_pd_title"), yesNoUnknown(hasAnxiousPD())
),
standardResult(
xstring("dependent_pd_title"), yesNoUnknown(hasDependentPD())
),
standardResult(
xstring("other_pd_title"),
yesNoUnknown(value(OTHER_PD_PRESENT))
),
standardResult(xstring("vignette"), valueString(VIGNETTE)),
};
}
OpenableWidget* Icd10SpecPD::editor(const bool read_only)
{
const NameValueOptions options = CommonOptions::falseTrueBoolean();
FieldRef::GetterFunction get_has_pd
= std::bind(&Icd10SpecPD::getHasPDYesNoUnknown, this);
FieldRef::SetterFunction set_has_pd
= std::bind(&Icd10SpecPD::ignoreValue, this, std::placeholders::_1);
m_fr_has_pd = FieldRefPtr(new FieldRef(get_has_pd, set_has_pd, false));
auto text = [this](const QString& xstringname) -> QuElement* {
return new QuText(xstring(xstringname));
};
#if 0
auto boldtext = [this](const QString& xstringname) -> QuElement* {
return (new QuText(xstring(xstringname)))->setBold();
};
#endif
auto heading = [this](const QString& xstringname) -> QuElement* {
return new QuHeading(xstring(xstringname));
};
auto gridbase
= [this, &options](
const QStringList& fieldnames, const QStringList& xstringnames
) -> QuElement* {
Q_ASSERT(fieldnames.length() == xstringnames.length());
QVector<QuestionWithOneField> qfields;
const int numq = fieldnames.length();
for (int i = 0; i < numq; ++i) {
qfields.append(QuestionWithOneField(
xstring(xstringnames.at(i)), fieldRef(fieldnames.at(i), false)
));
}
const int n = options.size();
const QVector<int> v(n, 1);
return (new QuMcqGrid(qfields, options))
->setExpand(true)
->setWidth(n, v);
};
auto grid = [&gridbase](
const QString& prefix, int end, int start = 1
) -> QuElement* {
// Assumes the xstring name matches the fieldname (as it does)
const QStringList field_xstring_names = strseq(prefix, start, end);
return gridbase(field_xstring_names, field_xstring_names);
};
auto generalpage = [this, &text, &gridbase]() -> QuPagePtr {
auto page = new QuPage();
page->setTitle(xstring("general"));
page->addElement(text("general"));
page->addElement(gridbase({strnum(G_PREFIX, 1)}, {"G1"}));
page->addElement(text("G1b"));
page->addElement(gridbase(
strseq(G1_PREFIX, 1, N_GENERAL_1), strseq("G1_", 1, N_GENERAL_1)
));
page->addElement(new QuText(TextConst::inAddition() + ":"));
page->addElement(
gridbase(strseq(G_PREFIX, 2, N_GENERAL), strseq("G", 2, N_GENERAL))
);
page->addElement(text("comments"));
return QuPagePtr(page);
};
auto pdpage = [this, &grid, &text](
const QString& prefix,
int n,
const QString& skipfield,
const QString& title_xstring,
const QString& q_xstring,
const QString& comment_xstring = ""
) -> QuPagePtr {
auto page = new QuPage();
page->setTitle(xstring(title_xstring));
page->addElement(
(new QuBoolean(xstring("skip_this_pd"), fieldRef(skipfield, false))
)
->setAsTextButton(true)
);
page->addElement(new QuText(xstring("general_criteria_must_be_met")));
page->addElement((new QuText(m_fr_has_pd))->setBold());
page->addElement(text(q_xstring));
page->addElement(grid(prefix, n));
if (!comment_xstring.isEmpty()) {
page->addElement(text(comment_xstring));
}
return QuPagePtr(page);
};
auto eupdpage = [this, &grid, &text, &heading]() -> QuPagePtr {
auto page = new QuPage();
page->setTitle(xstring("eu_pd_title"));
page->addElement(
(new QuBoolean(xstring("skip_this_pd"), fieldRef(SKIP_EU, false)))
->setAsTextButton(true)
);
page->addElement(text("general_criteria_must_be_met"));
page->addElement(new QuText(m_fr_has_pd));
page->addElement(heading("eu_pd_i_title"));
page->addElement(text("eu_pd_i_B"));
page->addElement(grid(EU_PREFIX, N_EUPD_I, 1));
page->addElement(heading("eu_pd_b_title"));
page->addElement(text("eu_pd_b_B"));
page->addElement(grid(EU_PREFIX, N_EU, N_EUPD_I + 1));
return QuPagePtr(page);
};
// Overview page
QVector<QuPagePtr> pages{
QuPagePtr((new QuPage{
getClinicianQuestionnaireBlockRawPointer(),
new QuText(appstring(appstrings::DATE_PERTAINS_TO)),
(new QuDateTime(fieldRef(DATE_PERTAINS_TO)))
->setMode(QuDateTime::Mode::DefaultDate)
->setOfferNowButton(true),
new QuText(TextConst::comments()),
new QuTextEdit(fieldRef(COMMENTS, false)),
})
->setTitle(longname()))};
// General criteria for personality disorders
pages.append(generalpage());
// Specific PDs
pages.append(pdpage(
PARANOID_PREFIX,
N_PARANOID,
SKIP_PARANOID,
"paranoid_pd_title",
"paranoid_pd_B"
));
pages.append(pdpage(
SCHIZOID_PREFIX,
N_SCHIZOID,
SKIP_SCHIZOID,
"schizoid_pd_title",
"schizoid_pd_B"
));
pages.append(pdpage(
DISSOCIAL_PREFIX,
N_DISSOCIAL,
SKIP_DISSOCIAL,
"dissocial_pd_title",
"dissocial_pd_B",
"dissocial_pd_comments"
));
pages.append(eupdpage()); // EUPD is more complex
pages.append(pdpage(
HISTRIONIC_PREFIX,
N_HISTRIONIC,
SKIP_HISTRIONIC,
"histrionic_pd_title",
"histrionic_pd_B",
"histrionic_pd_comments"
));
pages.append(pdpage(
ANANKASTIC_PREFIX,
N_ANANKASTIC,
SKIP_ANANKASTIC,
"anankastic_pd_title",
"anankastic_pd_B"
));
pages.append(pdpage(
ANXIOUS_PREFIX,
N_ANXIOUS,
SKIP_ANXIOUS,
"anxious_pd_title",
"anxious_pd_B"
));
pages.append(pdpage(
DEPENDENT_PREFIX,
N_DEPENDENT,
SKIP_DEPENDENT,
"dependent_pd_title",
"dependent_pd_B"
));
pages.append(QuPagePtr((new QuPage{
text("other_pd_comments"),
new QuBoolean(
xstring("other_pd_title"),
fieldRef(OTHER_PD_PRESENT)
),
text("vignette"),
new QuTextEdit(fieldRef(VIGNETTE, false)),
})
->setTitle(xstring("other_pd_title"))));
QStringList connected_fields = strseq(G_PREFIX, 1, N_GENERAL)
+ strseq(G1_PREFIX, 1, N_GENERAL_1)
+ QStringList{
SKIP_PARANOID,
SKIP_SCHIZOID,
SKIP_DISSOCIAL,
SKIP_EU,
SKIP_HISTRIONIC,
SKIP_ANANKASTIC,
SKIP_ANXIOUS,
SKIP_DEPENDENT,
OTHER_PD_PRESENT,
};
for (const QString& fieldname : connected_fields) {
connect(
fieldRef(fieldname).data(),
&FieldRef::valueChanged,
this,
&Icd10SpecPD::updateMandatory
);
}
updateMandatory();
auto questionnaire = new Questionnaire(m_app, pages);
questionnaire->setType(QuPage::PageType::Clinician);
questionnaire->setReadOnly(read_only);
return questionnaire;
}
// ============================================================================
// Task-specific calculations
// ============================================================================
bool Icd10SpecPD::isPDExcluded() const
{
const QVector<QVariant> g_values = values(strseq(G_PREFIX, 1, N_GENERAL));
const QVector<QVariant> g1_values
= values(strseq(G1_PREFIX, 1, N_GENERAL_1));
return anyFalse(g_values)
|| (noneNull(g1_values) && countTrue(g1_values) <= 1);
}
bool Icd10SpecPD::isCompleteGeneral() const
{
return noneNull(values(strseq(G_PREFIX, 1, N_GENERAL)))
&& noneNull(values(strseq(G1_PREFIX, 1, N_GENERAL_1)));
}
bool Icd10SpecPD::isCompleteParanoid() const
{
return noneNull(values(strseq(PARANOID_PREFIX, 1, N_PARANOID)));
}
bool Icd10SpecPD::isCompleteSchizoid() const
{
return noneNull(values(strseq(SCHIZOID_PREFIX, 1, N_SCHIZOID)));
}
bool Icd10SpecPD::isCompleteDissocial() const
{
return noneNull(values(strseq(DISSOCIAL_PREFIX, 1, N_DISSOCIAL)));
}
bool Icd10SpecPD::isCompleteEU() const
{
return noneNull(values(strseq(EU_PREFIX, 1, N_EU)));
}
bool Icd10SpecPD::isCompleteHistrionic() const
{
return noneNull(values(strseq(HISTRIONIC_PREFIX, 1, N_HISTRIONIC)));
}
bool Icd10SpecPD::isCompleteAnankastic() const
{
return noneNull(values(strseq(ANANKASTIC_PREFIX, 1, N_ANANKASTIC)));
}
bool Icd10SpecPD::isCompleteAnxious() const
{
return noneNull(values(strseq(ANXIOUS_PREFIX, 1, N_ANXIOUS)));
}
bool Icd10SpecPD::isCompleteDependent() const
{
return noneNull(values(strseq(DEPENDENT_PREFIX, 1, N_DEPENDENT)));
}
QVariant Icd10SpecPD::hasPD() const
{
if (isPDExcluded()) {
return false;
}
if (!isCompleteGeneral()) {
return QVariant();
}
return allTrue(values(strseq(G_PREFIX, 1, N_GENERAL)))
&& countTrue(values(strseq(G1_PREFIX, 1, N_GENERAL_1))) > 1;
}
QVariant Icd10SpecPD::hasParanoidPD() const
{
const QVariant has_pd = hasPD();
if (!has_pd.toBool()) {
return has_pd;
}
if (!isCompleteParanoid()) {
return QVariant();
}
return countTrue(values(strseq(PARANOID_PREFIX, 1, N_PARANOID))) >= 4;
}
QVariant Icd10SpecPD::hasSchizoidPD() const
{
const QVariant has_pd = hasPD();
if (!has_pd.toBool()) {
return has_pd;
}
if (!isCompleteSchizoid()) {
return QVariant();
}
return countTrue(values(strseq(SCHIZOID_PREFIX, 1, N_SCHIZOID))) >= 4;
}
QVariant Icd10SpecPD::hasDissocialPD() const
{
const QVariant has_pd = hasPD();
if (!has_pd.toBool()) {
return has_pd;
}
if (!isCompleteDissocial()) {
return QVariant();
}
return countTrue(values(strseq(DISSOCIAL_PREFIX, 1, N_DISSOCIAL))) >= 3;
}
QVariant Icd10SpecPD::hasEUPD_I() const
{
const QVariant has_pd = hasPD();
if (!has_pd.toBool()) {
return has_pd;
}
if (!isCompleteEU()) {
return QVariant();
}
return countTrue(values(strseq(EU_PREFIX, 1, N_EUPD_I))) >= 3
&& valueBool(strnum(EU_PREFIX, 2));
// It is tempting to add "&& !hasEUPD_B()", on the basis that EUPD(B)
// trumps EUPD(I), since one requires more symptoms for an EUPD(B)
// diagnosis. However, that's not what the DCR-10 says (perhaps in error!);
// it suggests that one can have both EUPD(B) and EUPD(I), if read
// strictly.
}
QVariant Icd10SpecPD::hasEUPD_B() const
{
const QVariant has_pd = hasPD();
if (!has_pd.toBool()) {
return has_pd;
}
if (!isCompleteEU()) {
return QVariant();
}
return countTrue(values(strseq(EU_PREFIX, 1, N_EUPD_I))) >= 3
&& countTrue(values(strseq(EU_PREFIX, N_EUPD_I + 1, N_EU))) >= 2;
}
QVariant Icd10SpecPD::hasHistrionicPD() const
{
const QVariant has_pd = hasPD();
if (!has_pd.toBool()) {
return has_pd;
}
if (!isCompleteHistrionic()) {
return QVariant();
}
return countTrue(values(strseq(HISTRIONIC_PREFIX, 1, N_HISTRIONIC))) >= 4;
}
QVariant Icd10SpecPD::hasAnankasticPD() const
{
const QVariant has_pd = hasPD();
if (!has_pd.toBool()) {
return has_pd;
}
if (!isCompleteAnankastic()) {
return QVariant();
}
return countTrue(values(strseq(ANANKASTIC_PREFIX, 1, N_ANANKASTIC))) >= 4;
}
QVariant Icd10SpecPD::hasAnxiousPD() const
{
const QVariant has_pd = hasPD();
if (!has_pd.toBool()) {
return has_pd;
}
if (!isCompleteAnxious()) {
return QVariant();
}
return countTrue(values(strseq(ANXIOUS_PREFIX, 1, N_ANXIOUS))) >= 4;
}
QVariant Icd10SpecPD::hasDependentPD() const
{
const QVariant has_pd = hasPD();
if (!has_pd.toBool()) {
return has_pd;
}
if (!isCompleteDependent()) {
return QVariant();
}
return countTrue(values(strseq(DEPENDENT_PREFIX, 1, N_DEPENDENT))) >= 4;
}
// ============================================================================
// Signal handlers
// ============================================================================
void Icd10SpecPD::updateMandatory()
{
auto set = [this](const QString& prefix, int n, bool mandatory) -> void {
for (const QString& fieldname : strseq(prefix, 1, n)) {
fieldRef(fieldname)->setMandatory(mandatory);
}
};
const bool pd_excluded = isPDExcluded();
const bool need_general = !pd_excluded;
const bool need_paranoid = !(pd_excluded || valueBool(SKIP_PARANOID));
const bool need_schizoid = !(pd_excluded || valueBool(SKIP_SCHIZOID));
const bool need_dissocial = !(pd_excluded || valueBool(SKIP_DISSOCIAL));
const bool need_eu = !(pd_excluded || valueBool(SKIP_EU));
const bool need_histrionic = !(pd_excluded || valueBool(SKIP_HISTRIONIC));
const bool need_anankastic = !(pd_excluded || valueBool(SKIP_ANANKASTIC));
const bool need_anxious = !(pd_excluded || valueBool(SKIP_ANXIOUS));
const bool need_dependent = !(pd_excluded || valueBool(SKIP_DEPENDENT));
const bool need_other = !pd_excluded;
const bool need_vignette = !pd_excluded && valueBool(OTHER_PD_PRESENT);
set(G_PREFIX, N_GENERAL, need_general);
set(G1_PREFIX, N_GENERAL_1, need_general);
set(PARANOID_PREFIX, N_PARANOID, need_paranoid);
set(SCHIZOID_PREFIX, N_SCHIZOID, need_schizoid);
set(DISSOCIAL_PREFIX, N_DISSOCIAL, need_dissocial);
set(EU_PREFIX, N_EU, need_eu);
set(HISTRIONIC_PREFIX, N_HISTRIONIC, need_histrionic);
set(ANANKASTIC_PREFIX, N_ANANKASTIC, need_anankastic);
set(ANXIOUS_PREFIX, N_ANXIOUS, need_anxious);
set(DEPENDENT_PREFIX, N_DEPENDENT, need_dependent);
fieldRef(OTHER_PD_PRESENT)->setMandatory(need_other);
fieldRef(VIGNETTE)->setMandatory(need_vignette);
}
QVariant Icd10SpecPD::getHasPDYesNoUnknown() const
{
return yesNoUnknown(hasPD());
}
bool Icd10SpecPD::ignoreValue(const QVariant& value) const
{
Q_UNUSED(value)
return false; // changed?
}