add code.
All checks were successful
Deploy / PullDocker (push) Successful in 4s
Deploy / Build (push) Successful in 2m8s
Deploy Docker Images / Docusaurus build and Server deploy (push) Successful in 13s

This commit is contained in:
amass 2024-11-02 00:30:14 +08:00
parent 27ffee57be
commit b6e0681489
31 changed files with 1984 additions and 33 deletions

View File

@ -6,6 +6,11 @@ static const char *FeedUrl = "/blog/feed/";
BlogApplication::BlogApplication(const Wt::WEnvironment &env, Wt::Dbo::SqlConnectionPool &blogDb) BlogApplication::BlogApplication(const Wt::WEnvironment &env, Wt::Dbo::SqlConnectionPool &blogDb)
: Wt::WApplication(env) { : Wt::WApplication(env) {
messageResourceBundle().use(Wt::WApplication::appRoot() + "blog");
useStyleSheet("/css/blog.css");
useStyleSheet("/css/asciidoc.css");
root()->addWidget(std::make_unique<BlogView>("/", blogDb, FeedUrl)); root()->addWidget(std::make_unique<BlogView>("/", blogDb, FeedUrl));
useStyleSheet("css/blogexample.css"); useStyleSheet("css/blogexample.css");
} }

View File

@ -5,11 +5,19 @@ add_library(WebApplication
Restful.h Restful.cpp Restful.h Restful.cpp
Dialog.h Dialog.cpp Dialog.h Dialog.cpp
Session.h Session.cpp Session.h Session.cpp
model/asciidoc.h model/asciidoc.cpp
model/BlogSession.h model/BlogSession.cpp model/BlogSession.h model/BlogSession.cpp
model/BlogUserDatabase.h model/BlogUserDatabase.cpp model/BlogUserDatabase.h model/BlogUserDatabase.cpp
model/Comment.h model/Comment.cpp
model/Post.h model/Post.cpp
model/Tag.h model/Tag.cpp
model/Token.h model/Token.cpp
model/User.h model/User.cpp model/User.h model/User.cpp
view/BlogLoginWidget.h view/BlogLoginWidget.cpp
view/BlogView.h view/BlogView.cpp view/BlogView.h view/BlogView.cpp
view/CommentView.h view/CommentView.cpp
view/EditUsers.h view/EditUsers.cpp view/EditUsers.h view/EditUsers.cpp
view/PostView.h view/PostView.cpp
) )
target_include_directories(WebApplication target_include_directories(WebApplication

View File

@ -24,13 +24,15 @@ static std::unique_ptr<Wt::WApplication> createWidgetSet(const Wt::WEnvironment
WebApplication::WebApplication() { WebApplication::WebApplication() {
try { try {
std::vector<std::string> args; std::vector<std::string> args;
args.push_back("--approot=./build");
args.push_back("--docroot=./build"); args.push_back("--docroot=./build");
args.push_back("--http-listen=127.0.0.1:8855"); args.push_back("--http-listen=127.0.0.1:8855");
// --docroot=. --no-compression --http-listen 127.0.0.1:8855 // --docroot=. --no-compression --http-listen 127.0.0.1:8855
m_server = std::make_unique<Wt::WServer>("./build", args); m_server = std::make_unique<Wt::WServer>("./build", args);
m_server->addEntryPoint(Wt::EntryPointType::Application, createApplication, "/hello"); m_server->addEntryPoint(Wt::EntryPointType::Application, createApplication, "/hello");
auto blogDb = BlogSession::createConnectionPool(m_server->appRoot() + "blog.db"); BlogSession::configureAuth();
auto blogDb = BlogSession::createConnectionPool(m_server->appRoot() + "database.sqlite");
m_server->addEntryPoint(Wt::EntryPointType::Application, m_server->addEntryPoint(Wt::EntryPointType::Application,
std::bind(&createBlogApplication, std::placeholders::_1, blogDb.get()), "/blog"); std::bind(&createBlogApplication, std::placeholders::_1, blogDb.get()), "/blog");
m_server->addEntryPoint(Wt::EntryPointType::WidgetSet, createWidgetSet, "/gui/hello.js"); m_server->addEntryPoint(Wt::EntryPointType::WidgetSet, createWidgetSet, "/gui/hello.js");

296
WebApplication/asciidoc.css Normal file
View File

@ -0,0 +1,296 @@
.asciidoc em {
font-style: italic;
color: #111111;
}
.asciidoc strong {
font-weight: bold;
color: #111111;
}
.asciidoc tt {
color: #111111;
}
.asciidoc h1, .asciidoc h2, .asciidoc h3, .asciidoc h4, .asciidoc h5, .asciidoc h6 {
color: #111111;
font-family: sans-serif;
margin-top: 1.2em;
margin-bottom: 0.5em;
line-height: 1.3;
}
.asciidoc h1, .asciidoc h2, .asciidoc h3 {
border-bottom: 2px solid silver;
}
.asciidoc h2 {
padding-top: 0.5em;
}
.asciidoc h3 {
float: left;
}
.asciidoc h3 + * {
clear: left;
}
.asciidoc div.sectionbody {
font-family: serif;
margin-left: 0;
}
.asciidoc hr {
border: 1px solid silver;
}
.asciidoc p {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.asciidoc ul, .asciidoc ol, .asciidoc li > p {
margin-top: 0;
}
.asciidoc pre {
padding: 0;
margin: 0;
line-height: 14px;
font-family: Consolas, 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace;
font-size: 12px;
}
.asciidoc div.tableblock, .asciidoc div.imageblock, .asciidoc div.exampleblock,
.asciidoc div.verseblock, .asciidoc div.quoteblock, .asciidoc div.literalblock,
.asciidoc div.listingblock, .asciidoc div.sidebarblock,
.asciidoc div.admonitionblock {
margin-top: 1.5em;
margin-bottom: 1.5em;
}
.asciidoc div.admonitionblock {
margin-top: 2.5em;
margin-bottom: 2.5em;
}
.asciidoc div.content { /* Block element content. */
padding: 0;
}
/* Block element titles. */
.asciidoc div.title, .asciidoc caption.title, .asciidoc div.sidebar-title {
color: #111111;
font-family: sans-serif;
font-weight: bold;
text-align: left;
margin-top: 1.0em;
margin-bottom: 0.5em;
}
.asciidoc div.title + * {
margin-top: 0;
}
.asciidoc td div.title:first-child {
margin-top: 0.0em;
}
.asciidoc div.content div.title:first-child {
margin-top: 0.0em;
}
.asciidoc div.content + div.title {
margin-top: 0.0em;
}
.asciidoc div.sidebarblock > div.sidebar-content {
background: #ffffee;
border: 1px solid silver;
padding: 0.5em;
}
.asciidoc div.listingblock > div.content {
border: 1px solid silver;
background: #f4f4f4;
padding: 0.5em;
}
.asciidoc div.quoteblock {
padding-left: 2.0em;
margin-right: 10%;
}
.asciidoc div.quoteblock > div.attribution {
padding-top: 0.5em;
text-align: right;
}
.asciidoc div.verseblock {
padding-left: 2.0em;
margin-right: 10%;
}
.asciidoc div.verseblock > div.content {
white-space: pre;
}
.asciidoc div.verseblock > div.attribution {
padding-top: 0.75em;
text-align: left;
}
/* DEPRECATED: Pre version 8.2.7 verse style literal block. */
.asciidoc div.verseblock + div.attribution {
text-align: left;
}
.asciidoc div.admonitionblock .icon {
vertical-align: top;
font-size: 1.1em;
font-weight: bold;
text-decoration: underline;
color: #111111;
padding-right: 0.5em;
}
.asciidoc div.admonitionblock td.content {
padding-left: 0.5em;
border-left: 2px solid silver;
}
.asciidoc div.exampleblock > div.content {
border-left: 2px solid silver;
padding: 0.5em;
}
.asciidoc div.imageblock div.content { padding-left: 0; }
.asciidoc span.image img { border-style: none; }
.asciidoc a.image:visited { color: white; }
.asciidoc dl {
margin-top: 0.8em;
margin-bottom: 0.8em;
}
.asciidoc dt {
margin-top: 0.5em;
margin-bottom: 0;
font-style: normal;
color: #111111;
}
.asciidoc dd > *:first-child {
margin-top: 0.1em;
}
.asciidoc ul, ol {
list-style-position: outside;
}
.asciidoc ol.arabic {
list-style-type: decimal;
}
.asciidoc ol.loweralpha {
list-style-type: lower-alpha;
}
.asciidoc ol.upperalpha {
list-style-type: upper-alpha;
}
.asciidoc ol.lowerroman {
list-style-type: lower-roman;
}
.asciidoc ol.upperroman {
list-style-type: upper-roman;
}
.asciidoc div.compact ul, .asciidoc div.compact ol,
.asciidoc div.compact p, .asciidoc div.compact p,
.asciidoc div.compact div, .asciidoc div.compact div {
margin-top: 0.1em;
margin-bottom: 0.1em;
}
.asciidoc div.tableblock > table {
border: 3px solid #527bbd;
}
.asciidoc thead {
font-family: sans-serif;
font-weight: bold;
}
.asciidoc tfoot {
font-weight: bold;
}
.asciidoc td > div.verse {
white-space: pre;
}
.asciidoc p.table {
margin-top: 0;
}
/* Because the table frame attribute is overriden by CSS in most browsers. */
.asciidoc div.tableblock > table[frame="void"] {
border-style: none;
}
.asciidoc div.tableblock > table[frame="hsides"] {
border-left-style: none;
border-right-style: none;
}
.asciidoc div.tableblock > table[frame="vsides"] {
border-top-style: none;
border-bottom-style: none;
}
.asciidoc div.hdlist {
margin-top: 0.8em;
margin-bottom: 0.8em;
}
.asciidoc div.hdlist tr {
padding-bottom: 15px;
}
.asciidoc dt.hdlist1.strong, .asciidoc td.hdlist1.strong {
font-weight: bold;
}
.asciidoc td.hdlist1 {
vertical-align: top;
font-style: normal;
padding-right: 0.8em;
color: #111111;
}
.asciidoc td.hdlist2 {
vertical-align: top;
}
.asciidoc div.hdlist.compact tr {
margin: 0;
padding-bottom: 0;
}
.asciidoc .comment {
background: yellow;
}
@media print {
div#footer-badges { display: none; }
}
.asciidoc div#toctitle {
color: #111111;
font-family: sans-serif;
font-size: 1.1em;
font-weight: bold;
margin-top: 1.0em;
margin-bottom: 0.1em;
}
.asciidoc div.toclevel1,
.asciidoc div.toclevel2,
.asciidoc div.toclevel3,
.asciidoc div.toclevel4 {
margin-top: 0;
margin-bottom: 0;
}
.asciidoc div.toclevel2 {
margin-left: 2em;
font-size: 0.9em;
}
.asciidoc div.toclevel3 {
margin-left: 4em;
font-size: 0.9em;
}
.asciidoc div.toclevel4 {
margin-left: 6em;
font-size: 0.9em;
}

128
WebApplication/blog.css Normal file
View File

@ -0,0 +1,128 @@
.login-box {
margin-bottom: -10px;
height: 25px;
}
.user-menu {
}
.login-menu {
float: right;
white-space: nowrap;
}
.Wt-auth-logged-in {
display: inline;
}
.link {
text-decoration: underline;
cursor: pointer;
}
.invalid {
background-color: #EE9999;
}
.comment-icon {
float: left;
margin-top: 3px;
width: 20px;
height: 20px;
background-image: url(comment.png);
background-repeat: no-repeat;
}
.comment-edit-icon {
float: left;
margin-top: 3px;
width: 20px;
height: 20px;
background-image: url(comment_edit.png);
background-repeat: no-repeat;
}
.comment-info {
color: rgb(136,136,136);
}
.poster {
color: rgb(51, 102, 53);
font-weight: bold;
}
.author-panel {
margin-top: 10px;
border: 1px #528B12 dashed;
padding: 5px;
}
.user-editor {
margin-top: 10px;
border: 1px #528B12 dashed;
padding: 5px;
}
.profile-panel {
margin-top: 10px;
border: 1px #528B12 dashed;
padding: 5px;
}
.comment-body {
overflow: hidden; /* trick that makes alignment work properly */
margin-left: 3px;
}
.comment-body .vspace {
margin: 10px 0px;
}
.comment-body pre {
line-height: 140%;
border: 1px solid silver;
background: #f4f4f4;
padding: 0.5em;
}
.comment-edit {
width: 400px;
}
.comment-edit textarea {
width: 396px;
}
.comment-links {
margin-bottom: 3px;
}
.blogpost-edit {
width: 500px;
margin: 10x;
}
.blogpost-edit div {
margin: 5px 0px;
}
.blogpost-edit input {
width: 450px;
}
.blogpost-edit textarea {
width: 496px;
height: 150px;
}
.archive-month-title {
color: #528B12;
display: block;
font-size: 1.3em;
line-height: 1.8;
margin-top: 15px;
font-weight: bold;
}
.asciidoc .subtitle {
font-size: 1.4em;
}

258
WebApplication/blog.xml Normal file
View File

@ -0,0 +1,258 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="blog-login">
${user-name} ${password} ${remember-me} Remember me
${login}${<if:oauth>}, or use ${icons}${</if:oauth>}
</message>
<message id="blog-login-status">
<div class="login-box">
<div class="login-menu">
${login} ${login-link} | ${register-link}
<a href="${feed-url}">
<img src="/css/rss.png" alt="Rss Feed"
style="margin-left: 6px; vertical-align: top;"/>
</a>
</div>
<div class="user-menu">
${archive-link} ${profile-link} ${author-panel-link} ${userlist-link}
</div>
</div>
</message>
<message id="edit-users-list">
<h4>Registered users</h4>
${user-list}
Limit list to user names containing : ${limit-edit} ${limit-button}
</message>
<message id="edit-user">
<div class="profile-panel">
<h4>Edit user ${username}</h4>
${role-button}
</div>
</message>
<message id="edit-one-user">
<h4>Editing user ${user}</h4>
${save-button} ${cancel-button}
</message>
<message id="blog-invaliduser">
<div class="profile-panel">
<h4>This user id is invalid</h4>
</div>
</message>
<message id="blog-mustlogin">
<div class="profile-panel">
<h4>You need to log in to access this function</h4>
</div>
</message>
<message id="blog-mustbeadministrator">
<div class="profile-panel">
<h4>You need to be administrator to access this function</h4>
</div>
</message>
<message id="blog-register">
<h4>Register</h4>
<p>
To register as a user to post comments with your own login,
please fill out the following form.
</p>
<table style="margin: 5px auto;">
<tr>
<td>Login:</td>
<td>${name}</td>
</tr>
<tr>
<td>Password:</td>
<td>${passwd}</td>
</tr>
<tr>
<td>Repeat password:</td>
<td>${passwd2}</td>
</tr>
<tr>
<td colspan="2" style="text-align: center">
${ok-button}
${cancel-button}
</td>
</tr>
<tr>
<td colspan="2" class="login-error">
${error}
</td>
</tr>
</table>
</message>
<message id="blog-userslist">
<div class="profile-panel">
<h4>Registered users</h4>
Search for ${searchstring} ${search-button}<br/>
</div>
</message>
<message id="blog-profile">
<div class="profile-panel">
<h4>Profile panel for <span class="poster">${user}</span></h4>
<table style="margin: 5px auto;">
<tr>
<td>New password:</td>
<td>${passwd}</td>
</tr>
<tr>
<td>Repeat password:</td>
<td>${passwd2}</td>
</tr>
<tr>
<td colspan="2" style="text-align: center">
${ok-button}
${cancel-button}
</td>
</tr>
<tr>
<td colspan="2" style="text-align: center; color: red;" class="login-error">
${error}
</td>
</tr>
</table>
</div>
</message>
<message id="blog-author-panel">
<div class="author-panel">
<h4>Author panel for <span class="poster">${user}</span></h4>
Statistics:
<ul>
<li>${unpublished-count} unpublished post(s)</li>
<li>${published-count} published post(s)</li>
</ul>
${new-post}
${unpublished-posts}
</div>
</message>
<message id="blog-post">
<h4>${title}</h4>
<div id="${anchor}">
<div>by ${author} on ${date}</div>
<div class="asciidoc">
${brief+body}
</div>
<div style="margin-top: 10px;">${comment-count}</div>
<div>
${publish} ${edit} ${delete}
</div>
</div>
<div id="${anchor}/comments">
${comments}
</div>
</message>
<message id="blog-post-brief">
<h4>${title}</h4>
<div>by ${author} on ${date}</div>
<div class="asciidoc">
${brief}
</div>
<div>${read-more}</div>
<div style="margin-top: 10px">
${publish} ${edit} ${delete}
</div>
<div style="margin-top: 10px;">${comment-count}</div>
</message>
<message id="blog-post-edit">
<h4></h4>
<div class="blogpost-edit">
<div>Title: ${title-edit}</div>
<div>${brief-edit}</div>
<div>${body-edit}</div>
<div>
${save} ${cancel}
</div>
</div>
</message>
<message id="blog-comment">
<div class="comment-icon" />
<div class="comment-body">
<div class="comment-info">
<span class="poster">${author}</span>
${date} ${collapse-expand}
</div>
${contents}
<div class="comment-links">
${reply} ${edit} ${delete}
</div>
${children}
</div>
</message>
<message id="blog-root-comment">
<div class="comment-links">
${reply}
</div>
${children}
</message>
<message id="blog-edit-comment">
<div class="comment-edit-icon" />
<div class="comment-body comment-edit">
<div>${area}</div>
<div style="float: right"><i>plain text</i> or (&lt;code&gt;...&lt;/code&gt;)</div>
${save}
${cancel}
</div>
</message>
<message id="blog-no-post">
<h4>No posts found</h4>
Sorry, no blog posts found that match your selection.
</message>
<message id="blog-no-author">
<h4>No author</h4>
Sorry, {1} is not a registered blog author.
</message>
<message id="archive-title">
<h4>Archive</h4>
</message>
<message id="login">Login</message>
<message id="login-tooshort">Login too short (must be at least {1} characters)</message>
<message id="passwd-mismatch">Passwords don't match.</message>
<message id="register">Register</message>
<message id="logout">Logout</message>
<message id="archive">Archive</message>
<message id="profile">Profile</message>
<message id="author-post">Authoring panel</message>
<message id="edit-users">Edit users</message>
<message id="comment-add">Add comment</message>
<message id="comment-reply">Reply</message>
<message id="comment-edit">Edit</message>
<message id="comment-delete">Delete</message>
<message id="comment-deleted"><i>[[Comment deleted]]</i></message>
<message id="blog-read-more">Read the rest of this post &gt;&gt;</message>
<message id="new-post">New post</message>
<message id="publish">Publish</message>
<message id="retract">Retract</message>
<message id="delete">Delete</message>
<message id="edit">Edit</message>
<message id="save">Save</message>
<message id="cancel">Cancel</message>
<message id="go-limit">Search</message>
<message id="demote-admin">Demote this administrator to a regular visitor</message>
<message id="promote-user">Promote this user to administrator</message>
<message id="no-users-found">No users found</message>
</messages>

View File

@ -0,0 +1,52 @@
body {
color: #333333;
font-family: arial,sans-serif;
font-size: 80%;
line-height:1.5em;
background-color:#FFF;
min-width:750px;
overflow-x: hidden;
}
a {
text-decoration: underline;
color: #528B12; /*#70BD1A;*/
}
.link {
color: #528B12; /*#70BD1A;*/
}
a.blank {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h4 {
font-size: 1.3em;
line-height: 1.8;
border-top : 1px solid #528B12;
padding-bottom:10px;
margin-top:15px;
color: #528B12;
display: block;
}
p {
font-size: 1.1em;
font-weight: normal;
line-height: 1.4;
margin-bottom : 15px;
color: #333333;
}
p.intro {
font-size: 1.3em;
font-weight: normal;
line-height: 1.4;
padding-bottom : 15px;
color: #333333;
}

View File

@ -1,10 +1,45 @@
#include "BlogSession.h" #include "BlogSession.h"
#include <Wt/Auth/AuthService.h>
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordService.h>
#include <Wt/Dbo/FixedSqlConnectionPool.h> #include <Wt/Dbo/FixedSqlConnectionPool.h>
#include <Wt/Auth/PasswordVerifier.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/Auth/GoogleService.h>
class BlogOAuth : public std::vector<const Wt::Auth::OAuthService *> {
public:
~BlogOAuth() {
for (unsigned i = 0; i < size(); ++i) delete (*this)[i];
}
};
Wt::Auth::AuthService blogAuth;
Wt::Auth::PasswordService blogPasswords(blogAuth);
BlogOAuth blogOAuth;
BlogSession::BlogSession(Wt::Dbo::SqlConnectionPool &connectionPool) BlogSession::BlogSession(Wt::Dbo::SqlConnectionPool &connectionPool)
: m_connectionPool(connectionPool), m_users(*this) { : m_connectionPool(connectionPool), m_users(*this) {
} }
void BlogSession::configureAuth() {
blogAuth.setAuthTokensEnabled(true, "bloglogin");
std::unique_ptr<Wt::Auth::PasswordVerifier> verifier = std::make_unique<Wt::Auth::PasswordVerifier>();
verifier->addHashFunction(std::make_unique<Wt::Auth::BCryptHashFunction>(7));
#ifdef WT_WITH_SSL
verifier->addHashFunction(std::make_unique<Wt::Auth::SHA1HashFunction>());
#endif
#ifdef HAVE_CRYPT
verifier->addHashFunction(std::make_unique<UnixCryptHashFunction>());
#endif
blogPasswords.setVerifier(std::move(verifier));
blogPasswords.setPasswordThrottle(std::make_unique<Wt::Auth::AuthThrottle>());
blogPasswords.setStrengthValidator(std::make_unique<Wt::Auth::PasswordStrengthValidator>());
if (Wt::Auth::GoogleService::configured()) blogOAuth.push_back(new Wt::Auth::GoogleService(blogAuth));
}
std::unique_ptr<Wt::Dbo::SqlConnectionPool> BlogSession::createConnectionPool(const std::string &sqlite3) { std::unique_ptr<Wt::Dbo::SqlConnectionPool> BlogSession::createConnectionPool(const std::string &sqlite3) {
auto connection = std::make_unique<Wt::Dbo::backend::Sqlite3>(sqlite3); auto connection = std::make_unique<Wt::Dbo::backend::Sqlite3>(sqlite3);
@ -14,3 +49,18 @@ std::unique_ptr<Wt::Dbo::SqlConnectionPool> BlogSession::createConnectionPool(co
return std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), 10); return std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), 10);
} }
Wt::Dbo::ptr<User> BlogSession::user() const {
if (m_login.loggedIn())
return m_users.find(m_login.user());
else
return Wt::Dbo::ptr<User>();
}
Wt::Auth::PasswordService *BlogSession::passwordAuth() const {
return &blogPasswords;
}
const std::vector<const Wt::Auth::OAuthService *> &BlogSession::oAuth() const {
return blogOAuth;
}

View File

@ -2,16 +2,36 @@
#define __BLOGSESSION_H__ #define __BLOGSESSION_H__
#include "BlogUserDatabase.h" #include "BlogUserDatabase.h"
#include <Wt/Auth/Login.h>
#include <Wt/Auth/OAuthService.h>
#include <Wt/Dbo/Session.h> #include <Wt/Dbo/Session.h>
#include <Wt/Dbo/backend/Sqlite3.h> #include <Wt/Dbo/backend/Sqlite3.h>
class Comment;
class BlogSession : public Wt::Dbo::Session { class BlogSession : public Wt::Dbo::Session {
public: public:
BlogSession(Wt::Dbo::SqlConnectionPool &connectionPool); BlogSession(Wt::Dbo::SqlConnectionPool &connectionPool);
static void configureAuth();
static std::unique_ptr<Wt::Dbo::SqlConnectionPool> createConnectionPool(const std::string &sqlite3); static std::unique_ptr<Wt::Dbo::SqlConnectionPool> createConnectionPool(const std::string &sqlite3);
Wt::Auth::Login &login() {
return m_login;
}
Wt::Dbo::ptr<User> user() const;
Wt::Signal<Wt::Dbo::ptr<Comment>> &commentsChanged() {
return commentsChanged_;
}
BlogUserDatabase &users() {
return m_users;
}
Wt::Auth::PasswordService *passwordAuth() const;
const std::vector<const Wt::Auth::OAuthService *> &oAuth() const;
private: private:
Wt::Dbo::SqlConnectionPool &m_connectionPool; Wt::Dbo::SqlConnectionPool &m_connectionPool;
BlogUserDatabase m_users; BlogUserDatabase m_users;
Wt::Auth::Login m_login;
Wt::Signal<Wt::Dbo::ptr<Comment>> commentsChanged_;
}; };
#endif // __BLOGSESSION_H__ #endif // __BLOGSESSION_H__

View File

@ -33,6 +33,11 @@ BlogUserDatabase::BlogUserDatabase(Wt::Dbo::Session &session) : m_session(sessio
BlogUserDatabase::~BlogUserDatabase() { BlogUserDatabase::~BlogUserDatabase() {
} }
Wt::Dbo::ptr<User> BlogUserDatabase::find(const Wt::Auth::User &user) const {
getUser(user.id());
return m_user;
}
BlogUserDatabase::Transaction *BlogUserDatabase::startTransaction() { BlogUserDatabase::Transaction *BlogUserDatabase::startTransaction() {
return new TransactionImpl(m_session); return new TransactionImpl(m_session);
} }

View File

@ -11,6 +11,8 @@ class BlogUserDatabase : public Wt::Auth::AbstractUserDatabase {
public: public:
BlogUserDatabase(Wt::Dbo::Session &session); BlogUserDatabase(Wt::Dbo::Session &session);
~BlogUserDatabase(); ~BlogUserDatabase();
Wt::Dbo::ptr<User> find(const Wt::Auth::User &user) const;
Transaction *startTransaction() final; Transaction *startTransaction() final;
Wt::Auth::User findWithId(const std::string &id) const final; Wt::Auth::User findWithId(const std::string &id) const final;
Wt::Auth::User findWithIdentity(const std::string &provider, const Wt::WString &identity) const final; Wt::Auth::User findWithIdentity(const std::string &provider, const Wt::WString &identity) const final;

View File

@ -0,0 +1,76 @@
#include "Comment.h"
#include "Post.h"
#include "Tag.h"
#include "User.h"
#include <Wt/Dbo/Impl.h>
#include <Wt/WWebWidget.h>
DBO_INSTANTIATE_TEMPLATES(Comment)
static std::string &replace(std::string &s, const std::string &k, const std::string &r) {
std::string::size_type p = 0;
while ((p = s.find(k, p)) != std::string::npos) {
s.replace(p, k.length(), r);
p += r.length();
}
return s;
}
void Comment::setText(const Wt::WString &text) {
textSrc_ = text;
std::string html = Wt::WWebWidget::escapeText(text, true).toUTF8();
std::string::size_type b = 0;
// Replace &lt;code&gt;...&lt/code&gt; with <pre>...</pre>
// This is kind of very ad-hoc!
while ((b = html.find("&lt;code&gt;", b)) != std::string::npos) {
std::string::size_type e = html.find("&lt;/code&gt;", b);
if (e == std::string::npos)
break;
else {
if (b > 6 && html.substr(b - 6, 6) == "<br />") {
html.erase(b - 6, 6);
b -= 6;
e -= 6;
}
html.replace(b, 12, "<pre>");
e -= 7;
if (html.substr(b + 5, 6) == "<br />") {
html.erase(b + 5, 6);
e -= 6;
}
if (html.substr(e - 6, 6) == "<br />") {
html.erase(e - 6, 6);
e -= 6;
}
html.replace(e, 13, "</pre>");
e += 6;
if (e + 6 <= html.length() && html.substr(e, 6) == "<br />") {
html.erase(e, 6);
e -= 6;
}
b = e;
}
}
// We would also want to replace <br /><br /> (empty line) with
// <div class="vspace"></div>
replace(html, "<br /><br />", "<div class=\"vspace\"></div>");
textHtml_ = Wt::WString(html);
}
void Comment::setDeleted() {
textHtml_ = Wt::WString::tr("comment-deleted");
}

View File

@ -0,0 +1,50 @@
#ifndef __COMMENT_H__
#define __COMMENT_H__
#include <Wt/Dbo/Types.h>
#include <Wt/Dbo/WtSqlTraits.h>
#include <Wt/WDateTime.h>
class Comment;
using Comments = Wt::Dbo::collection<Wt::Dbo::ptr<Comment>>;
class Post;
class User;
class Comment {
public:
Wt::Dbo::ptr<User> author;
Wt::Dbo::ptr<Post> post;
Wt::Dbo::ptr<Comment> parent;
Wt::WDateTime date;
void setText(const Wt::WString &text);
void setDeleted();
const Wt::WString &textSrc() const {
return textSrc_;
}
const Wt::WString &textHtml() const {
return textHtml_;
}
Comments children;
template <class Action>
void persist(Action &a) {
Wt::Dbo::field(a, date, "date");
Wt::Dbo::field(a, textSrc_, "text_source");
Wt::Dbo::field(a, textHtml_, "text_html");
Wt::Dbo::belongsTo(a, post, "post", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, author, "author");
Wt::Dbo::belongsTo(a, parent, "parent", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, children, Wt::Dbo::ManyToOne, "parent");
}
private:
Wt::WString textSrc_;
Wt::WString textHtml_;
};
#endif // __COMMENT_H__

View File

@ -0,0 +1,36 @@
#include "Post.h"
#include "User.h"
#include <Wt/Dbo/Impl.h>
DBO_INSTANTIATE_TEMPLATES(Post)
std::string Post::permaLink() const {
return date.toString("yyyy/MM/dd/'" + titleToUrl() + '\'').toUTF8();
}
std::string Post::commentCount() const {
int count = (int)comments.size() - 1;
if (count == 1)
return "1 comment";
else
return std::to_string(count) + " comments";
}
std::string Post::titleToUrl() const {
std::string result = title.narrow();
for (unsigned i = 0; i < result.length(); ++i) {
if (!isalnum(result[i]))
result[i] = '_';
else
result[i] = tolower(result[i]);
}
return result;
}
Wt::Dbo::ptr<Comment> Post::rootComment() const {
if (session())
return session()->find<Comment>().where("post_id = ?").bind(id()).where("parent_id is null");
else
return Wt::Dbo::ptr<Comment>();
}

View File

@ -0,0 +1,58 @@
#ifndef __POST_H__
#define __POST_H__
#include "Comment.h"
#include "Tag.h"
#include <Wt/Dbo/WtSqlTraits.h>
#include <Wt/WDateTime.h>
#include <Wt/WString.h>
class User;
typedef Wt::Dbo::collection<Wt::Dbo::ptr<Comment>> Comments;
typedef Wt::Dbo::collection<Wt::Dbo::ptr<Tag>> Tags;
class Post : public Wt::Dbo::Dbo<Post> {
public:
enum State {
Unpublished = 0,
Published = 1,
};
std::string permaLink() const;
std::string commentCount() const;
std::string titleToUrl() const;
Wt::Dbo::ptr<Comment> rootComment() const;
template <class Action>
void persist(Action &a) {
Wt::Dbo::field(a, state, "state");
Wt::Dbo::field(a, date, "date");
Wt::Dbo::field(a, title, "title");
Wt::Dbo::field(a, briefSrc, "brief_src");
Wt::Dbo::field(a, briefHtml, "brief_html");
Wt::Dbo::field(a, bodySrc, "body_src");
Wt::Dbo::field(a, bodyHtml, "body_html");
Wt::Dbo::belongsTo(a, author, "author");
Wt::Dbo::hasMany(a, comments, Wt::Dbo::ManyToOne, "post");
Wt::Dbo::hasMany(a, tags, Wt::Dbo::ManyToMany, "post_tag");
}
Wt::Dbo::ptr<User> author;
State state;
Wt::WDateTime date;
Wt::WString title;
Wt::WString briefSrc;
Wt::WString briefHtml;
Wt::WString bodySrc;
Wt::WString bodyHtml;
Comments comments;
Tags tags;
};
DBO_EXTERN_TEMPLATES(Post)
#endif // __POST_H__

View File

@ -0,0 +1,7 @@
#include "Tag.h"
#include "Comment.h"
#include "Post.h"
#include "User.h"
#include <Wt/Dbo/Impl.h>
DBO_INSTANTIATE_TEMPLATES(Tag)

View File

@ -0,0 +1,26 @@
#ifndef __TAG_H__
#define __TAG_H__
#include <Wt/Dbo/Types.h>
class Post;
using Posts= Wt::Dbo::collection<Wt::Dbo::ptr<Post>> ;
class Tag {
public:
Tag() = default;
Tag(const std::string &aName) : name(aName) {
}
template <class Action>
void persist(Action &a) {
Wt::Dbo::field(a, name, "name");
Wt::Dbo::hasMany(a, posts, Wt::Dbo::ManyToMany, "post_tag");
}
std::string name;
Posts posts;
};
DBO_EXTERN_TEMPLATES(Tag)
#endif // __TAG_H__

View File

@ -0,0 +1,11 @@
#include "Token.h"
#include "User.h"
#include <Wt/Dbo/Impl.h>
DBO_INSTANTIATE_TEMPLATES(Token)
Token::Token() {
}
Token::Token(const std::string &v, const Wt::WDateTime &e) : value(v), expires(e) {
}

View File

@ -0,0 +1,30 @@
#ifndef __TOKENS_H__
#define __TOKENS_H__
#include <Wt/Dbo/Types.h>
#include <Wt/WDateTime.h>
class User;
class Token : public Wt::Dbo::Dbo<Token> {
public:
Token();
Token(const std::string &value, const Wt::WDateTime &expires);
Wt::Dbo::ptr<User> user;
std::string value;
Wt::WDateTime expires;
template <class Action>
void persist(Action &a) {
Wt::Dbo::field(a, value, "value");
Wt::Dbo::field(a, expires, "expires");
Wt::Dbo::belongsTo(a, user, "user");
}
};
DBO_EXTERN_TEMPLATES(Token)
#endif // __TOKENS_H__

View File

@ -11,5 +11,13 @@ Wt::Dbo::dbo_traits<User>::IdType User::stringToId(const std::string &s) {
return result; return result;
} }
Posts User::latestPosts(int count) const {
return posts.find().where("state = ?").bind(Post::Published).orderBy("date desc").limit(count);
}
Posts User::allPosts(Post::State state) const {
return posts.find().where("state = ?").bind(state).orderBy("date desc");
}
User::User() : role(Visitor), failedLoginAttempts(0) { User::User() : role(Visitor), failedLoginAttempts(0) {
} }

View File

@ -1,12 +1,15 @@
#ifndef __USER_H__ #ifndef __USER_H__
#define __USER_H__ #define __USER_H__
#include <Wt/Dbo/Types.h> #include "Post.h"
#include "Token.h"
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include <Wt/WGlobal.h> #include <Wt/WGlobal.h>
#include <Wt/WString.h> #include <Wt/WString.h>
using Tokens = Wt::Dbo::collection<Wt::Dbo::ptr<Token>>;
class User { class User {
public: public:
enum Role { enum Role {
@ -16,6 +19,8 @@ public:
User(); User();
static Wt::Dbo::dbo_traits<User>::IdType stringToId(const std::string &s); static Wt::Dbo::dbo_traits<User>::IdType stringToId(const std::string &s);
Posts latestPosts(int count = 10) const;
Posts allPosts(Post::State state) const;
Wt::WString name; Wt::WString name;
Role role; Role role;
int failedLoginAttempts; int failedLoginAttempts;
@ -23,6 +28,10 @@ public:
std::string oAuthId; std::string oAuthId;
std::string oAuthProvider; std::string oAuthProvider;
Tokens authTokens;
Comments comments;
Posts posts;
template <class Action> template <class Action>
void persist(Action &a) { void persist(Action &a) {
Wt::Dbo::field(a, name, "name"); Wt::Dbo::field(a, name, "name");
@ -30,6 +39,10 @@ public:
Wt::Dbo::field(a, lastLoginAttempt, "last_login_attempt"); Wt::Dbo::field(a, lastLoginAttempt, "last_login_attempt");
Wt::Dbo::field(a, oAuthId, "oauth_id"); Wt::Dbo::field(a, oAuthId, "oauth_id");
Wt::Dbo::field(a, oAuthProvider, "oauth_provider"); Wt::Dbo::field(a, oAuthProvider, "oauth_provider");
Wt::Dbo::hasMany(a, comments, Wt::Dbo::ManyToOne, "author");
Wt::Dbo::hasMany(a, posts, Wt::Dbo::ManyToOne, "author");
Wt::Dbo::hasMany(a, authTokens, Wt::Dbo::ManyToOne, "user");
} }
}; };

View File

@ -0,0 +1,94 @@
#include "asciidoc.h"
#include <fstream>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include "Wt/WString.h"
#ifndef WT_WIN32
#include <unistd.h>
#endif
namespace {
std::string tempFileName()
{
#ifndef WT_WIN32
char spool[20];
strcpy(spool, "/tmp/wtXXXXXX");
int i = mkstemp(spool);
close(i);
#else
char spool[2 * L_tmpnam];
tmpnam(spool);
#endif
return std::string(spool);
}
std::string readFileToString(const std::string& fileName)
{
std::fstream file(fileName.c_str(), std::ios::in | std::ios::binary | std::ios::ate);
int length = file.tellg();
file.seekg(0, std::ios::beg);
std::unique_ptr<char[]> buf(new char[length]);
file.read(buf.get(), length);
file.close();
return std::string(buf.get(), length);
}
}
Wt::WString asciidoc(const Wt::WString& src)
{
std::string srcFileName = tempFileName();
std::string htmlFileName = tempFileName();
{
std::ofstream srcFile(srcFileName.c_str(), std::ios::out);
std::string ssrc = src.toUTF8();
srcFile.write(ssrc.c_str(), (std::streamsize)ssrc.length());
srcFile.close();
}
#if defined(ASCIIDOCTOR_EXECUTABLE)
#define xstr(s) str(s)
#define str(s) #s
std::string cmd = xstr(ASCIIDOCTOR_EXECUTABLE);
#else
std::string cmd = "asciidoctor";
#endif
std::string command = cmd + " -a htmlsyntax=xml -o " + htmlFileName + " -s " + srcFileName;
#ifndef WT_WIN32
/*
* So, asciidoc apparently sends a SIGINT which is caught by its parent
* process.. So we have to temporarily ignore it.
*/
struct sigaction newAction, oldAction;
newAction.sa_handler = SIG_IGN;
newAction.sa_flags = 0;
sigemptyset(&newAction.sa_mask);
sigaction(SIGINT, &newAction, &oldAction);
#endif
bool ok = system(command.c_str()) == 0;
#ifndef WT_WIN32
sigaction(SIGINT, &oldAction, 0);
#endif
Wt::WString result;
if (ok) {
result = Wt::WString(readFileToString(htmlFileName));
} else
result = Wt::WString("<i>Could not execute asciidoc</i>");
unlink(srcFileName.c_str());
unlink(htmlFileName.c_str());
return result;
}

View File

@ -0,0 +1,7 @@
#ifndef __ASCIIDOC_H__
#define __ASCIIDOC_H__
#include <Wt/WString.h>
Wt::WString asciidoc(const Wt::WString &src);
#endif // __ASCIIDOC_H__

BIN
WebApplication/rss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,5 +1,6 @@
#include "BlogLoginWidget.h" #include "BlogLoginWidget.h"
#include "model/BlogSession.h" #include "model/BlogSession.h"
#include <Wt/Auth/PasswordService.h>
BlogLoginWidget::BlogLoginWidget(BlogSession &session, const std::string &basePath) : AuthWidget(session.login()) { BlogLoginWidget::BlogLoginWidget(BlogSession &session, const std::string &basePath) : AuthWidget(session.login()) {
setInline(true); setInline(true);

View File

@ -1,22 +1,31 @@
#include "BlogView.h" #include "BlogView.h"
#include "BlogLoginWidget.h"
#include "EditUsers.h" #include "EditUsers.h"
#include "PostView.h"
#include "model/BlogSession.h" #include "model/BlogSession.h"
#include "model/Post.h"
#include "model/Tag.h"
#include <Wt/WApplication.h> #include <Wt/WApplication.h>
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
#include <Wt/WPushButton.h>
#include <Wt/WStackedWidget.h>
#include <Wt/WText.h> #include <Wt/WText.h>
#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;
}
class BlogImpl : public Wt::WContainerWidget { class BlogImpl : public Wt::WContainerWidget {
public: public:
BlogImpl(const std::string &basePath, Wt::Dbo::SqlConnectionPool &connectionPool, const std::string &rssFeedUrl, BlogImpl(const std::string &basePath, Wt::Dbo::SqlConnectionPool &connectionPool, const std::string &rssFeedUrl,
BlogView *blogView) BlogView *blogView)
: m_basePath(basePath), m_rssFeedUrl(rssFeedUrl), m_session(connectionPool) { : m_basePath(basePath), m_rssFeedUrl(rssFeedUrl), m_session(connectionPool) {
Wt::WApplication *app = Wt::WApplication::instance(); Wt::WApplication::instance()->internalPathChanged().connect(this, &BlogImpl::handlePathChange);
app->messageResourceBundle().use(Wt::WApplication::appRoot() + "blog");
app->useStyleSheet("/css/blog.css");
app->useStyleSheet("/css/asciidoc.css");
app->internalPathChanged().connect(this, &BlogImpl::handlePathChange);
m_loginStatus = this->addWidget(std::make_unique<Wt::WTemplate>(tr("blog-login-status"))); m_loginStatus = this->addWidget(std::make_unique<Wt::WTemplate>(tr("blog-login-status")));
m_panel = this->addWidget(std::make_unique<Wt::WStackedWidget>()); m_panel = this->addWidget(std::make_unique<Wt::WStackedWidget>());
m_items = this->addWidget(std::make_unique<Wt::WContainerWidget>()); m_items = this->addWidget(std::make_unique<Wt::WContainerWidget>());
@ -30,74 +39,342 @@ public:
auto loginLink = std::make_unique<Wt::WText>(tr("login")); auto loginLink = std::make_unique<Wt::WText>(tr("login"));
auto lPtr = loginLink.get(); auto lPtr = loginLink.get();
loginLink->setStyleClass("link"); loginLink->setStyleClass("link");
loginLink->clicked().connect(loginWidget_, &WWidget::show); loginLink->clicked().connect(m_loginWidget, &WWidget::show);
loginLink->clicked().connect(lPtr, &WWidget::hide); loginLink->clicked().connect(lPtr, &WWidget::hide);
auto registerLink = std::make_unique<Wt::WText>(tr("Wt.Auth.register")); auto registerLink = std::make_unique<Wt::WText>(tr("Wt.Auth.register"));
registerLink->setStyleClass("link"); registerLink->setStyleClass("link");
registerLink->clicked().connect(loginWidget_, &BlogLoginWidget::registerNewUser); registerLink->clicked().connect(m_loginWidget, &BlogLoginWidget::registerNewUser);
auto archiveLink = auto archiveLink =
std::make_unique<Wt::WAnchor>(Wt::WLink(Wt::LinkType::InternalPath, basePath_ + "all"), tr("archive")); std::make_unique<Wt::WAnchor>(Wt::WLink(Wt::LinkType::InternalPath, m_basePath + "all"), tr("archive"));
loginStatus_->bindWidget("login", std::move(loginWidget)); m_loginStatus->bindWidget("login", std::move(loginWidget));
loginStatus_->bindWidget("login-link", std::move(loginLink)); m_loginStatus->bindWidget("login-link", std::move(loginLink));
loginStatus_->bindWidget("register-link", std::move(registerLink)); m_loginStatus->bindWidget("register-link", std::move(registerLink));
loginStatus_->bindString("feed-url", rssFeedUrl_); m_loginStatus->bindString("feed-url", m_rssFeedUrl);
loginStatus_->bindWidget("archive-link", std::move(archiveLink)); m_loginStatus->bindWidget("archive-link", std::move(archiveLink));
onUserChanged(); onUserChanged();
loginWidget_->processEnvironment(); m_loginWidget->processEnvironment();
} }
protected: protected:
void handlePathChange(const std::string &) { void handlePathChange(const std::string &) {
Wt::WApplication *app = Wt::WApplication::instance(); Wt::WApplication *app = Wt::WApplication::instance();
if (app->internalPathMatches(basePath_)) { if (app->internalPathMatches(m_basePath)) {
dbo::Transaction t(session_); Wt::Dbo::Transaction t(m_session);
std::string path = app->internalPathNextPart(basePath_); std::string path = app->internalPathNextPart(m_basePath);
items_->clear(); m_items->clear();
if (users_) { if (m_users) {
users_ = 0; m_users = 0;
} }
if (path.empty()) if (path.empty())
showPosts(session_ showPosts(m_session
.find<Post>("where state = ? " .find<Post>("where state = ? "
"order by date desc " "order by date desc "
"limit 10") "limit 10")
.bind(Post::Published), .bind(Post::Published),
items_); m_items);
else if (path == "author") { else if (path == "author") {
std::string author = app->internalPathNextPart(basePath_ + path + '/'); std::string author = app->internalPathNextPart(m_basePath + path + '/');
dbo::ptr<User> user = findUser(author); Wt::Dbo::ptr<User> user = findUser(author);
if (user) if (user)
showPosts(user); showPosts(user);
else else
showError(tr("blog-no-author").arg(author)); showError(tr("blog-no-author").arg(author));
} else if (path == "edituser") { } else if (path == "edituser") {
editUser(app->internalPathNextPart(basePath_ + path + '/')); editUser(app->internalPathNextPart(m_basePath + path + '/'));
} else if (path == "all") { } else if (path == "all") {
showArchive(items_); showArchive(m_items);
} else { } else {
std::string remainder = app->internalPath().substr(basePath_.length()); std::string remainder = app->internalPath().substr(m_basePath.length());
showPostsByDateTopic(remainder, items_); showPostsByDateTopic(remainder, m_items);
} }
t.commit(); t.commit();
} }
} }
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();
}
private: private:
std::string m_basePath, m_rssFeedUrl; std::string m_basePath, m_rssFeedUrl;
BlogSession m_session; BlogLoginWidget *m_loginWidget=nullptr; BlogSession m_session;
BlogLoginWidget *m_loginWidget = nullptr;
Wt::WStackedWidget *m_panel = nullptr; Wt::WStackedWidget *m_panel = nullptr;
Wt::WTemplate *m_authorPanel = nullptr; Wt::WTemplate *m_authorPanel = nullptr;
EditUsers *m_users = nullptr; EditUsers *m_users = nullptr;

View File

@ -0,0 +1,146 @@
#include "CommentView.h"
#include "model/BlogSession.h"
#include "model/Comment.h"
#include "model/User.h"
#include <Wt/WPushButton.h>
#include <Wt/WTextArea.h>
CommentView::CommentView(BlogSession &session, long long parentId) : session_(session) {
Wt::Dbo::ptr<Comment> parent = session_.load<Comment>(parentId);
comment_ = std::make_unique<Comment>();
comment_.modify()->parent = parent;
comment_.modify()->post = parent->post;
edit();
}
CommentView::CommentView(BlogSession &session, Wt::Dbo::ptr<Comment> comment) : session_(session), comment_(comment) {
comment_ = comment;
renderView();
}
void CommentView::cancel() {
if (isNew())
removeFromParent();
else {
Wt::Dbo::Transaction t(session_);
renderView();
t.commit();
}
}
void CommentView::renderView() {
clear();
bool isRootComment = !comment_->parent;
setTemplateText(isRootComment ? tr("blog-root-comment") : tr("blog-comment"));
bindString("collapse-expand", Wt::WString::Empty); // NYI
auto replyText = std::make_unique<Wt::WText>(isRootComment ? tr("comment-add") : tr("comment-reply"));
replyText->setStyleClass("link");
replyText->clicked().connect(this, &CommentView::reply);
bindWidget("reply", std::move(replyText));
bool mayEdit = session_.user() && (comment_->author == session_.user() || session_.user()->role == User::Admin);
if (mayEdit) {
auto editText = std::make_unique<Wt::WText>(tr("comment-edit"));
editText->setStyleClass("link");
editText->clicked().connect(this, &CommentView::edit);
bindWidget("edit", std::move(editText));
} else
bindString("edit", Wt::WString::Empty);
bool mayDelete =
(session_.user() && session_.user() == comment_->author) || session_.user() == comment_->post->author;
if (mayDelete) {
auto deleteText = std::make_unique<Wt::WText>(tr("comment-delete"));
deleteText->setStyleClass("link");
deleteText->clicked().connect(this, &CommentView::rm);
bindWidget("delete", std::move(deleteText));
} else
bindString("delete", Wt::WString::Empty);
typedef std::vector<Wt::Dbo::ptr<Comment>> CommentVector;
CommentVector comments;
{
Wt::Dbo::collection<Wt::Dbo::ptr<Comment>> cmts = comment_->children.find().orderBy("date");
comments.insert(comments.end(), cmts.begin(), cmts.end());
}
auto children = std::make_unique<Wt::WContainerWidget>();
for (int i = (int)comments.size() - 1; i >= 0; --i)
children->addWidget(std::make_unique<CommentView>(session_, comments[i]));
bindWidget("children", std::move(children));
}
bool CommentView::isNew() const {
return comment_.id() == -1;
}
void CommentView::rm() {
Wt::Dbo::Transaction t(session_);
comment_.modify()->setDeleted();
renderView();
t.commit();
}
void CommentView::reply() {
Wt::Dbo::Transaction t(session_);
Wt::WContainerWidget *c = resolve<Wt::WContainerWidget *>("children");
c->insertWidget(0, std::make_unique<CommentView>(session_, comment_.id()));
t.commit();
}
void CommentView::save() {
Wt::Dbo::Transaction t(session_);
bool isNew = comment_.id() == -1;
Comment *comment = comment_.modify();
comment->setText(editArea_->text());
if (isNew) {
session_.add(comment_);
comment->date = Wt::WDateTime::currentDateTime();
comment->author = session_.user();
session_.commentsChanged().emit(comment_);
}
renderView();
t.commit();
}
void CommentView::edit() {
clear();
Wt::Dbo::Transaction t(session_);
setTemplateText(tr("blog-edit-comment"));
auto editArea = std::make_unique<Wt::WTextArea>();
editArea_ = editArea.get();
editArea_->setText(comment_->textSrc());
editArea_->setFocus();
auto save = std::make_unique<Wt::WPushButton>(tr("save"));
save->clicked().connect(this, &CommentView::save);
auto cancel = std::make_unique<Wt::WPushButton>(tr("cancel"));
cancel->clicked().connect(this, &CommentView::cancel);
bindWidget("area", std::move(editArea));
bindWidget("save", std::move(save));
bindWidget("cancel", std::move(cancel));
t.commit();
}

View File

@ -0,0 +1,27 @@
#ifndef __COMMENTVIEW_H__
#define __COMMENTVIEW_H__
#include <Wt/Dbo/ptr.h>
#include <Wt/WTemplate.h>
class BlogSession;
class Comment;
class CommentView : public Wt::WTemplate {
public:
CommentView(BlogSession &session, long long parentId);
CommentView(BlogSession& session, Wt::Dbo::ptr<Comment> comment);
protected:
void edit();
void save();
void cancel();
void renderView();
bool isNew() const;
void reply();
void rm();
private:
BlogSession &session_;
Wt::Dbo::ptr<Comment> comment_;
Wt::WTextArea *editArea_;
};
#endif // __COMMENTVIEW_H__

View File

@ -1,4 +1,6 @@
#include "EditUsers.h" #include "EditUsers.h"
#include <Wt/WApplication.h>
#include <Wt/WBreak.h>
#include <Wt/WLineEdit.h> #include <Wt/WLineEdit.h>
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
@ -15,6 +17,10 @@ EditUsers::EditUsers(Wt::Dbo::Session &aSession, const std::string &basePath)
limitList(); limitList();
} }
void EditUsers::onUserClicked(Wt::Dbo::dbo_traits<User>::IdType id) {
Wt::WApplication::instance()->setInternalPath(m_basePath + "edituser/" + std::to_string(id), true);
}
void EditUsers::limitList() { void EditUsers::limitList() {
auto listPtr = std::make_unique<Wt::WContainerWidget>(); auto listPtr = std::make_unique<Wt::WContainerWidget>();
auto list = listPtr.get(); auto list = listPtr.get();
@ -38,3 +44,27 @@ EditUser::EditUser(Wt::Dbo::Session &aSession) : WTemplate(tr("edit-user")), ses
roleButton_ = bindWidget("role-button", std::move(roleButton)); roleButton_ = bindWidget("role-button", std::move(roleButton));
roleButton_->clicked().connect(this, &EditUser::switchRole); roleButton_->clicked().connect(this, &EditUser::switchRole);
} }
void EditUser::switchUser(Wt::Dbo::ptr<User> target) {
target_ = target;
bindTemplate();
}
void EditUser::bindTemplate() {
bindString("username", target_->name);
if (target_->role == User::Admin)
roleButton_->setText(tr("demote-admin"));
else
roleButton_->setText(tr("promote-user"));
}
void EditUser::switchRole() {
Wt::Dbo::Transaction t(session_);
target_.reread();
if (target_->role == User::Admin)
target_.modify()->role = User::Visitor;
else
target_.modify()->role = User::Admin;
t.commit();
bindTemplate();
}

View File

@ -0,0 +1,188 @@
#include "PostView.h"
#include "CommentView.h"
#include "model/BlogSession.h"
#include "model/User.h"
#include "model/asciidoc.h"
#include <Wt/WAnchor.h>
#include <Wt/WLineEdit.h>
#include <Wt/WLink.h>
#include <Wt/WPushButton.h>
#include <Wt/WText.h>
#include <Wt/WTextArea.h>
PostView::PostView(BlogSession &session, const std::string &basePath, Wt::Dbo::ptr<Post> post, RenderType type)
: session_(session), basePath_(basePath), post_(post) {
viewType_ = Brief;
render(type);
}
void PostView::render(RenderType type) {
if (type != Edit) viewType_ = type;
clear();
switch (type) {
case Detail: {
setTemplateText(tr("blog-post"));
session_.commentsChanged().connect(this, &PostView::updateCommentCount);
commentCount_ = bindWidget("comment-count", std::make_unique<Wt::WText>(post_->commentCount()));
bindWidget("comments", std::make_unique<CommentView>(session_, post_->rootComment()));
bindString("anchor", basePath_ + post_->permaLink());
break;
}
case Brief: {
setTemplateText(tr("blog-post-brief"));
auto titleAnchor = std::make_unique<Wt::WAnchor>(
Wt::WLink(Wt::LinkType::InternalPath, basePath_ + post_->permaLink()), post_->title);
bindWidget("title", std::move(titleAnchor));
if (!post_->briefSrc.empty()) {
auto moreAnchor = std::make_unique<Wt::WAnchor>(
Wt::WLink(Wt::LinkType::InternalPath, basePath_ + post_->permaLink() + "/more"), tr("blog-read-more"));
bindWidget("read-more", std::move(moreAnchor));
} else {
bindString("read-more", Wt::WString::Empty);
}
auto commentsAnchor = std::make_unique<Wt::WAnchor>(
Wt::WLink(Wt::LinkType::InternalPath, basePath_ + post_->permaLink() + "/comments"));
commentCount_ = commentsAnchor->addWidget(std::make_unique<Wt::WText>("(" + post_->commentCount() + ")"));
bindWidget("comment-count", std::move(commentsAnchor));
break;
}
case Edit: {
setTemplateText(tr("blog-post-edit"));
titleEdit_ = bindWidget("title-edit", std::make_unique<Wt::WLineEdit>(post_->title));
briefEdit_ = bindWidget("brief-edit", std::make_unique<Wt::WTextArea>(post_->briefSrc));
bodyEdit_ = bindWidget("body-edit", std::make_unique<Wt::WTextArea>(post_->bodySrc));
auto saveButton = bindWidget("save", std::make_unique<Wt::WPushButton>(tr("save")));
auto cancelButton = bindWidget("cancel", std::make_unique<Wt::WPushButton>(tr("cancel")));
saveButton->clicked().connect(this, &PostView::saveEdit);
cancelButton->clicked().connect(this, &PostView::showView);
break;
}
}
if (type == Detail || type == Brief) {
if (session_.user() == post_->author) {
std::unique_ptr<Wt::WPushButton> publishButton;
if (post_->state != Post::Published) {
publishButton = std::make_unique<Wt::WPushButton>(tr("publish"));
publishButton->clicked().connect(this, &PostView::publish);
} else {
publishButton = std::make_unique<Wt::WPushButton>(tr("retract"));
publishButton->clicked().connect(this, &PostView::retract);
}
bindWidget("publish", std::move(publishButton));
auto editButton(std::make_unique<Wt::WPushButton>(tr("edit")));
editButton->clicked().connect(this, &PostView::showEdit);
bindWidget("edit", std::move(editButton));
auto deleteButton(std::make_unique<Wt::WPushButton>(tr("delete")));
deleteButton->clicked().connect(this, &PostView::rm);
bindWidget("delete", std::move(deleteButton));
} else {
bindString("publish", Wt::WString::Empty);
bindString("edit", Wt::WString::Empty);
bindString("delete", Wt::WString::Empty);
}
}
auto postAnchor = std::make_unique<Wt::WAnchor>(
Wt::WLink(Wt::LinkType::InternalPath, basePath_ + "author/" + post_->author->name.toUTF8()),
post_->author->name);
bindWidget("author", std::move(postAnchor));
}
void PostView::publish() {
setState(Post::Published);
}
void PostView::showEdit() {
Wt::Dbo::Transaction t(session_);
render(Edit);
t.commit();
}
void PostView::rm() {
Wt::Dbo::Transaction t(session_);
post_.remove();
t.commit();
this->removeFromParent();
}
void PostView::retract() {
setState(Post::Unpublished);
}
void PostView::setState(Post::State state) {
Wt::Dbo::Transaction t(session_);
post_.modify()->state = state;
if (state == Post::Published) post_.modify()->date = Wt::WDateTime::currentDateTime();
render(viewType_);
t.commit();
}
void PostView::saveEdit() {
Wt::Dbo::Transaction t(session_);
bool newPost = post_.id() == -1;
Post *post = post_.modify();
post->title = titleEdit_->text();
post->briefSrc = briefEdit_->text();
post->bodySrc = bodyEdit_->text();
post->briefHtml = asciidoc(post->briefSrc);
post->bodyHtml = asciidoc(post->bodySrc);
if (newPost) {
session_.add(post_);
post->date = Wt::WDateTime::currentDateTime();
post->state = Post::Unpublished;
post->author = session_.user();
Wt::Dbo::ptr<Comment> rootComment = session_.add(std::make_unique<Comment>());
rootComment.modify()->post = post_;
}
session_.flush();
render(viewType_);
t.commit();
}
void PostView::showView() {
if (post_.id() == -1)
this->removeFromParent();
else {
Wt::Dbo::Transaction t(session_);
render(viewType_);
t.commit();
}
}
void PostView::updateCommentCount(Wt::Dbo::ptr<Comment> comment) {
if (comment->post == post_) {
std::string count = comment->post->commentCount();
if (commentCount_->text().toUTF8()[0] == '(')
commentCount_->setText("(" + count + ")");
else
commentCount_->setText(count);
}
}

View File

@ -0,0 +1,40 @@
#ifndef __POSTVIEW_H__
#define __POSTVIEW_H__
#include "model/Post.h"
#include <Wt/WTemplate.h>
class BlogSession;
class PostView : public Wt::WTemplate {
public:
enum RenderType {
Brief,
Detail,
Edit,
};
PostView(BlogSession &session, const std::string &basePath, Wt::Dbo::ptr<Post> post, RenderType type);
protected:
void render(RenderType type);
void updateCommentCount(Wt::Dbo::ptr<Comment> comment);
void saveEdit();
void showView();
void publish();
void retract();
void setState(Post::State state);
void showEdit();
void rm();
private:
BlogSession &session_;
std::string basePath_;
Wt::Dbo::ptr<Post> post_;
RenderType viewType_;
Wt::WText *commentCount_;
Wt::WLineEdit *titleEdit_;
Wt::WTextArea *briefEdit_, *bodyEdit_;
};
#endif // __POSTVIEW_H__