From b6e06814890b04cd11e61696f0b9fcf99c4f21c9 Mon Sep 17 00:00:00 2001 From: amass <168062547@qq.com> Date: Sat, 2 Nov 2024 00:30:14 +0800 Subject: [PATCH] add code. --- WebApplication/BlogApplication.cpp | 5 + WebApplication/CMakeLists.txt | 8 + WebApplication/WebApplication.cpp | 4 +- WebApplication/asciidoc.css | 296 +++++++++++++++++++ WebApplication/blog.css | 128 ++++++++ WebApplication/blog.xml | 258 ++++++++++++++++ WebApplication/blogexample.css | 52 ++++ WebApplication/model/BlogSession.cpp | 50 ++++ WebApplication/model/BlogSession.h | 20 ++ WebApplication/model/BlogUserDatabase.cpp | 5 + WebApplication/model/BlogUserDatabase.h | 2 + WebApplication/model/Comment.cpp | 76 +++++ WebApplication/model/Comment.h | 50 ++++ WebApplication/model/Post.cpp | 36 +++ WebApplication/model/Post.h | 58 ++++ WebApplication/model/Tag.cpp | 7 + WebApplication/model/Tag.h | 26 ++ WebApplication/model/Token.cpp | 11 + WebApplication/model/Token.h | 30 ++ WebApplication/model/User.cpp | 8 + WebApplication/model/User.h | 15 +- WebApplication/model/asciidoc.cpp | 94 ++++++ WebApplication/model/asciidoc.h | 7 + WebApplication/rss.png | Bin 0 -> 1534 bytes WebApplication/view/BlogLoginWidget.cpp | 1 + WebApplication/view/BlogView.cpp | 339 ++++++++++++++++++++-- WebApplication/view/CommentView.cpp | 146 ++++++++++ WebApplication/view/CommentView.h | 27 ++ WebApplication/view/EditUsers.cpp | 30 ++ WebApplication/view/PostView.cpp | 188 ++++++++++++ WebApplication/view/PostView.h | 40 +++ 31 files changed, 1984 insertions(+), 33 deletions(-) create mode 100644 WebApplication/asciidoc.css create mode 100644 WebApplication/blog.css create mode 100644 WebApplication/blog.xml create mode 100644 WebApplication/blogexample.css create mode 100644 WebApplication/model/Comment.cpp create mode 100644 WebApplication/model/Comment.h create mode 100644 WebApplication/model/Post.cpp create mode 100644 WebApplication/model/Post.h create mode 100644 WebApplication/model/Tag.cpp create mode 100644 WebApplication/model/Tag.h create mode 100644 WebApplication/model/Token.cpp create mode 100644 WebApplication/model/Token.h create mode 100644 WebApplication/model/asciidoc.cpp create mode 100644 WebApplication/model/asciidoc.h create mode 100644 WebApplication/rss.png create mode 100644 WebApplication/view/CommentView.cpp create mode 100644 WebApplication/view/CommentView.h create mode 100644 WebApplication/view/PostView.cpp create mode 100644 WebApplication/view/PostView.h diff --git a/WebApplication/BlogApplication.cpp b/WebApplication/BlogApplication.cpp index c1a69ae..ccc949d 100644 --- a/WebApplication/BlogApplication.cpp +++ b/WebApplication/BlogApplication.cpp @@ -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("/", blogDb, FeedUrl)); useStyleSheet("css/blogexample.css"); } diff --git a/WebApplication/CMakeLists.txt b/WebApplication/CMakeLists.txt index 95cdc73..5e9c680 100644 --- a/WebApplication/CMakeLists.txt +++ b/WebApplication/CMakeLists.txt @@ -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 diff --git a/WebApplication/WebApplication.cpp b/WebApplication/WebApplication.cpp index ce6daf6..6e1f3a4 100644 --- a/WebApplication/WebApplication.cpp +++ b/WebApplication/WebApplication.cpp @@ -24,13 +24,15 @@ static std::unique_ptr createWidgetSet(const Wt::WEnvironment WebApplication::WebApplication() { try { std::vector 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("./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"); diff --git a/WebApplication/asciidoc.css b/WebApplication/asciidoc.css new file mode 100644 index 0000000..ac97edf --- /dev/null +++ b/WebApplication/asciidoc.css @@ -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; +} diff --git a/WebApplication/blog.css b/WebApplication/blog.css new file mode 100644 index 0000000..180b45b --- /dev/null +++ b/WebApplication/blog.css @@ -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; +} diff --git a/WebApplication/blog.xml b/WebApplication/blog.xml new file mode 100644 index 0000000..ec0ba0b --- /dev/null +++ b/WebApplication/blog.xml @@ -0,0 +1,258 @@ + + + + + ${user-name} ${password} ${remember-me} Remember me + ${login}${}, or use ${icons}${} + + + + + + + +

Registered users

+ ${user-list} + Limit list to user names containing : ${limit-edit} ${limit-button} +
+ + +
+

Edit user ${username}

+ ${role-button} +
+
+ + +

Editing user ${user}

+ ${save-button} ${cancel-button} +
+ + +
+

This user id is invalid

+
+
+ + +
+

You need to log in to access this function

+
+
+ + +
+

You need to be administrator to access this function

+
+
+ + +

Register

+

+ To register as a user to post comments with your own login, + please fill out the following form. +

+ + + + + + + + + + + + + + + + + + + + + + + +
Login:${name}
Password:${passwd}
Repeat password:${passwd2}
+ ${ok-button} + ${cancel-button} +
+
+ + +
+

Registered users

+ Search for ${searchstring} ${search-button}
+
+
+ + +
+

Profile panel for ${user}

+ + + + + + + + + + + + + + + +
New password:${passwd}
Repeat password:${passwd2}
+ ${ok-button} + ${cancel-button} +
+
+
+ + +
+

Author panel for ${user}

+ Statistics: +
    +
  • ${unpublished-count} unpublished post(s)
  • +
  • ${published-count} published post(s)
  • +
+ ${new-post} + ${unpublished-posts} +
+
+ + +

${title}

+
+
by ${author} on ${date}
+
+ ${brief+body} +
+
${comment-count}
+
+ ${publish} ${edit} ${delete} +
+
+
+ ${comments} +
+
+ + +

${title}

+
by ${author} on ${date}
+
+ ${brief} +
+
${read-more}
+
+ ${publish} ${edit} ${delete} +
+
${comment-count}
+
+ + +

+
+
Title: ${title-edit}
+
${brief-edit}
+
${body-edit}
+
+ ${save} ${cancel} +
+
+
+ + +
+
+
+ ${author} + ${date} ${collapse-expand} +
+ ${contents} + + ${children} +
+ + + + + ${children} + + + +
+
+
${area}
+
plain text or (<code>...</code>)
+ ${save} + ${cancel} +
+ + + +

No posts found

+ Sorry, no blog posts found that match your selection. +
+ + +

No author

+ Sorry, {1} is not a registered blog author. +
+ + +

Archive

+
+ + Login + Login too short (must be at least {1} characters) + Passwords don't match. + Register + Logout + Archive + Profile + Authoring panel + Edit users + Add comment + Reply + Edit + Delete + [[Comment deleted]] + Read the rest of this post >> + New post + Publish + Retract + Delete + Edit + Save + Cancel + Search + Demote this administrator to a regular visitor + Promote this user to administrator + No users found + diff --git a/WebApplication/blogexample.css b/WebApplication/blogexample.css new file mode 100644 index 0000000..c1f1666 --- /dev/null +++ b/WebApplication/blogexample.css @@ -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; +} + diff --git a/WebApplication/model/BlogSession.cpp b/WebApplication/model/BlogSession.cpp index 2200723..4b413a5 100644 --- a/WebApplication/model/BlogSession.cpp +++ b/WebApplication/model/BlogSession.cpp @@ -1,10 +1,45 @@ #include "BlogSession.h" +#include +#include +#include #include +#include +#include +#include + +class BlogOAuth : public std::vector { +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 verifier = std::make_unique(); + verifier->addHashFunction(std::make_unique(7)); +#ifdef WT_WITH_SSL + verifier->addHashFunction(std::make_unique()); +#endif +#ifdef HAVE_CRYPT + verifier->addHashFunction(std::make_unique()); +#endif + blogPasswords.setVerifier(std::move(verifier)); + blogPasswords.setPasswordThrottle(std::make_unique()); + blogPasswords.setStrengthValidator(std::make_unique()); + + if (Wt::Auth::GoogleService::configured()) blogOAuth.push_back(new Wt::Auth::GoogleService(blogAuth)); +} + std::unique_ptr BlogSession::createConnectionPool(const std::string &sqlite3) { auto connection = std::make_unique(sqlite3); @@ -14,3 +49,18 @@ std::unique_ptr BlogSession::createConnectionPool(co return std::make_unique(std::move(connection), 10); } + +Wt::Dbo::ptr BlogSession::user() const { + if (m_login.loggedIn()) + return m_users.find(m_login.user()); + else + return Wt::Dbo::ptr(); +} + +Wt::Auth::PasswordService *BlogSession::passwordAuth() const { + return &blogPasswords; +} + +const std::vector &BlogSession::oAuth() const { + return blogOAuth; +} diff --git a/WebApplication/model/BlogSession.h b/WebApplication/model/BlogSession.h index b19b8c3..084c279 100644 --- a/WebApplication/model/BlogSession.h +++ b/WebApplication/model/BlogSession.h @@ -2,16 +2,36 @@ #define __BLOGSESSION_H__ #include "BlogUserDatabase.h" +#include +#include #include #include +class Comment; + class BlogSession : public Wt::Dbo::Session { public: BlogSession(Wt::Dbo::SqlConnectionPool &connectionPool); + static void configureAuth(); + static std::unique_ptr createConnectionPool(const std::string &sqlite3); + Wt::Auth::Login &login() { + return m_login; + } + Wt::Dbo::ptr user() const; + Wt::Signal> &commentsChanged() { + return commentsChanged_; + } + BlogUserDatabase &users() { + return m_users; + } + Wt::Auth::PasswordService *passwordAuth() const; + const std::vector &oAuth() const; private: Wt::Dbo::SqlConnectionPool &m_connectionPool; BlogUserDatabase m_users; + Wt::Auth::Login m_login; + Wt::Signal> commentsChanged_; }; #endif // __BLOGSESSION_H__ \ No newline at end of file diff --git a/WebApplication/model/BlogUserDatabase.cpp b/WebApplication/model/BlogUserDatabase.cpp index ec322c1..09011bf 100644 --- a/WebApplication/model/BlogUserDatabase.cpp +++ b/WebApplication/model/BlogUserDatabase.cpp @@ -33,6 +33,11 @@ BlogUserDatabase::BlogUserDatabase(Wt::Dbo::Session &session) : m_session(sessio BlogUserDatabase::~BlogUserDatabase() { } +Wt::Dbo::ptr BlogUserDatabase::find(const Wt::Auth::User &user) const { + getUser(user.id()); + return m_user; +} + BlogUserDatabase::Transaction *BlogUserDatabase::startTransaction() { return new TransactionImpl(m_session); } diff --git a/WebApplication/model/BlogUserDatabase.h b/WebApplication/model/BlogUserDatabase.h index 3af3e25..6156258 100644 --- a/WebApplication/model/BlogUserDatabase.h +++ b/WebApplication/model/BlogUserDatabase.h @@ -11,6 +11,8 @@ class BlogUserDatabase : public Wt::Auth::AbstractUserDatabase { public: BlogUserDatabase(Wt::Dbo::Session &session); ~BlogUserDatabase(); + Wt::Dbo::ptr 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; diff --git a/WebApplication/model/Comment.cpp b/WebApplication/model/Comment.cpp new file mode 100644 index 0000000..2a04050 --- /dev/null +++ b/WebApplication/model/Comment.cpp @@ -0,0 +1,76 @@ +#include "Comment.h" +#include "Post.h" +#include "Tag.h" +#include "User.h" +#include +#include + +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
...
+ // 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) == "
") { + html.erase(b - 6, 6); + b -= 6; + e -= 6; + } + + html.replace(b, 12, "
");
+            e -= 7;
+
+            if (html.substr(b + 5, 6) == "
") { + html.erase(b + 5, 6); + e -= 6; + } + + if (html.substr(e - 6, 6) == "
") { + html.erase(e - 6, 6); + e -= 6; + } + + html.replace(e, 13, "
"); + e += 6; + + if (e + 6 <= html.length() && html.substr(e, 6) == "
") { + html.erase(e, 6); + e -= 6; + } + + b = e; + } + } + + // We would also want to replace

(empty line) with + //
+ replace(html, "

", "
"); + + textHtml_ = Wt::WString(html); +} + +void Comment::setDeleted() { + textHtml_ = Wt::WString::tr("comment-deleted"); +} diff --git a/WebApplication/model/Comment.h b/WebApplication/model/Comment.h new file mode 100644 index 0000000..390d91d --- /dev/null +++ b/WebApplication/model/Comment.h @@ -0,0 +1,50 @@ +#ifndef __COMMENT_H__ +#define __COMMENT_H__ + +#include +#include +#include + +class Comment; +using Comments = Wt::Dbo::collection>; +class Post; +class User; + +class Comment { +public: + Wt::Dbo::ptr author; + Wt::Dbo::ptr post; + Wt::Dbo::ptr 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 + 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__ \ No newline at end of file diff --git a/WebApplication/model/Post.cpp b/WebApplication/model/Post.cpp new file mode 100644 index 0000000..3de1cd5 --- /dev/null +++ b/WebApplication/model/Post.cpp @@ -0,0 +1,36 @@ +#include "Post.h" +#include "User.h" +#include + +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 Post::rootComment() const { + if (session()) + return session()->find().where("post_id = ?").bind(id()).where("parent_id is null"); + else + return Wt::Dbo::ptr(); +} diff --git a/WebApplication/model/Post.h b/WebApplication/model/Post.h new file mode 100644 index 0000000..7d9a668 --- /dev/null +++ b/WebApplication/model/Post.h @@ -0,0 +1,58 @@ +#ifndef __POST_H__ +#define __POST_H__ + +#include "Comment.h" +#include "Tag.h" +#include +#include +#include + +class User; + +typedef Wt::Dbo::collection> Comments; +typedef Wt::Dbo::collection> Tags; + +class Post : public Wt::Dbo::Dbo { +public: + enum State { + Unpublished = 0, + Published = 1, + }; + + std::string permaLink() const; + std::string commentCount() const; + std::string titleToUrl() const; + Wt::Dbo::ptr rootComment() const; + + template + 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 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__ \ No newline at end of file diff --git a/WebApplication/model/Tag.cpp b/WebApplication/model/Tag.cpp new file mode 100644 index 0000000..e296fdd --- /dev/null +++ b/WebApplication/model/Tag.cpp @@ -0,0 +1,7 @@ +#include "Tag.h" +#include "Comment.h" +#include "Post.h" +#include "User.h" +#include + +DBO_INSTANTIATE_TEMPLATES(Tag) \ No newline at end of file diff --git a/WebApplication/model/Tag.h b/WebApplication/model/Tag.h new file mode 100644 index 0000000..c1e9641 --- /dev/null +++ b/WebApplication/model/Tag.h @@ -0,0 +1,26 @@ +#ifndef __TAG_H__ +#define __TAG_H__ + +#include + +class Post; +using Posts= Wt::Dbo::collection> ; + +class Tag { +public: + Tag() = default; + Tag(const std::string &aName) : name(aName) { + } + template + 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__ \ No newline at end of file diff --git a/WebApplication/model/Token.cpp b/WebApplication/model/Token.cpp new file mode 100644 index 0000000..90106dd --- /dev/null +++ b/WebApplication/model/Token.cpp @@ -0,0 +1,11 @@ +#include "Token.h" +#include "User.h" +#include + +DBO_INSTANTIATE_TEMPLATES(Token) + +Token::Token() { +} + +Token::Token(const std::string &v, const Wt::WDateTime &e) : value(v), expires(e) { +} diff --git a/WebApplication/model/Token.h b/WebApplication/model/Token.h new file mode 100644 index 0000000..5e83cd6 --- /dev/null +++ b/WebApplication/model/Token.h @@ -0,0 +1,30 @@ +#ifndef __TOKENS_H__ +#define __TOKENS_H__ + +#include +#include + +class User; + +class Token : public Wt::Dbo::Dbo { +public: + Token(); + Token(const std::string &value, const Wt::WDateTime &expires); + + Wt::Dbo::ptr user; + + std::string value; + Wt::WDateTime expires; + + template + 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__ \ No newline at end of file diff --git a/WebApplication/model/User.cpp b/WebApplication/model/User.cpp index f8b815a..4669d13 100644 --- a/WebApplication/model/User.cpp +++ b/WebApplication/model/User.cpp @@ -11,5 +11,13 @@ Wt::Dbo::dbo_traits::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) { } diff --git a/WebApplication/model/User.h b/WebApplication/model/User.h index d4c86e5..5336569 100644 --- a/WebApplication/model/User.h +++ b/WebApplication/model/User.h @@ -1,12 +1,15 @@ #ifndef __USER_H__ #define __USER_H__ -#include +#include "Post.h" +#include "Token.h" #include #include #include #include +using Tokens = Wt::Dbo::collection>; + class User { public: enum Role { @@ -16,6 +19,8 @@ public: User(); static Wt::Dbo::dbo_traits::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 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"); } }; diff --git a/WebApplication/model/asciidoc.cpp b/WebApplication/model/asciidoc.cpp new file mode 100644 index 0000000..21912f1 --- /dev/null +++ b/WebApplication/model/asciidoc.cpp @@ -0,0 +1,94 @@ +#include "asciidoc.h" + +#include +#include +#include +#include + +#include "Wt/WString.h" + +#ifndef WT_WIN32 +#include +#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 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("Could not execute asciidoc"); + + unlink(srcFileName.c_str()); + unlink(htmlFileName.c_str()); + + return result; +} diff --git a/WebApplication/model/asciidoc.h b/WebApplication/model/asciidoc.h new file mode 100644 index 0000000..3b1c607 --- /dev/null +++ b/WebApplication/model/asciidoc.h @@ -0,0 +1,7 @@ +#ifndef __ASCIIDOC_H__ +#define __ASCIIDOC_H__ + +#include + +Wt::WString asciidoc(const Wt::WString &src); +#endif // __ASCIIDOC_H__ \ No newline at end of file diff --git a/WebApplication/rss.png b/WebApplication/rss.png new file mode 100644 index 0000000000000000000000000000000000000000..09e756e25f83d53e989a95a200edf50368eec04f GIT binary patch literal 1534 zcmVP000^Y0ssI2%^zTz00001b5ch_0Itp) z=>Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^RU3j_@wCwRnI<^TW%kV!;AR5;6h zlu3+TXAyq4$pNv; zjujxWKq3neC`=X-ScWGfE8LO(e{Lk{$V|wL{S_2fgkeZPYfh#0rCP6}F%%DM-AZii_Lcip6e17{wU%slbE3J`IT7oJ_(Dr!Rv7hZx z+&(LW5e_=w?jj_K)%oErXEdGd&<+tW%;^wZlt{9mL#jk!Loha)0Z9%VZD3jRyBJ~zwt93{I<>< zl6}u$ayqVD58Wl5eJ6eR*Q(ys?T?G)*~VSIezTqbV-QGHptp#;gE*+}xV}8&to5Ym zSwi^Scj_A+kk$s>@L;y%34ZEpg(KX0%Zr>J{Vk1b;mnQweB-MJ+<&Qk!;<4|K! z_8R(w8I>^;pg_nz{Z(sErkDhe%KG8mO)Fizis+ z89WLI*b4R7jBw1{0M&TrFXfqE%GFaT4ZSrPTI0#8Fnw^?7!4Orgw>_wh0c5#FdyCG zm)?-h)lxKbgTPjUet$+VW-vIBRsAqnu=x*o?s&ZRFKbT9mOV_dKE1zKxuw@n+uZy3 zDb7VrI8g)r1V`$9P}m8B~E zV2wRbdC1_T(+RkV8KP;X0ez8xnxE#Dm;*Whgf9(vAB<%9AB;Zs*3`YNm+QrBmVz?i{}#jdn(h z^H;dGHL=4TSoDdw z(cz$bAVAQGk+^c_$A?;9f5mmYd;EtJ{n;c8!NbzU{QPf}DJ@(JsZ@bpK3RwvD2o?7 zfFeQCkiykxCz_MXa3;7o`V|Fei>H7@skxUBuM!D5G-wpfhGirifIxzZdw?yhOiTIX zx6%T@OiW;0r)a6WfC*VLOP-}v7i2^bz*2~%2LyqN1QM2_SU!hv=mi=5%F*6^`_pnM z>I}dd3jVJK_`N>Y(7Q9HE&yi{cR$lQbcA_T`qJ#`Yd>oI{nZfrAlH)j|BLX*6q2vD kx7WXLWZjpJ>d5$i0Y@5vdo-T(jq07*qoM6N<$f+(iyc>n+a literal 0 HcmV?d00001 diff --git a/WebApplication/view/BlogLoginWidget.cpp b/WebApplication/view/BlogLoginWidget.cpp index 2befed7..dc035dc 100644 --- a/WebApplication/view/BlogLoginWidget.cpp +++ b/WebApplication/view/BlogLoginWidget.cpp @@ -1,5 +1,6 @@ #include "BlogLoginWidget.h" #include "model/BlogSession.h" +#include BlogLoginWidget::BlogLoginWidget(BlogSession &session, const std::string &basePath) : AuthWidget(session.login()) { setInline(true); diff --git a/WebApplication/view/BlogView.cpp b/WebApplication/view/BlogView.cpp index b760098..f00c76d 100644 --- a/WebApplication/view/BlogView.cpp +++ b/WebApplication/view/BlogView.cpp @@ -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 #include +#include +#include #include +#include +#include + +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(tr("blog-login-status"))); m_panel = this->addWidget(std::make_unique()); m_items = this->addWidget(std::make_unique()); @@ -30,74 +39,342 @@ public: auto loginLink = std::make_unique(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(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::WLink(Wt::LinkType::InternalPath, basePath_ + "all"), tr("archive")); + std::make_unique(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("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 = findUser(author); + std::string author = app->internalPathNextPart(m_basePath + path + '/'); + Wt::Dbo::ptr 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(tr("archive-title"))); + + Posts posts = m_session.find("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(post->date.date().toString(dateFormat))); + title->setStyleClass("archive-month-title"); + } + + Wt::WAnchor *a = parent->addWidget(std::make_unique( + 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 findUser(const std::string &name) { + return m_session.find("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(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(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::IdType id = User::stringToId(ids); + + m_panel->show(); + try { + Wt::Dbo::Transaction t(m_session); + Wt::Dbo::ptr target(m_session.load(id)); + if (!m_userEditor) { + m_userEditor = m_panel->addWidget(std::make_unique(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(tr("blog-invaliduser"))); + } + m_panel->setCurrentWidget(m_invalidUser); + } + } + + void showPostsByDateTopic(const std::string &path, WContainerWidget *parent) { + std::vector 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("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) { + 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(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(tr("profile")); + profileLink->setStyleClass("link"); + profileLink->clicked().connect(this, &BlogImpl::editProfile); + + Wt::Dbo::ptr user = session().user(); + + if (user->role == User::Admin) { + auto editUsersLink = std::make_unique(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(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(msg)); + } + + void authorPanel() { + m_panel->show(); + if (!m_authorPanel) { + m_authorPanel = m_panel->addWidget(std::make_unique(tr("blog-author-panel"))); + bindPanelTemplates(); + } + m_panel->setCurrentWidget(m_authorPanel); + } + + void showPost(const Wt::Dbo::ptr post, PostView::RenderType type, Wt::WContainerWidget *parent) { + parent->addWidget(std::make_unique(m_session, m_basePath, post, type)); + } + + void newPost() { + Wt::Dbo::Transaction t(m_session); + + authorPanel(); + WContainerWidget *unpublishedPosts = m_authorPanel->resolve("unpublished-posts"); + + Wt::Dbo::ptr post(std::make_unique()); + + 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(tr("new-post")); + newPost->clicked().connect(this, &BlogImpl::newPost); + auto unpublishedPosts = std::make_unique(); + 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; diff --git a/WebApplication/view/CommentView.cpp b/WebApplication/view/CommentView.cpp new file mode 100644 index 0000000..b7d0278 --- /dev/null +++ b/WebApplication/view/CommentView.cpp @@ -0,0 +1,146 @@ +#include "CommentView.h" +#include "model/BlogSession.h" +#include "model/Comment.h" +#include "model/User.h" +#include +#include + +CommentView::CommentView(BlogSession &session, long long parentId) : session_(session) { + Wt::Dbo::ptr parent = session_.load(parentId); + + comment_ = std::make_unique(); + comment_.modify()->parent = parent; + comment_.modify()->post = parent->post; + + edit(); +} + +CommentView::CommentView(BlogSession &session, Wt::Dbo::ptr 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(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(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(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> CommentVector; + CommentVector comments; + { + Wt::Dbo::collection> cmts = comment_->children.find().orderBy("date"); + comments.insert(comments.end(), cmts.begin(), cmts.end()); + } + + auto children = std::make_unique(); + for (int i = (int)comments.size() - 1; i >= 0; --i) + children->addWidget(std::make_unique(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("children"); + c->insertWidget(0, std::make_unique(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(); + editArea_ = editArea.get(); + editArea_->setText(comment_->textSrc()); + editArea_->setFocus(); + + auto save = std::make_unique(tr("save")); + save->clicked().connect(this, &CommentView::save); + + auto cancel = std::make_unique(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(); +} diff --git a/WebApplication/view/CommentView.h b/WebApplication/view/CommentView.h new file mode 100644 index 0000000..a61f49f --- /dev/null +++ b/WebApplication/view/CommentView.h @@ -0,0 +1,27 @@ +#ifndef __COMMENTVIEW_H__ +#define __COMMENTVIEW_H__ + +#include +#include + +class BlogSession; +class Comment; + +class CommentView : public Wt::WTemplate { +public: + CommentView(BlogSession &session, long long parentId); + CommentView(BlogSession& session, Wt::Dbo::ptr comment); +protected: + void edit(); + void save(); + void cancel(); + void renderView(); + bool isNew() const; + void reply(); + void rm(); +private: + BlogSession &session_; + Wt::Dbo::ptr comment_; + Wt::WTextArea *editArea_; +}; +#endif // __COMMENTVIEW_H__ \ No newline at end of file diff --git a/WebApplication/view/EditUsers.cpp b/WebApplication/view/EditUsers.cpp index 4ef40f2..c2a1123 100644 --- a/WebApplication/view/EditUsers.cpp +++ b/WebApplication/view/EditUsers.cpp @@ -1,4 +1,6 @@ #include "EditUsers.h" +#include +#include #include #include @@ -15,6 +17,10 @@ EditUsers::EditUsers(Wt::Dbo::Session &aSession, const std::string &basePath) limitList(); } +void EditUsers::onUserClicked(Wt::Dbo::dbo_traits::IdType id) { + Wt::WApplication::instance()->setInternalPath(m_basePath + "edituser/" + std::to_string(id), true); +} + void EditUsers::limitList() { auto listPtr = std::make_unique(); 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 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(); +} diff --git a/WebApplication/view/PostView.cpp b/WebApplication/view/PostView.cpp new file mode 100644 index 0000000..cb33003 --- /dev/null +++ b/WebApplication/view/PostView.cpp @@ -0,0 +1,188 @@ +#include "PostView.h" +#include "CommentView.h" +#include "model/BlogSession.h" +#include "model/User.h" +#include "model/asciidoc.h" +#include +#include +#include +#include +#include +#include + +PostView::PostView(BlogSession &session, const std::string &basePath, Wt::Dbo::ptr 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(post_->commentCount())); + bindWidget("comments", std::make_unique(session_, post_->rootComment())); + bindString("anchor", basePath_ + post_->permaLink()); + + break; + } + case Brief: { + setTemplateText(tr("blog-post-brief")); + + auto titleAnchor = std::make_unique( + 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::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::WLink(Wt::LinkType::InternalPath, basePath_ + post_->permaLink() + "/comments")); + commentCount_ = commentsAnchor->addWidget(std::make_unique("(" + post_->commentCount() + ")")); + bindWidget("comment-count", std::move(commentsAnchor)); + + break; + } + case Edit: { + setTemplateText(tr("blog-post-edit")); + + titleEdit_ = bindWidget("title-edit", std::make_unique(post_->title)); + briefEdit_ = bindWidget("brief-edit", std::make_unique(post_->briefSrc)); + bodyEdit_ = bindWidget("body-edit", std::make_unique(post_->bodySrc)); + + auto saveButton = bindWidget("save", std::make_unique(tr("save"))); + auto cancelButton = bindWidget("cancel", std::make_unique(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 publishButton; + if (post_->state != Post::Published) { + publishButton = std::make_unique(tr("publish")); + publishButton->clicked().connect(this, &PostView::publish); + } else { + publishButton = std::make_unique(tr("retract")); + publishButton->clicked().connect(this, &PostView::retract); + } + bindWidget("publish", std::move(publishButton)); + + auto editButton(std::make_unique(tr("edit"))); + editButton->clicked().connect(this, &PostView::showEdit); + bindWidget("edit", std::move(editButton)); + + auto deleteButton(std::make_unique(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::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 rootComment = session_.add(std::make_unique()); + 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) { + if (comment->post == post_) { + std::string count = comment->post->commentCount(); + + if (commentCount_->text().toUTF8()[0] == '(') + commentCount_->setText("(" + count + ")"); + else + commentCount_->setText(count); + } +} diff --git a/WebApplication/view/PostView.h b/WebApplication/view/PostView.h new file mode 100644 index 0000000..916acdc --- /dev/null +++ b/WebApplication/view/PostView.h @@ -0,0 +1,40 @@ +#ifndef __POSTVIEW_H__ +#define __POSTVIEW_H__ + +#include "model/Post.h" +#include + +class BlogSession; + +class PostView : public Wt::WTemplate { +public: + enum RenderType { + Brief, + Detail, + Edit, + }; + + PostView(BlogSession &session, const std::string &basePath, Wt::Dbo::ptr post, RenderType type); + +protected: + void render(RenderType type); + void updateCommentCount(Wt::Dbo::ptr 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_; + + RenderType viewType_; + Wt::WText *commentCount_; + Wt::WLineEdit *titleEdit_; + Wt::WTextArea *briefEdit_, *bodyEdit_; +}; +#endif // __POSTVIEW_H__ \ No newline at end of file