15.1.564. 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 "core/camcopsapp.h"
#include "common/preprocessor_aid.h"  // IWYU pragma: keep
#include "common/textconst.h"
#include "common/uiconst.h"
#include "common/varconst.h"
#include "db/databasemanager.h"
#include "dbobjects/patient.h"
#include "lib/datetime.h"
#include "lib/version.h"
#include "maths/mathfunc.h"
#include "lib/stringfunc.h"
#include "lib/uifunc.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>");
}