add code.
This commit is contained in:
parent
27ffee57be
commit
b6e0681489
@ -6,6 +6,11 @@ static const char *FeedUrl = "/blog/feed/";
|
||||
|
||||
BlogApplication::BlogApplication(const Wt::WEnvironment &env, Wt::Dbo::SqlConnectionPool &blogDb)
|
||||
: 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));
|
||||
useStyleSheet("css/blogexample.css");
|
||||
}
|
||||
|
@ -5,11 +5,19 @@ add_library(WebApplication
|
||||
Restful.h Restful.cpp
|
||||
Dialog.h Dialog.cpp
|
||||
Session.h Session.cpp
|
||||
model/asciidoc.h model/asciidoc.cpp
|
||||
model/BlogSession.h model/BlogSession.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
|
||||
view/BlogLoginWidget.h view/BlogLoginWidget.cpp
|
||||
view/BlogView.h view/BlogView.cpp
|
||||
view/CommentView.h view/CommentView.cpp
|
||||
view/EditUsers.h view/EditUsers.cpp
|
||||
view/PostView.h view/PostView.cpp
|
||||
)
|
||||
|
||||
target_include_directories(WebApplication
|
||||
|
@ -24,13 +24,15 @@ static std::unique_ptr<Wt::WApplication> createWidgetSet(const Wt::WEnvironment
|
||||
WebApplication::WebApplication() {
|
||||
try {
|
||||
std::vector<std::string> args;
|
||||
args.push_back("--approot=./build");
|
||||
args.push_back("--docroot=./build");
|
||||
args.push_back("--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->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,
|
||||
std::bind(&createBlogApplication, std::placeholders::_1, blogDb.get()), "/blog");
|
||||
m_server->addEntryPoint(Wt::EntryPointType::WidgetSet, createWidgetSet, "/gui/hello.js");
|
||||
|
296
WebApplication/asciidoc.css
Normal file
296
WebApplication/asciidoc.css
Normal 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
128
WebApplication/blog.css
Normal 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
258
WebApplication/blog.xml
Normal 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 (<code>...</code>)</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 >></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>
|
52
WebApplication/blogexample.css
Normal file
52
WebApplication/blogexample.css
Normal 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;
|
||||
}
|
||||
|
@ -1,10 +1,45 @@
|
||||
#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/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)
|
||||
: 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -2,16 +2,36 @@
|
||||
#define __BLOGSESSION_H__
|
||||
|
||||
#include "BlogUserDatabase.h"
|
||||
#include <Wt/Auth/Login.h>
|
||||
#include <Wt/Auth/OAuthService.h>
|
||||
#include <Wt/Dbo/Session.h>
|
||||
#include <Wt/Dbo/backend/Sqlite3.h>
|
||||
|
||||
class Comment;
|
||||
|
||||
class BlogSession : public Wt::Dbo::Session {
|
||||
public:
|
||||
BlogSession(Wt::Dbo::SqlConnectionPool &connectionPool);
|
||||
static void configureAuth();
|
||||
|
||||
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:
|
||||
Wt::Dbo::SqlConnectionPool &m_connectionPool;
|
||||
BlogUserDatabase m_users;
|
||||
Wt::Auth::Login m_login;
|
||||
Wt::Signal<Wt::Dbo::ptr<Comment>> commentsChanged_;
|
||||
};
|
||||
#endif // __BLOGSESSION_H__
|
@ -33,6 +33,11 @@ BlogUserDatabase::BlogUserDatabase(Wt::Dbo::Session &session) : m_session(sessio
|
||||
BlogUserDatabase::~BlogUserDatabase() {
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<User> BlogUserDatabase::find(const Wt::Auth::User &user) const {
|
||||
getUser(user.id());
|
||||
return m_user;
|
||||
}
|
||||
|
||||
BlogUserDatabase::Transaction *BlogUserDatabase::startTransaction() {
|
||||
return new TransactionImpl(m_session);
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ class BlogUserDatabase : public Wt::Auth::AbstractUserDatabase {
|
||||
public:
|
||||
BlogUserDatabase(Wt::Dbo::Session &session);
|
||||
~BlogUserDatabase();
|
||||
Wt::Dbo::ptr<User> find(const Wt::Auth::User &user) const;
|
||||
|
||||
Transaction *startTransaction() 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;
|
||||
|
76
WebApplication/model/Comment.cpp
Normal file
76
WebApplication/model/Comment.cpp
Normal 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 <code>...</code> with <pre>...</pre>
|
||||
// This is kind of very ad-hoc!
|
||||
|
||||
while ((b = html.find("<code>", b)) != std::string::npos) {
|
||||
std::string::size_type e = html.find("</code>", 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");
|
||||
}
|
50
WebApplication/model/Comment.h
Normal file
50
WebApplication/model/Comment.h
Normal 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__
|
36
WebApplication/model/Post.cpp
Normal file
36
WebApplication/model/Post.cpp
Normal 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>();
|
||||
}
|
58
WebApplication/model/Post.h
Normal file
58
WebApplication/model/Post.h
Normal 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__
|
7
WebApplication/model/Tag.cpp
Normal file
7
WebApplication/model/Tag.cpp
Normal 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)
|
26
WebApplication/model/Tag.h
Normal file
26
WebApplication/model/Tag.h
Normal 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__
|
11
WebApplication/model/Token.cpp
Normal file
11
WebApplication/model/Token.cpp
Normal 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) {
|
||||
}
|
30
WebApplication/model/Token.h
Normal file
30
WebApplication/model/Token.h
Normal 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__
|
@ -11,5 +11,13 @@ Wt::Dbo::dbo_traits<User>::IdType User::stringToId(const std::string &s) {
|
||||
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) {
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
#ifndef __USER_H__
|
||||
#define __USER_H__
|
||||
|
||||
#include <Wt/Dbo/Types.h>
|
||||
#include "Post.h"
|
||||
#include "Token.h"
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/WGlobal.h>
|
||||
#include <Wt/WString.h>
|
||||
|
||||
using Tokens = Wt::Dbo::collection<Wt::Dbo::ptr<Token>>;
|
||||
|
||||
class User {
|
||||
public:
|
||||
enum Role {
|
||||
@ -16,6 +19,8 @@ public:
|
||||
User();
|
||||
|
||||
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;
|
||||
Role role;
|
||||
int failedLoginAttempts;
|
||||
@ -23,6 +28,10 @@ public:
|
||||
std::string oAuthId;
|
||||
std::string oAuthProvider;
|
||||
|
||||
Tokens authTokens;
|
||||
Comments comments;
|
||||
Posts posts;
|
||||
|
||||
template <class Action>
|
||||
void persist(Action &a) {
|
||||
Wt::Dbo::field(a, name, "name");
|
||||
@ -30,6 +39,10 @@ public:
|
||||
Wt::Dbo::field(a, lastLoginAttempt, "last_login_attempt");
|
||||
Wt::Dbo::field(a, oAuthId, "oauth_id");
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
||||
|
94
WebApplication/model/asciidoc.cpp
Normal file
94
WebApplication/model/asciidoc.cpp
Normal 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;
|
||||
}
|
7
WebApplication/model/asciidoc.h
Normal file
7
WebApplication/model/asciidoc.h
Normal 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
BIN
WebApplication/rss.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -1,5 +1,6 @@
|
||||
#include "BlogLoginWidget.h"
|
||||
#include "model/BlogSession.h"
|
||||
#include <Wt/Auth/PasswordService.h>
|
||||
|
||||
BlogLoginWidget::BlogLoginWidget(BlogSession &session, const std::string &basePath) : AuthWidget(session.login()) {
|
||||
setInline(true);
|
||||
|
@ -1,22 +1,31 @@
|
||||
#include "BlogView.h"
|
||||
#include "BlogLoginWidget.h"
|
||||
#include "EditUsers.h"
|
||||
#include "PostView.h"
|
||||
#include "model/BlogSession.h"
|
||||
#include "model/Post.h"
|
||||
#include "model/Tag.h"
|
||||
#include <Wt/WApplication.h>
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/WStackedWidget.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 {
|
||||
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) {
|
||||
Wt::WApplication *app = Wt::WApplication::instance();
|
||||
|
||||
app->messageResourceBundle().use(Wt::WApplication::appRoot() + "blog");
|
||||
app->useStyleSheet("/css/blog.css");
|
||||
app->useStyleSheet("/css/asciidoc.css");
|
||||
app->internalPathChanged().connect(this, &BlogImpl::handlePathChange);
|
||||
|
||||
Wt::WApplication::instance()->internalPathChanged().connect(this, &BlogImpl::handlePathChange);
|
||||
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>());
|
||||
@ -30,74 +39,342 @@ public:
|
||||
auto loginLink = std::make_unique<Wt::WText>(tr("login"));
|
||||
auto lPtr = loginLink.get();
|
||||
loginLink->setStyleClass("link");
|
||||
loginLink->clicked().connect(loginWidget_, &WWidget::show);
|
||||
loginLink->clicked().connect(m_loginWidget, &WWidget::show);
|
||||
loginLink->clicked().connect(lPtr, &WWidget::hide);
|
||||
|
||||
auto registerLink = std::make_unique<Wt::WText>(tr("Wt.Auth.register"));
|
||||
registerLink->setStyleClass("link");
|
||||
registerLink->clicked().connect(loginWidget_, &BlogLoginWidget::registerNewUser);
|
||||
registerLink->clicked().connect(m_loginWidget, &BlogLoginWidget::registerNewUser);
|
||||
|
||||
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));
|
||||
loginStatus_->bindWidget("login-link", std::move(loginLink));
|
||||
loginStatus_->bindWidget("register-link", std::move(registerLink));
|
||||
loginStatus_->bindString("feed-url", rssFeedUrl_);
|
||||
loginStatus_->bindWidget("archive-link", std::move(archiveLink));
|
||||
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));
|
||||
|
||||
onUserChanged();
|
||||
|
||||
loginWidget_->processEnvironment();
|
||||
m_loginWidget->processEnvironment();
|
||||
}
|
||||
|
||||
protected:
|
||||
void handlePathChange(const std::string &) {
|
||||
Wt::WApplication *app = Wt::WApplication::instance();
|
||||
|
||||
if (app->internalPathMatches(basePath_)) {
|
||||
dbo::Transaction t(session_);
|
||||
if (app->internalPathMatches(m_basePath)) {
|
||||
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_) {
|
||||
users_ = 0;
|
||||
if (m_users) {
|
||||
m_users = 0;
|
||||
}
|
||||
|
||||
if (path.empty())
|
||||
showPosts(session_
|
||||
showPosts(m_session
|
||||
.find<Post>("where state = ? "
|
||||
"order by date desc "
|
||||
"limit 10")
|
||||
.bind(Post::Published),
|
||||
items_);
|
||||
m_items);
|
||||
|
||||
else if (path == "author") {
|
||||
std::string author = app->internalPathNextPart(basePath_ + path + '/');
|
||||
dbo::ptr<User> user = findUser(author);
|
||||
std::string author = app->internalPathNextPart(m_basePath + path + '/');
|
||||
Wt::Dbo::ptr<User> user = findUser(author);
|
||||
|
||||
if (user)
|
||||
showPosts(user);
|
||||
else
|
||||
showError(tr("blog-no-author").arg(author));
|
||||
} else if (path == "edituser") {
|
||||
editUser(app->internalPathNextPart(basePath_ + path + '/'));
|
||||
editUser(app->internalPathNextPart(m_basePath + path + '/'));
|
||||
} else if (path == "all") {
|
||||
showArchive(items_);
|
||||
showArchive(m_items);
|
||||
} else {
|
||||
std::string remainder = app->internalPath().substr(basePath_.length());
|
||||
showPostsByDateTopic(remainder, items_);
|
||||
std::string remainder = app->internalPath().substr(m_basePath.length());
|
||||
showPostsByDateTopic(remainder, m_items);
|
||||
}
|
||||
|
||||
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:
|
||||
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::WTemplate *m_authorPanel = nullptr;
|
||||
EditUsers *m_users = nullptr;
|
||||
|
146
WebApplication/view/CommentView.cpp
Normal file
146
WebApplication/view/CommentView.cpp
Normal 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();
|
||||
}
|
27
WebApplication/view/CommentView.h
Normal file
27
WebApplication/view/CommentView.h
Normal 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__
|
@ -1,4 +1,6 @@
|
||||
#include "EditUsers.h"
|
||||
#include <Wt/WApplication.h>
|
||||
#include <Wt/WBreak.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
@ -15,6 +17,10 @@ EditUsers::EditUsers(Wt::Dbo::Session &aSession, const std::string &basePath)
|
||||
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() {
|
||||
auto listPtr = std::make_unique<Wt::WContainerWidget>();
|
||||
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_->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();
|
||||
}
|
||||
|
188
WebApplication/view/PostView.cpp
Normal file
188
WebApplication/view/PostView.cpp
Normal 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);
|
||||
}
|
||||
}
|
40
WebApplication/view/PostView.h
Normal file
40
WebApplication/view/PostView.h
Normal 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__
|
Loading…
Reference in New Issue
Block a user