From 9a3dcbdcbfac49403125041472066c97676bc091 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 17 Apr 2020 23:36:34 +0200 Subject: [PATCH] Subsonic API: Added an option to only list album artists. fixes #27 --- approot/admin-subsonic.xml | 40 ++++ approot/messages.xml | 8 + approot/messages_fr.xml | 12 +- conf/lms.conf | 3 - src/libs/database/CMakeLists.txt | 1 + src/libs/database/impl/Session.cpp | 15 +- src/libs/database/impl/SubsonicSettings.cpp | 48 +++++ .../include/database/SubsonicSettings.hpp | 69 +++++++ src/libs/subsonic/impl/SubsonicResource.cpp | 41 +++- src/lms/CMakeLists.txt | 1 + src/lms/main.cpp | 10 +- src/lms/ui/LmsApplication.cpp | 11 +- src/lms/ui/admin/SubsonicView.cpp | 180 ++++++++++++++++++ src/lms/ui/admin/SubsonicView.hpp | 37 ++++ src/lms/ui/admin/UserView.cpp | 1 - src/lms/ui/admin/UsersView.cpp | 14 +- 16 files changed, 470 insertions(+), 21 deletions(-) create mode 100644 approot/admin-subsonic.xml create mode 100644 src/libs/database/impl/SubsonicSettings.cpp create mode 100644 src/libs/database/include/database/SubsonicSettings.hpp create mode 100644 src/lms/ui/admin/SubsonicView.cpp create mode 100644 src/lms/ui/admin/SubsonicView.hpp diff --git a/approot/admin-subsonic.xml b/approot/admin-subsonic.xml new file mode 100644 index 00000000..f7ce118d --- /dev/null +++ b/approot/admin-subsonic.xml @@ -0,0 +1,40 @@ + + + + + + ${tr:Lms.Admin.Subsonic.subsonic} +
+ +
+ +
+
+
+
+ ${enable-info} +
+
+
+ +
+ ${artist-list-mode} +
+
+ ${artist-list-mode-info} +
+
+ +
+
+ ${apply-btn class="btn-primary"} ${discard-btn} +
+
+
+
+ +
diff --git a/approot/messages.xml b/approot/messages.xml index f515240b..7eac4c4e 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -68,6 +68,14 @@ Scheduled on {1} Scanning {1} of {2} files ({3} %) + +All artists +Album artists +Enable +Artist list mode +You must restart the service for this change to take effect +Subsonic API + New user Admin diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 742251af..be11cab6 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -68,6 +68,14 @@ Planifié le {1} Scan de {1} fichiers sur {2} ({3} %) + +Tous les artistes +Artistes d'album +Activer +Mode de listage des artistes +Vous devez redémarrer le service pour que cette option soit prise en compte +API Subsonic + Ajouter Admin @@ -116,8 +124,8 @@ Tous les artistes -Artiste de piste -Artiste d'album +Artistes de piste +Artistes d'album Ajouts récents Artistes populaires diff --git a/conf/lms.conf b/conf/lms.conf index e1666f60..e84bfbdf 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -34,9 +34,6 @@ deploy-path = "/"; # Acoustic brainz's root API acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/"; -# API -api-subsonic = true; - # Turn on this option to allow the demo account creation/use demo = false; diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index a7eba7e2..cb6fc22e 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(lmsdatabase SHARED impl/Session.cpp impl/SessionPool.cpp impl/SqlQuery.cpp + impl/SubsonicSettings.cpp impl/Track.cpp impl/TrackBookmark.cpp impl/User.cpp diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 70ebabea..3aa85a27 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -31,6 +31,7 @@ #include "database/Db.hpp" #include "database/Release.hpp" #include "database/ScanSettings.hpp" +#include "database/SubsonicSettings.hpp" #include "database/Track.hpp" #include "database/TrackBookmark.hpp" #include "database/TrackArtistLink.hpp" @@ -40,7 +41,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 18 +#define LMS_DATABASE_VERSION 19 using Version = std::size_t; @@ -206,6 +207,16 @@ CREATE TABLE "release_backup" ( // Just increment the scan version of the settings to make the next scheduled scan rescan everything ScanSettings::get(*this).modify()->incScanVersion(); } + else if (version == 18) + { + _session.execute(R"( +CREATE TABLE IF NOT EXISTS "subsonic_settings" ( + "id" integer primary key autoincrement, + "version" integer not null, + "api_enabled" boolean not null, + "artist_list_mode" integer not null +))"); + } else { LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; @@ -228,6 +239,7 @@ Session::Session(Db& db) _session.mapClass("cluster_type"); _session.mapClass("release"); _session.mapClass("scan_settings"); + _session.mapClass("subsonic_settings"); _session.mapClass("track"); _session.mapClass("track_bookmark"); _session.mapClass("track_artist_link"); @@ -353,6 +365,7 @@ Session::prepareTables() auto uniqueTransaction {createUniqueTransaction()}; ScanSettings::init(*this); + SubsonicSettings::init(*this); } } diff --git a/src/libs/database/impl/SubsonicSettings.cpp b/src/libs/database/impl/SubsonicSettings.cpp new file mode 100644 index 00000000..3393afe6 --- /dev/null +++ b/src/libs/database/impl/SubsonicSettings.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#include "database/SubsonicSettings.hpp" + +#include "database/Session.hpp" + +namespace Database { + +void +SubsonicSettings::init(Session& session) +{ + session.checkUniqueLocked(); + + pointer settings {get(session)}; + if (settings) + return; + + settings = session.getDboSession().add(std::make_unique()); +} + +SubsonicSettings::pointer +SubsonicSettings::get(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().find(); +} + + +} // ns Database + diff --git a/src/libs/database/include/database/SubsonicSettings.hpp b/src/libs/database/include/database/SubsonicSettings.hpp new file mode 100644 index 00000000..55154186 --- /dev/null +++ b/src/libs/database/include/database/SubsonicSettings.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#pragma once + +#include + +namespace Database { + +class Session; + +class SubsonicSettings : public Wt::Dbo::Dbo +{ + public: + using pointer = Wt::Dbo::ptr; + + enum class ArtistListMode + { + AllArtists, + ReleaseArtists, + }; + + static inline constexpr bool defaultSubsonicAPIEnabled {true}; + static inline constexpr ArtistListMode defaultArtistListMode {ArtistListMode::AllArtists}; + + static void init(Session& session); + + static pointer get(Session& session); + + + // Getters + bool isAPIEnabled() const { return _isAPIEnabled; } + ArtistListMode getArtistListMode() const { return _artistListMode; } + + // Setters + void setAPIEnabled(bool enabled) { _isAPIEnabled = enabled; } + void setArtistListMode(ArtistListMode mode) {_artistListMode = mode; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _isAPIEnabled, "api_enabled"); + Wt::Dbo::field(a, _artistListMode, "artist_list_mode"); + } + + private: + + bool _isAPIEnabled {defaultSubsonicAPIEnabled}; + ArtistListMode _artistListMode {defaultArtistListMode}; +}; + +} // namespace Database + diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 80c69a85..5b97899b 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -30,6 +30,7 @@ #include "database/Db.hpp" #include "database/Release.hpp" #include "database/Session.hpp" +#include "database/SubsonicSettings.hpp" #include "database/Track.hpp" #include "database/TrackBookmark.hpp" #include "database/TrackList.hpp" @@ -901,7 +902,23 @@ handleGetArtistsRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)}; + std::optional linkType; + switch (SubsonicSettings::get(context.dbSession)->getArtistListMode()) + { + case SubsonicSettings::ArtistListMode::AllArtists: + break; + case SubsonicSettings::ArtistListMode::ReleaseArtists: + linkType = TrackArtistLink::Type::ReleaseArtist; + break; + } + + bool more {}; + const std::vector artists {Artist::getByFilter(context.dbSession, + {}, + {}, + linkType, + Artist::SortMethod::BySortName, + {}, {}, more)}; for (const Artist::pointer& artist : artists) indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */)); @@ -1028,7 +1045,23 @@ handleGetIndexesRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)}; + std::optional linkType; + switch (SubsonicSettings::get(context.dbSession)->getArtistListMode()) + { + case SubsonicSettings::ArtistListMode::AllArtists: + break; + case SubsonicSettings::ArtistListMode::ReleaseArtists: + linkType = TrackArtistLink::Type::ReleaseArtist; + break; + } + + bool more {}; + const std::vector artists {Artist::getByFilter(context.dbSession, + {}, + {}, + linkType, + Artist::SortMethod::BySortName, + {}, {}, more)}; for (const Artist::pointer& artist : artists) indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */)); @@ -1752,7 +1785,7 @@ static std::unordered_map requestEntryPoints {"search", {handleNotImplemented, false}}, {"search2", {handleSearch2Request, false}}, {"search3", {handleSearch3Request, false}}, - + // Playlists {"getPlaylists", {handleGetPlaylistsRequest, false}}, {"getPlaylist", {handleGetPlaylistRequest, false}}, @@ -1796,7 +1829,7 @@ static std::unordered_map requestEntryPoints {"createInternetRadioStation", {handleNotImplemented, false}}, {"updateInternetRadioStation", {handleNotImplemented, false}}, {"deleteInternetRadioStation", {handleNotImplemented, false}}, - + // Chat {"getChatMessages", {handleNotImplemented, false}}, {"addChatMessages", {handleNotImplemented, false}}, diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index bac11ebd..a3f94268 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable(lms ui/admin/DatabaseSettingsView.cpp ui/admin/DatabaseStatus.cpp ui/admin/InitWizardView.cpp + ui/admin/SubsonicView.cpp ui/admin/UserView.cpp ui/admin/UsersView.cpp ui/common/Validators.cpp diff --git a/src/lms/main.cpp b/src/lms/main.cpp index bbaa1985..44e3ea62 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -28,6 +28,7 @@ #include "av/AvTranscoder.hpp" #include "cover/ICoverArtGrabber.hpp" #include "database/Db.hpp" +#include "database/SubsonicSettings.hpp" #include "scanner/IMediaScanner.hpp" #include "recommendation/IEngine.hpp" #include "subsonic/SubsonicResource.hpp" @@ -169,8 +170,13 @@ int main(int argc, char* argv[]) API::Subsonic::SubsonicResource subsonicResource {database}; // bind API resources - if (ServiceProvider::get()->getBool("api-subsonic", true)) - server.addResource(&subsonicResource, subsonicResource.getPath()); + { + Database::Session session {database}; + auto transaction {session.createSharedTransaction()}; + + if (Database::SubsonicSettings::get(session)->isAPIEnabled()) + server.addResource(&subsonicResource, subsonicResource.getPath()); + } // bind UI entry point server.addEntryPoint(Wt::EntryPointType::Application, diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index addce9dd..c27171e8 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -43,6 +43,7 @@ #include "admin/DatabaseSettingsView.hpp" #include "admin/UserView.hpp" #include "admin/UsersView.hpp" +#include "admin/SubsonicView.hpp" #include "resource/AudioFileResource.hpp" #include "resource/AudioTranscodeResource.hpp" #include "resource/ImageResource.hpp" @@ -121,9 +122,10 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, // Add a resource bundle messageResourceBundle().use(appRoot() + "admin-database"); + messageResourceBundle().use(appRoot() + "admin-initwizard"); + messageResourceBundle().use(appRoot() + "admin-subsonic"); messageResourceBundle().use(appRoot() + "admin-user"); messageResourceBundle().use(appRoot() + "admin-users"); - messageResourceBundle().use(appRoot() + "admin-initwizard"); messageResourceBundle().use(appRoot() + "artist"); messageResourceBundle().use(appRoot() + "artistinfo"); messageResourceBundle().use(appRoot() + "artistlink"); @@ -342,6 +344,7 @@ enum IdxRoot IdxAdminDatabase, IdxAdminUsers, IdxAdminUser, + IdxAdminSubsonic, }; static void @@ -365,6 +368,7 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin) { "/admin/database", IdxAdminDatabase, true }, { "/admin/users", IdxAdminUsers, true }, { "/admin/user", IdxAdminUser, true }, + { "/admin/subsonic", IdxAdminSubsonic, true }, }; LMS_LOG(UI, DEBUG) << "Internal path changed to '" << wApp->internalPath() << "'"; @@ -484,6 +488,10 @@ LmsApplication::createHome() usersSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/users")); usersSettings->setSelectable(false); + auto subsonicSettings = admin->insertItem(2, Wt::WString::tr("Lms.Admin.Subsonic.subsonic")); + subsonicSettings->setLink(Wt::WLink(Wt::LinkType::InternalPath, "/admin/subsonic")); + subsonicSettings->setSelectable(false); + menuItem->setMenu(std::move(admin)); } @@ -514,6 +522,7 @@ LmsApplication::createHome() mainStack->addNew(); mainStack->addNew(); mainStack->addNew(); + mainStack->addNew(); } explore->tracksAdd.connect([=] (const std::vector& trackIds) diff --git a/src/lms/ui/admin/SubsonicView.cpp b/src/lms/ui/admin/SubsonicView.cpp new file mode 100644 index 00000000..9e289eb7 --- /dev/null +++ b/src/lms/ui/admin/SubsonicView.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2018 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#include "SubsonicView.hpp" + +#include +#include +#include +#include + +#include + +#include "database/SubsonicSettings.hpp" +#include "utils/Logger.hpp" + +#include "common/ValueStringModel.hpp" +#include "LmsApplication.hpp" + +namespace UserInterface { + +using namespace Database; + +class SubsonicModel : public Wt::WFormModel +{ + + public: + static inline const Field EnableField {"enable"}; + static inline const Field ArtistListModeField {"artist-list-mode"}; + + SubsonicModel() + { + addField(EnableField); + addField(ArtistListModeField); + + initializeModels(); + + loadData(); + } + + std::shared_ptr getArtistListModeModel() { return _artistListModeModel; } + + void saveData() + { + auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; + + auto settings {SubsonicSettings::get(LmsApp->getDbSession())}; + + settings.modify()->setAPIEnabled(Wt::asNumber(value(EnableField))); + auto artistListModeRow {_artistListModeModel->getRowFromString(valueText(ArtistListModeField))}; + if (artistListModeRow) + settings.modify()->setArtistListMode(_artistListModeModel->getValue(*artistListModeRow)); + } + + void loadData() + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const auto settings {SubsonicSettings::get(LmsApp->getDbSession())}; + + setValue(EnableField, settings->isAPIEnabled()); + auto artistListModeRow {_artistListModeModel->getRowFromValue(settings->getArtistListMode())}; + + if (artistListModeRow) + setValue(ArtistListModeField, _artistListModeModel->getString(*artistListModeRow)); + } + + void initializeModels() + { + _artistListModeModel = std::make_shared>(); + + _artistListModeModel->add(Wt::WString::tr("Lms.Admin.Subsonic.all-artists"), SubsonicSettings::ArtistListMode::AllArtists); + _artistListModeModel->add(Wt::WString::tr("Lms.Admin.Subsonic.release-artists"), SubsonicSettings::ArtistListMode::ReleaseArtists); + } + + private: + + bool validateField(Field field) override + { + if (field == EnableField) + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + if (SubsonicSettings::get(LmsApp->getDbSession())->isAPIEnabled() != Wt::asNumber(value(EnableField))) + setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Valid, Wt::WString::tr("Lms.Admin.Subsonic.need-restart"))); + else + setValidation(field, Wt::WValidator::Result( Wt::ValidationState::Valid)); + + return true; + } + + return Wt::WFormModel::validateField(field); + } + + std::shared_ptr> _artistListModeModel; +}; + +SubsonicView::SubsonicView() +{ + wApp->internalPathChanged().connect([this]() + { + refreshView(); + }); + + refreshView(); +} + +void +SubsonicView::refreshView() +{ + if (!wApp->internalPathMatches("/admin/subsonic")) + return; + + clear(); + + Wt::WTemplateFormView* t {addNew(Wt::WString::tr("Lms.Admin.Subsonic.template"))}; + + auto model {std::make_shared()}; + + // Enable + auto enable {std::make_unique()}; + auto* enableRaw {enable.get()}; + t->setFormWidget(SubsonicModel::EnableField, std::move(enable)); + + // Artis tlist mode + auto artistListMode = std::make_unique(); + artistListMode->setModel(model->getArtistListModeModel()); + t->setFormWidget(SubsonicModel::ArtistListModeField, std::move(artistListMode)); + + enableRaw->changed().connect([=]() + { + bool enable {enableRaw->checkState() == Wt::CheckState::Checked}; + model->setReadOnly(SubsonicModel::ArtistListModeField, !enable); + t->updateModel(model.get()); + t->updateView(model.get()); + }); + + Wt::WPushButton* saveBtn = t->bindNew("apply-btn", Wt::WString::tr("Lms.apply")); + Wt::WPushButton *discardBtn {t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard")))}; + + saveBtn->clicked().connect([=]() + { + t->updateModel(model.get()); + + if (model->validate()) + { + model->saveData(); + LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Settings.settings-saved")); + } + t->updateView(model.get()); + }); + + discardBtn->clicked().connect([=]() + { + model->loadData(); + model->validate(); + t->updateView(model.get()); + }); + + t->updateView(model.get()); +} + +} // namespace UserInterface + + diff --git a/src/lms/ui/admin/SubsonicView.hpp b/src/lms/ui/admin/SubsonicView.hpp new file mode 100644 index 00000000..775057eb --- /dev/null +++ b/src/lms/ui/admin/SubsonicView.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#pragma once + +#include + +namespace UserInterface { + +class SubsonicView : public Wt::WContainerWidget +{ + public: + SubsonicView(); + + private: + void refreshView(); +}; + +} // namespace UserInterface + + diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 0b4d7556..154c64f5 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -19,7 +19,6 @@ #include "UserView.hpp" -#include #include #include #include diff --git a/src/lms/ui/admin/UsersView.cpp b/src/lms/ui/admin/UsersView.cpp index 76b2e6a7..144a9703 100644 --- a/src/lms/ui/admin/UsersView.cpp +++ b/src/lms/ui/admin/UsersView.cpp @@ -38,15 +38,15 @@ UsersView::UsersView() _container = bindNew("users"); Wt::WPushButton* addBtn = bindNew("add-btn", Wt::WString::tr("Lms.Admin.Users.add")); - addBtn->clicked().connect(std::bind([=] + addBtn->clicked().connect([]() { LmsApp->setInternalPath("/admin/user", true); - })); + }); - wApp->internalPathChanged().connect(std::bind([=] + wApp->internalPathChanged().connect([this]() { refreshView(); - })); + }); refreshView(); } @@ -83,10 +83,10 @@ UsersView::refreshView() entry->setCondition("if-edit", true); Wt::WPushButton* editBtn = entry->bindNew("edit-btn", Wt::WString::tr("Lms.Admin.Users.edit")); - editBtn->clicked().connect(std::bind([=] + editBtn->clicked().connect([=]() { - LmsApp->setInternalPath("/admin/user/" + std::to_string(user.id()), true); - })); + LmsApp->setInternalPath("/admin/user/" + std::to_string(userId), true); + }); Wt::WPushButton* delBtn = entry->bindNew("del-btn", Wt::WString::tr("Lms.Admin.Users.del")); delBtn->clicked().connect([=]