2024-11-01 19:05:20 +08:00
|
|
|
#include "BlogView.h"
|
2024-11-02 00:30:14 +08:00
|
|
|
#include "BlogLoginWidget.h"
|
2024-11-01 19:05:20 +08:00
|
|
|
#include "EditUsers.h"
|
2024-11-02 00:30:14 +08:00
|
|
|
#include "PostView.h"
|
2024-11-01 19:05:20 +08:00
|
|
|
#include "model/BlogSession.h"
|
2024-11-02 00:30:14 +08:00
|
|
|
#include "model/Post.h"
|
|
|
|
#include "model/Tag.h"
|
2024-11-01 19:05:20 +08:00
|
|
|
#include <Wt/WApplication.h>
|
|
|
|
#include <Wt/WContainerWidget.h>
|
2024-11-02 00:30:14 +08:00
|
|
|
#include <Wt/WPushButton.h>
|
|
|
|
#include <Wt/WStackedWidget.h>
|
2024-11-01 19:05:20 +08:00
|
|
|
#include <Wt/WText.h>
|
2024-11-02 00:30:14 +08:00
|
|
|
#include <boost/algorithm/string.hpp>
|
|
|
|
#include <boost/algorithm/string/split.hpp>
|
|
|
|
|
|
|
|
static int try_stoi(const std::string &v) {
|
|
|
|
std::size_t pos;
|
|
|
|
auto result = std::stoi(v, &pos);
|
|
|
|
if (pos != v.length()) throw std::invalid_argument("stoi() of " + v + " failed");
|
|
|
|
return result;
|
|
|
|
}
|
2024-11-01 19:05:20 +08:00
|
|
|
|
|
|
|
class BlogImpl : public Wt::WContainerWidget {
|
|
|
|
public:
|
|
|
|
BlogImpl(const std::string &basePath, Wt::Dbo::SqlConnectionPool &connectionPool, const std::string &rssFeedUrl,
|
|
|
|
BlogView *blogView)
|
|
|
|
: m_basePath(basePath), m_rssFeedUrl(rssFeedUrl), m_session(connectionPool) {
|
2024-11-02 00:30:14 +08:00
|
|
|
Wt::WApplication::instance()->internalPathChanged().connect(this, &BlogImpl::handlePathChange);
|
2024-11-01 19:05:20 +08:00
|
|
|
m_loginStatus = this->addWidget(std::make_unique<Wt::WTemplate>(tr("blog-login-status")));
|
|
|
|
m_panel = this->addWidget(std::make_unique<Wt::WStackedWidget>());
|
|
|
|
m_items = this->addWidget(std::make_unique<Wt::WContainerWidget>());
|
|
|
|
|
|
|
|
m_session.login().changed().connect(this, &BlogImpl::onUserChanged);
|
|
|
|
|
|
|
|
auto loginWidget = std::make_unique<BlogLoginWidget>(m_session, basePath);
|
|
|
|
m_loginWidget = loginWidget.get();
|
|
|
|
m_loginWidget->hide();
|
|
|
|
|
|
|
|
auto loginLink = std::make_unique<Wt::WText>(tr("login"));
|
|
|
|
auto lPtr = loginLink.get();
|
|
|
|
loginLink->setStyleClass("link");
|
2024-11-02 00:30:14 +08:00
|
|
|
loginLink->clicked().connect(m_loginWidget, &WWidget::show);
|
2024-11-01 19:05:20 +08:00
|
|
|
loginLink->clicked().connect(lPtr, &WWidget::hide);
|
|
|
|
|
|
|
|
auto registerLink = std::make_unique<Wt::WText>(tr("Wt.Auth.register"));
|
|
|
|
registerLink->setStyleClass("link");
|
2024-11-02 00:30:14 +08:00
|
|
|
registerLink->clicked().connect(m_loginWidget, &BlogLoginWidget::registerNewUser);
|
2024-11-01 19:05:20 +08:00
|
|
|
|
|
|
|
auto archiveLink =
|
2024-11-02 00:30:14 +08:00
|
|
|
std::make_unique<Wt::WAnchor>(Wt::WLink(Wt::LinkType::InternalPath, m_basePath + "all"), tr("archive"));
|
2024-11-01 19:05:20 +08:00
|
|
|
|
2024-11-02 00:30:14 +08:00
|
|
|
m_loginStatus->bindWidget("login", std::move(loginWidget));
|
|
|
|
m_loginStatus->bindWidget("login-link", std::move(loginLink));
|
|
|
|
m_loginStatus->bindWidget("register-link", std::move(registerLink));
|
|
|
|
m_loginStatus->bindString("feed-url", m_rssFeedUrl);
|
|
|
|
m_loginStatus->bindWidget("archive-link", std::move(archiveLink));
|
2024-11-01 19:05:20 +08:00
|
|
|
|
|
|
|
onUserChanged();
|
|
|
|
|
2024-11-02 00:30:14 +08:00
|
|
|
m_loginWidget->processEnvironment();
|
2024-11-01 19:05:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
void handlePathChange(const std::string &) {
|
|
|
|
Wt::WApplication *app = Wt::WApplication::instance();
|
|
|
|
|
2024-11-02 00:30:14 +08:00
|
|
|
if (app->internalPathMatches(m_basePath)) {
|
|
|
|
Wt::Dbo::Transaction t(m_session);
|
2024-11-01 19:05:20 +08:00
|
|
|
|
2024-11-02 00:30:14 +08:00
|
|
|
std::string path = app->internalPathNextPart(m_basePath);
|
2024-11-01 19:05:20 +08:00
|
|
|
|
2024-11-02 00:30:14 +08:00
|
|
|
m_items->clear();
|
2024-11-01 19:05:20 +08:00
|
|
|
|
2024-11-02 00:30:14 +08:00
|
|
|
if (m_users) {
|
|
|
|
m_users = 0;
|
2024-11-01 19:05:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (path.empty())
|
2024-11-02 00:30:14 +08:00
|
|
|
showPosts(m_session
|
2024-11-01 19:05:20 +08:00
|
|
|
.find<Post>("where state = ? "
|
|
|
|
"order by date desc "
|
|
|
|
"limit 10")
|
|
|
|
.bind(Post::Published),
|
2024-11-02 00:30:14 +08:00
|
|
|
m_items);
|
2024-11-01 19:05:20 +08:00
|
|
|
|
|
|
|
else if (path == "author") {
|
2024-11-02 00:30:14 +08:00
|
|
|
std::string author = app->internalPathNextPart(m_basePath + path + '/');
|
|
|
|
Wt::Dbo::ptr<User> user = findUser(author);
|
2024-11-01 19:05:20 +08:00
|
|
|
|
|
|
|
if (user)
|
|
|
|
showPosts(user);
|
|
|
|
else
|
|
|
|
showError(tr("blog-no-author").arg(author));
|
|
|
|
} else if (path == "edituser") {
|
2024-11-02 00:30:14 +08:00
|
|
|
editUser(app->internalPathNextPart(m_basePath + path + '/'));
|
2024-11-01 19:05:20 +08:00
|
|
|
} else if (path == "all") {
|
2024-11-02 00:30:14 +08:00
|
|
|
showArchive(m_items);
|
2024-11-01 19:05:20 +08:00
|
|
|
} else {
|
2024-11-02 00:30:14 +08:00
|
|
|
std::string remainder = app->internalPath().substr(m_basePath.length());
|
|
|
|
showPostsByDateTopic(remainder, m_items);
|
2024-11-01 19:05:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
t.commit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-02 00:30:14 +08:00
|
|
|
void showArchive(WContainerWidget *parent) {
|
|
|
|
static const char *dateFormat = "MMMM yyyy";
|
|
|
|
|
|
|
|
parent->addWidget(std::make_unique<Wt::WText>(tr("archive-title")));
|
|
|
|
|
|
|
|
Posts posts = m_session.find<Post>("order by date desc");
|
|
|
|
|
|
|
|
Wt::WDateTime formerDate;
|
|
|
|
for (auto post : posts) {
|
|
|
|
if (post->state != Post::Published) continue;
|
|
|
|
|
|
|
|
if (formerDate.isNull() || yearMonthDiffer(formerDate, post->date)) {
|
|
|
|
Wt::WText *title =
|
|
|
|
parent->addWidget(std::make_unique<Wt::WText>(post->date.date().toString(dateFormat)));
|
|
|
|
title->setStyleClass("archive-month-title");
|
|
|
|
}
|
|
|
|
|
|
|
|
Wt::WAnchor *a = parent->addWidget(std::make_unique<Wt::WAnchor>(
|
|
|
|
Wt::WLink(Wt::LinkType::InternalPath, m_basePath + post->permaLink()), post->title));
|
|
|
|
a->setInline(false);
|
|
|
|
|
|
|
|
formerDate = post->date;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool yearMonthDiffer(const Wt::WDateTime &dt1, const Wt::WDateTime &dt2) {
|
|
|
|
return dt1.date().year() != dt2.date().year() || dt1.date().month() != dt2.date().month();
|
|
|
|
}
|
|
|
|
|
|
|
|
Wt::Dbo::ptr<User> findUser(const std::string &name) {
|
|
|
|
return m_session.find<User>("where name = ?").bind(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool checkLoggedIn() {
|
|
|
|
if (m_session.user()) return true;
|
|
|
|
m_panel->show();
|
|
|
|
if (!m_mustLoginWarning) {
|
|
|
|
m_mustLoginWarning = m_panel->addWidget(std::make_unique<Wt::WTemplate>(tr("blog-mustlogin")));
|
|
|
|
}
|
|
|
|
m_panel->setCurrentWidget(m_mustLoginWarning);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool checkAdministrator() {
|
|
|
|
if (m_session.user() && (m_session.user()->role == User::Admin)) return true;
|
|
|
|
m_panel->show();
|
|
|
|
if (!m_mustBeAdministratorWarning) {
|
|
|
|
m_mustBeAdministratorWarning =
|
|
|
|
m_panel->addWidget(std::make_unique<Wt::WTemplate>(tr("blog-mustbeadministrator")));
|
|
|
|
}
|
|
|
|
m_panel->setCurrentWidget(m_mustBeAdministratorWarning);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void editUser(const std::string &ids) {
|
|
|
|
if (!checkLoggedIn()) return;
|
|
|
|
if (!checkAdministrator()) return;
|
|
|
|
Wt::Dbo::dbo_traits<User>::IdType id = User::stringToId(ids);
|
|
|
|
|
|
|
|
m_panel->show();
|
|
|
|
try {
|
|
|
|
Wt::Dbo::Transaction t(m_session);
|
|
|
|
Wt::Dbo::ptr<User> target(m_session.load<User>(id));
|
|
|
|
if (!m_userEditor) {
|
|
|
|
m_userEditor = m_panel->addWidget(std::make_unique<EditUser>(m_session));
|
|
|
|
}
|
|
|
|
m_userEditor->switchUser(target);
|
|
|
|
m_panel->setCurrentWidget(m_userEditor);
|
|
|
|
} catch (Wt::Dbo::ObjectNotFoundException &) {
|
|
|
|
if (!m_invalidUser) {
|
|
|
|
m_invalidUser = m_panel->addWidget(std::make_unique<Wt::WTemplate>(tr("blog-invaliduser")));
|
|
|
|
}
|
|
|
|
m_panel->setCurrentWidget(m_invalidUser);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void showPostsByDateTopic(const std::string &path, WContainerWidget *parent) {
|
|
|
|
std::vector<std::string> parts;
|
|
|
|
boost::split(parts, path, boost::is_any_of("/"));
|
|
|
|
|
|
|
|
Wt::WDate lower, upper;
|
|
|
|
try {
|
|
|
|
int year = try_stoi(parts[0]);
|
|
|
|
|
|
|
|
if (parts.size() > 1) {
|
|
|
|
int month = try_stoi(parts[1]);
|
|
|
|
|
|
|
|
if (parts.size() > 2) {
|
|
|
|
int day = try_stoi(parts[2]);
|
|
|
|
|
|
|
|
lower.setDate(year, month, day);
|
|
|
|
upper = lower.addDays(1);
|
|
|
|
} else {
|
|
|
|
lower.setDate(year, month, 1);
|
|
|
|
upper = lower.addMonths(1);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
lower.setDate(year, 1, 1);
|
|
|
|
upper = lower.addYears(1);
|
|
|
|
}
|
|
|
|
} catch (std::invalid_argument &) {
|
|
|
|
showError(tr("blog-no-post"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Posts posts = m_session
|
|
|
|
.find<Post>("where date >= ? "
|
|
|
|
"and date < ? "
|
|
|
|
"and (state = ? or author_id = ?)")
|
|
|
|
.bind(Wt::WDateTime(lower))
|
|
|
|
.bind(Wt::WDateTime(upper))
|
|
|
|
.bind(Post::Published)
|
|
|
|
.bind(m_session.user().id());
|
|
|
|
|
|
|
|
if (parts.size() > 3) {
|
|
|
|
std::string title = parts[3];
|
|
|
|
|
|
|
|
for (auto post : posts)
|
|
|
|
if (post->titleToUrl() == title) {
|
|
|
|
showPost(post, PostView::Detail, parent);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
showError(tr("blog-no-post"));
|
|
|
|
} else {
|
|
|
|
showPosts(posts, parent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void showPosts(Wt::Dbo::ptr<User> user) {
|
|
|
|
showPosts(user->latestPosts(), m_items);
|
|
|
|
}
|
|
|
|
|
|
|
|
void showPosts(const Posts &posts, WContainerWidget *parent) {
|
|
|
|
for (auto post : posts) showPost(post, PostView::Brief, parent);
|
|
|
|
}
|
|
|
|
|
|
|
|
void onUserChanged() {
|
|
|
|
if (m_session.login().loggedIn())
|
|
|
|
loggedIn();
|
|
|
|
else
|
|
|
|
loggedOut();
|
|
|
|
}
|
|
|
|
|
|
|
|
void editUsers() {
|
|
|
|
m_panel->show();
|
|
|
|
|
|
|
|
if (!m_users) {
|
|
|
|
m_users = m_panel->addWidget(std::make_unique<EditUsers>(m_session, m_basePath));
|
|
|
|
bindPanelTemplates();
|
|
|
|
}
|
|
|
|
|
|
|
|
m_panel->setCurrentWidget(m_users);
|
|
|
|
}
|
|
|
|
BlogSession &session() {
|
|
|
|
return m_session;
|
|
|
|
}
|
|
|
|
void loggedIn() {
|
|
|
|
Wt::WApplication::instance()->changeSessionId();
|
|
|
|
|
|
|
|
refresh();
|
|
|
|
|
|
|
|
m_loginStatus->resolveWidget("login")->show();
|
|
|
|
m_loginStatus->resolveWidget("login-link")->hide();
|
|
|
|
m_loginStatus->resolveWidget("register-link")->hide();
|
|
|
|
|
|
|
|
auto profileLink = std::make_unique<Wt::WText>(tr("profile"));
|
|
|
|
profileLink->setStyleClass("link");
|
|
|
|
profileLink->clicked().connect(this, &BlogImpl::editProfile);
|
|
|
|
|
|
|
|
Wt::Dbo::ptr<User> user = session().user();
|
|
|
|
|
|
|
|
if (user->role == User::Admin) {
|
|
|
|
auto editUsersLink = std::make_unique<Wt::WText>(tr("edit-users"));
|
|
|
|
editUsersLink->setStyleClass("link");
|
|
|
|
editUsersLink->clicked().connect(this, &BlogImpl::editUsers);
|
|
|
|
m_loginStatus->bindWidget("userlist-link", std::move(editUsersLink));
|
|
|
|
|
|
|
|
auto authorPanelLink = std::make_unique<Wt::WText>(tr("author-post"));
|
|
|
|
authorPanelLink->setStyleClass("link");
|
|
|
|
authorPanelLink->clicked().connect(this, &BlogImpl::authorPanel);
|
|
|
|
m_loginStatus->bindWidget("author-panel-link", std::move(authorPanelLink));
|
|
|
|
} else {
|
|
|
|
m_loginStatus->bindEmpty("userlist-link");
|
|
|
|
m_loginStatus->bindEmpty("author-panel-link");
|
|
|
|
}
|
|
|
|
|
|
|
|
m_loginStatus->bindWidget("profile-link", std::move(profileLink));
|
|
|
|
|
|
|
|
bindPanelTemplates();
|
|
|
|
}
|
|
|
|
|
|
|
|
void loggedOut() {
|
|
|
|
m_loginStatus->bindEmpty("profile-link");
|
|
|
|
m_loginStatus->bindEmpty("author-panel-link");
|
|
|
|
m_loginStatus->bindEmpty("userlist-link");
|
|
|
|
|
|
|
|
m_loginStatus->resolveWidget("login")->hide();
|
|
|
|
m_loginStatus->resolveWidget("login-link")->show();
|
|
|
|
m_loginStatus->resolveWidget("register-link")->show();
|
|
|
|
|
|
|
|
refresh();
|
|
|
|
m_panel->hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
void editProfile() {
|
|
|
|
m_loginWidget->letUpdatePassword(m_session.login().user(), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void showError(const Wt::WString &msg) {
|
|
|
|
m_items->addWidget(std::make_unique<Wt::WText>(msg));
|
|
|
|
}
|
|
|
|
|
|
|
|
void authorPanel() {
|
|
|
|
m_panel->show();
|
|
|
|
if (!m_authorPanel) {
|
|
|
|
m_authorPanel = m_panel->addWidget(std::make_unique<Wt::WTemplate>(tr("blog-author-panel")));
|
|
|
|
bindPanelTemplates();
|
|
|
|
}
|
|
|
|
m_panel->setCurrentWidget(m_authorPanel);
|
|
|
|
}
|
|
|
|
|
|
|
|
void showPost(const Wt::Dbo::ptr<Post> post, PostView::RenderType type, Wt::WContainerWidget *parent) {
|
|
|
|
parent->addWidget(std::make_unique<PostView>(m_session, m_basePath, post, type));
|
|
|
|
}
|
|
|
|
|
|
|
|
void newPost() {
|
|
|
|
Wt::Dbo::Transaction t(m_session);
|
|
|
|
|
|
|
|
authorPanel();
|
|
|
|
WContainerWidget *unpublishedPosts = m_authorPanel->resolve<WContainerWidget *>("unpublished-posts");
|
|
|
|
|
|
|
|
Wt::Dbo::ptr<Post> post(std::make_unique<Post>());
|
|
|
|
|
|
|
|
Post *p = post.modify();
|
|
|
|
p->state = Post::Unpublished;
|
|
|
|
p->author = m_session.user();
|
|
|
|
p->title = "Title";
|
|
|
|
p->briefSrc = "Brief ...";
|
|
|
|
p->bodySrc = "Body ...";
|
|
|
|
|
|
|
|
showPost(post, PostView::Edit, unpublishedPosts);
|
|
|
|
|
|
|
|
t.commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
void bindPanelTemplates() {
|
|
|
|
if (!m_session.user()) return;
|
|
|
|
|
|
|
|
Wt::Dbo::Transaction t(m_session);
|
|
|
|
|
|
|
|
if (m_authorPanel) {
|
|
|
|
auto newPost = std::make_unique<Wt::WPushButton>(tr("new-post"));
|
|
|
|
newPost->clicked().connect(this, &BlogImpl::newPost);
|
|
|
|
auto unpublishedPosts = std::make_unique<Wt::WContainerWidget>();
|
|
|
|
showPosts(m_session.user()->allPosts(Post::Unpublished), unpublishedPosts.get());
|
|
|
|
|
|
|
|
m_authorPanel->bindString("user", m_session.user()->name);
|
|
|
|
m_authorPanel->bindInt("unpublished-count", (int)m_session.user()->allPosts(Post::Unpublished).size());
|
|
|
|
m_authorPanel->bindInt("published-count", (int)m_session.user()->allPosts(Post::Published).size());
|
|
|
|
m_authorPanel->bindWidget("new-post", std::move(newPost));
|
|
|
|
m_authorPanel->bindWidget("unpublished-posts", std::move(unpublishedPosts));
|
|
|
|
}
|
|
|
|
|
|
|
|
t.commit();
|
|
|
|
}
|
|
|
|
|
2024-11-01 19:05:20 +08:00
|
|
|
private:
|
|
|
|
std::string m_basePath, m_rssFeedUrl;
|
2024-11-02 00:30:14 +08:00
|
|
|
BlogSession m_session;
|
|
|
|
BlogLoginWidget *m_loginWidget = nullptr;
|
2024-11-01 19:05:20 +08:00
|
|
|
Wt::WStackedWidget *m_panel = nullptr;
|
|
|
|
Wt::WTemplate *m_authorPanel = nullptr;
|
|
|
|
EditUsers *m_users = nullptr;
|
|
|
|
EditUser *m_userEditor = nullptr;
|
|
|
|
Wt::WTemplate *m_mustLoginWarning = nullptr;
|
|
|
|
Wt::WTemplate *m_mustBeAdministratorWarning = nullptr;
|
|
|
|
Wt::WTemplate *m_invalidUser = nullptr;
|
|
|
|
Wt::WTemplate *m_loginStatus = nullptr;
|
|
|
|
WContainerWidget *m_items = nullptr;
|
|
|
|
};
|
|
|
|
|
|
|
|
BlogView::BlogView(const std::string &basePath, Wt::Dbo::SqlConnectionPool &db, const std::string &rssFeedUrl)
|
|
|
|
: WCompositeWidget(), m_userChanged() {
|
|
|
|
m_impl = setImplementation(std::make_unique<BlogImpl>(basePath, db, rssFeedUrl, this));
|
|
|
|
}
|