1 /*
2  * Copyright (C) 2018 Emeric Poupon
3  *
4  * This file is part of LMS.
5  *
6  * LMS is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * LMS is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with LMS.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "DatabaseSettingsView.hpp"
21 
22 #include <Wt/WComboBox.h>
23 #include <Wt/WFormModel.h>
24 #include <Wt/WLineEdit.h>
25 #include <Wt/WPushButton.h>
26 #include <Wt/WString.h>
27 #include <Wt/WTemplateFormView.h>
28 
29 #include "database/Cluster.hpp"
30 #include "database/ScanSettings.hpp"
31 #include "database/Session.hpp"
32 #include "scanner/IScanner.hpp"
33 #include "utils/Logger.hpp"
34 #include "utils/Service.hpp"
35 #include "utils/String.hpp"
36 
37 #include "common/DirectoryValidator.hpp"
38 #include "common/MandatoryValidator.hpp"
39 #include "common/ValueStringModel.hpp"
40 #include "ScannerController.hpp"
41 #include "LmsApplication.hpp"
42 
43 namespace UserInterface {
44 
45 using namespace Database;
46 
47 
48 class DatabaseSettingsModel : public Wt::WFormModel
49 {
50 	public:
51 		// Associate each field with a unique string literal.
52 		static inline constexpr Field MediaDirectoryField {"media-directory"};
53 		static inline constexpr Field UpdatePeriodField {"update-period"};
54 		static inline constexpr Field UpdateStartTimeField {"update-start-time"};
55 		static inline constexpr Field RecommendationEngineTypeField {"recommendation-engine-type"};
56 		static inline constexpr Field TagsField {"tags"};
57 
58 		using UpdatePeriodModel = ValueStringModel<ScanSettings::UpdatePeriod>;
59 
DatabaseSettingsModel()60 		DatabaseSettingsModel()
61 		{
62 			initializeModels();
63 
64 			addField(MediaDirectoryField);
65 			addField(UpdatePeriodField);
66 			addField(UpdateStartTimeField);
67 			addField(RecommendationEngineTypeField);
68 			addField(TagsField);
69 
70 			auto dirValidator {createDirectoryValidator()};
71 			dirValidator->setMandatory(true);
72 			setValidator(MediaDirectoryField, dirValidator);
73 
74 			setValidator(UpdatePeriodField, createMandatoryValidator());
75 			setValidator(UpdateStartTimeField, createMandatoryValidator());
76 			setValidator(RecommendationEngineTypeField, createMandatoryValidator());
77 			setValidator(TagsField, createTagsValidator());
78 
79 			// populate the model with initial data
80 			loadData();
81 		}
82 
updatePeriodModel()83 		std::shared_ptr<UpdatePeriodModel> updatePeriodModel() { return _updatePeriodModel; }
updateStartTimeModel()84 		std::shared_ptr<Wt::WAbstractItemModel> updateStartTimeModel() { return _updateStartTimeModel; }
recommendationEngineTypeModel()85 		std::shared_ptr<Wt::WAbstractItemModel> recommendationEngineTypeModel() { return _recommendationEngineTypeModel; }
86 
loadData()87 		void loadData()
88 		{
89 			auto transaction {LmsApp->getDbSession().createSharedTransaction()};
90 
91 			const ScanSettings::pointer scanSettings {ScanSettings::get(LmsApp->getDbSession())};
92 
93 			setValue(MediaDirectoryField, scanSettings->getMediaDirectory().string());
94 
95 			auto periodRow {_updatePeriodModel->getRowFromValue(scanSettings->getUpdatePeriod())};
96 			if (periodRow)
97 				setValue(UpdatePeriodField, _updatePeriodModel->getString(*periodRow));
98 
99 			auto startTimeRow {_updateStartTimeModel->getRowFromValue(scanSettings->getUpdateStartTime())};
100 			if (startTimeRow)
101 				setValue(UpdateStartTimeField, _updateStartTimeModel->getString(*startTimeRow));
102 
103 			if (scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Hourly
104 					|| scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Never)
105 			{
106 				setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, true);
107 			}
108 
109 			auto recommendationEngineTypeRow {_recommendationEngineTypeModel->getRowFromValue(scanSettings->getRecommendationEngineType())};
110 			if (recommendationEngineTypeRow)
111 				setValue(RecommendationEngineTypeField, _recommendationEngineTypeModel->getString(*recommendationEngineTypeRow));
112 
113 			auto clusterTypes {scanSettings->getClusterTypes()};
114 			if (!clusterTypes.empty())
115 			{
116 				std::vector<std::string> names;
117 				std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names),  [](auto clusterType) { return clusterType->getName(); });
118 				setValue(TagsField, StringUtils::joinStrings(names, " "));
119 			}
120 		}
121 
saveData()122 		void saveData()
123 		{
124 			auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
125 
126 			ScanSettings::pointer scanSettings {ScanSettings::get(LmsApp->getDbSession())};
127 
128 			scanSettings.modify()->setMediaDirectory(valueText(MediaDirectoryField).toUTF8());
129 
130 			auto updatePeriodRow {_updatePeriodModel->getRowFromString(valueText(UpdatePeriodField))};
131 			if (updatePeriodRow)
132 				scanSettings.modify()->setUpdatePeriod(_updatePeriodModel->getValue(*updatePeriodRow));
133 
134 			auto startTimeRow {_updateStartTimeModel->getRowFromString(valueText(UpdateStartTimeField))};
135 			if (startTimeRow)
136 				scanSettings.modify()->setUpdateStartTime(_updateStartTimeModel->getValue(*startTimeRow));
137 
138 			auto recommendationEngineTypeRow {_recommendationEngineTypeModel->getRowFromString(valueText(RecommendationEngineTypeField))};
139 			if (recommendationEngineTypeRow)
140 				scanSettings.modify()->setRecommendationEngineType(_recommendationEngineTypeModel->getValue(*recommendationEngineTypeRow));
141 
142 			auto clusterTypes {StringUtils::splitString(valueText(TagsField).toUTF8(), " ")};
143 			scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set<std::string>(clusterTypes.begin(), clusterTypes.end()));
144 		}
145 
146 	private:
createTagsValidator()147 		static std::shared_ptr<Wt::WValidator> createTagsValidator()
148 		{
149 			auto v = std::make_shared<Wt::WValidator>();
150 			return v;
151 		}
152 
initializeModels()153 		void initializeModels()
154 		{
155 			_updatePeriodModel = std::make_shared<ValueStringModel<ScanSettings::UpdatePeriod>>();
156 			_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.never"), ScanSettings::UpdatePeriod::Never);
157 			_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.hourly"), ScanSettings::UpdatePeriod::Hourly);
158 			_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.daily"), ScanSettings::UpdatePeriod::Daily);
159 			_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.weekly"), ScanSettings::UpdatePeriod::Weekly);
160 			_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.monthly"), ScanSettings::UpdatePeriod::Monthly);
161 
162 			_updateStartTimeModel = std::make_shared<ValueStringModel<Wt::WTime>>();
163 			for (std::size_t i = 0; i < 24; ++i)
164 			{
165 				Wt::WTime time {static_cast<int>(i), 0};
166 				_updateStartTimeModel->add(time.toString(), time);
167 			}
168 
169 			_recommendationEngineTypeModel = std::make_shared<ValueStringModel<ScanSettings::RecommendationEngineType>>();
170 			_recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.clusters"), ScanSettings::RecommendationEngineType::Clusters);
171 			_recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.features"), ScanSettings::RecommendationEngineType::Features);
172 		}
173 
174 		std::shared_ptr<UpdatePeriodModel>											_updatePeriodModel;
175 		std::shared_ptr<ValueStringModel<Wt::WTime>>								_updateStartTimeModel;
176 		std::shared_ptr<ValueStringModel<ScanSettings::RecommendationEngineType>>	_recommendationEngineTypeModel;
177 };
178 
DatabaseSettingsView()179 DatabaseSettingsView::DatabaseSettingsView()
180 {
181 	wApp->internalPathChanged().connect(this, [this]
182 	{
183 		refreshView();
184 	});
185 
186 	refreshView();
187 }
188 
189 void
refreshView()190 DatabaseSettingsView::refreshView()
191 {
192 	if (!wApp->internalPathMatches("/admin/database"))
193 		return;
194 
195 	clear();
196 
197 	auto t {addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.Database.template"))};
198 	auto model {std::make_shared<DatabaseSettingsModel>()};
199 
200 	// Media Directory
201 	t->setFormWidget(DatabaseSettingsModel::MediaDirectoryField, std::make_unique<Wt::WLineEdit>());
202 
203 	// Update Period
204 	auto updatePeriod {std::make_unique<Wt::WComboBox>()};
205 	updatePeriod->setModel(model->updatePeriodModel());
206 	updatePeriod->activated().connect([=](int row)
207 	{
208 		const ScanSettings::UpdatePeriod period {model->updatePeriodModel()->getValue(row)};
209 		model->setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, period == ScanSettings::UpdatePeriod::Hourly || period == ScanSettings::UpdatePeriod::Never);
210 		t->updateModel(model.get());
211 		t->updateView(model.get());
212 	});
213 	t->setFormWidget(DatabaseSettingsModel::UpdatePeriodField, std::move(updatePeriod));
214 
215 	// Update Start Time
216 	auto updateStartTime {std::make_unique<Wt::WComboBox>()};
217 	updateStartTime->setModel(model->updateStartTimeModel());
218 	t->setFormWidget(DatabaseSettingsModel::UpdateStartTimeField, std::move(updateStartTime));
219 
220 	// recommendation engine type
221 	auto recommendationEngineType {std::make_unique<Wt::WComboBox>()};
222 	recommendationEngineType->setModel(model->recommendationEngineTypeModel());
223 	t->setFormWidget(DatabaseSettingsModel::RecommendationEngineTypeField, std::move(recommendationEngineType));
224 
225 	// Tags
226 	t->setFormWidget(DatabaseSettingsModel::TagsField, std::make_unique<Wt::WLineEdit>());
227 
228 	// Buttons
229 	Wt::WPushButton *saveBtn = t->bindWidget("apply-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.apply")));
230 	Wt::WPushButton *discardBtn = t->bindWidget("discard-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.discard")));
231 	Wt::WPushButton *immScanBtn = t->bindWidget("immediate-scan-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Admin.Database.immediate-scan")));
232 
233 	t->bindNew<ScannerController>("scanner-controller");
234 
235 	saveBtn->clicked().connect([=]
236 	{
237 		t->updateModel(model.get());
238 
239 		if (model->validate())
240 		{
241 			model->saveData();
242 
243 			Service<Scanner::IScanner>::get()->requestImmediateScan(false);
244 			LmsApp->notifyMsg(LmsApplication::MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved"));
245 		}
246 
247 		// Udate the view: Delete any validation message in the view, etc.
248 		t->updateView(model.get());
249 	});
250 
251 	discardBtn->clicked().connect([=]
252 	{
253 		model->loadData();
254 		model->validate();
255 		t->updateView(model.get());
256 	});
257 
258 	immScanBtn->clicked().connect([=]
259 	{
260 		Service<Scanner::IScanner>::get()->requestImmediateScan(false);
261 	});
262 
263 	t->updateView(model.get());
264 }
265 
266 } // namespace UserInterface
267 
268 
269