15.1.846. tablet_qt/tasks/slums.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 "slums.h"

#include "common/colourdefs.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/qucanvas.h"
#include "questionnairelib/qucountdown.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/qumcqgrid.h"
#include "questionnairelib/qutext.h"
#include "questionnairelib/qutextedit.h"
#include "tasklib/taskfactory.h"
#include "tasklib/taskregistrar.h"
using mathfunc::noneNull;
using mathfunc::sumInt;
using mathfunc::totalScorePhrase;
using stringfunc::standardResult;

const QString Slums::SLUMS_TABLENAME("slums");

const QString ALERT("alert");
const QString HIGHSCHOOLEDUCATION("highschooleducation");
const QString Q1("q1");  // scores 1
const QString Q2("q2");  // scores 1
const QString Q3("q3");  // scores 1
// Q4 is "please remember these [...] objects[...]"
const QString Q5A("q5a");  // scores 1
const QString Q5B("q5b");  // scores 2
const QString Q6("q6");  // scores 3
const QString Q7A("q7a");  // scores 1
const QString Q7B("q7b");  // scores 1
const QString Q7C("q7c");  // scores 1
const QString Q7D("q7d");  // scores 1
const QString Q7E("q7e");  // scores 1
// Q8a is not scored (the first backwards digit span)
const QString Q8B("q8b");  // scores 1
const QString Q8C("q8c");  // scores 1
const QString Q9A("q9a");  // scores 2
const QString Q9B("q9b");  // scores 2
const QString Q10A("q10a");  // scores 1
const QString Q10B("q10b");  // scores 1
const QString Q11A("q11a");  // scores 2
const QString Q11B("q11b");  // scores 2
const QString Q11C("q11c");  // scores 2
const QString Q11D("q11d");  // scores 2
const QStringList QLIST{
    Q1,  Q2,  Q3,  Q5A, Q5B,  Q6,   Q7A,  Q7B,  Q7C,  Q7D,  Q7E,
    Q8B, Q8C, Q9A, Q9B, Q10A, Q10B, Q11A, Q11B, Q11C, Q11D,
};
const QString CLOCKPICTURE_BLOBID("clockpicture_blobid");
const QString SHAPESPICTURE_BLOBID("shapespicture_blobid");
const QString COMMENTS("comments");

const int MAX_QUESTION_SCORE = 30;
const int NORMAL_IF_GEQ_HIGHSCHOOL = 27;
const int MCI_IF_GEQ_HIGHSCHOOL = 21;
const int NORMAL_IF_GEQ_NO_HIGHSCHOOL = 25;
const int MCI_IF_GEQ_NO_HIGHSCHOOL = 20;
const int COUNTDOWN_TIME_S = 60;

const QString IMAGE_CIRCLE("slums/circle.png");
const QString IMAGE_SHAPES("slums/shapes.png");

void initializeSlums(TaskFactory& factory)
{
    static TaskRegistrar<Slums> registered(factory);
}

Slums::Slums(CamcopsApp& app, DatabaseManager& db, const int load_pk) :
    Task(app, db, SLUMS_TABLENAME, false, true, false)  // ... anon, clin, resp
{
    addField(ALERT, QMetaType::fromType<int>());
    addField(HIGHSCHOOLEDUCATION, QMetaType::fromType<int>());
    addFields(QLIST, QMetaType::fromType<int>());
    addField(CLOCKPICTURE_BLOBID, QMetaType::fromType<int>());
    // ... FK to BLOB table
    addField(SHAPESPICTURE_BLOBID, QMetaType::fromType<int>());
    // ... FK to BLOB table
    addField(COMMENTS, QMetaType::fromType<QString>());

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

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

QString Slums::shortname() const
{
    return "SLUMS";
}

QString Slums::longname() const
{
    return tr("St Louis University Mental Status");
}

QString Slums::description() const
{
    return tr("30-point clinician-administered brief cognitive assessment.");
}

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

bool Slums::isComplete() const
{
    return noneNull(values({ALERT, HIGHSCHOOLEDUCATION}))
        && noneNull(values(QLIST));
}

QStringList Slums::summary() const
{
    return QStringList{totalScorePhrase(totalScore(), MAX_QUESTION_SCORE)};
}

QStringList Slums::detail() const
{
    const int score = totalScore();
    const QString category = valueBool(HIGHSCHOOLEDUCATION)
        ? (score >= NORMAL_IF_GEQ_HIGHSCHOOL
               ? TextConst::normal()
               : (score >= MCI_IF_GEQ_HIGHSCHOOL
                      ? xstring("category_mci")
                      : xstring("category_dementia")))
        : (score >= NORMAL_IF_GEQ_NO_HIGHSCHOOL
               ? TextConst::normal()
               : (score >= MCI_IF_GEQ_NO_HIGHSCHOOL
                      ? xstring("category_mci")
                      : xstring("category_dementia")));

    QStringList lines = completenessInfo();
    lines.append(fieldSummaryYesNoNull(ALERT, xstring("alert_s")));
    lines.append(
        fieldSummaryYesNoNull(HIGHSCHOOLEDUCATION, xstring("highschool_s"))
    );
    for (const QString& q : QLIST) {
        lines.append(fieldSummary(q, xstring(q)));
    }
    lines.append("");
    lines += summary();
    lines.append("");
    lines.append(standardResult(TextConst::category(), category));
    return lines;
}

OpenableWidget* Slums::editor(const bool read_only)
{
    auto qfields
        = [this](
              const QVector<QPair<QString, QString>>& fieldnames_xstringnames,
              bool mandatory = true
          ) -> QVector<QuestionWithOneField> {
        QVector<QuestionWithOneField> qf;
        for (const QPair<QString, QString>& fx : fieldnames_xstringnames) {
            qf.append(QuestionWithOneField(
                fieldRef(fx.first, mandatory), xstring(fx.second)
            ));
        }
        return qf;
    };
    auto mcqgrid = [this](
                       const QStringList& field_and_xstring_names,
                       const NameValueOptions& options,
                       bool mandatory = true
                   ) -> QuElement* {
        QVector<QuestionWithOneField> qfields;
        for (const QString& qx : field_and_xstring_names) {
            qfields.append(
                QuestionWithOneField(xstring(qx), fieldRef(qx, mandatory))
            );
        }
        return new QuMcqGrid(qfields, options);
    };
    auto textRaw = [](const QString& string) -> QuElement* {
        return new QuText(string);
    };
    auto text = [this, textRaw](const QString& stringname) -> QuElement* {
        return textRaw(xstring(stringname));
    };
    auto textRawItalic = [](const QString& string) -> QuElement* {
        return (new QuText(string))->setItalic();
    };
    auto textItalic
        = [this, textRawItalic](const QString& stringname) -> QuElement* {
        return textRawItalic(xstring(stringname));
    };
    auto canvas
        = [this](
              const QString& blob_id_fieldname, const QString& image_filename
          ) -> QuElement* {
        QuCanvas* c = new QuCanvas(
            blobFieldRef(blob_id_fieldname, true),
            uifunc::resourceFilename(image_filename)
        );
        c->setBorderWidth(0);
        c->setBorderColour(QCOLOR_TRANSPARENT);
        c->setBackgroundColour(QCOLOR_TRANSPARENT);
        c->setAllowShrink(true);
        return c;
    };

    const QString plural = xstring("title_prefix_plural");
    const QString singular = xstring("title_prefix_singular");
    const QString scoring = xstring("scoring");
    const NameValueOptions incorrect_correct_options
        = CommonOptions::incorrectCorrectInteger();
    const NameValueOptions incorr_0_corr_2_options{
        {CommonOptions::incorrect(), 0},
        {CommonOptions::correct(), 2},  // NB different scoring
    };
    const NameValueOptions q6_options{
        {xstring("q6_option0"), 0},
        {xstring("q6_option1"), 1},
        {xstring("q6_option2"), 2},
        {xstring("q6_option3"), 3},
    };
    const NameValueOptions q7_options{
        {TextConst::notRecalled(), 0},
        {TextConst::recalled(), 1},
    };
    const QDateTime now = datetime::now();
    const QString correct_date = "     " + now.toString("dddd d MMMM yyyy");
    QVector<QuPagePtr> pages;

    pages.append(QuPagePtr((new QuPage{
                                getClinicianQuestionnaireBlockRawPointer(),
                                new QuMcqGrid(
                                    qfields(
                                        {{ALERT, "q_alert"},
                                         {HIGHSCHOOLEDUCATION, "q_highschool"}}
                                    ),
                                    CommonOptions::noYesInteger()
                                ),
                            })
                               ->setTitle(xstring("title_preamble"))));

    pages.append(
        QuPagePtr((new QuPage{
                       mcqgrid({Q1, Q2, Q3}, incorrect_correct_options),
                       textItalic("date_now_is"),
                       textRawItalic(correct_date),
                   })
                      ->setTitle(plural + " 1–3"))
    );

    pages.append(QuPagePtr((new QuPage{
                                text("q4"),
                            })
                               ->setTitle(singular + " 4")));

    pages.append(QuPagePtr((new QuPage{
                                text("q5"),
                                mcqgrid({Q5A}, incorrect_correct_options),
                                mcqgrid({Q5B}, incorr_0_corr_2_options),
                            })
                               ->setTitle(singular + " 5")));

    pages.append(QuPagePtr((new QuPage{
                                text("q6"),
                                new QuCountdown(COUNTDOWN_TIME_S),
                                mcqgrid({Q6}, q6_options),
                            })
                               ->setTitle(singular + " 6")));

    pages.append(QuPagePtr((new QuPage{
                                text("q7"),
                                mcqgrid({Q7A, Q7B, Q7C, Q7D, Q7E}, q7_options),
                            })
                               ->setTitle(singular + " 7")));

    pages.append(QuPagePtr((new QuPage{
                                text("q8"),
                                mcqgrid({Q8B, Q8C}, incorrect_correct_options),
                            })
                               ->setTitle(singular + " 8")));

    pages.append(QuPagePtr((new QuPage{
                                text("q9"),
                                canvas(CLOCKPICTURE_BLOBID, IMAGE_CIRCLE),
                            })
                               ->setTitle(singular + " 9")
                               ->allowScroll(false)
                               ->setType(QuPage::PageType::ClinicianWithPatient
                               )));

    pages.append(QuPagePtr((new QuPage{
                                mcqgrid({Q9A, Q9B}, incorr_0_corr_2_options),
                            })
                               ->setTitle(singular + " 9 " + scoring)));

    pages.append(QuPagePtr((new QuPage{
                                canvas(SHAPESPICTURE_BLOBID, IMAGE_SHAPES),
                                text("q10_part1"),
                                text("q10_part2"),
                            })
                               ->setTitle(singular + " 10")
                               ->allowScroll(false)
                               ->setType(QuPage::PageType::ClinicianWithPatient
                               )));

    pages.append(
        QuPagePtr((new QuPage{
                       mcqgrid({Q10A, Q10B}, incorrect_correct_options),
                   })
                      ->setTitle(singular + " 10 " + scoring))
    );

    pages.append(QuPagePtr(
        (new QuPage{
             text("q11"),
             mcqgrid({Q11A, Q11B, Q11C, Q11D}, incorr_0_corr_2_options),
         })
            ->setTitle(singular + " 11")
    ));

    pages.append(
        QuPagePtr((new QuPage{
                       textRaw(TextConst::examinerComments()),
                       (new QuTextEdit(fieldRef(COMMENTS, false)))
                           ->setHint(TextConst::examinerCommentsPrompt()),
                   })
                      ->setTitle(singular + " 12"))
    );

    auto questionnaire = new Questionnaire(m_app, pages);
    questionnaire->setType(QuPage::PageType::Clinician);
    questionnaire->setReadOnly(read_only);
    return questionnaire;
}

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

int Slums::totalScore() const
{
    return sumInt(values(QLIST));
}