15.1.620. tablet_qt/tasks/cape42.cpp

/*
    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 "cape42.h"

#include "lib/convert.h"
#include "lib/stringfunc.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/qumcq.h"
#include "questionnairelib/qutext.h"
#include "tasklib/taskfactory.h"
#include "tasklib/taskregistrar.h"
using stringfunc::bold;
using stringfunc::strnum;
using stringfunc::strseq;

const int FIRST_Q = 1;
const int N_QUESTIONS = 42;

const QString Cape42::CAPE42_TABLENAME("cape42");

const QString FN_FREQ_PREFIX("frequency");
const QString FN_DISTRESS_PREFIX("distress");

const QString TAG_DISTRESS("distress");
const QVector<int> POSITIVE{2,  5,  6,  7,  10, 11, 13, 15, 17, 20,
                            22, 24, 26, 28, 30, 31, 33, 34, 41, 42};
const QVector<int> DEPRESSIVE{1, 9, 12, 14, 19, 38, 39, 40};
const QVector<int> NEGATIVE{
    3, 4, 8, 16, 18, 21, 23, 25, 27, 29, 32, 35, 36, 37};
// not the most elegant ;)
const QVector<int> ALL{1,  2,  3,  4,  5,  6,  7,  8,  9,  10, 11, 12, 13, 14,
                       15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
                       29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42};
const int MIN_SCORE_PER_Q = 1;
const int MAX_SCORE_PER_Q = 4;

void initializeCape42(TaskFactory& factory)
{
    static TaskRegistrar<Cape42> registered(factory);
}

Cape42::Cape42(CamcopsApp& app, DatabaseManager& db, const int load_pk) :
    Task(app, db, CAPE42_TABLENAME, false, false, false),
    // ... anon, clin, resp
    m_questionnaire(nullptr)
{
    addFields(
        strseq(FN_FREQ_PREFIX, FIRST_Q, N_QUESTIONS),
        QMetaType::fromType<int>()
    );
    addFields(
        strseq(FN_DISTRESS_PREFIX, FIRST_Q, N_QUESTIONS),
        QMetaType::fromType<int>()
    );

    load(load_pk);  // MUST ALWAYS CALL from derived Task constructor.
}

// ============================================================================
// Class info
// ============================================================================

QString Cape42::shortname() const
{
    return "CAPE-42";
}

QString Cape42::longname() const
{
    return tr("Community Assessment of Psychic Experiences");
}

QString Cape42::description() const
{
    return tr(
        "42-item self-rated scale for psychosis with positive, "
        "negative, and depressive dimensions."
    );
}

QString Cape42::infoFilenameStem() const
{
    return "cape";
}

// ============================================================================
// Instance info
// ============================================================================

bool Cape42::isComplete() const
{
    for (auto q : ALL) {
        if (!questionComplete(q)) {
            return false;
        }
    }
    return true;
}

QStringList Cape42::summary() const
{
    QStringList lines;
    auto addbit = [this, &lines](
                      const QVector<int>& questions, const QString& name
                  ) -> void {
        const int n = questions.length();
        const int min_score = MIN_SCORE_PER_Q * n;
        const int max_score = MAX_SCORE_PER_Q * n;
        lines.append(QString("%1: frequency %2 (%3–%4), distress %5 (%6–%7).")
                         .arg(
                             name,
                             bold(QString::number(frequencyScore(questions))),
                             QString::number(min_score),
                             QString::number(max_score),
                             bold(QString::number(distressScore(questions))),
                             QString::number(min_score),
                             QString::number(max_score)
                         ));
    };
    addbit(ALL, "ALL");
    addbit(POSITIVE, "POSITIVE");
    addbit(NEGATIVE, "NEGATIVE");
    addbit(DEPRESSIVE, "DEPRESSIVE");
    return lines;
}

QStringList Cape42::detail() const
{
    QStringList lines = completenessInfo();
    for (auto q : ALL) {
        const QVariant freq = value(strnum(FN_FREQ_PREFIX, q));
        QString msg = QString("%1 F:%2").arg(
            xstring(strnum("q", q)), bold(convert::prettyValue(freq))
        );
        if (freq.toInt() > MIN_SCORE_PER_Q) {
            msg += QString(" (D:%1)").arg(
                bold(prettyValue(strnum(FN_DISTRESS_PREFIX, q)))
            );
        }
        lines.append(msg);
    };
    lines.append("");
    lines += summary();
    return lines;
}

OpenableWidget* Cape42::editor(const bool read_only)
{
    const NameValueOptions options_distress{
        {xstring("distress_option1"), 1},
        {xstring("distress_option2"), 2},
        {xstring("distress_option3"), 3},
        {xstring("distress_option4"), 4},
    };
    const NameValueOptions options_frequency{
        {xstring("frequency_option1"), 1},
        {xstring("frequency_option2"), 2},
        {xstring("frequency_option3"), 3},
        {xstring("frequency_option4"), 4},
    };
    QVector<QuPagePtr> pages;
    const QString distress_stem = xstring("distress_stem");
    m_distress_fieldrefs.clear();

    auto addpage =
        [this, &pages, &options_distress, &options_frequency, &distress_stem](
            const int q
        ) -> void {
        const QString pagetag = QString::number(q);
        const QString pagetitle
            = QString("CAPE-42 (%1 / %2)").arg(q).arg(N_QUESTIONS);
        const QString question = xstring(strnum("q", q));
        const bool need_distress = needDistress(q);
        const QString freq_fieldname = strnum(FN_FREQ_PREFIX, q);
        const QString distress_fieldname = strnum(FN_DISTRESS_PREFIX, q);
        FieldRefPtr fr_freq = fieldRef(freq_fieldname);
        fr_freq->setHint(q);
        FieldRefPtr fr_distress = fieldRef(distress_fieldname, need_distress);
        m_distress_fieldrefs[q] = fr_distress;
        QuPagePtr page((new QuPage{
                            (new QuText(question))->setBold(),
                            new QuMcq(fr_freq, options_frequency),
                            (new QuText(distress_stem))
                                ->setBold()
                                ->addTag(TAG_DISTRESS)
                                ->setVisible(need_distress),
                            (new QuMcq(fr_distress, options_distress))
                                ->addTag(TAG_DISTRESS)
                                ->setVisible(need_distress),
                        })
                           ->setTitle(pagetitle)
                           ->addTag(pagetag));
        pages.append(page);
        connect(
            fr_freq.data(),
            &FieldRef::valueChanged,
            this,
            &Cape42::frequencyChanged
        );
    };

    for (auto q : ALL) {
        addpage(q);
    }

    m_questionnaire = new Questionnaire(m_app, pages);
    m_questionnaire->setType(QuPage::PageType::Patient);
    m_questionnaire->setReadOnly(read_only);
    return m_questionnaire;
}

// ============================================================================
// Task-specific calculations
// ============================================================================

int Cape42::distressScore(const QVector<int>& questions) const
{
    int score = 0;
    for (auto q : questions) {
        const int freq = valueInt(strnum(FN_FREQ_PREFIX, q));  // 0 for null
        if (freq > MIN_SCORE_PER_Q) {
            score += valueInt(strnum(FN_DISTRESS_PREFIX, q));  // 0 for null
        } else {
            score += MIN_SCORE_PER_Q;
            // ... if frequency is 1, score 1 for distress?
        }
    }
    return score;
}

int Cape42::frequencyScore(const QVector<int>& questions) const
{
    int score = 0;
    for (auto q : questions) {
        score += qMax(MIN_SCORE_PER_Q, valueInt(strnum(FN_FREQ_PREFIX, q)));
        // valueInt(...) will be 0 if null
    }
    return score;
}

bool Cape42::questionComplete(const int q) const
{
    const QVariant freq = value(strnum(FN_FREQ_PREFIX, q));
    if (freq.isNull()) {
        return false;
    }
    if (freq.toInt() <= MIN_SCORE_PER_Q) {
        return true;
    }
    QVariant distress = value(strnum(FN_DISTRESS_PREFIX, q));
    return !distress.isNull();
}

// ============================================================================
// Signal handlers
// ============================================================================

void Cape42::frequencyChanged(const FieldRef* fieldref)
{
    Q_ASSERT(fieldref);
    const QVariant hint = fieldref->getHint();
    const int q = hint.toInt();
    Q_ASSERT(q >= FIRST_Q && q <= N_QUESTIONS);
    setDistressItems(q);
}

bool Cape42::needDistress(const int q)
{
    Q_ASSERT(q >= FIRST_Q && q <= N_QUESTIONS);
    return valueInt(strnum(FN_FREQ_PREFIX, q)) > MIN_SCORE_PER_Q;
    // ... we need a distress rating if the frequency rating is above minimum
}

void Cape42::setDistressItems(const int q)
{
    if (!m_questionnaire) {
        return;
    }
    const QString pagetag = QString::number(q);
    const bool need_distress = needDistress(q);
    m_questionnaire->setVisibleByTag(
        TAG_DISTRESS, need_distress, false, pagetag
    );
    Q_ASSERT(m_distress_fieldrefs.contains(q));
    FieldRefPtr distress_fieldref = m_distress_fieldrefs[q];
    Q_ASSERT(distress_fieldref);
    distress_fieldref->setMandatory(need_distress);
}