Removes a lot of unused code from backend.
This commit is contained in:
parent
4f2f95efb5
commit
4c0ef19344
3 changed files with 26 additions and 1450 deletions
584
src/forms.tmpl
584
src/forms.tmpl
|
|
@ -1,584 +0,0 @@
|
|||
#? stdtmpl | standard
|
||||
#
|
||||
#template `!`(idx: untyped): untyped =
|
||||
# row[idx]
|
||||
#end template
|
||||
#
|
||||
#
|
||||
#proc genThreadsList(c: TForumData, count: var int): string =
|
||||
# const queryModAdmin = sql"""select id, name, views, modified from thread
|
||||
# where id in (select thread from post where author in
|
||||
# (select id from person where status not in ('Spammer') or id = ?))
|
||||
# order by modified desc limit ?, ?"""
|
||||
# const query = sql"""select id, name, views, modified from thread
|
||||
# where id in (select thread from post where author in
|
||||
# (select id from person where status not in ('Moderated', 'Spammer') or id = ?))
|
||||
# order by modified desc limit ?, ?"""
|
||||
# const threadId = 0
|
||||
# const name = 1
|
||||
# const views = 2
|
||||
#
|
||||
# result = ""
|
||||
# count = 0
|
||||
<div id="talk-heads">
|
||||
<div class="topic">
|
||||
<div>
|
||||
Topic
|
||||
<a href="${c.req.makeUri("/threadActivity.xml")}">
|
||||
<img src="/images/Feed-icon.svg" class="rssfeed">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="users"><div>Users</div></div>
|
||||
<div class="detail"><div>Details</div></div>
|
||||
<div class="activity">
|
||||
<div>
|
||||
Activity
|
||||
<a href="${c.req.makeUri("/postActivity.xml")}">
|
||||
<img src="/images/Feed-icon.svg" class="rssfeed">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="talk-threads">
|
||||
# for row in rows(db, if c.rank >= Moderator: queryModAdmin else: query,
|
||||
# c.userId, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage):
|
||||
# inc(count)
|
||||
<div>
|
||||
<div class="topic">
|
||||
<div>
|
||||
<a href="${c.genThreadUrl(threadid = !threadid)}"
|
||||
title="${xmlEncode(!name)}">${xmlEncode(!name)}</a>
|
||||
${genPagenumLocalNav(c, (!threadid).parseInt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#let users = getAllRows(db,
|
||||
# sql("select distinct name, email from person where id in " &
|
||||
# "(select author from post where thread = ?)"), !threadId)
|
||||
<div class="users">
|
||||
<div>
|
||||
#for i in 0 .. min(6, users.len-1):
|
||||
<img src="${getGravatarUrl(users[i][1], 20)}" title="${users[i][0]}">
|
||||
#end for
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#let latestReplyAuthor = getValue(db, sql("select name from person where id = " &
|
||||
# "(select author from post where id = " &
|
||||
# "(select max(id) from post where thread = ?))"), !threadId)
|
||||
|
||||
#let replyProfileUrl = c.req.makeUri("profile/", false) &
|
||||
# xmlEncode(latestReplyAuthor)
|
||||
|
||||
# let posts = getValue(db, sql"select count(*) from post where thread = ?", !threadId)
|
||||
<div class="detail">
|
||||
<div><div title="Views">${xmlEncode(!views)}</div></div>
|
||||
<div><div title="Posts">$posts</div></div>
|
||||
</div>
|
||||
|
||||
#let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " &
|
||||
# "(select creation from post where id = (select max(id) from post where thread = ?)))"), !threadId)
|
||||
#let timeStr = formatTimestamp(latestReplyDate.parseInt())
|
||||
<div class="activity">
|
||||
<div>
|
||||
<a href="$replyProfileUrl">$latestReplyAuthor</a> replied $timeStr
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
# end for
|
||||
</div>
|
||||
|
||||
#end proc
|
||||
#
|
||||
#
|
||||
#proc genPostPreview(c: TForumData,
|
||||
# title, content, author, date: string): string =
|
||||
# result = ""
|
||||
<a name="preview"></a>
|
||||
<div id="talk-thread">
|
||||
<div>
|
||||
<div class="author">
|
||||
<div>
|
||||
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(author)
|
||||
<a class="name" href="$profileUrl">${xmlEncode(author)}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic">
|
||||
<div>
|
||||
#try:
|
||||
${content.rstToHtml}
|
||||
#except EParseError:
|
||||
# c.errorMsg = getCurrentExceptionMsg()
|
||||
#end
|
||||
<span class="date">${xmlEncode(date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>appendRunBtn()</script>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#
|
||||
#proc genPostsList(c: TForumData, threadId: string, count: var int): string =
|
||||
# let query = sql("""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p,
|
||||
# person u
|
||||
# where u.id = p.author and p.thread = ? and $#
|
||||
# and (u.status <> 'Spammer' or p.author = ?)
|
||||
# order by p.id limit ?, ?""" %
|
||||
# (if c.rank >= Moderator: "(1 or u.id = ?)" else: "(u.status <> 'Moderated' or p.author = ?)"))
|
||||
# const postId = 0
|
||||
# const userName = 1
|
||||
# const postHeader = 2
|
||||
# const postContent = 3
|
||||
# const postCreation = 4
|
||||
# const postAuthor = 5
|
||||
# const userEmail = 6
|
||||
# result = ""
|
||||
# count = 0
|
||||
# let posts = getAllRows(db, query, threadId, c.userId, c.userId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage)
|
||||
# if posts.len < 1: return ""
|
||||
# end if
|
||||
<div id="talk-head">
|
||||
<div class="info-post">
|
||||
<div>
|
||||
<a href="${c.req.makeUri("/")}"><b>forum index</b></a> >
|
||||
<a href="${c.req.makeUri("/t/" & $threadId)}">${posts[0][postHeader]}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="talk-thread">
|
||||
# for row in posts:
|
||||
# inc(count)
|
||||
<a name="${!postId}"></a>
|
||||
<div id="${!postId}">
|
||||
<div class="author">
|
||||
<div>
|
||||
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(!userName)
|
||||
<div class="avatar">${genGravatar(!userEmail)}</div>
|
||||
<a class="name" href="$profileUrl">${xmlEncode(!userName)}</a>
|
||||
#if c.userId == !postAuthor and c.currentPost.subject.len == 0:
|
||||
<hr/><a href="${c.genThreadUrl(!postId, "edit")}">Edit post</a>
|
||||
#elif c.rank >= Moderator and c.currentPost.subject.len == 0:
|
||||
<hr/><a style="color: red;" href="${c.genThreadUrl(!postId, "edit")}">Edit post</a>
|
||||
#end if
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic">
|
||||
<div>
|
||||
#try:
|
||||
${(!postContent).rstToHtml}
|
||||
#except EParseError:
|
||||
# c.errorMsg = getCurrentExceptionMsg()
|
||||
#end
|
||||
<span class="date"><a href="${c.genThreadUrl(!postId, "", $threadId, $(c.pageNum))}">${xmlEncode(!postCreation)}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
# end for
|
||||
<script>
|
||||
// tiny helper that run the code in the playground
|
||||
function appendEachRunBtn(element) {
|
||||
if (element.getElementsByClassName("runDiv").length>0){
|
||||
return; // already has a run btn
|
||||
}
|
||||
var runDiv = document.createElement("DIV");
|
||||
var btn = document.createElement("BUTTON");
|
||||
var t = document.createTextNode("Run");
|
||||
var playLink = document.createElement("a")
|
||||
var code = element.textContent;
|
||||
var spinners = [" ◆ "," ◇ "];
|
||||
playLink.title = "Edit in playground"
|
||||
playLink.textContent = "Edit"
|
||||
playLink.setAttribute('href', "https://play.nim-lang.org?code="+encodeURIComponent(code));
|
||||
playLink.setAttribute("class", "button");
|
||||
playLink.setAttribute("target", "_blank");
|
||||
runDiv.setAttribute("class", "runDiv");
|
||||
btn.appendChild(t);
|
||||
btn.onclick = function(){
|
||||
var httpRequest = new XMLHttpRequest();
|
||||
var start = null;
|
||||
httpRequest.open("POST", "https://play.nim-lang.org/compile", true);
|
||||
btn.textContent = spinners[1];
|
||||
function step(timestamp) {
|
||||
if (!start) start = timestamp;
|
||||
if (timestamp - start > 500) {
|
||||
if (btn.textContent == spinners[0]) { btn.textContent = spinners[1] }
|
||||
else { btn.textContent = spinners[0] }
|
||||
start = null;
|
||||
}
|
||||
if (httpRequest.status != 200){ window.requestAnimationFrame(step) }
|
||||
else { btn.textContent = "Run" }
|
||||
}
|
||||
function displayContents(e){
|
||||
if (httpRequest.readyState!=httpRequest.DONE){ return }
|
||||
if (httpRequest.status == 200){
|
||||
var res = JSON.parse(httpRequest.responseText);
|
||||
// this works because only 1 `resDiv` is inside `element`
|
||||
var resDiv = element.getElementsByClassName("resDiv")[0];
|
||||
if (resDiv == null) {
|
||||
resDiv = document.createElement("DIV");
|
||||
runDiv.appendChild(resDiv);
|
||||
}
|
||||
if (res.compileLog.lastIndexOf("[SuccessX]") != -1){
|
||||
resDiv.textContent = res.log;
|
||||
resDiv.setAttribute("class", "resDiv successComp");
|
||||
} else {
|
||||
resDiv.textContent = res.compileLog;
|
||||
resDiv.setAttribute("class","resDiv failedComp");
|
||||
}
|
||||
} else {
|
||||
console.log("There was a problem with the request.");
|
||||
}
|
||||
}
|
||||
httpRequest.onreadystatechange = displayContents;
|
||||
httpRequest.send(JSON.stringify({"code": code, "compilationTarget": "c"}));
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
runDiv.appendChild(document.createElement("HR"));
|
||||
runDiv.appendChild(btn);
|
||||
runDiv.appendChild(playLink);
|
||||
element.appendChild(runDiv);
|
||||
}
|
||||
function appendRunBtn(){
|
||||
var els = Array.prototype.slice.call(document.getElementsByClassName("langNim"));
|
||||
els.forEach(appendEachRunBtn, this);
|
||||
}
|
||||
appendRunBtn()
|
||||
</script>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#proc genMarkHelp(): string
|
||||
#end proc
|
||||
#proc genFormPost(c: TForumData, action: string,
|
||||
# topText, title, content: string, isEdit: bool): string =
|
||||
# result = ""
|
||||
<br />
|
||||
<a name="reply"></a>
|
||||
<div id="replywrapper">
|
||||
<div id="talk-head">
|
||||
<div class="info-post">
|
||||
<div>
|
||||
<a href="${c.req.makeUri("/")}"><b>forum index</b></a> >
|
||||
$topText
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form action="${c.req.makeUri(action, false) & "#preview"}" method="POST">
|
||||
#if action == "doreply":
|
||||
${hiddenField(c, "subject", title)}
|
||||
#else:
|
||||
${fieldValid(c, "subject", "Subject:")}
|
||||
${textWidget(c, "subject", title, maxlength=100)}
|
||||
<br />
|
||||
#end if
|
||||
${fieldValid(c, "content", "Content:")}<br />
|
||||
${textAreaWidget(c, "content", content)}<br />
|
||||
${formSession(c, action)}
|
||||
|
||||
# if isEdit:
|
||||
<input type="checkbox" name="delete" value="Delete">Delete Post<br />
|
||||
# end if
|
||||
#if c.errorMsg != "":
|
||||
<div style="float: left; width: 100%;">
|
||||
<span class="error">$c.errorMsg</span>
|
||||
</div>
|
||||
#end if
|
||||
<br/>
|
||||
|
||||
<input type="submit" name="previewBtn" value="Preview" />
|
||||
<input type="submit" name="postBtn" value="Submit" />
|
||||
|
||||
${genMarkHelp()}
|
||||
</form>
|
||||
</div>
|
||||
#end proc
|
||||
#
|
||||
#
|
||||
#proc genFormRegister(c: TForumData): string =
|
||||
# result = ""
|
||||
<div id="talk-head">
|
||||
<div class="info-post">
|
||||
<div>
|
||||
<a href="${c.req.makeUri("/")}"><b>forum index</b></a> >
|
||||
Register
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form action="${c.req.makeUri("/doregister", false)}" method="POST">
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td>${fieldValid(c, "name", "Username:")}</td>
|
||||
<td>${textWidget(c, "name", reuseText, maxlength=20)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${fieldValid(c, "new_password", "Password:")}</td>
|
||||
<td><input type="password" name="new_password" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${fieldValid(c, "email", "E-Mail:")}</td>
|
||||
<td>${textWidget(c, "email", reuseText, maxlength=300)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${fieldValid(c, "g-recaptcha-response", "Captcha:")}</td>
|
||||
<td>${captcha.render(includeNoScript=true)}</td>
|
||||
</table>
|
||||
#if c.errorMsg != "":
|
||||
<div style="float: left; width: 100%;">
|
||||
<span class="error">$c.errorMsg</span>
|
||||
</div>
|
||||
#end if
|
||||
<input type="submit" value="Register">
|
||||
</form>
|
||||
#end proc
|
||||
#
|
||||
#proc genFormSetRank(c: TForumData; ui: TUserInfo): string =
|
||||
# result = ""
|
||||
<form action="${c.req.makeUri("/dosetrank/" & ui.nick, false)}" method="POST">
|
||||
<table border="0">
|
||||
<tr>
|
||||
<th>Reason</th>
|
||||
<td>${textWidget(c, "reason", ui.ban, maxlength=100)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<td><select name = "rank">
|
||||
# for i in low(Rank)..high(Rank):
|
||||
<option ${if i == ui.rank: "selected" else: ""}>
|
||||
$i
|
||||
</option>
|
||||
# end for
|
||||
</td></select>
|
||||
</tr>
|
||||
</table>
|
||||
<input type="submit" value="Change">
|
||||
</form>
|
||||
#end proc
|
||||
#
|
||||
#proc genFormLogin(c: TForumData): string =
|
||||
# result = ""
|
||||
# if not c.loggedIn:
|
||||
<form action="${c.req.makeUri("/dologin", false)}" method="POST">
|
||||
<table border="0">
|
||||
<tr><td>Username:</td><td>
|
||||
<input type="text" name="name" maxlength="20"></td></tr>
|
||||
<tr><td>Password:</td><td>
|
||||
<input type="password" name="password" maxlength="20"></td></tr>
|
||||
</table>
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
<span style="color:red">$c.loginErrorMsg</span>
|
||||
# else:
|
||||
<span style="color:red">You're already logged in!</span>
|
||||
# end if
|
||||
#end proc
|
||||
#
|
||||
#
|
||||
#proc genListOnline(c: TForumData, stats: TForumStats): string =
|
||||
# result = ""
|
||||
# var active: seq[string] = @[]
|
||||
# for i in stats.activeUsers:
|
||||
# active.add(i.nick)
|
||||
# end for
|
||||
# let profileUrl = c.req.makeUri("profile/", false) &
|
||||
# xmlEncode(stats.newestMember.nick)
|
||||
<span class="forum-user-info" title="${active.join(", ")}">
|
||||
<b>${stats.activeUsers.len}</b> of <b>${stats.totalUsers}</b> users online</span> |
|
||||
<b>${stats.totalThreads}</b> threads | <b>${stats.totalPosts}</b> posts |
|
||||
newest member: <a href="$profileUrl">${stats.newestMember.nick}</a>
|
||||
#end proc
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
#proc genSearchResults(c: TForumData,
|
||||
# results: iterator: db_sqlite.Row {.closure, tags: [ReadDbEffect].},
|
||||
# count: var int): string =
|
||||
# const threadId = 0
|
||||
# const threadName = 1
|
||||
# const postId = 2
|
||||
# const postHeader = 3
|
||||
# const postContent = 4
|
||||
# const userName = 5
|
||||
# const postCreation = 6
|
||||
# const postAuthor = 7
|
||||
# const userEmail = 8
|
||||
# const what = 9
|
||||
# result = ""
|
||||
# count = 0
|
||||
# var whCount: array[bool, int]
|
||||
<div id="talk-head">
|
||||
<div class="info-post">
|
||||
<div>
|
||||
Search results for: <i style="color: #332299">${xmlEncode(c.search.replace(""","\""))}</i>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="talk-thread" class="searchResults">
|
||||
# for row in results():
|
||||
# inc(count)
|
||||
# let isThread = !what == "0"
|
||||
# inc(whCount[isThread])
|
||||
# let postUrl = c.genThreadUrl(!postId,"",!threadId,"")
|
||||
# let threadUrl = c.genThreadUrl("","",!threadId)
|
||||
# var headersDiffer = false
|
||||
<div>
|
||||
<div class="author">
|
||||
<div>
|
||||
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(!userName)
|
||||
<div><a href="$profileUrl">${genGravatar(!userEmail, 40)}</a></div>
|
||||
<div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(!userName)}</a></div>
|
||||
#if c.userId == !postAuthor and c.currentPost.subject.len == 0:
|
||||
<hr/><a href="${c.genThreadUrl(!postId, "edit", !threadId)}">Edit post</a>
|
||||
#elif c.rank >= Moderator and c.currentPost.subject.len == 0:
|
||||
<hr/><a style="color: red;" href="${c.genThreadUrl(!postId, "edit", !threadId)}">Edit post</a>
|
||||
#end if
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic">
|
||||
<div>
|
||||
#if !postHeader != "":
|
||||
<div class="postTitle">
|
||||
<span class="titleHeader">Post:</span>
|
||||
<a href="${postUrl}">
|
||||
<span>${!postHeader}</span>
|
||||
</a>
|
||||
</div>
|
||||
#end if
|
||||
#if not isThread:
|
||||
#try:
|
||||
${(!postContent).rstToHtml}
|
||||
#except EParseError:
|
||||
# c.errorMsg = getCurrentExceptionMsg()
|
||||
${xmlEncode(!postContent)}
|
||||
#end
|
||||
#end if
|
||||
<span class="date">${xmlEncode(!postCreation)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
# end for
|
||||
</div>
|
||||
# if c.pageNum > 1:
|
||||
<form action="/search/${$(c.pageNum-1)}" method="post" class="searchNav">
|
||||
<input type="hidden" name="q" value="${c.search}">
|
||||
<input type="submit" value="Previous ${ThreadsPerPage} results">
|
||||
</form>
|
||||
# end if
|
||||
# if whCount[true] == ThreadsPerPage or whCount[false] == ThreadsPerPage:
|
||||
<form action="/search/${$(c.pageNum+1)}" method="post" class="searchNav">
|
||||
<input type="hidden" name="q" value="${c.search}">
|
||||
<input type="submit" value="Next ${ThreadsPerPage} results (if any)">
|
||||
</form>
|
||||
# end if
|
||||
#end proc
|
||||
#
|
||||
#
|
||||
#proc genFormResetPassword(c: TForumData): string =
|
||||
# result = ""
|
||||
<div id="talk-head">
|
||||
<div class="info-post">
|
||||
<div>
|
||||
<a href="${c.req.makeUri("/")}"><b>forum index</b></a> >
|
||||
Reset Password
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form action="${c.req.makeUri("/doresetpassword", false)}" method="POST">
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td>${fieldValid(c, "nick", "Your nickname:")}</td>
|
||||
<td><input type="text" name="nick" maxlength="20" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>${fieldValid(c, "g-recaptcha-response", "Captcha:")}</td>
|
||||
<td>${captcha.render(includeNoScript=true)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
#if c.errorMsg != "":
|
||||
<div style="float: left; width: 100%;">
|
||||
<span class="error">$c.errorMsg</span>
|
||||
</div>
|
||||
#end if
|
||||
<input type="submit" value="Email me">
|
||||
</form>
|
||||
#end proc
|
||||
#proc genMarkHelp(): string =
|
||||
#result = ""
|
||||
<div id="markhelp">
|
||||
<p>nimforum uses a slightly-customized version of
|
||||
<a href="http://www.sphinx-doc.org/en/stable/rest.html">reStructuredText</a> for formatting. See below for some basics, or check
|
||||
<a href="/rst">this link</a> for a more detailed help reference.</p>
|
||||
<table class="rst">
|
||||
<tbody>
|
||||
<tr class="markheading">
|
||||
<td><em>you type:</em>
|
||||
</td>
|
||||
<td><em>you see:</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>*italics*</td>
|
||||
<td><em>italics</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>**bold**</td>
|
||||
<td><b>bold</b>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`nim! <http://nim-lang.org>`_</td>
|
||||
<td><a href="http://nim-lang.org">nim!</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>* item 1
|
||||
<br>* item 2
|
||||
<br>* item 3</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>item 1</li>
|
||||
<li>item 2</li>
|
||||
<li>item 3</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>> quoted text</td>
|
||||
<td>
|
||||
<blockquote>quoted text</blockquote>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>The forum supports the Github Markdown syntax
|
||||
<br>for code listings:
|
||||
<br>
|
||||
<br>```nim
|
||||
<br>if 1 * 2 < 3:
|
||||
<br><span class="spaces"> </span>echo "hello, world!"
|
||||
<br>```
|
||||
<br>
|
||||
</td>
|
||||
<td>The forum supports the Github Markdown syntax
|
||||
<br>for code listings:
|
||||
<br>
|
||||
<pre class="listing">
|
||||
<span class="Keyword">if</span> <span class="DecNumber">1</span><span class=
|
||||
"Operator">*</span><span class="DecNumber">2</span> <span class=
|
||||
"Operator"><</span> <span class="DecNumber">3</span><span class=
|
||||
"Punctuation">:</span>
|
||||
<span class="Identifier">echo</span> <span class=
|
||||
"StringLit">"hello, world!"</span>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>A horizontal rule can be created<br/>----<br/>but it needs text after it</td>
|
||||
<td>A horizontal rule can be created<hr/>but it needs text after it</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
#end proc
|
||||
718
src/forum.nim
718
src/forum.nim
|
|
@ -63,25 +63,6 @@ type
|
|||
noPagenumumNav: bool
|
||||
config: Config
|
||||
|
||||
TStyledButton = tuple[text: string, link: string]
|
||||
|
||||
TForumStats = object
|
||||
totalUsers: int
|
||||
totalPosts: int
|
||||
totalThreads: int
|
||||
newestMember: tuple[nick: string, id: int]
|
||||
activeUsers: seq[tuple[nick: string, id: int]]
|
||||
|
||||
TUserInfo = object
|
||||
nick: string
|
||||
posts: int
|
||||
threads: int
|
||||
lastOnline: int
|
||||
email: string
|
||||
ban: string
|
||||
rank: Rank
|
||||
lastIp: string
|
||||
|
||||
ForumError = object of Exception
|
||||
data: PostError
|
||||
|
||||
|
|
@ -121,36 +102,6 @@ proc loggedIn(c: TForumData): bool =
|
|||
|
||||
# --------------- HTML widgets ------------------------------------------------
|
||||
|
||||
# for widgets "" means the empty string as usual; should the old value be
|
||||
# used again, pass `reuseText` instead:
|
||||
const
|
||||
reuseText = "\1"
|
||||
|
||||
proc textWidget(c: TForumData, name, defaultText: string,
|
||||
maxlength = 30, size = -1): string =
|
||||
let x = if defaultText != reuseText: defaultText
|
||||
else: xmlEncode(c.req.params.getOrDefault(name))
|
||||
return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [
|
||||
name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""]
|
||||
|
||||
proc hiddenField(c: TForumData, name, defaultText: string): string =
|
||||
let x = xmlencode(
|
||||
if defaultText != reuseText: defaultText
|
||||
else: c.req.params.getOrDefault(name)
|
||||
)
|
||||
return """<input type="hidden" name="$1" value="$2"/>""" % [name, x]
|
||||
|
||||
proc textAreaWidget(c: TForumData, name, defaultText: string): string =
|
||||
let x = if defaultText != reuseText: defaultText
|
||||
else: xmlEncode(c.req.params.getOrDefault(name))
|
||||
return """<textarea name="$1">$2</textarea>""" % [
|
||||
name, x]
|
||||
|
||||
proc fieldValid(c: TForumData, name, text: string): string =
|
||||
if name == c.invalidField:
|
||||
result = """<span style="color:red">$1</span>""" % text
|
||||
else:
|
||||
result = text
|
||||
|
||||
proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNum = ""): string =
|
||||
result = "/t/" & (if threadid == "": $c.threadId else: threadid)
|
||||
|
|
@ -164,84 +115,17 @@ proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNu
|
|||
result.add("#" & postId)
|
||||
result = c.req.makeUri(result, absolute = false)
|
||||
|
||||
proc formSession(c: TForumData, nextAction: string): string =
|
||||
return """<input type="hidden" name="threadid" value="$1" />
|
||||
<input type="hidden" name="postid" value="$2" />""" % [
|
||||
$c.threadId, $c.postid]
|
||||
|
||||
proc urlButton(c: TForumData, text, url: string): string =
|
||||
return ("""<a class="url_button" href="$1">$2</a>""") % [
|
||||
url, text]
|
||||
|
||||
proc genButtons(c: TForumData, btns: seq[TStyledButton]): string =
|
||||
if btns.len == 1:
|
||||
var anchor = ""
|
||||
|
||||
result = ("""<a class="active button" href="$1$3">$2</a>""") % [
|
||||
btns[0].link, btns[0].text, anchor]
|
||||
else:
|
||||
result = ""
|
||||
for i, btn in pairs(btns):
|
||||
var anchor = ""
|
||||
|
||||
var class = ""
|
||||
if i == 0: class = "left "
|
||||
elif i == btns.len()-1: class = "right "
|
||||
else: class = "middle "
|
||||
result.add(("""<a class="$3active button" href="$1$4">$2</a>""") % [
|
||||
btns[i].link, btns[i].text, class, anchor])
|
||||
|
||||
proc toInterval(diff: int64): TimeInterval =
|
||||
var remaining = diff
|
||||
let years = remaining div 31536000
|
||||
remaining -= years * 31536000
|
||||
let months = remaining div 2592000
|
||||
remaining -= months * 2592000
|
||||
let days = remaining div 86400
|
||||
remaining -= days * 86400
|
||||
let hours = remaining div 3600
|
||||
remaining -= hours * 3600
|
||||
let minutes = remaining div 60
|
||||
remaining -= minutes * 60
|
||||
result = initInterval(remaining.int, minutes.int, hours.int, days.int,
|
||||
months.int, years.int)
|
||||
|
||||
proc formatTimestamp(t: int): string =
|
||||
let t2 = fromUnix(t)
|
||||
let now = getTime()
|
||||
# let diff = (now - t2).toInterval()
|
||||
# if diff.years > 0:
|
||||
# return getGMTime(t2).format("MMMM d',' yyyy")
|
||||
# elif diff.months > 0:
|
||||
# return $diff.months & (if diff.months > 1: " months ago" else: " month ago")
|
||||
# elif diff.days > 0:
|
||||
# return $diff.days & (if diff.days > 1: " days ago" else: " day ago")
|
||||
# elif diff.hours > 0:
|
||||
# return $diff.hours & (if diff.hours > 1: " hours ago" else: " hour ago")
|
||||
# elif diff.minutes > 0:
|
||||
# return $diff.minutes &
|
||||
# (if diff.minutes > 1: " minutes ago" else: " minute ago")
|
||||
# else:
|
||||
return "just now"
|
||||
|
||||
proc getGravatarUrl(email: string, size = 80): string =
|
||||
let emailMD5 = email.toLowerAscii.toMD5
|
||||
return ("https://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size &
|
||||
"&d=identicon")
|
||||
|
||||
proc genGravatar(email: string, size: int = 80): string =
|
||||
result = "<img width=\"$1\" height=\"$2\" src=\"$3\" />" %
|
||||
[$size, $size, getGravatarUrl(email, size)]
|
||||
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
template `||`(x: untyped): untyped = (if not isNil(x): x else: "")
|
||||
|
||||
proc validThreadId(c: TForumData): bool =
|
||||
result = getValue(db, sql"select id from thread where id = ?",
|
||||
$c.threadId).len > 0
|
||||
|
||||
proc setError(c: TForumData, field, msg: string): bool {.inline.} =
|
||||
c.invalidField = field
|
||||
c.errorMsg = "Error: " & msg
|
||||
|
|
@ -290,16 +174,6 @@ proc logout(c: TForumData) =
|
|||
c.userpass = ""
|
||||
exec(db, query, c.req.ip, c.req.cookies["sid"])
|
||||
|
||||
proc getBanErrorMsg(banValue: string; rank: Rank): string =
|
||||
if banValue.len > 0:
|
||||
return "You have been banned: " & banValue
|
||||
case rank
|
||||
of Spammer, Troll, Banned: return "You have been banned."
|
||||
of EmailUnconfirmed:
|
||||
return "You need to confirm your email first."
|
||||
of Moderated, Rank.User, Moderator, Admin:
|
||||
return ""
|
||||
|
||||
proc checkLoggedIn(c: TForumData) =
|
||||
if not c.req.cookies.hasKey("sid"): return
|
||||
let pass = c.req.cookies["sid"]
|
||||
|
|
@ -325,15 +199,9 @@ proc checkLoggedIn(c: TForumData) =
|
|||
else:
|
||||
echo("SID not found in sessions. Assuming logged out.")
|
||||
|
||||
proc incrementViews(c: TForumData) =
|
||||
proc incrementViews(c: TForumData, threadId: int) =
|
||||
const query = sql"update thread set views = views + 1 where id = ?"
|
||||
exec(db, query, $c.threadId)
|
||||
|
||||
proc isPreview(c: TForumData): bool =
|
||||
result = c.req.params.hasKey("previewBtn")
|
||||
|
||||
proc isDelete(c: TForumData): bool =
|
||||
result = c.req.params.hasKey("delete")
|
||||
exec(db, query, threadId)
|
||||
|
||||
proc validateRst(c: TForumData, content: string): bool =
|
||||
result = true
|
||||
|
|
@ -370,131 +238,6 @@ proc crud(c: TCrud, table: string, data: varargs[string]): SqlQuery =
|
|||
of crDelete:
|
||||
result = sql("delete from " & table & " where id = ?")
|
||||
|
||||
template retrSubject(c: untyped) =
|
||||
if not c.req.params.hasKey("subject"):
|
||||
raise newException(ForumError, "Subject empty")
|
||||
let subject {.inject.} = c.req.params["subject"]
|
||||
if subject.strip.len < 3:
|
||||
return setError(c, "subject", "Subject not long enough")
|
||||
|
||||
template retrContent(c: untyped) =
|
||||
if not c.req.params.hasKey("content"):
|
||||
raise newException(ForumError, "Content empty")
|
||||
let content {.inject.} = c.req.params["content"]
|
||||
if content.strip.len < 2:
|
||||
return setError(c, "content", "Content not long enough")
|
||||
|
||||
if not validateRst(c, content): return false
|
||||
|
||||
template retrPost(c: untyped) =
|
||||
retrSubject(c)
|
||||
retrContent(c)
|
||||
|
||||
template checkLogin(c: untyped) =
|
||||
if not loggedIn(c): return setError(c, "", "User is not logged in")
|
||||
|
||||
template checkOwnership(c, postId: untyped) =
|
||||
if c.rank < Moderator:
|
||||
let x = getValue(db, sql"select author from post where id = ?",
|
||||
postId)
|
||||
if x != c.userId:
|
||||
return setError(c, "", "You are not the owner of this post")
|
||||
|
||||
template setPreviewData(c: untyped) {.dirty.} =
|
||||
c.currentPost.subject = subject
|
||||
c.currentPost.content = content
|
||||
|
||||
template writeToDb(c, cr, setPostId: untyped) =
|
||||
# insert a comment in the DB
|
||||
let retID = insertID(db, crud(cr, "post", "author", "ip", "header", "content", "thread"),
|
||||
c.userId, c.req.ip, subject, content, $c.threadId, "")
|
||||
discard tryExec(db, crud(cr, "post_fts", "id", "header", "content"),
|
||||
retID.int, subject, content)
|
||||
if setPostId:
|
||||
c.postId = retID.int
|
||||
|
||||
proc updateThreads(c: TForumData): int =
|
||||
## Removes threads if they have no posts, or changes their modified field
|
||||
## if they still contain posts.
|
||||
const query =
|
||||
sql"delete from thread where id not in (select thread from post)"
|
||||
result = execAffectedRows(db, query).int
|
||||
if result > 0:
|
||||
discard tryExec(db, sql"delete from thread_fts where id not in (select thread from post)")
|
||||
else:
|
||||
# Update corresponding thread's modified field.
|
||||
let getModifiedSql = "(select creation from post where post.thread = ?" &
|
||||
" order by creation desc limit 1)"
|
||||
let updateSql = sql("update thread set modified=" & getModifiedSql &
|
||||
" where id = ?")
|
||||
if not tryExec(db, updateSql, $c.threadId, $c.threadId):
|
||||
result = -1
|
||||
discard setError(c, "", "database error")
|
||||
|
||||
proc edit(c: TForumData, postId: int): bool =
|
||||
checkLogin(c)
|
||||
if c.isPreview:
|
||||
retrPost(c)
|
||||
setPreviewData(c)
|
||||
elif c.isDelete:
|
||||
checkOwnership(c, $postId)
|
||||
if not tryExec(db, crud(crDelete, "post"), $postId):
|
||||
return setError(c, "", "database error")
|
||||
discard tryExec(db, crud(crDelete, "post_fts"), $postId)
|
||||
result = true
|
||||
# delete corresponding thread:
|
||||
let updateResult = updateThreads(c)
|
||||
if updateResult > 0:
|
||||
# whole thread has been deleted, so:
|
||||
c.threadId = unselectedThread
|
||||
elif updateResult < 0:
|
||||
# error occurred
|
||||
return false
|
||||
else:
|
||||
checkOwnership(c, $postId)
|
||||
retrPost(c)
|
||||
exec(db, crud(crUpdate, "post", "header", "content"),
|
||||
subject, content, $postId)
|
||||
exec(db, crud(crUpdate, "post_fts", "header", "content"),
|
||||
subject, content, $postId)
|
||||
# Check if post is the first post of the thread.
|
||||
let rows = db.getAllRows(sql("select id, thread, creation from post " &
|
||||
"where thread = ? order by creation asc"), $c.threadId)
|
||||
if rows[0][0] == $postId:
|
||||
exec(db, crud(crUpdate, "thread", "name"), subject, $c.threadId)
|
||||
result = true
|
||||
|
||||
proc gatherUserInfo(c: TForumData, nick: string, ui: var TUserInfo): bool
|
||||
proc spamCheck(c: TForumData, subject, content: string): bool =
|
||||
# Check current user's info
|
||||
var ui: TUserInfo
|
||||
if gatherUserInfo(c, c.userName, ui):
|
||||
if ui.posts > 1: return
|
||||
|
||||
# Strip all punctuation
|
||||
var subjAlphabet = ""
|
||||
for i in subject:
|
||||
if i in Letters:
|
||||
subjAlphabet.add(i)
|
||||
case i
|
||||
of '!':
|
||||
subjAlphabet.add("i")
|
||||
else: discard
|
||||
var contentAlphabet = ""
|
||||
for i in content:
|
||||
if i in Letters:
|
||||
contentAlphabet.add(i)
|
||||
case i
|
||||
of '!':
|
||||
subjAlphabet.add("i")
|
||||
else: discard
|
||||
|
||||
for word in ["appliance", "kitchen", "cheap", "sale", "relocating",
|
||||
"packers", "lenders", "fifa", "coins"]:
|
||||
if word in subjAlphabet.toLowerAscii() or
|
||||
word in contentAlphabet.toLowerAscii():
|
||||
return true
|
||||
|
||||
proc rateLimitCheck(c: TForumData): bool =
|
||||
const query40 =
|
||||
sql("SELECT count(*) FROM post where author = ? and " &
|
||||
|
|
@ -514,56 +257,6 @@ proc rateLimitCheck(c: TForumData): bool =
|
|||
if last300s > 6: return true
|
||||
return false
|
||||
|
||||
proc makeThreadURL(c: TForumData): string =
|
||||
c.req.makeUri("/t/" & $c.threadId)
|
||||
|
||||
template postChecks() {.dirty.} =
|
||||
if spamCheck(c, subject, content):
|
||||
echo("[WARNING] Found spam: ", subject)
|
||||
return true
|
||||
if rateLimitCheck(c):
|
||||
return setError(c, "subject", "You're posting too fast.")
|
||||
|
||||
proc reply(c: TForumData): bool =
|
||||
# reply to an existing thread
|
||||
checkLogin(c)
|
||||
retrPost(c)
|
||||
if c.isPreview:
|
||||
setPreviewData(c)
|
||||
else:
|
||||
postChecks()
|
||||
writeToDb(c, crCreate, true)
|
||||
|
||||
exec(db, sql"update thread set modified = DATETIME('now') where id = ?",
|
||||
$c.threadId)
|
||||
if c.rank >= Rank.User:
|
||||
asyncCheck sendMailToMailingList(c.config, c.username, c.email,
|
||||
subject, content, threadId=c.threadId, postId=c.postID, is_reply=true,
|
||||
threadUrl=c.makeThreadURL())
|
||||
result = true
|
||||
|
||||
proc newThread(c: TForumData): bool =
|
||||
# create new conversation thread (permanent or transient)
|
||||
const query = sql"insert into thread(name, views, modified) values (?, 0, DATETIME('now'))"
|
||||
checkLogin(c)
|
||||
retrPost(c)
|
||||
if c.isPreview:
|
||||
setPreviewData(c)
|
||||
c.threadID = transientThread
|
||||
else:
|
||||
postChecks()
|
||||
c.threadID = tryInsertID(db, query, c.req.params["subject"]).int
|
||||
if c.threadID < 0: return setError(c, "subject", "Subject already exists")
|
||||
discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"),
|
||||
c.threadID, c.req.params["subject"])
|
||||
writeToDb(c, crCreate, false)
|
||||
discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')")
|
||||
discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')")
|
||||
if c.rank >= Rank.User:
|
||||
asyncCheck sendMailToMailingList(c.config, c.username, c.email,
|
||||
subject, content, threadId=c.threadID, postId=c.postID, is_reply=false,
|
||||
threadUrl=c.makeThreadURL())
|
||||
result = true
|
||||
|
||||
proc verifyIdentHash(c: TForumData, name, epoch, ident: string): bool =
|
||||
const query =
|
||||
|
|
@ -576,284 +269,6 @@ proc verifyIdentHash(c: TForumData, name, epoch, ident: string): bool =
|
|||
if row[2].parseInt > (epoch.parseInt + 60): return false
|
||||
result = newIdent == ident
|
||||
|
||||
proc deleteAll(c: TForumData, nick: string): bool =
|
||||
const query =
|
||||
sql("delete from post where author = (select id from person where name = ?)")
|
||||
result = tryExec(db, query, nick)
|
||||
result = result and updateThreads(c) >= 0
|
||||
|
||||
proc setStatus(c: TForumData, nick: string, status: Rank;
|
||||
reason: string): bool =
|
||||
const query =
|
||||
sql("update person set status = ?, ban = ? where name = ?")
|
||||
result = tryExec(db, query, $status, reason, nick)
|
||||
when false:
|
||||
# for now we filter Spammers in forms.tmpl, so that a moderator
|
||||
# cannot accidentically delete all of a user's posts. We go even
|
||||
# further than that and show spammers their own spam postings.
|
||||
if status == Spammer and result:
|
||||
result = deleteAll(c, nick)
|
||||
|
||||
proc setPassword(c: TForumData, nick, pass: string): bool =
|
||||
const query =
|
||||
sql("update person set password = ?, salt = ? where name = ?")
|
||||
var salt = makeSalt()
|
||||
result = tryExec(db, query, makePassword(pass, salt), salt, nick)
|
||||
|
||||
proc hasReplyBtn(c: TForumData): bool =
|
||||
result = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply"
|
||||
result = result and c.req.params.getOrDefault("action") notin ["reply", "edit"]
|
||||
# If the user is not logged in and there are no page numbers then we shouldn't
|
||||
# generate the div.
|
||||
let pages = ceil(c.totalPosts / PostsPerPage).int
|
||||
result = result and (pages > 1 or c.loggedIn)
|
||||
return c.threadId >= 0 and result
|
||||
|
||||
proc getStats(c: TForumData, simple: bool): TForumStats =
|
||||
const totalUsersQuery =
|
||||
sql"select count(*) from person"
|
||||
result.totalUsers = getValue(db, totalUsersQuery).parseInt
|
||||
const totalPostsQuery =
|
||||
sql"select count(*) from post"
|
||||
result.totalPosts = getValue(db, totalPostsQuery).parseInt
|
||||
const totalThreadsQuery =
|
||||
sql"select count(*) from thread"
|
||||
result.totalThreads = getValue(db, totalThreadsQuery).parseInt
|
||||
if not simple:
|
||||
var newestMemberCreation = 0
|
||||
result.activeUsers = @[]
|
||||
result.newestMember = ("", -1)
|
||||
const getUsersQuery =
|
||||
sql"select id, name, strftime('%s', lastOnline), strftime('%s', creation) from person"
|
||||
for row in fastRows(db, getUsersQuery):
|
||||
let secs = if row[3] == "": 0 else: row[3].parseint
|
||||
when false:
|
||||
let lastOnlineSeconds = getTime() - Time(secs)
|
||||
if lastOnlineSeconds < (60 * 5): # 5 minutes
|
||||
result.activeUsers.add((row[1], row[0].parseInt))
|
||||
if row[3].parseInt > newestMemberCreation:
|
||||
result.newestMember = (row[1], row[0].parseInt)
|
||||
newestMemberCreation = row[3].parseInt
|
||||
|
||||
proc genPagenumNav(c: TForumData, stats: TForumStats): string =
|
||||
result = ""
|
||||
var
|
||||
firstUrl = ""
|
||||
prevUrl = ""
|
||||
totalPages = 0
|
||||
lastUrl = ""
|
||||
nextUrl = ""
|
||||
|
||||
if c.isThreadsList:
|
||||
firstUrl = c.req.makeUri("/")
|
||||
prevUrl = c.req.makeUri(if c.pageNum == 1: "/" else: "/page/" & $(c.pageNum-1))
|
||||
totalPages = ceil(stats.totalThreads / ThreadsPerPage).int
|
||||
lastUrl = c.req.makeUri("/page/" & $(totalPages))
|
||||
nextUrl = c.req.makeUri("/page/" & $(c.pageNum+1))
|
||||
else:
|
||||
firstUrl = c.makeThreadURL()
|
||||
if c.pageNum == 1:
|
||||
prevUrl = firstUrl
|
||||
else:
|
||||
prevUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum-1))
|
||||
totalPages = ceil(c.totalPosts / PostsPerPage).int
|
||||
lastUrl = c.req.makeUri(firstUrl & "/" & $(totalPages))
|
||||
nextUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum+1))
|
||||
|
||||
if totalPages <= 1:
|
||||
return ""
|
||||
|
||||
var firstTag = ""
|
||||
var prevTag = ""
|
||||
if c.pageNum == 1:
|
||||
firstTag = span("<<")
|
||||
prevTag = span("<••")
|
||||
else:
|
||||
firstTag = htmlgen.a(href=firstUrl, "<<")
|
||||
prevTag = htmlgen.a(href=prevUrl, "<••")
|
||||
prevTag.add(htmlgen.link(rel="previous", href=prevUrl))
|
||||
result.add(firstTag)
|
||||
result.add(prevTag)
|
||||
|
||||
# Numbers
|
||||
var pages = "" # Tags
|
||||
# cutting numbers to the left and to the right tp MaxPagesFromCurrent
|
||||
let firstToShow = max(1, c.pageNum - MaxPagesFromCurrent)
|
||||
let lastToShow = min(totalPages, c.pageNum + MaxPagesFromCurrent)
|
||||
if firstToShow > 1: pages.add(span("..."))
|
||||
for i in firstToShow .. lastToShow:
|
||||
if i == c.pageNum:
|
||||
pages.add(span($(i)))
|
||||
else:
|
||||
var pageUrl = ""
|
||||
if c.isThreadsList:
|
||||
pageUrl = c.req.makeUri("/page/" & $(i))
|
||||
else:
|
||||
pageUrl = c.req.makeUri(firstUrl & "/" & $(i))
|
||||
|
||||
pages.add(htmlgen.a(href = pageUrl, $(i)))
|
||||
if lastToShow < totalPages: pages.add(span("..."))
|
||||
result.add(pages)
|
||||
|
||||
# Right
|
||||
var lastTag = ""
|
||||
var nextTag = ""
|
||||
if c.pageNum == totalPages:
|
||||
lastTag = span(">>")
|
||||
nextTag = span("••>")
|
||||
else:
|
||||
lastTag = htmlgen.a(href=lastUrl, ">>")
|
||||
nextTag = htmlgen.a(href=nextUrl, "••>")
|
||||
nextTag.add(htmlgen.link(rel="next",href=nextUrl))
|
||||
result.add(nextTag)
|
||||
result.add(lastTag)
|
||||
|
||||
proc gatherTotalPostsByID(c: TForumData, thrid: int): int =
|
||||
## Gets the total post count of a thread.
|
||||
result = getValue(db, sql"select count(*) from post where thread = ?", $thrid).parseInt
|
||||
|
||||
proc gatherTotalPosts(c: TForumData) =
|
||||
if c.totalPosts > 0: return
|
||||
# Gather some data.
|
||||
const totalPostsQuery =
|
||||
sql"select count(*) from post p, person u where u.id = p.author and p.thread = ?"
|
||||
c.totalPosts = getValue(db, totalPostsQuery, $c.threadId).parseInt
|
||||
|
||||
proc getPagesInThread(c: TForumData): int =
|
||||
c.gatherTotalPosts() # Get total post count
|
||||
result = ceil(c.totalPosts / PostsPerPage).int-1
|
||||
|
||||
proc getPagesInThreadByID(c: TForumData, thrid: int): int =
|
||||
result = ceil(c.gatherTotalPostsByID(thrid) / PostsPerPage).int
|
||||
|
||||
proc getThreadTitle(thrid: int, pageNum: int): string =
|
||||
echo thrid
|
||||
result = getValue(db, sql"select name from thread where id = ?", $thrid)
|
||||
if pageNum notin {0,1}:
|
||||
result.add(" - Page " & $pageNum)
|
||||
|
||||
proc genPagenumLocalNav(c: TForumData, thrid: int): string =
|
||||
result = ""
|
||||
const maxPostPages = 6 # Maximum links to pages shown.
|
||||
const hmpp = maxPostPages div 2
|
||||
# 1 2 3 ... 10 11 12
|
||||
var currentThrURL = "/t/" & $thrid & "/"
|
||||
let totalPagesInThread = c.getPagesInThreadByID(thrid)
|
||||
if totalPagesInThread <= 1: return
|
||||
var i = 1
|
||||
while i <= totalPagesInThread:
|
||||
result.add(htmlgen.a(href=c.req.makeUri(currentThrURL & $i), $i))
|
||||
if i == hmpp and totalPagesInThread-i > hmpp:
|
||||
result.add(span("..."))
|
||||
# skip to the last 3
|
||||
i = totalPagesInThread-(hmpp-1)
|
||||
else:
|
||||
inc(i)
|
||||
|
||||
result = htmlgen.span(class = "pages", result)
|
||||
|
||||
proc gatherUserInfo(c: TForumData, nick: string, ui: var TUserInfo): bool =
|
||||
ui.nick = nick
|
||||
const getUIDQuery = sql"select id from person where name = ?"
|
||||
var uid = getValue(db, getUIDQuery, nick)
|
||||
if uid == "": return false
|
||||
result = true
|
||||
const totalPostsQuery =
|
||||
sql"select count(*) from post where author = ?"
|
||||
ui.posts = getValue(db, totalPostsQuery, uid).parseInt
|
||||
const totalThreadsQuery =
|
||||
sql("select count(*) from thread where id in (select thread from post where" &
|
||||
" author = ? and post.id in (select min(id) from post group by thread))")
|
||||
|
||||
ui.threads = getValue(db, totalThreadsQuery, uid).parseInt
|
||||
const lastOnlineQuery =
|
||||
sql"""select strftime('%s', lastOnline), email, ban, status
|
||||
from person where id = ?"""
|
||||
let row = db.getRow(lastOnlineQuery, $uid)
|
||||
ui.lastOnline = if row[0].len > 0: row[0].parseInt else: -1
|
||||
ui.email = row[1]
|
||||
ui.ban = row[2]
|
||||
ui.rank = parseEnum[Rank](row[3])
|
||||
|
||||
const lastIpQuery = sql"select `ip` from `session` where `userid` = ? order by `id` desc limit 1;"
|
||||
let ipRow = db.getRow(lastIpQuery, $uid)
|
||||
ui.lastIp = ipRow[0]
|
||||
|
||||
include "forms.tmpl"
|
||||
include "main.tmpl"
|
||||
|
||||
proc genProfile(c: TForumData, ui: TUserInfo): string =
|
||||
result = ""
|
||||
|
||||
result.add(htmlgen.`div`(id = "talk-head",
|
||||
htmlgen.`div`(class="info-post",
|
||||
htmlgen.`div`(
|
||||
htmlgen.a(href = c.req.makeUri("/"),
|
||||
span(style = "font-weight: bold;", "forum index")
|
||||
),
|
||||
" > " & ui.nick & "'s profile"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
result.add(htmlgen.`div`(id = "avatar", genGravatar(ui.email, 250)))
|
||||
let t2 = if ui.lastOnline != -1: getGMTime(fromUnix(ui.lastOnline))
|
||||
else: getGMTime(getTime())
|
||||
|
||||
result.add(htmlgen.`div`(id = "info",
|
||||
htmlgen.table(
|
||||
tr(
|
||||
th("Nickname"),
|
||||
td(ui.nick)
|
||||
),
|
||||
tr(
|
||||
th("Threads"),
|
||||
td($ui.threads)
|
||||
),
|
||||
tr(
|
||||
th("Posts"),
|
||||
td($ui.posts)
|
||||
),
|
||||
tr(
|
||||
th("Last Online"),
|
||||
td(if ui.lastOnline != -1: t2.format("dd/MM/yy HH':'mm 'UTC'")
|
||||
else: "Never")
|
||||
),
|
||||
tr(
|
||||
th("Status"),
|
||||
td($ui.rank)
|
||||
),
|
||||
tr(
|
||||
th(if c.rank >= Moderator: "Last IP" else: ""),
|
||||
td(if c.rank >= Moderator:
|
||||
htmlgen.a(href="http://whatismyipaddress.com/ip/" & encodeUrl(ui.lastIp), ui.lastIp)
|
||||
else: "")
|
||||
),
|
||||
tr(
|
||||
th(""),
|
||||
td(if c.rank >= Moderator and c.rank > ui.rank:
|
||||
c.genFormSetRank(ui)
|
||||
else: "")
|
||||
),
|
||||
tr(
|
||||
th(""),
|
||||
td(if c.rank >= Moderator:
|
||||
htmlgen.a(href=c.req.makeUri("/deleteAll?nick=$1" % ui.nick),
|
||||
"Delete all user's posts and threads")
|
||||
else: "")
|
||||
),
|
||||
)
|
||||
))
|
||||
|
||||
result = htmlgen.`div`(id = "profile",
|
||||
htmlgen.`div`(id = "left", result))
|
||||
|
||||
proc prependRe(s: string): string =
|
||||
result = if s.len == 0:
|
||||
""
|
||||
elif s.startswith("Re:"): s
|
||||
else: "Re: " & s
|
||||
|
||||
proc initialise() =
|
||||
randomize()
|
||||
|
||||
|
|
@ -1319,6 +734,8 @@ proc updateProfile(
|
|||
$rank, email, username
|
||||
)
|
||||
|
||||
include "main.tmpl"
|
||||
|
||||
initialise()
|
||||
|
||||
routes:
|
||||
|
|
@ -1869,64 +1286,6 @@ routes:
|
|||
createTFD()
|
||||
resp genPostsRSS(c), "application/atom+xml"
|
||||
|
||||
get "/deleteAll/?":
|
||||
createTFD()
|
||||
cond(@"nick" != "")
|
||||
var formBody = "<input type=\"hidden\" name=\"nick\" value=\"" &
|
||||
@"nick" & "\">"
|
||||
var del = false
|
||||
var content = ""
|
||||
formBody.add "<input type='submit' name='deleteAllBtn' value='Delete All' />"
|
||||
content = htmlgen.p("Are you sure you wish to delete all " &
|
||||
"the posts and threads created by ", htmlgen.b(@"nick"), "?")
|
||||
content = content & htmlgen.form(action = c.req.makeUri("/dodeleteall"),
|
||||
`method` = "POST", formBody)
|
||||
resp genMain(c, content, "Delete all user's posts & threads - Nim Forum")
|
||||
|
||||
post "/dodeleteall/?":
|
||||
createTFD()
|
||||
cond(@"nick" != "")
|
||||
if c.rank < Moderator:
|
||||
resp genMain(c, "You cannot delete this user's data.", "Error - Nim Forum")
|
||||
let res = deleteAll(c, @"nick")
|
||||
if res:
|
||||
redirect(c.req.makeUri("/profile/" & @"nick"))
|
||||
else:
|
||||
resp genMain(c, "Failed to delete all user's posts and threads.",
|
||||
"Error - NimForum")
|
||||
|
||||
post "/dosetrank/?@nick?/?":
|
||||
createTFD()
|
||||
cond(@"nick" != "")
|
||||
|
||||
if c.rank < Moderator:
|
||||
resp genMain(c, "You cannot change this user's rank.", "Error - Nim Forum")
|
||||
|
||||
var ui: TUserInfo
|
||||
if not gatherUserInfo(c, @"nick", ui):
|
||||
resp genMain(c, "User " & @"nick" & " does not exist.", "Error - Nim Forum")
|
||||
let newRank = parseEnum[Rank](@"rank")
|
||||
if newRank > c.rank:
|
||||
resp genMain(c, "You cannot change this user's rank to this value.", "Error - Nim Forum")
|
||||
|
||||
if setStatus(c, @"nick", newRank, @"reason"):
|
||||
redirect(c.req.makeUri("/profile/" & @"nick"))
|
||||
else:
|
||||
resp genMain(c, "Failed to change the ban status of user.",
|
||||
"Error - Nim Forum")
|
||||
|
||||
get "/setpassword/?":
|
||||
createTFD()
|
||||
cond(@"nick" != "")
|
||||
cond(@"pass" != "")
|
||||
if c.rank < Moderator:
|
||||
resp genMain(c, "You cannot change this user's pass.", "Error - Nim Forum")
|
||||
let res = setPassword(c, @"nick", @"pass")
|
||||
if res:
|
||||
resp genMain(c, "Success", "Nim Forum")
|
||||
else:
|
||||
resp genMain(c, "Failure", "Nim Forum")
|
||||
|
||||
get "/activateEmail/?":
|
||||
createTFD()
|
||||
cond(@"nick" != "")
|
||||
|
|
@ -1937,13 +1296,9 @@ routes:
|
|||
var success = false
|
||||
if verifyIdentHash(c, @"nick", $epoch, @"ident"):
|
||||
let ban = parseEnum[Rank](db.getValue(sql"select status from person where name = ?", @"nick"))
|
||||
if ban == EmailUnconfirmed:
|
||||
success = setStatus(c, @"nick", Moderated, "")
|
||||
# if ban == EmailUnconfirmed:
|
||||
# success = setStatus(c, @"nick", Moderated, "")
|
||||
|
||||
if success:
|
||||
resp genMain(c, "Account activated", "Nim Forum")
|
||||
else:
|
||||
resp genMain(c, "Account activation failed", "Nim Forum")
|
||||
|
||||
get "/emailResetPassword/?":
|
||||
createTFD()
|
||||
|
|
@ -1965,9 +1320,10 @@ routes:
|
|||
let content = htmlgen.form(action=c.req.makeUri("/doemailresetpassword"),
|
||||
`method`="POST", message & formBody)
|
||||
|
||||
resp genMain(c, content, "Reset password - Nim Forum")
|
||||
# resp genMain(c, content, "Reset password - Nim Forum")
|
||||
else:
|
||||
resp genMain(c, "Invalid ident hash", "Error - Nim Forum")
|
||||
discard
|
||||
# resp genMain(c, "Invalid ident hash", "Error - Nim Forum")
|
||||
|
||||
post "/doemailresetpassword":
|
||||
createTFD()
|
||||
|
|
@ -1977,29 +1333,24 @@ routes:
|
|||
cond(@"password" != "")
|
||||
var epoch: BiggestInt = 0
|
||||
cond(parseBiggestInt(@"epoch", epoch) > 0)
|
||||
if verifyIdentHash(c, @"nick", $epoch, @"ident"):
|
||||
let res = setPassword(c, @"nick", @"password")
|
||||
if res:
|
||||
resp genMain(c, "Password reset successfully!", "Nim Forum")
|
||||
else:
|
||||
resp genMain(c, "Password reset failure", "Nim Forum")
|
||||
else:
|
||||
resp genMain(c, "Invalid ident hash", "Nim Forum")
|
||||
|
||||
get "/resetPassword/?":
|
||||
createTFD()
|
||||
|
||||
resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum")
|
||||
# if verifyIdentHash(c, @"nick", $epoch, @"ident"):
|
||||
# let res = setPassword(c, @"nick", @"password")
|
||||
# if res:
|
||||
# resp genMain(c, "Password reset successfully!", "Nim Forum")
|
||||
# else:
|
||||
# resp genMain(c, "Password reset failure", "Nim Forum")
|
||||
# else:
|
||||
# resp genMain(c, "Invalid ident hash", "Nim Forum")
|
||||
|
||||
post "/doresetpassword":
|
||||
createTFD()
|
||||
echo(request.params)
|
||||
cond(@"nick" != "")
|
||||
|
||||
if await resetPassword(c, @"nick", @"g-recaptcha-response", request.host):
|
||||
resp genMain(c, "Email sent!", "Reset Password - Nim Forum")
|
||||
else:
|
||||
resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum")
|
||||
# if await resetPassword(c, @"nick", @"g-recaptcha-response", request.host):
|
||||
# resp genMain(c, "Email sent!", "Reset Password - Nim Forum")
|
||||
# else:
|
||||
# resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum")
|
||||
|
||||
post "/search/?@page?":
|
||||
cond isFTSAvailable
|
||||
|
|
@ -2021,28 +1372,5 @@ routes:
|
|||
[q,q,$ThreadsPerPage,$c.pageNum,$ThreadsPerPage,q,
|
||||
q,q,$ThreadsPerPage,$c.pageNum,$ThreadsPerPage,q]):
|
||||
yield rowFT
|
||||
resp genMain(c, genSearchResults(c, searchResults, count),
|
||||
additionalHeaders = genRSSHeaders(c), showRssLinks = true)
|
||||
|
||||
# tries first to read html, then to read rst, convert ot html, cache and return
|
||||
template textPage(path: string) =
|
||||
createTFD()
|
||||
#c.isThreadsList = true
|
||||
var page = ""
|
||||
if existsFile(path):
|
||||
page = readFile(path)
|
||||
else:
|
||||
let basePath =
|
||||
if path[path.high] == '/': path & "index"
|
||||
elif path.endsWith(".html"): path[^5 .. ^1]
|
||||
else: path
|
||||
if existsFile(basePath & ".html"):
|
||||
page = readFile(basePath & ".html")
|
||||
elif existsFile(basePath & ".rst"):
|
||||
page = readFile(basePath & ".rst").rstToHtml
|
||||
writeFile(basePath & ".html", page)
|
||||
resp genMain(c, page)
|
||||
get "/search-help":
|
||||
textPage "static/search-help"
|
||||
get "/rst":
|
||||
textPage "static/rst"
|
||||
# resp genMain(c, genSearchResults(c, searchResults, count),
|
||||
# additionalHeaders = genRSSHeaders(c), showRssLinks = true)
|
||||
174
src/main.tmpl
174
src/main.tmpl
|
|
@ -1,175 +1,7 @@
|
|||
#? stdtmpl | standard
|
||||
#proc genMain(c: TForumData, content: string, title = "Nim Forum",
|
||||
# additional_headers = "", showRssLinks = false): string =
|
||||
# result = ""
|
||||
# var stats: TForumStats
|
||||
# if c.isThreadsList: stats = c.getStats(false)
|
||||
# else:
|
||||
# stats = c.getStats(true)
|
||||
# end if
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>${xmlEncode(title)}</title>
|
||||
<link rel="stylesheet" href="${c.req.makeUri("css/style.css", absolute = false)}">
|
||||
<link rel="shortcut icon" href="${c.req.makeUri("favicon.ico", absolute = false)}">${additional_headers}
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<!--- #head --->
|
||||
#let frontQuery = c.req.makeUri("/")
|
||||
<header id="head" class="forum">
|
||||
<div class="wide-layout tall">
|
||||
<div id="head-logo"></div>
|
||||
<a id="head-logo-link" href="/"></a>
|
||||
<nav id="head-links">
|
||||
<a href="http://nim-lang.org/">home</a>
|
||||
<a href="http://nim-lang.org/learn.html">learn</a>
|
||||
<a href="http://nim-lang.org/documentation.html">docs</a>
|
||||
<a href="http://nim-lang.org/download.html">download</a>
|
||||
<a href="http://nim-lang.org/support.html">support</a>
|
||||
<a href="${frontQuery}" class="active">forum</a>
|
||||
<a href="http://nim-lang.org/question.html">faq</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!--- #body --->
|
||||
<section id="body" class="forum">
|
||||
<div id="body-border"></div>
|
||||
<div id="body-border-left"></div>
|
||||
<div id="body-border-right"></div>
|
||||
<div id="body-border-bottom"></div>
|
||||
<div id="glow-line"></div>
|
||||
<div id="glow-line-bottom"></div>
|
||||
<div class="talk-layout">
|
||||
<article id="content">
|
||||
${content}
|
||||
#if c.isThreadsList:
|
||||
<div id="talk-info">
|
||||
<div class="info">
|
||||
<div>
|
||||
${c.genListOnline(stats)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
#if not c.noPagenumumNav:
|
||||
<div id="talk-nav">
|
||||
${genPagenumNav(c, stats)}
|
||||
</div>
|
||||
#end if
|
||||
#elif hasReplyBtn(c):
|
||||
<div id="talk-info">
|
||||
<div class="info-post">
|
||||
<div>
|
||||
${genPagenumNav(c, stats)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-post">
|
||||
#if c.loggedIn():
|
||||
#let replyUri = c.req.makeUri(c.req.path & "?action=reply#reply")
|
||||
<a href="$replyUri">
|
||||
<div>
|
||||
<span class="reply">Reply</span>
|
||||
</div>
|
||||
</a>
|
||||
#end if
|
||||
</div>
|
||||
</div>
|
||||
#end if
|
||||
|
||||
</article>
|
||||
<div id="sidebar">
|
||||
<div class="title">Search</div>
|
||||
<div class="content">
|
||||
#if isFTSAvailable:
|
||||
<a href="/search-help" target="_blank" class="searchHelp">?</a>
|
||||
<form method="post" action="/search" class="searchForm">
|
||||
<input type="text" name="q" maxlength="255" value="${c.search}" title="Search this forum" />
|
||||
<input type="submit" value="Search" class="button search">
|
||||
</form>
|
||||
#else:
|
||||
<form method="get" action="http://www.google.com/search" target="_blank" class="searchForm">
|
||||
<input type="text" name="q" maxlength="255" value="${c.search}" title="Search this forum" />
|
||||
<input type="submit" value="Search" class="button search">
|
||||
<input type="hidden" name="sitesearch" value="http://forum.nim-lang.org" />
|
||||
</form>
|
||||
#end if
|
||||
</div>
|
||||
#if c.loggedIn:
|
||||
<div class="title">Your account</div>
|
||||
<div class="content user">
|
||||
#let profileUrl = c.req.makeUri("profile/", false) &
|
||||
# xmlEncode(c.username)
|
||||
<a href="$profileUrl" class="user">${c.username}</a>
|
||||
<a href="$profileUrl" class="avatar">${genGravatar(c.email)}</a>
|
||||
<a href="${frontQuery}newthread" class="button">New Thread</a>
|
||||
<a href="$profileUrl" class="button">My Profile</a>
|
||||
|
||||
<a href="${frontQuery}logout" class="button logout">Logout</a>
|
||||
|
||||
</div>
|
||||
#else:
|
||||
<div class="title">Login</div>
|
||||
<div class="content">
|
||||
<form name="login" action="${frontQuery}dologin" method="POST">
|
||||
<span>Username: </span><input type="text" name="name" />
|
||||
<span>Password: </span><input type="password" name="password" />
|
||||
<input type="submit" style="display: none;"
|
||||
id="hdnLogin" value="Login" />
|
||||
</form>
|
||||
<a id="passreset" href=${c.req.makeUri("/resetPassword")}>Reset password</a>
|
||||
#if c.errorMsg != "":
|
||||
<span class="error">$c.errorMsg</span>
|
||||
#end if
|
||||
<a href="${frontQuery}register" class="button"
|
||||
style="float: left;">Register</a>
|
||||
<a href="#" onclick="document.forms['login'].submit()"
|
||||
class="button">Login</a>
|
||||
</div>
|
||||
#end if
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--- #foot --->
|
||||
<footer id="foot" class="forum">
|
||||
<div class="talk-layout tall">
|
||||
<div id="foot-links">
|
||||
<div>
|
||||
<h4>Documentation</h4>
|
||||
<a href="http://nim-lang.org/documentation.html">Stable Documentation</a>
|
||||
<a href="https://github.com/nim-lang/nim/issues">Github Issues & Requests</a>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Community</h4>
|
||||
<a href="http://forum.nim-lang.org">User Forum</a>
|
||||
<a href="http://webchat.freenode.net/?channels=nim">Online IRC</a>
|
||||
<a href="http://irclogs.nim-lang.org/">IRC Logs</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="foot-legal">
|
||||
<h4>Written in Nim - Powered by <a href="https://github.com/dom96/jester">Jester</a></h4>
|
||||
Web Design by <a href="http://reign-studios.net/philipwitte/">Philip Witte</a> & <a href="http://picheta.me">Dominik Picheta</a><br>
|
||||
Copyright © 2015 - <a href="https://nim-lang.org/araq/index.html">Andreas Rumpf</a> & <a href="https://github.com/nim-lang/nim/graphs/contributors">Contributors</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-58103537-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
#end proc
|
||||
#
|
||||
#template `!`(idx: untyped): untyped =
|
||||
# row[idx]
|
||||
#end template
|
||||
#proc genRSSHeaders(c: TForumData): string =
|
||||
# result = ""
|
||||
<link href="${c.req.makeUri("/threadActivity.xml")}" title="Thread activity"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue