15.1.569. tablet_qt/tasklib/task.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 "task.h"

#include <QObject>
#include <QVariant>

#include "common/preprocessor_aid.h"  // IWYU pragma: keep
#include "common/textconst.h"
#include "common/uiconst.h"
#include "common/varconst.h"
#include "core/camcopsapp.h"
#include "db/databasemanager.h"
#include "dbobjects/patient.h"
#include "lib/datetime.h"
#include "lib/stringfunc.h"
#include "lib/uifunc.h"
#include "lib/version.h"
#include "maths/mathfunc.h"
#include "questionnairelib/commonoptions.h"
#include "questionnairelib/questionnairefunc.h"
#include "questionnairelib/qulineedit.h"
#include "questionnairelib/qupage.h"
#include "questionnairelib/quspacer.h"
#include "version/camcopsversion.h"
#include "widgets/openablewidget.h"
#include "widgets/screenlikegraphicsview.h"

const QString Task::PATIENT_FK_FIELDNAME("patient_id");
const QString Task::FIRSTEXIT_IS_FINISH_FIELDNAME("firstexit_is_finish");
const QString Task::FIRSTEXIT_IS_ABORT_FIELDNAME("firstexit_is_abort");
const QString Task::WHEN_FIRSTEXIT_FIELDNAME("when_firstexit");
const QString Task::EDITING_TIME_S_FIELDNAME("editing_time_s");

const QString Task::CLINICIAN_SPECIALTY("clinician_specialty");
const QString Task::CLINICIAN_NAME("clinician_name");
const QString Task::CLINICIAN_PROFESSIONAL_REGISTRATION(
    "clinician_professional_registration"
);
const QString Task::CLINICIAN_POST("clinician_post");
const QString Task::CLINICIAN_SERVICE("clinician_service");
const QString Task::CLINICIAN_CONTACT_DETAILS("clinician_contact_details");

const QString Task::RESPONDENT_NAME("respondent_name");
const QString Task::RESPONDENT_RELATIONSHIP("respondent_relationship");

Task::Task(
    CamcopsApp& app,
    DatabaseManager& db,
    const QString& tablename,
    const bool is_anonymous,
    const bool has_clinician,
    const bool has_respondent,
    QObject* parent
) :
    DatabaseObject(
        app,
        db,
        tablename,
        dbconst::PK_FIELDNAME,
        true,
        true,
        true,
        true,
        parent
    ),
    m_patient(nullptr),
    m_editing(false),
    m_is_complete_is_cached(false),
    m_is_anonymous(is_anonymous),
    m_has_clinician(has_clinician),
    m_has_respondent(has_respondent)
{
    // WATCH OUT: you can't call a derived class's overloaded function
    // here; its vtable is incomplete.
    // http://stackoverflow.com/questions/6561429/calling-virtual-function-of-derived-class-from-base-class-constructor

    addField(FIRSTEXIT_IS_FINISH_FIELDNAME, QMetaType::fromType<bool>());
    addField(FIRSTEXIT_IS_ABORT_FIELDNAME, QMetaType::fromType<bool>());
    addField(WHEN_FIRSTEXIT_FIELDNAME, QMetaType::fromType<QDateTime>());
    addField(Field(EDITING_TIME_S_FIELDNAME, QMetaType::fromType<double>())
                 .setCppDefaultValue(0.0));

    if (!is_anonymous) {
        addField(PATIENT_FK_FIELDNAME, QMetaType::fromType<int>());
    }
    if (has_clinician) {
        addField(CLINICIAN_SPECIALTY, QMetaType::fromType<QString>());
        addField(CLINICIAN_NAME, QMetaType::fromType<QString>());
        addField(
            CLINICIAN_PROFESSIONAL_REGISTRATION, QMetaType::fromType<QString>()
        );
        addField(CLINICIAN_POST, QMetaType::fromType<QString>());
        addField(CLINICIAN_SERVICE, QMetaType::fromType<QString>());
        addField(CLINICIAN_CONTACT_DETAILS, QMetaType::fromType<QString>());
    }
    if (has_respondent) {
        addField(RESPONDENT_NAME, QMetaType::fromType<QString>());
        addField(RESPONDENT_RELATIONSHIP, QMetaType::fromType<QString>());
    }

    connect(this, &DatabaseObject::dataChanged, this, &Task::onDataChanged);
}

// ============================================================================
// General info
// ============================================================================

QString Task::implementationTypeDescription() const
{
    switch (implementationType()) {
        case TaskImplementationType::Full:
#ifdef COMPILER_WANTS_DEFAULT_IN_EXHAUSTIVE_SWITCH
        default:
#endif
            return TextConst::fullTask();
        case TaskImplementationType::UpgradableSkeleton:
            return TextConst::DATA_COLLECTION_ONLY_UNLESS_UPGRADED_SYMBOL;
        case TaskImplementationType::Skeleton:
            return TextConst::DATA_COLLECTION_ONLY_SYMBOL;
    }
}

QString Task::menuTitleSuffix() const
{
    QStringList suffixes;
    if (hasClinician()) {
        suffixes += TextConst::HAS_CLINICIAN_SYMBOL;
    }
    if (hasRespondent()) {
        suffixes += TextConst::HAS_RESPONDENT_SYMBOL;
    }
    switch (implementationType()) {
        case TaskImplementationType::Full:
            break;
        case TaskImplementationType::UpgradableSkeleton:
            suffixes += TextConst::DATA_COLLECTION_ONLY_UNLESS_UPGRADED_SYMBOL;
            break;
        case TaskImplementationType::Skeleton:
            suffixes += TextConst::DATA_COLLECTION_ONLY_SYMBOL;
            break;
    }
    if (isExperimental()) {
        suffixes += TextConst::EXPERIMENTAL_SYMBOL;
    }
    if (isDefunct()) {
        suffixes += TextConst::DEFUNCT_SYMBOL;
    }
    return suffixes.isEmpty() ? ""
                              : QString(" <i>[%1]</i>").arg(suffixes.join(""));
}

QString Task::menutitle() const
{
    return QString("%1 (%2)%3")
        .arg(longname(), shortname(), menuTitleSuffix());
}

QString Task::menuSubtitleSuffix() const
{
    auto makeSuffix
        = [](const QString& title, const QString& subtitle) -> QString {
        return QString("%1: %2").arg(title, subtitle);
    };

    QStringList suffixes;
    if (hasClinician()) {
        suffixes += makeSuffix(
            TextConst::HAS_CLINICIAN_SYMBOL,
            TextConst::hasClinicianSubtitleSuffix()
        );
    }
    if (hasRespondent()) {
        suffixes += makeSuffix(
            TextConst::HAS_RESPONDENT_SYMBOL,
            TextConst::hasRespondentSubtitleSuffix()
        );
    }
    switch (implementationType()) {
        case TaskImplementationType::Full:
            break;
        case TaskImplementationType::UpgradableSkeleton:
            suffixes += makeSuffix(
                TextConst::DATA_COLLECTION_ONLY_UNLESS_UPGRADED_SYMBOL,
                TextConst::dataCollectionOnlyUnlessUpgradedSubtitleSuffix()
            );
            break;
        case TaskImplementationType::Skeleton:
            suffixes += makeSuffix(
                TextConst::DATA_COLLECTION_ONLY_SYMBOL,
                TextConst::dataCollectionOnlySubtitleSuffix()
            );
            break;
    }
    if (isExperimental()) {
        suffixes += makeSuffix(
            TextConst::EXPERIMENTAL_SYMBOL,
            TextConst::experimentalSubtitleSuffix()
        );
    }
    if (isDefunct()) {
        suffixes += makeSuffix(
            TextConst::DEFUNCT_SYMBOL, TextConst::defunctSubtitleSuffix()
        );
    }
    return suffixes.isEmpty()
        ? ""
        : QString(" <i>[%1]</i>").arg(suffixes.join(" "));
}

QString Task::menusubtitle() const
{
    return description() + menuSubtitleSuffix();
}

bool Task::isCrippled() const
{
    QString failure_reason_dummy;
    return implementationType() == TaskImplementationType::Skeleton
        || !hasExtraStrings() || !isTaskProperlyCreatable(failure_reason_dummy)
        || !isTaskUploadable(failure_reason_dummy);
}

bool Task::hasExtraStrings() const
{
    return m_app.hasExtraStrings(xstringTaskname());
}

QString Task::infoFilenameStem() const
{
    return m_tablename;
}

QString Task::xstringTaskname() const
{
    return m_tablename;
}

QString Task::instanceTitle(bool with_pid) const
{
    if (isAnonymous() || !with_pid) {
        return QString("%1; %2").arg(
            shortname(),
            whenCreated().toString(datetime::SHORT_DATETIME_FORMAT)
        );
    }
    Patient* pt = patient();
    return QString("%1; %2; %3")
        .arg(
            shortname(),
            pt ? pt->surnameUpperForename() : tr("MISSING PATIENT"),
            whenCreated().toString(datetime::SHORT_DATETIME_FORMAT)
        );
}

bool Task::isAnonymous() const
{
    return m_is_anonymous;
}

bool Task::hasClinician() const
{
    return m_has_clinician;
}

bool Task::hasRespondent() const
{
    return m_has_respondent;
}

bool Task::isTaskPermissible(QString& failure_reason) const
{
    const QVariant commercial = m_app.var(varconst::IP_USE_COMMERCIAL);
    const QVariant clinical = m_app.var(varconst::IP_USE_CLINICAL);
    const QVariant educational = m_app.var(varconst::IP_USE_EDUCATIONAL);
    const QVariant research = m_app.var(varconst::IP_USE_RESEARCH);

    auto not_definitely_false = [](const QVariant& v) -> bool {
        return !mathfunc::eq(v, false);
    };
    auto is_unknown = [](const QVariant& v) -> bool {
        return v.isNull() || v.toInt() == CommonOptions::UNKNOWN_INT;
    };

    const QString PROHIBITED_YES(
        " "
        + tr("You have said you ARE using this software in that context "
             "(see Settings). To use this task, you must seek permission "
             "from the copyright holder (see Task Information).")
    );
    const QString PROHIBITED_UNKNOWN(
        " "
        + tr("You have NOT SAID whether you are using this "
             "software in that context (see Settings).")
    );

    if (prohibitsCommercial() && not_definitely_false(commercial)) {
        failure_reason
            = tr("Task not allowed for commercial use (see Task Information).")
            + (is_unknown(commercial) ? PROHIBITED_UNKNOWN : PROHIBITED_YES);
        return false;
    }
    if (prohibitsClinical() && not_definitely_false(clinical)) {
        failure_reason
            = tr("Task not allowed for clinical use (see Task Information).")
            + (is_unknown(clinical) ? PROHIBITED_UNKNOWN : PROHIBITED_YES);
        return false;
    }
    if (prohibitsEducational() && not_definitely_false(educational)) {
        failure_reason
            = tr("Task not allowed for educational use (see Task Information)."
              )
            + (is_unknown(educational) ? PROHIBITED_UNKNOWN : PROHIBITED_YES);
        return false;
    }
    if (prohibitsResearch() && not_definitely_false(research)) {
        failure_reason
            = tr("Task not allowed for research use (see Task Information).")
            + (is_unknown(research) ? PROHIBITED_UNKNOWN : PROHIBITED_YES);
        return false;
    }

    if (implementationType() == TaskImplementationType::UpgradableSkeleton
        && prohibitedIfSkeleton() && !hasExtraStrings()) {
        failure_reason = tr(
            "Task may not be created in 'skeleton' form "
            "(strings need to be downloaded from server)."
        );
        return false;
    }

    // Task doesn't have its data (e.g. strings present but too old)?
    if (!isTaskProperlyCreatable(failure_reason)) {
        return false;
    }

    return true;
}

Version Task::minimumServerVersion() const
{
    return camcopsversion::MINIMUM_SERVER_VERSION;
}

bool Task::isTaskUploadable(QString& failure_reason) const
{
    bool server_has_table;
    Version min_client_version;
    Version min_server_version;
    const Version overall_min_server_version = Task::minimumServerVersion();
    const Version server_version = m_app.serverVersion();
    const QString table = tablename();
    const bool may_upload = m_app.mayUploadTable(
        table,
        server_version,
        server_has_table,
        min_client_version,
        min_server_version
    );
#if 0
    qDebug() << "table" << table
             << "server_version" << server_version
             << "may_upload" << may_upload
             << "server_has_table" << server_has_table
             << "min_client_version" << min_client_version
             << "min_server_version" << min_server_version;
#endif
    if (may_upload) {
        return true;
    }
    if (!server_has_table) {
        failure_reason = tr("Table '%1' absent on server.").arg(table);
    } else if (camcopsversion::CAMCOPS_CLIENT_VERSION < min_client_version) {
        failure_reason
            = tr("Server requires client version >=%1 for table '%2', "
                 "but we are only client version %3.")
                  .arg(
                      min_client_version.toString(),
                      table,
                      camcopsversion::CAMCOPS_CLIENT_VERSION.toString()
                  );
    } else if (server_version < overall_min_server_version) {
        failure_reason = tr("This client requires server version >=%1, "
                            "but the server is only version %2.")
                             .arg(
                                 overall_min_server_version.toString(),
                                 server_version.toString()
                             );
    } else if (server_version < min_server_version) {
        failure_reason
            = tr("This client requires server version >=%1 for table '%2', "
                 "but the server is only version %3.")
                  .arg(
                      min_server_version.toString(),
                      table,
                      server_version.toString()
                  );
    } else {
        failure_reason
            = "? [bug in Task::isTaskUploadable, "
              "versus CamcopsApp::mayUploadTable]";
    }
    return false;
}

bool Task::isTaskProperlyCreatable(QString& failure_reason) const
{
    Q_UNUSED(failure_reason)
    return true;
}

bool Task::isServerStringVersionEnough(
    const Version& minimum_server_version, QString& failure_reason
) const
{
    const Version server_version = m_app.serverVersion();
    if (server_version < minimum_server_version) {
        failure_reason = tr("This client requires content strings from server "
                            "version >=%1, "
                            "but the server is only version %2. If the server "
                            "has recently "
                            "been updated, re-fetch the server information "
                            "from the Settings "
                            "menu.")
                             .arg(
                                 minimum_server_version.toString(),
                                 server_version.toString()
                             );
        return false;
    }
    return true;
}

// ============================================================================
// Tables
// ============================================================================

QStringList Task::allTables() const
{
    QStringList all_tables(tablename());
    all_tables += ancillaryTables();
    return all_tables;
}

void Task::makeTables()
{
    makeTable();
    makeAncillaryTables();
}

int Task::count(const WhereConditions& where) const
{
    return m_db.count(m_tablename, where);
}

int Task::countForPatient(const int patient_id) const
{
    if (isAnonymous()) {
        return 0;
    }
    WhereConditions where;
    where.add(PATIENT_FK_FIELDNAME, patient_id);
    return count(where);
}

void Task::upgradeDatabase(
    const Version& old_version, const Version& new_version
)
{
    Q_UNUSED(old_version)
    Q_UNUSED(new_version)
}

// ============================================================================
// Database object functions
// ============================================================================

bool Task::load(const int pk)
{
    if (pk == dbconst::NONEXISTENT_PK) {
        return false;
    }
    return DatabaseObject::load(pk);
}

bool Task::save()
{
    // Sanity checks before we permit saving
    if (!isAnonymous() && value(PATIENT_FK_FIELDNAME).isNull()) {
        uifunc::stopApp(
            "Task has no patient ID (and is not anonymous); "
            "cannot save"
        );
    }
    return DatabaseObject::save();
}

// ============================================================================
// Specific info
// ============================================================================

bool Task::isCompleteCached() const
{
    if (!m_is_complete_is_cached) {
        m_is_complete_cached_value = isComplete();
        m_is_complete_is_cached = true;
    }
    return m_is_complete_cached_value;
}

void Task::onDataChanged()
{
    m_is_complete_is_cached = false;
}

QStringList Task::summary() const
{
    return QStringList{tr("MISSING SUMMARY")};
}

QStringList Task::detail() const
{
    QStringList result = completenessInfo() + summary();
    result.append("");  // blank line
    result += recordSummaryLines();
    return result;
}

OpenableWidget* Task::editor(const bool read_only)
{
    Q_UNUSED(read_only)
    qWarning() << "Base class Task::edit called - not a good thing!";
    return nullptr;
}

// ============================================================================
// Assistance functions
// ============================================================================

QDateTime Task::whenCreated() const
{
    return value(dbconst::CREATION_TIMESTAMP_FIELDNAME).toDateTime();
}

QStringList Task::completenessInfo() const
{
    QStringList result;
    if (!isCompleteCached()) {
        result.append(incompleteMarker());
    }
    return result;
}

QString
    Task::xstring(const QString& stringname, const QString& default_str) const
{
    return m_app.xstring(xstringTaskname(), stringname, default_str);
}

QString Task::appstring(
    const QString& stringname, const QString& default_str
) const
{
    return m_app.appstring(stringname, default_str);
}

QStringList Task::fieldSummaries(
    const QString& xstringprefix,
    const QString& xstringsuffix,
    const QString& spacer,
    const QString& fieldprefix,
    const int first,
    const int last,
    const QString& suffix
) const
{
    using stringfunc::strseq;
    const QStringList xstringnames
        = strseq(xstringprefix, first, last, xstringsuffix);
    const QStringList fieldnames = strseq(fieldprefix, first, last);
    QStringList list;
    for (int i = 0; i < fieldnames.length(); ++i) {
        const QString& fieldname = fieldnames.at(i);
        const QString& xstringname = xstringnames.at(i);
        list.append(
            fieldSummary(fieldname, xstring(xstringname), spacer, suffix)
        );
    }
    return list;
}

QStringList Task::fieldSummariesYesNo(
    const QString& xstringprefix,
    const QString& xstringsuffix,
    const QString& spacer,
    const QString& fieldprefix,
    const int first,
    const int last,
    const QString& suffix
) const
{
    using stringfunc::strseq;
    const QStringList xstringnames
        = strseq(xstringprefix, first, last, xstringsuffix);
    const QStringList fieldnames = strseq(fieldprefix, first, last);
    QStringList list;
    for (int i = 0; i < fieldnames.length(); ++i) {
        const QString& fieldname = fieldnames.at(i);
        const QString& xstringname = xstringnames.at(i);
        list.append(
            fieldSummaryYesNo(fieldname, xstring(xstringname), spacer, suffix)
        );
    }
    return list;
}

QStringList Task::clinicianDetails(const QString& separator) const
{
    if (!hasClinician()) {
        return QStringList();
    }
    return QStringList{
        fieldSummary(
            CLINICIAN_SPECIALTY, TextConst::clinicianSpecialty(), separator
        ),
        fieldSummary(CLINICIAN_NAME, TextConst::clinicianName(), separator),
        fieldSummary(
            CLINICIAN_PROFESSIONAL_REGISTRATION,
            TextConst::clinicianProfessionalRegistration(),
            separator
        ),
        fieldSummary(CLINICIAN_POST, TextConst::clinicianPost(), separator),
        fieldSummary(
            CLINICIAN_SERVICE, TextConst::clinicianService(), separator
        ),
        fieldSummary(
            CLINICIAN_CONTACT_DETAILS,
            TextConst::clinicianContactDetails(),
            separator
        ),
    };
}

QStringList Task::respondentDetails() const
{
    if (!hasRespondent()) {
        return QStringList();
    }
    return QStringList{
        fieldSummary(RESPONDENT_NAME, TextConst::respondentNameThirdPerson()),
        fieldSummary(
            RESPONDENT_RELATIONSHIP,
            TextConst::respondentRelationshipThirdPerson()
        ),
    };
}

// ============================================================================
// Editing
// ============================================================================

void Task::setupForEditingAndSave(const int patient_id)
{
    if (!isAnonymous()) {
        setPatient(patient_id);
    }
    setDefaultClinicianVariablesAtFirstUse();
    setDefaultsAtFirstUse();
    save();
}

double Task::editingTimeSeconds() const
{
    return valueDouble(EDITING_TIME_S_FIELDNAME);
}

void Task::setDefaultClinicianVariablesAtFirstUse()
{
    if (!m_has_clinician) {
        return;
    }
    setValue(
        CLINICIAN_SPECIALTY,
        m_app.varString(varconst::DEFAULT_CLINICIAN_SPECIALTY)
    );
    setValue(
        CLINICIAN_NAME, m_app.varString(varconst::DEFAULT_CLINICIAN_NAME)
    );
    setValue(
        CLINICIAN_PROFESSIONAL_REGISTRATION,
        m_app.varString(varconst::DEFAULT_CLINICIAN_PROFESSIONAL_REGISTRATION)
    );
    setValue(
        CLINICIAN_POST, m_app.varString(varconst::DEFAULT_CLINICIAN_POST)
    );
    setValue(
        CLINICIAN_SERVICE, m_app.varString(varconst::DEFAULT_CLINICIAN_SERVICE)
    );
    setValue(
        CLINICIAN_CONTACT_DETAILS,
        m_app.varString(varconst::DEFAULT_CLINICIAN_CONTACT_DETAILS)
    );
}

OpenableWidget* Task::makeGraphicsWidget(
    QGraphicsScene* scene,
    const QColor& background_colour,
    const bool fullscreen,
    const bool esc_can_abort
)
{
    auto view = new ScreenLikeGraphicsView(scene);
    view->setBackgroundColour(background_colour);
    auto widget = new OpenableWidget();
    widget->setWidgetAsOnlyContents(view, 0, fullscreen, esc_can_abort);
    return widget;
}

OpenableWidget* Task::makeGraphicsWidgetForImmediateEditing(
    QGraphicsScene* scene,
    const QColor& background_colour,
    const bool fullscreen,
    const bool esc_can_abort
)
{
    OpenableWidget* widget = makeGraphicsWidget(
        scene, background_colour, fullscreen, esc_can_abort
    );
    connect(
        widget, &OpenableWidget::aborting, this, &Task::onEditFinishedAbort
    );
    onEditStarted();
    return widget;
}

QuElement* Task::getClinicianQuestionnaireBlockRawPointer()
{
    return questionnairefunc::defaultGridRawPointer(
        {
            {TextConst::clinicianSpecialty(),
             new QuLineEdit(fieldRef(CLINICIAN_SPECIALTY))},
            {TextConst::clinicianName(),
             new QuLineEdit(fieldRef(CLINICIAN_NAME))},
            {TextConst::clinicianProfessionalRegistration(),
             new QuLineEdit(fieldRef(CLINICIAN_PROFESSIONAL_REGISTRATION))},
            {TextConst::clinicianPost(),
             new QuLineEdit(fieldRef(CLINICIAN_POST))},
            {TextConst::clinicianService(),
             new QuLineEdit(fieldRef(CLINICIAN_SERVICE))},
            {TextConst::clinicianContactDetails(),
             new QuLineEdit(fieldRef(CLINICIAN_CONTACT_DETAILS))},
        },
        uiconst::DEFAULT_COLSPAN_Q,
        uiconst::DEFAULT_COLSPAN_A
    );
}

QuElementPtr Task::getClinicianQuestionnaireBlockElementPtr()
{
    return QuElementPtr(getClinicianQuestionnaireBlockRawPointer());
}

QuPagePtr Task::getClinicianDetailsPage()
{
    return QuPagePtr((new QuPage{getClinicianQuestionnaireBlockRawPointer()})
                         ->setTitle(TextConst::clinicianDetails())
                         ->setType(QuPage::PageType::Clinician));
}

bool Task::isClinicianComplete() const
{
    if (!m_has_clinician) {
        return false;
    }
    return !valueIsNullOrEmpty(CLINICIAN_NAME);
}

bool Task::isRespondentComplete() const
{
    if (!m_has_respondent) {
        return false;
    }
    return !valueIsNullOrEmpty(RESPONDENT_NAME)
        && !valueIsNullOrEmpty(RESPONDENT_RELATIONSHIP);
}

QVariant Task::respondentRelationship() const
{
    if (!m_has_respondent) {
        return QVariant();
    }
    return value(RESPONDENT_RELATIONSHIP);
}

QuElement*
    Task::getRespondentQuestionnaireBlockRawPointer(const bool second_person)
{
    const QString name = second_person
        ? TextConst::respondentNameSecondPerson()
        : TextConst::respondentNameThirdPerson();
    const QString relationship = second_person
        ? TextConst::respondentRelationshipSecondPerson()
        : TextConst::respondentRelationshipThirdPerson();
    return questionnairefunc::defaultGridRawPointer(
        {
            {name, new QuLineEdit(fieldRef(RESPONDENT_NAME))},
            {relationship, new QuLineEdit(fieldRef(RESPONDENT_RELATIONSHIP))},
        },
        uiconst::DEFAULT_COLSPAN_Q,
        uiconst::DEFAULT_COLSPAN_A
    );
}

QuElementPtr
    Task::getRespondentQuestionnaireBlockElementPtr(const bool second_person)
{
    return QuElementPtr(getRespondentQuestionnaireBlockRawPointer(second_person
    ));
}

QuPagePtr Task::getRespondentDetailsPage(const bool second_person)
{
    return QuPagePtr(
        (new QuPage{getRespondentQuestionnaireBlockRawPointer(second_person)})
            ->setTitle(TextConst::respondentDetails())
            ->setType(
                second_person ? QuPage::PageType::Patient
                              : QuPage::PageType::Clinician
            )
    );
}

QuPagePtr Task::getClinicianAndRespondentDetailsPage(const bool second_person)
{
    return QuPagePtr(
        (new QuPage{
             getClinicianQuestionnaireBlockRawPointer(),
             questionnairefunc::defaultGridRawPointer(
                 {
                     {"", new QuSpacer()},
                 },
                 uiconst::DEFAULT_COLSPAN_Q,
                 uiconst::DEFAULT_COLSPAN_A
             ),
             getRespondentQuestionnaireBlockRawPointer(second_person)})
            ->setTitle(TextConst::clinicianAndRespondentDetails())
            ->setType(
                second_person ? QuPage::PageType::ClinicianWithPatient
                              : QuPage::PageType::Clinician
            )
    );
}

NameValueOptions Task::makeOptionsFromXstrings(
    const QString& xstring_prefix,
    const int first,
    const int last,
    const QString& xstring_suffix
)
{
    using stringfunc::strnum;
    NameValueOptions options;
    if (first > last) {  // descending order
        for (int i = first; i >= last; --i) {
            options.append(NameValuePair(
                xstring(strnum(xstring_prefix, i, xstring_suffix)), i
            ));
        }
    } else {  // ascending order
        for (int i = first; i <= last; ++i) {
            options.append(NameValuePair(
                xstring(strnum(xstring_prefix, i, xstring_suffix)), i
            ));
        }
    }
    return options;
}

void Task::onEditStarted()
{
    m_editing = true;
    m_editing_started = datetime::now();
}

void Task::onEditFinished(const bool aborted)
{
    if (!m_editing) {
        qDebug() << Q_FUNC_INFO << "wasn't editing";
        return;
    }
    m_editing = false;
    // Time
    const QDateTime now = datetime::now();
    double editing_time_s = valueDouble(EDITING_TIME_S_FIELDNAME);
    editing_time_s += datetime::doubleSecondsFrom(m_editing_started, now);
    setValue(EDITING_TIME_S_FIELDNAME, editing_time_s);
    // Exit flags
    if (!valueBool(FIRSTEXIT_IS_FINISH_FIELDNAME)
        && !valueBool(FIRSTEXIT_IS_ABORT_FIELDNAME)) {
        // First exit, so set flags:
        setValue(WHEN_FIRSTEXIT_FIELDNAME, now);
        setValue(FIRSTEXIT_IS_ABORT_FIELDNAME, aborted);
        setValue(FIRSTEXIT_IS_FINISH_FIELDNAME, !aborted);
    }
    save();
    if (aborted) {
        emit editingAborted();
    } else {
        emit editingFinished();
    }
}

void Task::onEditFinishedProperly()
{
    onEditFinished(false);
}

void Task::onEditFinishedAbort()
{
    onEditFinished(true);
}

// ============================================================================
// Patient functions (for non-anonymous tasks)
// ============================================================================

void Task::setPatient(const int patient_id)
{
    // It's a really dangerous thing to set a patient ID invalidly, so this
    // function will just stop the app if something stupid is attempted.
    if (isAnonymous()) {
        uifunc::stopApp("Attempt to set patient ID for an anonymous task");
    }
    if (!value(PATIENT_FK_FIELDNAME).isNull()) {
        uifunc::stopApp("Setting patient ID, but it was already set");
    }
    setValue(PATIENT_FK_FIELDNAME, patient_id);
    m_patient.clear();
}

void Task::moveToPatient(const int patient_id)
{
    // This is used for patient merges.
    // It is therefore more liberal than setPatient().
    if (isAnonymous()) {
        qWarning() << "Attempt to set patient ID for an anonymous task";
        return;
    }
    setValue(PATIENT_FK_FIELDNAME, patient_id);
    m_patient.clear();
}

Patient* Task::patient() const
{
    if (!m_patient && !isAnonymous()) {
        const QVariant patient_id_var = value(PATIENT_FK_FIELDNAME);
        if (!patient_id_var.isNull()) {
            const int patient_id = patient_id_var.toInt();
            m_patient
                = QSharedPointer<Patient>(new Patient(m_app, m_db, patient_id)
                );
        }
    }
    return m_patient.data();
}

QString Task::getPatientName() const
{
    Patient* pt = patient();
    if (!pt) {
        return "";
    }
    return pt->forenameSurname();
}

bool Task::isFemale() const
{
    Patient* pt = patient();
    return pt ? pt->isFemale() : false;
}

bool Task::isMale() const
{
    Patient* pt = patient();
    return pt ? pt->isMale() : false;
}

// ============================================================================
// Translatable text
// ============================================================================

QString Task::incompleteMarker()
{
    return tr("<b>(INCOMPLETE)</b>");
}