15.1.597. tablet_qt/tasks/aq.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 "aq.h"

#include "db/databaseobject.h"
#include "lib/convert.h"
#include "lib/stringfunc.h"
#include "maths/mathfunc.h"
#include "questionnairelib/namevaluepair.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/questionnairefunc.h"
#include "questionnairelib/quheading.h"
#include "questionnairelib/qumcqgrid.h"
#include "tasklib/taskfactory.h"
#include "tasklib/taskregistrar.h"
using mathfunc::anyNull;
using stringfunc::strseq;

const int FIRST_Q = 1;
const int LAST_Q = 50;
const int FIRST_OPTION = 0;
const int LAST_OPTION = 3;
const int MIN_SCORE = 0;
const int MAX_SCORE = 50;
const int MIN_AREA_SCORE = 0;
const int MAX_AREA_SCORE = 10;

const QVector<int> AGREE_OPTIONS = {0, 1};  // definitely agree, slightly agree
const QVector<int> DISAGREE_OPTIONS = {2, 3};
// ... slightly disagree, definitely disagree
const QVector<int> AGREE_SCORING_QUESTIONS = {
    2,  4,  5,  6,  7,  9,  12, 13, 16, 18, 19, 20,
    21, 22, 23, 26, 33, 35, 39, 41, 42, 43, 45, 46,
};
// ... see aq.py re error re Q1 in published Baron-Cohen et al. (2001).

const QVector<int> SOCIAL_SKILL_QUESTIONS
    = {1, 11, 13, 15, 22, 36, 44, 45, 47, 48};
const QVector<int> ATTENTION_SWITCHING_QUESTIONS
    = {2, 4, 10, 16, 25, 32, 34, 37, 43, 46};
const QVector<int> ATTENTION_TO_DETAIL_QUESTIONS
    = {5, 6, 9, 12, 19, 23, 28, 29, 30, 49};
const QVector<int> COMMUNICATION_QUESTIONS
    = {7, 17, 18, 26, 27, 31, 33, 35, 38, 39};
const QVector<int> IMAGINATION_QUESTIONS
    = {3, 8, 14, 20, 21, 24, 40, 41, 42, 50};

const QString Q_PREFIX("q");
const QString Aq::AQ_TABLENAME("aq");

void initializeAq(TaskFactory& factory)
{
    static TaskRegistrar<Aq> registered(factory);
}

Aq::Aq(CamcopsApp& app, DatabaseManager& db, const int load_pk) :
    Task(app, db, AQ_TABLENAME, false, false, false)  // ... anon, clin, resp
{
    addFields(strseq(Q_PREFIX, FIRST_Q, LAST_Q), QMetaType::fromType<int>());

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

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

QString Aq::shortname() const
{
    return "AQ";
}

QString Aq::longname() const
{
    return tr("Adult Autism Spectrum Quotient");
}

QString Aq::description() const
{
    return tr(
        "A 50-item self-report measure used to assess traits of autism in "
        "adults and adolescents aged 16 years and over."
    );
}

QStringList Aq::fieldNames() const
{
    return strseq(Q_PREFIX, FIRST_Q, LAST_Q);
}

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


bool Aq::isComplete() const
{
    if (anyNull(values(fieldNames()))) {
        return false;
    }

    return true;
}

QVariant Aq::score() const
{
    QVector<int> all_questions(LAST_Q);
    std::iota(all_questions.begin(), all_questions.end(), FIRST_Q);

    return questionsScore(all_questions);
}

QVariant Aq::socialSkillScore() const
{
    return questionsScore(SOCIAL_SKILL_QUESTIONS);
}

QVariant Aq::attentionSwitchingScore() const
{
    return questionsScore(ATTENTION_SWITCHING_QUESTIONS);
}

QVariant Aq::attentionToDetailScore() const
{
    return questionsScore(ATTENTION_TO_DETAIL_QUESTIONS);
}

QVariant Aq::communicationScore() const
{
    return questionsScore(COMMUNICATION_QUESTIONS);
}

QVariant Aq::imaginationScore() const
{
    return questionsScore(IMAGINATION_QUESTIONS);
}

QVariant Aq::questionsScore(const QVector<int> qnums) const
{
    int total = 0;
    QVariant v;

    for (int qnum : qnums) {
        v = questionScore(qnum);
        if (v.isNull()) {
            return v;
        }
        total += v.toInt();
    }

    return total;
}

QVariant Aq::questionScore(const int qnum) const
{
    const QString fieldname = Q_PREFIX + QString::number(qnum);
    const QVariant v = value(fieldname);
    if (v.isNull()) {
        return v;
    }
    const int answer = v.toInt();

    if (AGREE_SCORING_QUESTIONS.contains(qnum)) {
        // Questions where agreement indicates autistic-like traits
        if (AGREE_OPTIONS.contains(answer)) {
            return 1;
        } else if (DISAGREE_OPTIONS.contains(answer)) {
            return 0;
        } else {
            // Shouldn't happen, but for defensiveness:
            return QVariant();
        }
    } else {
        // Questions where disagreement indicates autistic-like traits
        if (AGREE_OPTIONS.contains(answer)) {
            return 0;
        } else if (DISAGREE_OPTIONS.contains(answer)) {
            return 1;
        } else {
            // Shouldn't happen, but for defensiveness:
            return QVariant();
        }
    }
}

QStringList Aq::summary() const
{
    auto rangeScore = [](const QString& description,
                         const QVariant score,
                         const int min,
                         const int max) {
        return QString("%1: <b>%2</b> [%3–%4].")
            .arg(
                description,
                convert::prettyValue(score),
                QString::number(min),
                QString::number(max)
            );
    };

    return QStringList{
        rangeScore(
            xstring("social_skill_score"),
            socialSkillScore(),
            MIN_AREA_SCORE,
            MAX_AREA_SCORE
        ),
        rangeScore(
            xstring("attention_switching_score"),
            attentionSwitchingScore(),
            MIN_AREA_SCORE,
            MAX_AREA_SCORE
        ),
        rangeScore(
            xstring("attention_to_detail_score"),
            attentionToDetailScore(),
            MIN_AREA_SCORE,
            MAX_AREA_SCORE
        ),
        rangeScore(
            xstring("communication_score"),
            communicationScore(),
            MIN_AREA_SCORE,
            MAX_AREA_SCORE
        ),
        rangeScore(
            xstring("imagination_score"),
            imaginationScore(),
            MIN_AREA_SCORE,
            MAX_AREA_SCORE
        ),
        rangeScore(xstring("score"), score(), MIN_SCORE, MAX_SCORE),
    };
}

QStringList Aq::detail() const
{
    QStringList lines = completenessInfo();

    const QString altname = "";
    const QString spacer = " ";
    const QString suffix = "";

    const QStringList fieldnames = fieldNames();

    QSharedPointer<NameValueOptions> options = buildOptions();

    for (int i = 0; i < fieldnames.length(); ++i) {
        const QString& fieldname = fieldnames.at(i);
        lines.append(fieldSummaryNameValueOptions(
            fieldname, *options, altname, spacer, suffix
        ));
    }

    lines.append("");
    lines += summary();

    return lines;
}

OpenableWidget* Aq::editor(const bool read_only)
{
    auto options = buildOptions();

    const int min_width_px = 100;
    const QVector<int> min_option_widths_px = {50, 50, 50, 50};

    auto instructions = new QuHeading(xstring("instructions"));
    auto grid = buildGrid(FIRST_Q, LAST_Q, options);
    grid->setMinimumWidthInPixels(min_width_px, min_option_widths_px);

    QVector<QuElement*> elements{
        instructions,
        grid,
    };

    QuPagePtr page((new QuPage(elements))->setTitle(xstring("title")));

    auto questionnaire = new Questionnaire(m_app, {page});
    questionnaire->setType(QuPage::PageType::Patient);
    questionnaire->setReadOnly(read_only);

    return questionnaire;
}

QSharedPointer<NameValueOptions> Aq::buildOptions() const
{
    QSharedPointer<NameValueOptions> options
        = QSharedPointer<NameValueOptions>(new NameValueOptions());

    for (int i = FIRST_OPTION; i <= LAST_OPTION; ++i) {
        auto name = QString("option_%1").arg(i);

        options->append({xstring(name), i});
    }

    return options;
}

QuMcqGrid* Aq::buildGrid(
    int first_qnum, int last_qnum, QSharedPointer<NameValueOptions> options
)
{
    QVector<QuestionWithOneField> q_field_pairs;

    for (int qnum = first_qnum; qnum <= last_qnum; qnum++) {
        const QString& qnumstr = QString::number(qnum);
        const QString& fieldname = Q_PREFIX + qnumstr;
        const QString& description = qnumstr + ". " + xstring(fieldname);
        // const lvalue references prolong the lifespan of temporary objects;
        // https://pvs-studio.com/en/blog/posts/cpp/1006/

        q_field_pairs.append(
            QuestionWithOneField(description, fieldRef(fieldname))
        );
    }

    auto grid = new QuMcqGrid(q_field_pairs, *options);
    // Repeat options every five lines
    QVector<McqGridSubtitle> subtitles{
        {5, ""},
        {10, ""},
        {15, ""},
        {20, ""},
        {25, ""},
        {30, ""},
        {35, ""},
        {40, ""},
        {45, ""},
    };
    grid->setSubtitles(subtitles);

    const int question_width = 4;
    const QVector<int> option_widths = {1, 1, 1, 1};
    grid->setWidth(question_width, option_widths);
    grid->setQuestionsBold(false);

    return grid;
}