~aleteoryx/muditaos

3ef3a8cce0fa69532b3bcbdc21ad6f02d21d4d0f — Pawel Olejniczak 3 years ago bdd300e
[CP-702] Add API for managing templates order

This API allows for managing message templates order.
DB Migration: adding a new column to templates tables.
M module-db/Interface/SMSTemplateRecord.cpp => module-db/Interface/SMSTemplateRecord.cpp +7 -4
@@ 1,4 1,4 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SMSTemplateRecord.hpp"


@@ 19,6 19,7 @@ SMSTemplateRecord::SMSTemplateRecord(const SMSTemplateTableRow &w)
    ID                 = w.ID;
    text               = w.text;
    lastUsageTimestamp = w.lastUsageTimestamp;
    order              = w.order;
}

SMSTemplateRecordInterface::SMSTemplateRecordInterface(SmsDB *smsDb) : smsDB(smsDb)


@@ 51,13 52,15 @@ std::unique_ptr<std::vector<SMSTemplateRecord>> SMSTemplateRecordInterface::GetL

bool SMSTemplateRecordInterface::Update(const SMSTemplateRecord &rec)
{
    auto templ = smsDB->templates.getById(rec.ID);
    const auto templ = smsDB->templates.getById(rec.ID);
    if (templ.ID == DB_ID_NONE) {
        return false;
    }
    const auto templateText  = rec.text.empty() ? templ.text : rec.text;
    const auto templateOrder = rec.order == 0 ? templ.order : rec.order;

    return smsDB->templates.update(
        SMSTemplateTableRow{Record(rec.ID), .text = rec.text, .lastUsageTimestamp = rec.lastUsageTimestamp});
    return smsDB->templates.update(SMSTemplateTableRow{
        Record(rec.ID), .text = templateText, .lastUsageTimestamp = rec.lastUsageTimestamp, .order = templateOrder});
}

bool SMSTemplateRecordInterface::RemoveByID(uint32_t id)

M module-db/Interface/SMSTemplateRecord.hpp => module-db/Interface/SMSTemplateRecord.hpp +2 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 14,6 14,7 @@ struct SMSTemplateRecord : public Record
{
    UTF8 text;
    time_t lastUsageTimestamp = 0;
    std::uint32_t order       = 0;

    SMSTemplateRecord() = default;
    SMSTemplateRecord(const SMSTemplateTableRow &);

M module-db/Tables/SMSTemplateTable.cpp => module-db/Tables/SMSTemplateTable.cpp +18 -9
@@ 1,7 1,8 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SMSTemplateTable.hpp"
#include "Common/Types.hpp"

#include <Utils.hpp>
#include <log/log.hpp>


@@ 21,10 22,16 @@ bool SMSTemplateTable::create()

bool SMSTemplateTable::add(SMSTemplateTableRow entry)
{
    return db->execute("INSERT or ignore INTO templates (text, lastUsageTimestamp) VALUES ('%q', '%q');",

    auto retQuery = db->query("SELECT MAX(rowOrder) FROM templates;");
    if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
        return false;
    }
    auto lastOrderValue = (*retQuery)[0].getUInt32() + 1;
    return db->execute("INSERT or ignore INTO templates (text, lastUsageTimestamp, rowOrder) VALUES (" str_c str_c u32_
                       ");",
                       entry.text.c_str(),
                       utils::to_string(entry.lastUsageTimestamp).c_str());
                       utils::to_string(entry.lastUsageTimestamp).c_str(),
                       lastOrderValue);
}

bool SMSTemplateTable::removeById(uint32_t id)


@@ 40,9 47,11 @@ bool SMSTemplateTable::removeByField(SMSTemplateTableFields field, const char *s

bool SMSTemplateTable::update(SMSTemplateTableRow entry)
{
    return db->execute("UPDATE templates SET text = '%q', lastUsageTimestamp = %q WHERE _id=%" PRIu32 ";",
    return db->execute("UPDATE templates SET text = '%q', lastUsageTimestamp = %q, rowOrder = %" PRIu32
                       " WHERE _id=%" PRIu32 ";",
                       entry.text.c_str(),
                       utils::to_string(entry.lastUsageTimestamp).c_str(),
                       entry.order,
                       entry.ID);
}



@@ 58,15 67,14 @@ SMSTemplateTableRow SMSTemplateTable::getById(uint32_t id)
        (*retQuery)[0].getUInt32(),                      // ID
        (*retQuery)[1].getString(),                      // text
        static_cast<time_t>((*retQuery)[2].getUInt64()), // lastUsageTimestamp
        (*retQuery)[3].getUInt32(),                      // order
    };
}

std::vector<SMSTemplateTableRow> SMSTemplateTable::getLimitOffset(uint32_t offset, uint32_t limit)
{
    auto retQuery =
        db->query("SELECT * from templates ORDER BY lastUsageTimestamp DESC LIMIT %" PRIu32 " OFFSET %" PRIu32 ";",
                  limit,
                  offset);
    auto retQuery = db->query(
        "SELECT * from templates ORDER BY rowOrder DESC LIMIT %" PRIu32 " OFFSET %" PRIu32 ";", limit, offset);

    if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
        return std::vector<SMSTemplateTableRow>();


@@ 79,6 87,7 @@ std::vector<SMSTemplateTableRow> SMSTemplateTable::getLimitOffset(uint32_t offse
            (*retQuery)[0].getUInt32(),                      // ID
            (*retQuery)[1].getString(),                      // text
            static_cast<time_t>((*retQuery)[2].getUInt64()), // lastUsageTimestamp
            (*retQuery)[3].getUInt32(),                      // order
        });
    } while (retQuery->nextRow());


M module-db/Tables/SMSTemplateTable.hpp => module-db/Tables/SMSTemplateTable.hpp +3 -2
@@ 1,4 1,4 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 14,6 14,7 @@ struct SMSTemplateTableRow : public Record
{
    UTF8 text;
    time_t lastUsageTimestamp = 0;
    std::uint32_t order       = 0;
};

enum class SMSTemplateTableFields


@@ 23,7 24,7 @@ enum class SMSTemplateTableFields
class SMSTemplateTable : public Table<SMSTemplateTableRow, SMSTemplateTableFields>
{
  public:
    SMSTemplateTable(Database *db);
    explicit SMSTemplateTable(Database *db);
    virtual ~SMSTemplateTable();

    bool create() override final;

M module-db/tests/Helpers.cpp => module-db/tests/Helpers.cpp +27 -4
@@ 1,4 1,4 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "Helpers.hpp"


@@ 76,6 76,23 @@ namespace
        return versions;
    }

    std::vector<std::filesystem::path> searchForScripts(const std::filesystem::path &pathToVersion,
                                                        const std::string &scriptName)
    {
        std::vector<std::filesystem::path> scripts{};
        if (std::filesystem::exists(pathToVersion / scriptName)) {
            scripts.push_back(pathToVersion / scriptName);
        }
        else {
            for (const auto &subVersion : std::filesystem::directory_iterator(pathToVersion)) {
                if (std::filesystem::exists(subVersion.path() / scriptName)) {
                    scripts.push_back(subVersion.path() / scriptName);
                }
            }
        }
        return scripts;
    }

    std::vector<std::filesystem::path> listFiles(const std::filesystem::path &path,
                                                 const std::string &prefix,
                                                 const bool withDevelopment)


@@ 84,9 101,15 @@ namespace
        constexpr auto devel_sql = "devel.sql";
        std::vector<std::filesystem::path> files;
        for (const auto &version : listVersionDirectories(path, prefix)) {
            files.push_back(version / up_sql);
            if (withDevelopment and std::filesystem::exists(version / devel_sql)) {
                files.push_back(version / devel_sql);
            auto scriptsPath = searchForScripts(version, up_sql);
            if (not scriptsPath.empty()) {
                files.insert(files.end(), scriptsPath.begin(), scriptsPath.end());
            }
            if (withDevelopment) {
                scriptsPath = searchForScripts(version, devel_sql);
                if (not scriptsPath.empty()) {
                    files.insert(files.end(), scriptsPath.begin(), scriptsPath.end());
                }
            }
        }
        return files;

M module-db/tests/SMSTemplateRecord_tests.cpp => module-db/tests/SMSTemplateRecord_tests.cpp +12 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <catch2/catch.hpp>


@@ 39,15 39,26 @@ TEST_CASE("SMS templates Record tests")
        REQUIRE(templ.lastUsageTimestamp == testRec.lastUsageTimestamp);
    }

    SECTION("Check entry order")
    {
        for (std::uint32_t templateNumber = 1; templateNumber <= 4; templateNumber++) {
            auto messageTemplate = SMSTemplateRecordInterface.GetByID(templateNumber);
            REQUIRE(messageTemplate.ID == templateNumber);
            REQUIRE(messageTemplate.order == templateNumber);
        }
    }

    SECTION("Entry update")
    {
        testRec.ID                 = 4;
        testRec.text               = "New text";
        testRec.order              = 9;
        testRec.lastUsageTimestamp = 200;
        REQUIRE(SMSTemplateRecordInterface.Update(testRec));
        auto templ = SMSTemplateRecordInterface.GetByID(4);
        REQUIRE(templ.ID == 4);
        REQUIRE(templ.text == testRec.text);
        REQUIRE(templ.order == testRec.order);
        REQUIRE(templ.lastUsageTimestamp == testRec.lastUsageTimestamp);
    }


M module-db/tests/SMSTemplateTable_tests.cpp => module-db/tests/SMSTemplateTable_tests.cpp +3 -0
@@ 42,6 42,7 @@ TEST_CASE("SMS Templates Table tests")
        auto templ = templatesTbl.getById(4);
        REQUIRE(templ.ID == 4);
        REQUIRE(templ.text == testRow.text);
        REQUIRE(templ.order == 4);
        REQUIRE(templ.lastUsageTimestamp == testRow.lastUsageTimestamp);
    }



@@ 49,11 50,13 @@ TEST_CASE("SMS Templates Table tests")
    {
        testRow.ID                 = 4;
        testRow.text               = "New text";
        testRow.order              = 5;
        testRow.lastUsageTimestamp = 200;
        REQUIRE(templatesTbl.update(testRow));
        auto templ = templatesTbl.getById(4);
        REQUIRE(templ.ID == 4);
        REQUIRE(templ.text == testRow.text);
        REQUIRE(templ.order == testRow.order);
        REQUIRE(templ.lastUsageTimestamp == testRow.lastUsageTimestamp);
    }


M module-services/service-desktop/endpoints/include/endpoints/JsonKeyNames.hpp => module-services/service-desktop/endpoints/include/endpoints/JsonKeyNames.hpp +1 -0
@@ 80,6 80,7 @@ namespace sdesktop::endpoints::json

        inline constexpr auto limit              = "limit";
        inline constexpr auto offset             = "offset";
        inline constexpr auto order              = "order";
        inline constexpr auto totalCount         = "totalCount";
        inline constexpr auto nextPage           = "nextPage";
        inline constexpr auto entries            = "entries";

M products/PurePhone/services/db/databases/migration/sms/0/devel.sql => products/PurePhone/services/db/databases/migration/sms/0/devel.sql +1 -1
@@ 1,4 1,4 @@
-- Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
-- Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
-- For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
BEGIN TRANSACTION;
INSERT OR REPLACE INTO "sms" ("_id","thread_id","contact_id","date","error_code","body","type") VALUES (1,2,2,1547492320,0,'Thank you for today!' || CHAR(10) || 'You chose a fantastic place :)',8);

M products/PurePhone/services/db/databases/migration/sms/0/up.sql => products/PurePhone/services/db/databases/migration/sms/0/up.sql +1 -1
@@ 1,4 1,4 @@
-- Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
-- Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
-- For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

CREATE TABLE IF NOT EXISTS sms

A products/PurePhone/services/db/databases/migration/sms/current/111a2d1b_Adding_new_column_to_templates_table_to_manage_templates_order/.meta => products/PurePhone/services/db/databases/migration/sms/current/111a2d1b_Adding_new_column_to_templates_table_to_manage_templates_order/.meta +6 -0
@@ 0,0 1,6 @@
{
 "id": "111a2d1b-dc32-40a3-9a2a-02a9e186bcac",
 "date": "2023-03-23 10:11:44",
 "message": "Adding new column to templates table to manage templates order",
 "parent": 0
}

A products/PurePhone/services/db/databases/migration/sms/current/111a2d1b_Adding_new_column_to_templates_table_to_manage_templates_order/down.sql => products/PurePhone/services/db/databases/migration/sms/current/111a2d1b_Adding_new_column_to_templates_table_to_manage_templates_order/down.sql +9 -0
@@ 0,0 1,9 @@
-- Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
-- For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

-- Message: Adding new column to templates table to manage templates order
-- Revision: 111a2d1b-dc32-40a3-9a2a-02a9e186bcac
-- Create Date: 2023-03-23 10:11:44

ALTER TABLE templates
DROP COLUMN rowOrder;

A products/PurePhone/services/db/databases/migration/sms/current/111a2d1b_Adding_new_column_to_templates_table_to_manage_templates_order/up.sql => products/PurePhone/services/db/databases/migration/sms/current/111a2d1b_Adding_new_column_to_templates_table_to_manage_templates_order/up.sql +10 -0
@@ 0,0 1,10 @@
-- Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
-- For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

-- Message: Adding new column to templates table to manage templates order
-- Revision: 111a2d1b-dc32-40a3-9a2a-02a9e186bcac
-- Create Date: 2023-03-23 10:11:44

ALTER TABLE templates ADD rowOrder INTEGER;

UPDATE OR IGNORE templates SET rowOrder = _id;

M products/PurePhone/services/desktop/endpoints/messages/MessageHelper.cpp => products/PurePhone/services/desktop/endpoints/messages/MessageHelper.cpp +13 -5
@@ 55,7 55,8 @@ namespace sdesktop::endpoints
        auto recordEntry =
            json11::Json::object{{json::messages::templateID, static_cast<int>(record.ID)},
                                 {json::messages::templateBody, record.text.c_str()},
                                 {json::messages::lastUsedAt, static_cast<int>(record.lastUsageTimestamp)}};
                                 {json::messages::lastUsedAt, static_cast<int>(record.lastUsageTimestamp)},
                                 {json::messages::order, static_cast<int>(record.order)}};
        return recordEntry;
    }



@@ 314,16 315,23 @@ namespace sdesktop::endpoints
            return sys::ReturnCodes::Unresolved;
        }

        if (!context.getBody()[json::messages::templateBody].is_string()) {
            LOG_ERROR("Bad request! templateBody is incorrect or missing!");
        auto updateTemplateBody  = context.getBody()[json::messages::templateBody].is_string();
        auto updateTemplateOrder = context.getBody()[json::messages::order].is_number();
        if (!updateTemplateBody && !updateTemplateOrder) {
            LOG_ERROR("Bad request! templateBody/order is incorrect or missing!");
            context.setResponseStatus(http::Code::BadRequest);
            putToSendQueue(context.createSimpleResponse());
            return sys::ReturnCodes::Unresolved;
        }

        SMSTemplateRecord record;
        record.ID   = context.getBody()[json::messages::templateID].int_value();
        record.text = context.getBody()[json::messages::templateBody].string_value();
        record.ID = context.getBody()[json::messages::templateID].int_value();
        if (updateTemplateBody) {
            record.text = context.getBody()[json::messages::templateBody].string_value();
        }
        if (updateTemplateOrder) {
            record.order = context.getBody()[json::messages::order].int_value();
        }

        auto query    = std::make_unique<db::query::SMSTemplateUpdate>(record);
        auto listener = std::make_unique<db::EndpointListener>(

M test/pytest/service-desktop/test_templates.py => test/pytest/service-desktop/test_templates.py +42 -2
@@ 100,7 100,6 @@ class TemplatesTester:
        for template in templates:

            if template["templateBody"] == self.template_body:

                # Change template
                new_template_body = "NEW TEMPLATE BODY TEST"
                body = {"category": "template", "templateID": template["templateID"], "templateBody": new_template_body}


@@ 117,6 116,37 @@ class TemplatesTester:
        assert total_count == initial_count
        assert test_passed == True

    def test_changing_template_order(self):
        template_id = 1
        initial_order = 1
        new_order = 9
        body = {"category": "template", "templateID": template_id}
        ret = self.harness.endpoint_request("messages", "get", body)

        assert ret["status"] == status["OK"]
        assert ret["body"]["templateID"] == template_id
        assert ret["body"]["order"] == initial_order

        body = {"category": "template", "templateID": template_id, "order": new_order}
        ret = self.harness.endpoint_request("messages", "put", body)
        assert ret["status"] == status["NoContent"]

        body = {"category": "template", "templateID": template_id}
        ret = self.harness.endpoint_request("messages", "get", body)
        assert ret["status"] == status["OK"]
        assert ret["body"]["templateID"] == template_id
        assert ret["body"]["order"] == new_order

        body = {"category": "template", "templateID": template_id, "order": initial_order}
        ret = self.harness.endpoint_request("messages", "put", body)
        assert ret["status"] == status["NoContent"]

        body = {"category": "template", "templateID": template_id}
        ret = self.harness.endpoint_request("messages", "get", body)
        assert ret["status"] == status["OK"]
        assert ret["body"]["templateID"] == template_id
        assert ret["body"]["order"] == initial_order

    def test_getting_templates_with_pagination(self):
        initial_count = self.__get_count()



@@ 158,13 188,23 @@ def test_get_templates_without_pagination(harness):
    templates_tester = TemplatesTester(harness)
    templates_tester.test_getting_templates_without_pagination()


@pytest.mark.rt1051
@pytest.mark.service_desktop_test
@pytest.mark.usefixtures("phone_unlocked")
def test_change_template(harness):
def test_change_template_text(harness):
    templates_tester = TemplatesTester(harness)
    templates_tester.test_changing_template_body()


@pytest.mark.rt1051
@pytest.mark.service_desktop_test
@pytest.mark.usefixtures("phone_unlocked")
def test_change_template_order(harness):
    templates_tester = TemplatesTester(harness)
    templates_tester.test_changing_template_order()


@pytest.mark.rt1051
@pytest.mark.service_desktop_test
@pytest.mark.usefixtures("phone_unlocked")