Older/WebApplication/view/BlogView.cpp

393 lines
14 KiB
C++
Raw Normal View History

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-03 22:31:23 +08:00
loginLink->clicked().connect(m_loginWidget, &BlogLoginWidget::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));
}