Merge branch 'new_async'
Conflicts: forum.nim main.tmpl
1
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
# Wildcard patterns.
|
# Wildcard patterns.
|
||||||
*.swp
|
*.swp
|
||||||
nimcache/
|
nimcache/
|
||||||
|
*.db
|
||||||
|
|
||||||
# Specific paths
|
# Specific paths
|
||||||
/createdb
|
/createdb
|
||||||
|
|
|
||||||
32
cache.nim
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import tables, uri
|
||||||
|
type
|
||||||
|
CacheInfo = object
|
||||||
|
valid: bool
|
||||||
|
value: string
|
||||||
|
|
||||||
|
CacheHolder = ref object
|
||||||
|
caches: Table[string, CacheInfo]
|
||||||
|
|
||||||
|
proc normalizePath(x: string): string =
|
||||||
|
let u = parseUri(x)
|
||||||
|
result = u.path & (if u.query != "": '?' & u.query else: "")
|
||||||
|
|
||||||
|
proc newCacheHolder*(): CacheHolder =
|
||||||
|
new result
|
||||||
|
result.caches = initTable[string, CacheInfo]()
|
||||||
|
|
||||||
|
proc invalidate*(cache: CacheHolder, name: string) =
|
||||||
|
cache.caches.mget(name.normalizePath()).valid = false
|
||||||
|
|
||||||
|
proc invalidateAll*(cache: CacheHolder) =
|
||||||
|
for key, val in mpairs(cache.caches):
|
||||||
|
val.valid = false
|
||||||
|
|
||||||
|
template get*(cache: CacheHolder, name: string, grabValue: expr): expr =
|
||||||
|
## Check to see if the cache contains value for ``name``. If it does and the
|
||||||
|
## cache is valid then doesn't recalculate it but returns the cached version.
|
||||||
|
mixin normalizePath
|
||||||
|
let nName = name.normalizePath()
|
||||||
|
if not (cache.caches.hasKey(nName) and cache.caches[nName].valid):
|
||||||
|
cache.caches[nName] = CacheInfo(valid: true, value: grabValue)
|
||||||
|
cache.caches[nName].value
|
||||||
|
|
@ -11,7 +11,7 @@ import cairo, os, strutils, jester
|
||||||
proc getCaptchaFilename*(i: int): string {.inline.} =
|
proc getCaptchaFilename*(i: int): string {.inline.} =
|
||||||
result = "public/captchas/capture_" & $i & ".png"
|
result = "public/captchas/capture_" & $i & ".png"
|
||||||
|
|
||||||
proc getCaptchaUrl*(req: var TRequest, i: int): string =
|
proc getCaptchaUrl*(req: PRequest, i: int): string =
|
||||||
result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false)
|
result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false)
|
||||||
|
|
||||||
proc createCaptcha*(file, text: string) =
|
proc createCaptcha*(file, text: string) =
|
||||||
|
|
|
||||||
54
createdb.nim
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import strutils, db_sqlite
|
import strutils, db_sqlite
|
||||||
|
|
||||||
var db = Open(connection="nimforum.db", user="postgres", password="",
|
var db = open(connection="nimforum.db", user="postgres", password="",
|
||||||
database="nimforum")
|
database="nimforum")
|
||||||
|
|
||||||
const
|
const
|
||||||
|
|
@ -16,7 +16,7 @@ const
|
||||||
TPassword = "varchar(32)"
|
TPassword = "varchar(32)"
|
||||||
TEmail = "varchar(30)"
|
TEmail = "varchar(30)"
|
||||||
|
|
||||||
db.Exec(sql"""
|
db.exec(sql"""
|
||||||
create table if not exists thread(
|
create table if not exists thread(
|
||||||
id integer primary key,
|
id integer primary key,
|
||||||
name varchar(100) not null,
|
name varchar(100) not null,
|
||||||
|
|
@ -24,11 +24,11 @@ create table if not exists thread(
|
||||||
modified timestamp not null default (DATETIME('now'))
|
modified timestamp not null default (DATETIME('now'))
|
||||||
);""", [])
|
);""", [])
|
||||||
|
|
||||||
db.Exec(sql"""
|
db.exec(sql"""
|
||||||
create unique index if not exists ThreadNameIx on thread (name);
|
create unique index if not exists ThreadNameIx on thread (name);
|
||||||
""", [])
|
""", [])
|
||||||
|
|
||||||
db.Exec(sql("""
|
db.exec(sql("""
|
||||||
create table if not exists person(
|
create table if not exists person(
|
||||||
id integer primary key,
|
id integer primary key,
|
||||||
name $# not null,
|
name $# not null,
|
||||||
|
|
@ -42,14 +42,19 @@ create table if not exists person(
|
||||||
);""" % [TUserName, TPassword, TEmail]), [])
|
);""" % [TUserName, TPassword, TEmail]), [])
|
||||||
# echo "person table already exists"
|
# echo "person table already exists"
|
||||||
|
|
||||||
db.Exec(sql"""
|
db.exec(sql("""
|
||||||
|
alter table person
|
||||||
|
add ban varchar(128) not null default ''
|
||||||
|
"""))
|
||||||
|
|
||||||
|
db.exec(sql"""
|
||||||
create unique index if not exists UserNameIx on person (name);
|
create unique index if not exists UserNameIx on person (name);
|
||||||
""", [])
|
""", [])
|
||||||
|
|
||||||
# ----------------------- Forum ------------------------------------------------
|
# ----------------------- Forum ------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
if not db.TryExec(sql"""
|
if not db.tryExec(sql"""
|
||||||
create table if not exists post(
|
create table if not exists post(
|
||||||
id integer primary key,
|
id integer primary key,
|
||||||
author integer not null,
|
author integer not null,
|
||||||
|
|
@ -66,7 +71,7 @@ create table if not exists post(
|
||||||
|
|
||||||
# -------------------- Session -------------------------------------------------
|
# -------------------- Session -------------------------------------------------
|
||||||
|
|
||||||
if not db.TryExec(sql("""
|
if not db.tryExec(sql("""
|
||||||
create table if not exists session(
|
create table if not exists session(
|
||||||
id integer primary key,
|
id integer primary key,
|
||||||
ip inet not null,
|
ip inet not null,
|
||||||
|
|
@ -77,7 +82,7 @@ create table if not exists session(
|
||||||
);""" % [TPassword]), []):
|
);""" % [TPassword]), []):
|
||||||
echo "session table already exists"
|
echo "session table already exists"
|
||||||
|
|
||||||
if not db.TryExec(sql"""
|
if not db.tryExec(sql"""
|
||||||
create table if not exists antibot(
|
create table if not exists antibot(
|
||||||
id integer primary key,
|
id integer primary key,
|
||||||
ip inet not null,
|
ip inet not null,
|
||||||
|
|
@ -86,8 +91,35 @@ create table if not exists antibot(
|
||||||
);""", []):
|
);""", []):
|
||||||
echo "antibot table already exists"
|
echo "antibot table already exists"
|
||||||
|
|
||||||
|
# -------------------- Search --------------------------------------------------
|
||||||
|
|
||||||
|
if not db.tryExec(sql"""
|
||||||
|
CREATE VIRTUAL TABLE thread_fts USING fts4 (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL
|
||||||
|
);""", []):
|
||||||
|
echo "thread_fts table already exists or fts4 not supported"
|
||||||
|
else:
|
||||||
|
db.exec(sql"""
|
||||||
|
INSERT INTO thread_fts
|
||||||
|
SELECT id, name FROM thread;
|
||||||
|
""", [])
|
||||||
|
if not db.tryExec(sql"""
|
||||||
|
CREATE VIRTUAL TABLE post_fts USING fts4 (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
header VARCHAR(100) NOT NULL,
|
||||||
|
content VARCHAR(1000) NOT NULL
|
||||||
|
);""", []):
|
||||||
|
echo "post_fts table already exists or fts4 not supported"
|
||||||
|
else:
|
||||||
|
db.exec(sql"""
|
||||||
|
INSERT INTO post_fts
|
||||||
|
SELECT id, header, content FROM post;
|
||||||
|
""", [])
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
#discard stdin.readline()
|
#discard stdin.readline()
|
||||||
|
|
||||||
Close(db)
|
close(db)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
327
forms.tmpl
|
|
@ -13,43 +13,74 @@
|
||||||
#
|
#
|
||||||
# result = ""
|
# result = ""
|
||||||
# count = 0
|
# count = 0
|
||||||
<table id="threads">
|
<div id="talk-heads">
|
||||||
<tr>
|
<div class="topic">
|
||||||
<th>Topics</th>
|
<div>
|
||||||
<th>Author</th>
|
Topic
|
||||||
<th>Posts</th>
|
<a href="${c.req.makeUri("/threadActivity.xml")}">
|
||||||
<th>Views</th>
|
<img src="/images/Feed-icon.svg" class="rssfeed">
|
||||||
<th>Last reply</th>
|
</a>
|
||||||
</tr>
|
</div>
|
||||||
# for row in Rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage):
|
</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, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage):
|
||||||
# inc(count)
|
# inc(count)
|
||||||
<tr>
|
<div>
|
||||||
<td class="topic">
|
<div class="topic">
|
||||||
${UrlButton(c, XMLencode(%name), c.genThreadUrl(threadid = %threadid))}
|
<div>
|
||||||
|
<a href="${c.genThreadUrl(threadid = %threadid)}"
|
||||||
|
title="${xmlEncode(%name)}">${xmlEncode(%name)}</a>
|
||||||
${genPagenumLocalNav(c, (%threadid).parseInt)}
|
${genPagenumLocalNav(c, (%threadid).parseInt)}
|
||||||
</td>
|
</div>
|
||||||
#let authorName = getValue(db, sql("select name from person where id = " &
|
</div>
|
||||||
# "(select author from post where id = " &
|
|
||||||
# "(select min(id) from post where thread = ?))"), %threadId)
|
#let users = getAllRows(db,
|
||||||
#let profileUrl = c.req.makeUri("profile/", false) & XMLEncode(authorName)
|
# sql("select distinct name, email from person where id in " &
|
||||||
<td class="author"><a href="$profileUrl">${authorName}</a></td>
|
# "(select author from post where thread = ?)"), %threadId)
|
||||||
# let posts = GetValue(db, sql"select count(*) from post where thread = ?", %threadId)
|
<div class="users">
|
||||||
<td class="posts">$posts</td>
|
<div>
|
||||||
<td class="views">${XMLencode(%views)}</td>
|
#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 = " &
|
#let latestReplyAuthor = getValue(db, sql("select name from person where id = " &
|
||||||
# "(select author from post where id = " &
|
# "(select author from post where id = " &
|
||||||
# "(select max(id) from post where thread = ?))"), %threadId)
|
# "(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', " &
|
#let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " &
|
||||||
# "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId)
|
# "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId)
|
||||||
<td class="lastreply">
|
#let timeStr = formatTimestamp(latestReplyDate.parseInt())
|
||||||
<span>${formatTimestamp(latestReplyDate.parseInt())}</span><br/>
|
<div class="activity">
|
||||||
#let replyProfileUrl = c.req.makeUri("profile/", false) &
|
<div>
|
||||||
# XMLEncode(latestReplyAuthor)
|
<a href="$replyProfileUrl">$latestReplyAuthor</a> replied $timeStr
|
||||||
<span><a href="$replyProfileUrl">${latestReplyAuthor}</a></span>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
# end for
|
# end for
|
||||||
</table>
|
</div>
|
||||||
|
|
||||||
#end proc
|
#end proc
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
@ -57,26 +88,26 @@
|
||||||
# title, content, author, date: string): string =
|
# title, content, author, date: string): string =
|
||||||
# result = ""
|
# result = ""
|
||||||
<a name="preview"></a>
|
<a name="preview"></a>
|
||||||
<table class="post">
|
<div id="talk-thread">
|
||||||
<tr>
|
<div>
|
||||||
<th colspan="2">
|
<div class="author">
|
||||||
<span>${XMLEncode(title)}</span>
|
<div>
|
||||||
<span style="float:right;">${XMLencode(date)}</span>
|
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(author)
|
||||||
</th>
|
<a class="name" href="$profileUrl">${xmlEncode(author)}</a>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
</div>
|
||||||
<td class="left">
|
<div class="topic">
|
||||||
<span>${XMLencode(author)}</span>
|
<div>
|
||||||
</td>
|
|
||||||
<td class="content">
|
|
||||||
#try:
|
#try:
|
||||||
${content.rstToHtml}
|
${content.rstToHtml}
|
||||||
#except EParseError:
|
#except EParseError:
|
||||||
# c.errorMsg = getCurrentExceptionMsg()
|
# c.errorMsg = getCurrentExceptionMsg()
|
||||||
#end
|
#end
|
||||||
</td>
|
<span class="date">${xmlEncode(date)}</span>
|
||||||
</tr>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
#end proc
|
#end proc
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
@ -92,39 +123,47 @@
|
||||||
# const userEmail = 6
|
# const userEmail = 6
|
||||||
# result = ""
|
# result = ""
|
||||||
# count = 0
|
# count = 0
|
||||||
# for row in FastRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage):
|
# let posts = getAllRows(db, query, threadId, $((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)
|
# inc(count)
|
||||||
<a name="${%postId}"></a>
|
<a name="${%postId}"></a>
|
||||||
<table class="post">
|
<div id="${%postId}">
|
||||||
<tr>
|
<div class="author">
|
||||||
<th colspan="2">
|
<div>
|
||||||
<span>${XMLencode(%postHeader)}</span>
|
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName)
|
||||||
<span style="float:right;">${XMLencode(%postCreation)}</span>
|
<div class="avatar">${genGravatar(%userEmail)}</div>
|
||||||
</th>
|
<a class="name" href="$profileUrl">${xmlEncode(%userName)}</a>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="left">
|
|
||||||
#let profileUrl = c.req.makeUri("profile/", false) & XMLencode(%userName)
|
|
||||||
<span><a href="$profileUrl">${XMLencode(%userName)}</a></span>
|
|
||||||
<hr/>
|
|
||||||
<a href="$profileUrl">${genGravatar(%userEmail)}</a>
|
|
||||||
#if c.userId == %postAuthor and c.currentPost.subject.len == 0:
|
#if c.userId == %postAuthor and c.currentPost.subject.len == 0:
|
||||||
<hr/>${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))}
|
<hr/><a href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
|
||||||
#elif c.isAdmin and c.currentPost.subject.len == 0:
|
#elif c.isAdmin and c.currentPost.subject.len == 0:
|
||||||
<hr/><span style="color:red">
|
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
|
||||||
${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))}</span>
|
|
||||||
#end if
|
#end if
|
||||||
</td>
|
</div>
|
||||||
<td class="content">
|
</div>
|
||||||
|
<div class="topic">
|
||||||
|
<div>
|
||||||
#try:
|
#try:
|
||||||
${(%postContent).rstToHtml}
|
${(%postContent).rstToHtml}
|
||||||
#except EParseError:
|
#except EParseError:
|
||||||
# c.errorMsg = getCurrentExceptionMsg()
|
# c.errorMsg = getCurrentExceptionMsg()
|
||||||
#end
|
#end
|
||||||
</td>
|
<span class="date">${xmlEncode(%postCreation)}</span>
|
||||||
</tr>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
</div>
|
||||||
# end for
|
# end for
|
||||||
|
</div>
|
||||||
#end proc
|
#end proc
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
@ -134,25 +173,39 @@
|
||||||
<br />
|
<br />
|
||||||
<a name="reply"></a>
|
<a name="reply"></a>
|
||||||
<div id="replywrapper">
|
<div id="replywrapper">
|
||||||
<div id="replytop">
|
<div id="talk-head">
|
||||||
<span>${topText}</span>
|
<div class="info-post">
|
||||||
|
<div>
|
||||||
|
<a href="${c.req.makeUri("/")}"><b>forum index</b></a> >
|
||||||
|
$topText
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form action="${c.req.makeUri(action, false) & "#preview"}" method="POST">
|
<form action="${c.req.makeUri(action, false) & "#preview"}" method="POST">
|
||||||
|
#if action == "doreply":
|
||||||
|
${HiddenField(c, "subject", title)}
|
||||||
|
#else:
|
||||||
${FieldValid(c, "subject", "Subject:")}
|
${FieldValid(c, "subject", "Subject:")}
|
||||||
${TextWidget(c, "subject", title, maxlength=100)}
|
${TextWidget(c, "subject", title, maxlength=100)}
|
||||||
<br />
|
<br />
|
||||||
|
#end if
|
||||||
${FieldValid(c, "content", "Content:")}<br />
|
${FieldValid(c, "content", "Content:")}<br />
|
||||||
${TextAreaWidget(c, "content", content, width=100, height=20)}<br />
|
${TextAreaWidget(c, "content", content)}<br />
|
||||||
${FormSession(c, action)}
|
${FormSession(c, action)}
|
||||||
|
|
||||||
# if isEdit:
|
# if isEdit:
|
||||||
<input type="checkbox" name="delete" value="Delete">Delete Post<br />
|
<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
|
#end if
|
||||||
<br/>
|
<br/>
|
||||||
<input type="submit" name="previewBtn" value="Preview" />
|
<input type="submit" name="previewBtn" value="Preview" />
|
||||||
<input type="submit" name="postBtn" value="Submit" />
|
<input type="submit" name="postBtn" value="Submit" />
|
||||||
|
|
||||||
<a href="http://nimrod-lang.org/rst.html">Syntax Cheatsheet</a>
|
<a href="http://nim-lang.org/rst.html">Syntax Cheatsheet</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
#end proc
|
#end proc
|
||||||
|
|
@ -160,8 +213,15 @@
|
||||||
#
|
#
|
||||||
#proc genFormRegister(c: var TForumData): string =
|
#proc genFormRegister(c: var TForumData): string =
|
||||||
# result = ""
|
# 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">
|
<form action="${c.req.makeUri("/doregister", false)}" method="POST">
|
||||||
<b>Register</b><br />
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td>${FieldValid(c, "name", "Username:")}</td>
|
<td>${FieldValid(c, "name", "Username:")}</td>
|
||||||
|
|
@ -180,6 +240,11 @@
|
||||||
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td>
|
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
#if c.errorMsg != "":
|
||||||
|
<div style="float: left; width: 100%;">
|
||||||
|
<span class="error">$c.errorMsg</span>
|
||||||
|
</div>
|
||||||
|
#end if
|
||||||
<input type="submit" value="Register">
|
<input type="submit" value="Register">
|
||||||
</form>
|
</form>
|
||||||
#end proc
|
#end proc
|
||||||
|
|
@ -205,38 +270,100 @@
|
||||||
#
|
#
|
||||||
#proc genListOnline(c: var TForumData, stats: TForumStats): string =
|
#proc genListOnline(c: var TForumData, stats: TForumStats): string =
|
||||||
# result = ""
|
# result = ""
|
||||||
<div id="whoisonline">
|
# var active: seq[string] = @[]
|
||||||
<div class="wioHeader">
|
# for i in stats.activeUsers:
|
||||||
<span>Who is online?<span>
|
# active.add(i.nick)
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<span>Out of ${stats.totalUsers} users ${stats.activeUsers.len} are online${if stats.activeUsers.len == 0: "." else: ":"}
|
|
||||||
#for index, usr in stats.activeUsers:
|
|
||||||
# let profileHref = """<a href="""" & c.req.makeUri("profile/", false) &
|
|
||||||
# XMLencode(usr.nick) & """">"""
|
|
||||||
# let hrefEnd = """</a>"""
|
|
||||||
# if usr.isAdmin:
|
|
||||||
#if index != 0: result.add ','
|
|
||||||
#end if
|
|
||||||
#result.add("""<span class="user admin"> """ & profileHref &
|
|
||||||
# usr.nick & hrefEnd & """</span>""")
|
|
||||||
# else:
|
|
||||||
#if index != 0: result.add ','
|
|
||||||
#end if
|
|
||||||
#result.add("""<span class="user"> """ & profileHref &
|
|
||||||
# usr.nick & hrefEnd & """</span>""")
|
|
||||||
# end if
|
|
||||||
# end for
|
# end for
|
||||||
</span>
|
|
||||||
<hr/>
|
|
||||||
#if stats.newestMember.nick != "":
|
|
||||||
# let profileUrl = c.req.makeUri("profile/", false) &
|
# let profileUrl = c.req.makeUri("profile/", false) &
|
||||||
# XMLEncode(stats.newestMember.nick)
|
# xmlEncode(stats.newestMember.nick)
|
||||||
<span>Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} | Newest member: <a href="$profileUrl">${stats.newestMember.nick}</a></span>
|
<span class="forum-user-info" title="${active.join(", ")}">
|
||||||
#else:
|
<b>${stats.activeUsers.len}</b> of <b>${stats.totalUsers}</b> users online</span> |
|
||||||
<span>Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts}</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: var TForumData,
|
||||||
|
# results: iterator: db_sqlite.TRow {.closure, tags: [FReadDB].},
|
||||||
|
# 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.isAdmin and c.currentPost.subject.len == 0:
|
||||||
|
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
|
||||||
#end if
|
#end if
|
||||||
</div>
|
</div>
|
||||||
</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
|
#end proc
|
||||||
|
#
|
||||||
|
|
|
||||||
467
forum.nim
|
|
@ -1,6 +1,6 @@
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# The Nimrod Forum
|
# The Nim Forum
|
||||||
# (c) Copyright 2012 Andreas Rumpf, Dominik Picheta
|
# (c) Copyright 2012 Andreas Rumpf, Dominik Picheta
|
||||||
# Look at license.txt for more info.
|
# Look at license.txt for more info.
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
|
|
@ -8,7 +8,10 @@
|
||||||
|
|
||||||
import
|
import
|
||||||
os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers,
|
os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers,
|
||||||
rst, rstgen, captchas, sockets, scgi, jester
|
rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils
|
||||||
|
|
||||||
|
when not defined(windows):
|
||||||
|
import bcrypt # TODO
|
||||||
|
|
||||||
from htmlgen import tr, th, td, span
|
from htmlgen import tr, th, td, span
|
||||||
|
|
||||||
|
|
@ -18,13 +21,15 @@ const
|
||||||
|
|
||||||
ThreadsPerPage = 15
|
ThreadsPerPage = 15
|
||||||
PostsPerPage = 10
|
PostsPerPage = 10
|
||||||
|
MaxPagesFromCurrent = 8
|
||||||
noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"]
|
noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"]
|
||||||
noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"]
|
noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"]
|
||||||
|
banReasonDeactivated = "DEACTIVATED"
|
||||||
|
|
||||||
type
|
type
|
||||||
TCrud = enum crCreate, crRead, crUpdate, crDelete
|
TCrud = enum crCreate, crRead, crUpdate, crDelete
|
||||||
|
|
||||||
TSession = object of TObject
|
TSession = object of RootObj
|
||||||
threadid: int
|
threadid: int
|
||||||
postid: int
|
postid: int
|
||||||
userName, userPass, email: string
|
userName, userPass, email: string
|
||||||
|
|
@ -33,7 +38,7 @@ type
|
||||||
TPost = tuple[subject, content: string]
|
TPost = tuple[subject, content: string]
|
||||||
|
|
||||||
TForumData = object of TSession
|
TForumData = object of TSession
|
||||||
req: TRequest
|
req: PRequest
|
||||||
userid: string
|
userid: string
|
||||||
actionContent: string
|
actionContent: string
|
||||||
errorMsg, loginErrorMsg: string
|
errorMsg, loginErrorMsg: string
|
||||||
|
|
@ -43,6 +48,8 @@ type
|
||||||
isThreadsList: bool
|
isThreadsList: bool
|
||||||
pageNum: int
|
pageNum: int
|
||||||
totalPosts: int
|
totalPosts: int
|
||||||
|
search: string
|
||||||
|
noPagenumumNav: bool
|
||||||
|
|
||||||
TStyledButton = tuple[text: string, link: string]
|
TStyledButton = tuple[text: string, link: string]
|
||||||
|
|
||||||
|
|
@ -59,10 +66,12 @@ type
|
||||||
threads: int
|
threads: int
|
||||||
lastOnline: int
|
lastOnline: int
|
||||||
email: string
|
email: string
|
||||||
|
ban: string
|
||||||
|
|
||||||
var
|
var
|
||||||
db: TDbConn
|
db: TDbConn
|
||||||
docConfig: PStringTable
|
docConfig: StringTableRef
|
||||||
|
isFTSAvailable: bool
|
||||||
|
|
||||||
proc init(c: var TForumData) =
|
proc init(c: var TForumData) =
|
||||||
c.userPass = ""
|
c.userPass = ""
|
||||||
|
|
@ -77,6 +86,8 @@ proc init(c: var TForumData) =
|
||||||
c.invalidField = ""
|
c.invalidField = ""
|
||||||
c.currentPost = (subject: "", content: "")
|
c.currentPost = (subject: "", content: "")
|
||||||
|
|
||||||
|
c.search = ""
|
||||||
|
|
||||||
proc loggedIn(c: TForumData): bool =
|
proc loggedIn(c: TForumData): bool =
|
||||||
result = c.userName.len > 0
|
result = c.userName.len > 0
|
||||||
|
|
||||||
|
|
@ -90,16 +101,20 @@ const
|
||||||
proc TextWidget(c: TForumData, name, defaultText: string,
|
proc TextWidget(c: TForumData, name, defaultText: string,
|
||||||
maxlength = 30, size = -1): string =
|
maxlength = 30, size = -1): string =
|
||||||
let x = if defaultText != reuseText: defaultText
|
let x = if defaultText != reuseText: defaultText
|
||||||
else: XMLencode(c.req.params[name])
|
else: xmlEncode(c.req.params[name])
|
||||||
return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [
|
return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [
|
||||||
name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""]
|
name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""]
|
||||||
|
|
||||||
proc TextAreaWidget(c: TForumData, name, defaultText: string,
|
proc HiddenField(c: TForumData, name, defaultText: string): string =
|
||||||
width = 80, height = 20): string =
|
|
||||||
let x = if defaultText != reuseText: defaultText
|
let x = if defaultText != reuseText: defaultText
|
||||||
else: XMLencode(c.req.params[name])
|
else: xmlEncode(c.req.params[name])
|
||||||
return """<textarea name="$1" cols="$2" rows="$3">$4</textarea>""" % [
|
return """<input type="hidden" name="$1" value="$2"/>""" % [name, x]
|
||||||
name, $width, $height, x]
|
|
||||||
|
proc TextAreaWidget(c: TForumData, name, defaultText: string): string =
|
||||||
|
let x = if defaultText != reuseText: defaultText
|
||||||
|
else: xmlEncode(c.req.params[name])
|
||||||
|
return """<textarea name="$1">$2</textarea>""" % [
|
||||||
|
name, x]
|
||||||
|
|
||||||
proc FieldValid(c: TForumData, name, text: string): string =
|
proc FieldValid(c: TForumData, name, text: string): string =
|
||||||
if name == c.invalidField:
|
if name == c.invalidField:
|
||||||
|
|
@ -115,6 +130,8 @@ proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNu
|
||||||
result.add("?action=" & action)
|
result.add("?action=" & action)
|
||||||
if postId != "":
|
if postId != "":
|
||||||
result.add("&postid=" & postid)
|
result.add("&postid=" & postid)
|
||||||
|
elif postId != "":
|
||||||
|
result.add("#" & postId)
|
||||||
result = c.req.makeUri(result, absolute = false)
|
result = c.req.makeUri(result, absolute = false)
|
||||||
|
|
||||||
proc FormSession(c: var TForumData, nextAction: string): string =
|
proc FormSession(c: var TForumData, nextAction: string): string =
|
||||||
|
|
@ -144,15 +161,47 @@ proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string =
|
||||||
result.add(("""<a class="$3active button" href="$1$4">$2</a>""") % [
|
result.add(("""<a class="$3active button" href="$1$4">$2</a>""") % [
|
||||||
btns[i].link, btns[i].text, class, anchor])
|
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(0, remaining.int, minutes.int, hours.int, days.int,
|
||||||
|
months.int, years.int)
|
||||||
|
|
||||||
proc formatTimestamp(t: int): string =
|
proc formatTimestamp(t: int): string =
|
||||||
let t2 = getGMTime(TTime(t))
|
let t2 = Time(t)
|
||||||
return t2.format("ddd',' d MMM yyyy HH':'mm 'UTC'")
|
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.toLower.toMD5
|
||||||
|
return ("http://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size &
|
||||||
|
"&d=identicon")
|
||||||
|
|
||||||
proc genGravatar(email: string, size: int = 80): string =
|
proc genGravatar(email: string, size: int = 80): string =
|
||||||
let emailMD5 = email.toLower.toMD5
|
result = "<img width=\"$1\" height=\"$2\" src=\"$3\" />" %
|
||||||
result = "<img src=\"$1\" />" %
|
[$size, $size, getGravatarUrl(email, size)]
|
||||||
("http://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size &
|
|
||||||
"&d=identicon")
|
|
||||||
|
|
||||||
proc randomSalt(): string =
|
proc randomSalt(): string =
|
||||||
result = ""
|
result = ""
|
||||||
|
|
@ -176,20 +225,32 @@ proc devRandomSalt(): string =
|
||||||
|
|
||||||
proc makeSalt(): string =
|
proc makeSalt(): string =
|
||||||
## Creates a salt using a cryptographically secure random number generator.
|
## Creates a salt using a cryptographically secure random number generator.
|
||||||
|
##
|
||||||
|
## Ensures that the resulting salt contains no ``\0``.
|
||||||
try:
|
try:
|
||||||
result = devRandomSalt()
|
result = devRandomSalt()
|
||||||
except EIO:
|
except IOError:
|
||||||
result = randomSalt()
|
result = randomSalt()
|
||||||
|
|
||||||
proc makePassword(password, salt: string): string =
|
var newResult = ""
|
||||||
|
for i in 0 .. <result.len:
|
||||||
|
if result[i] != '\0':
|
||||||
|
newResult.add result[i]
|
||||||
|
return newResult
|
||||||
|
|
||||||
|
proc makePassword(password, salt: string, comparingTo = ""): string =
|
||||||
## Creates an MD5 hash by combining password and salt.
|
## Creates an MD5 hash by combining password and salt.
|
||||||
|
when defined(windows):
|
||||||
result = getMD5(salt & getMD5(password))
|
result = getMD5(salt & getMD5(password))
|
||||||
|
else:
|
||||||
|
let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8)
|
||||||
|
result = hash(getMD5(salt & getMD5(password)), bcryptSalt)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
template `||`(x: expr): expr = (if not isNil(x): x else: "")
|
template `||`(x: expr): expr = (if not isNil(x): x else: "")
|
||||||
|
|
||||||
proc validThreadId(c: TForumData): bool =
|
proc validThreadId(c: TForumData): bool =
|
||||||
result = GetValue(db, sql"select id from thread where id = ?",
|
result = getValue(db, sql"select id from thread where id = ?",
|
||||||
$c.threadId).len > 0
|
$c.threadId).len > 0
|
||||||
|
|
||||||
proc antibot(c: var TForumData): string =
|
proc antibot(c: var TForumData): string =
|
||||||
|
|
@ -197,12 +258,12 @@ proc antibot(c: var TForumData): string =
|
||||||
let b = math.random(1000)+1
|
let b = math.random(1000)+1
|
||||||
let answer = $(a+b)
|
let answer = $(a+b)
|
||||||
|
|
||||||
Exec(db, sql"delete from antibot where ip = ?", c.req.ip)
|
exec(db, sql"delete from antibot where ip = ?", c.req.ip)
|
||||||
let CaptchaId = TryInsertID(db,
|
let captchaId = tryInsertID(db,
|
||||||
sql"insert into antibot(ip, answer) values (?, ?)", c.req.ip,
|
sql"insert into antibot(ip, answer) values (?, ?)", c.req.ip,
|
||||||
answer).int mod 10_000
|
answer).int mod 10_000
|
||||||
let CaptchaFile = getCaptchaFilename(CaptchaId)
|
let captchaFile = getCaptchaFilename(captchaId)
|
||||||
createCaptcha(CaptchaFile, $a & "+" & $b)
|
createCaptcha(captchaFile, $a & "+" & $b)
|
||||||
result = """<img src="$1" />""" % c.req.getCaptchaUrl(captchaId)
|
result = """<img src="$1" />""" % c.req.getCaptchaUrl(captchaId)
|
||||||
|
|
||||||
const
|
const
|
||||||
|
|
@ -217,7 +278,7 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool =
|
||||||
# Username validation:
|
# Username validation:
|
||||||
if name.len == 0 or not allCharsInSet(name, SecureChars):
|
if name.len == 0 or not allCharsInSet(name, SecureChars):
|
||||||
return setError(c, "name", "Invalid username!")
|
return setError(c, "name", "Invalid username!")
|
||||||
if GetValue(db, sql"select name from person where name = ?", name).len > 0:
|
if getValue(db, sql"select name from person where name = ?", name).len > 0:
|
||||||
return setError(c, "name", "Username already exists!")
|
return setError(c, "name", "Username already exists!")
|
||||||
|
|
||||||
# Password validation:
|
# Password validation:
|
||||||
|
|
@ -225,7 +286,7 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool =
|
||||||
return setError(c, "new_password", "Invalid password!")
|
return setError(c, "new_password", "Invalid password!")
|
||||||
|
|
||||||
# antibot validation:
|
# antibot validation:
|
||||||
let correctRes = GetValue(db,
|
let correctRes = getValue(db,
|
||||||
sql"select answer from antibot where ip = ?", c.req.ip)
|
sql"select answer from antibot where ip = ?", c.req.ip)
|
||||||
if antibot != correctRes:
|
if antibot != correctRes:
|
||||||
return setError(c, "antibot", "You seem to be a bot!")
|
return setError(c, "antibot", "You seem to be a bot!")
|
||||||
|
|
@ -236,8 +297,9 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool =
|
||||||
|
|
||||||
# perform registration:
|
# perform registration:
|
||||||
var salt = makeSalt()
|
var salt = makeSalt()
|
||||||
Exec(db, sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " &
|
exec(db,
|
||||||
"VALUES (?, ?, ?, ?, 'user', DATETIME('now'))"), name,
|
sql("INSERT INTO person(name, password, email, salt, status, lastOnline, " &
|
||||||
|
"ban) VALUES (?, ?, ?, ?, 'user', DATETIME('now'), '')"), name,
|
||||||
makePassword(pass, salt), email, salt)
|
makePassword(pass, salt), email, salt)
|
||||||
# return setError(c, "", "Could not create your account!")
|
# return setError(c, "", "Could not create your account!")
|
||||||
return true
|
return true
|
||||||
|
|
@ -245,12 +307,12 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool =
|
||||||
proc checkLoggedIn(c: var TForumData) =
|
proc checkLoggedIn(c: var TForumData) =
|
||||||
let pass = c.req.cookies["sid"]
|
let pass = c.req.cookies["sid"]
|
||||||
if pass.len == 0: return
|
if pass.len == 0: return
|
||||||
if ExecAffectedRows(db,
|
if execAffectedRows(db,
|
||||||
sql("update session set lastModified = DATETIME('now') " &
|
sql("update session set lastModified = DATETIME('now') " &
|
||||||
"where ip = ? and password = ?"),
|
"where ip = ? and password = ?"),
|
||||||
c.req.ip, pass) > 0:
|
c.req.ip, pass) > 0:
|
||||||
c.userpass = pass
|
c.userpass = pass
|
||||||
c.userid = GetValue(db,
|
c.userid = getValue(db,
|
||||||
sql"select userid from session where ip = ? and password = ?",
|
sql"select userid from session where ip = ? and password = ?",
|
||||||
c.req.ip, pass)
|
c.req.ip, pass)
|
||||||
|
|
||||||
|
|
@ -270,11 +332,11 @@ proc logout(c: var TForumData) =
|
||||||
const query = sql"delete from session where ip = ? and password = ?"
|
const query = sql"delete from session where ip = ? and password = ?"
|
||||||
c.username = ""
|
c.username = ""
|
||||||
c.userpass = ""
|
c.userpass = ""
|
||||||
Exec(db, query, c.req.ip, c.req.cookies["sid"])
|
exec(db, query, c.req.ip, c.req.cookies["sid"])
|
||||||
|
|
||||||
proc incrementViews(c: var TForumData) =
|
proc incrementViews(c: var TForumData) =
|
||||||
const query = sql"update thread set views = views + 1 where id = ?"
|
const query = sql"update thread set views = views + 1 where id = ?"
|
||||||
Exec(db, query, $c.threadId)
|
exec(db, query, $c.threadId)
|
||||||
|
|
||||||
proc isPreview(c: TForumData): bool =
|
proc isPreview(c: TForumData): bool =
|
||||||
result = c.req.params["previewBtn"].len > 0 # TODO: Could be wrong?
|
result = c.req.params["previewBtn"].len > 0 # TODO: Could be wrong?
|
||||||
|
|
@ -353,6 +415,8 @@ template setPreviewData(c: expr) {.immediate, dirty.} =
|
||||||
template writeToDb(c, cr, setPostId: expr) =
|
template writeToDb(c, cr, setPostId: expr) =
|
||||||
let retID = insertID(db, crud(cr, "post", "author", "ip", "header", "content", "thread"),
|
let retID = insertID(db, crud(cr, "post", "author", "ip", "header", "content", "thread"),
|
||||||
c.userId, c.req.ip, subject, content, $c.threadId, "")
|
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:
|
if setPostId:
|
||||||
c.postId = retID.int
|
c.postId = retID.int
|
||||||
|
|
||||||
|
|
@ -363,19 +427,36 @@ proc edit(c: var TForumData, postId: int): bool =
|
||||||
setPreviewData(c)
|
setPreviewData(c)
|
||||||
elif c.isDelete:
|
elif c.isDelete:
|
||||||
checkOwnership(c, $postId)
|
checkOwnership(c, $postId)
|
||||||
if not TryExec(db, crud(crDelete, "post"), $postId):
|
if not tryExec(db, crud(crDelete, "post"), $postId):
|
||||||
return setError(c, "", "database error")
|
return setError(c, "", "database error")
|
||||||
|
discard tryExec(db, crud(crDelete, "post_fts"), $postId)
|
||||||
# delete corresponding thread:
|
# delete corresponding thread:
|
||||||
if ExecAffectedRows(db,
|
if execAffectedRows(db,
|
||||||
sql"delete from thread where id not in (select thread from post)") > 0:
|
sql"delete from thread where id not in (select thread from post)") > 0:
|
||||||
# whole thread has been deleted, so:
|
# whole thread has been deleted, so:
|
||||||
c.threadId = unselectedThread
|
c.threadId = unselectedThread
|
||||||
|
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):
|
||||||
|
return setError(c, "", "database error")
|
||||||
result = true
|
result = true
|
||||||
else:
|
else:
|
||||||
checkOwnership(c, $postId)
|
checkOwnership(c, $postId)
|
||||||
retrPost(c)
|
retrPost(c)
|
||||||
exec(db, crud(crUpdate, "post", "header", "content"),
|
exec(db, crud(crUpdate, "post", "header", "content"),
|
||||||
subject, content, $postId)
|
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
|
result = true
|
||||||
|
|
||||||
proc reply(c: var TForumData): bool =
|
proc reply(c: var TForumData): bool =
|
||||||
|
|
@ -398,20 +479,30 @@ proc newThread(c: var TForumData): bool =
|
||||||
setPreviewData(c)
|
setPreviewData(c)
|
||||||
c.threadID = transientThread
|
c.threadID = transientThread
|
||||||
else:
|
else:
|
||||||
c.threadID = TryInsertID(db, query, c.req.params["subject"]).int
|
c.threadID = tryInsertID(db, query, c.req.params["subject"]).int
|
||||||
if c.threadID < 0: return setError(c, "subject", "Subject already exists")
|
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)
|
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')")
|
||||||
result = true
|
result = true
|
||||||
|
|
||||||
proc login(c: var TForumData, name, pass: string): bool =
|
proc login(c: var TForumData, name, pass: string): bool =
|
||||||
# get form data:
|
# get form data:
|
||||||
const query =
|
const query =
|
||||||
sql"select id, name, password, email, salt, admin from person where name = ?"
|
sql"select id, name, password, email, salt, admin, ban from person where name = ?"
|
||||||
if name.len == 0:
|
if name.len == 0:
|
||||||
return c.setError("name", "Username cannot be nil.")
|
return c.setError("name", "Username cannot be nil.")
|
||||||
var success = false
|
var success = false
|
||||||
for row in FastRows(db, query, name):
|
for row in fastRows(db, query, name):
|
||||||
if row[2] == makePassword(pass, row[4]):
|
if row[2] == makePassword(pass, row[4], row[2]):
|
||||||
|
case row[6]
|
||||||
|
of "": discard
|
||||||
|
of banReasonDeactivated:
|
||||||
|
return c.setError("name", "Your account has been deactivated.")
|
||||||
|
else:
|
||||||
|
return c.setError("name", "You have been banned: " & row[6])
|
||||||
c.userid = row[0]
|
c.userid = row[0]
|
||||||
c.username = row[1]
|
c.username = row[1]
|
||||||
c.userpass = row[2]
|
c.userpass = row[2]
|
||||||
|
|
@ -421,24 +512,39 @@ proc login(c: var TForumData, name, pass: string): bool =
|
||||||
break
|
break
|
||||||
if success:
|
if success:
|
||||||
# create session:
|
# create session:
|
||||||
Exec(db,
|
exec(db,
|
||||||
sql"insert into session (ip, password, userid) values (?, ?, ?)",
|
sql"insert into session (ip, password, userid) values (?, ?, ?)",
|
||||||
c.req.ip, c.userpass, c.userid)
|
c.req.ip, c.userpass, c.userid)
|
||||||
return true
|
return true
|
||||||
else:
|
else:
|
||||||
return c.setError("password", "Login failed!")
|
return c.setError("password", "Login failed!")
|
||||||
|
|
||||||
|
proc setBan(c: var TForumData, nick, reason: string): bool =
|
||||||
|
const query =
|
||||||
|
sql("update person set ban = ? where name = ?")
|
||||||
|
return tryExec(db, query, reason, nick)
|
||||||
|
|
||||||
|
proc hasReplyBtn(c: var TForumData): bool =
|
||||||
|
result = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply"
|
||||||
|
result = result and c.req.params["action"] != "reply"
|
||||||
|
# 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 genActionMenu(c: var TForumData): string =
|
proc genActionMenu(c: var TForumData): string =
|
||||||
result = ""
|
result = ""
|
||||||
var btns: seq[TStyledButton] = @[]
|
var btns: seq[TStyledButton] = @[]
|
||||||
# TODO: Make this detection better?
|
# TODO: Make this detection better?
|
||||||
if c.req.pathInfo.normalizeUri notin noHomeBtn and not c.isThreadsList:
|
if c.req.pathInfo.normalizeUri notin noHomeBtn and not c.isThreadsList:
|
||||||
btns.add(("Thread List", c.req.makeUri("/", false)))
|
btns.add(("Thread List", c.req.makeUri("/", false)))
|
||||||
|
#echo c.loggedIn
|
||||||
if c.loggedIn:
|
if c.loggedIn:
|
||||||
let hasReplyBtn = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply"
|
let hasReplyBtn = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply"
|
||||||
if c.threadId >= 0 and hasReplyBtn:
|
if c.threadId >= 0 and hasReplyBtn:
|
||||||
let replyUrl = c.genThreadUrl(action = "reply",
|
let replyUrl = c.genThreadUrl(action = "reply",
|
||||||
pageNum = $(ceil(c.totalPosts / postsPerPage).int)) & "#reply"
|
pageNum = $(ceil(c.totalPosts / PostsPerPage).int)) & "#reply"
|
||||||
btns.add(("Reply", replyUrl))
|
btns.add(("Reply", replyUrl))
|
||||||
btns.add(("New Thread", c.req.makeUri("/newthread", false)))
|
btns.add(("New Thread", c.req.makeUri("/newthread", false)))
|
||||||
result = c.genButtons(btns)
|
result = c.genButtons(btns)
|
||||||
|
|
@ -461,7 +567,7 @@ proc getStats(c: var TForumData, simple: bool): TForumStats =
|
||||||
sql"select id, name, admin, strftime('%s', lastOnline), strftime('%s', creation) from person"
|
sql"select id, name, admin, strftime('%s', lastOnline), strftime('%s', creation) from person"
|
||||||
for row in fastRows(db, getUsersQuery):
|
for row in fastRows(db, getUsersQuery):
|
||||||
let secs = if row[3] == "": 0 else: row[3].parseint
|
let secs = if row[3] == "": 0 else: row[3].parseint
|
||||||
let lastOnlineSeconds = getTime() - TTime(secs)
|
let lastOnlineSeconds = getTime() - Time(secs)
|
||||||
if lastOnlineSeconds < (60 * 5): # 5 minutes
|
if lastOnlineSeconds < (60 * 5): # 5 minutes
|
||||||
result.activeUsers.add((row[1], row[0].parseInt, row[2].parseBool))
|
result.activeUsers.add((row[1], row[0].parseInt, row[2].parseBool))
|
||||||
if row[4].parseInt > newestMemberCreation:
|
if row[4].parseInt > newestMemberCreation:
|
||||||
|
|
@ -489,7 +595,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string =
|
||||||
prevUrl = firstUrl
|
prevUrl = firstUrl
|
||||||
else:
|
else:
|
||||||
prevUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum-1))
|
prevUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum-1))
|
||||||
totalPages = ceil(c.totalPosts / postsPerPage).int
|
totalPages = ceil(c.totalPosts / PostsPerPage).int
|
||||||
lastUrl = c.req.makeUri(firstUrl & "/" & $(totalPages))
|
lastUrl = c.req.makeUri(firstUrl & "/" & $(totalPages))
|
||||||
nextUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum+1))
|
nextUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum+1))
|
||||||
|
|
||||||
|
|
@ -499,30 +605,22 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string =
|
||||||
var firstTag = ""
|
var firstTag = ""
|
||||||
var prevTag = ""
|
var prevTag = ""
|
||||||
if c.pageNum == 1:
|
if c.pageNum == 1:
|
||||||
firstTag = span("First")
|
firstTag = span("<<")
|
||||||
prevTag = span("Prev")
|
prevTag = span("<••")
|
||||||
else:
|
else:
|
||||||
firstTag = htmlgen.a(href=firstUrl, "First")
|
firstTag = htmlgen.a(href=firstUrl, "<<")
|
||||||
prevTag = htmlgen.a(href=prevUrl, "Prev")
|
prevTag = htmlgen.a(href=prevUrl, "<••")
|
||||||
result.add(htmlgen.`div`(class = "left",
|
prevTag.add(htmlgen.link(rel="previous", href=prevUrl))
|
||||||
firstTag,
|
result.add(firstTag)
|
||||||
prevTag))
|
result.add(prevTag)
|
||||||
# Right
|
|
||||||
var lastTag = ""
|
|
||||||
var nextTag = ""
|
|
||||||
if c.pageNum == totalPages:
|
|
||||||
lastTag = span("Last")
|
|
||||||
nextTag = span("Next")
|
|
||||||
else:
|
|
||||||
lastTag = htmlgen.a(href=lastUrl, "Last")
|
|
||||||
nextTag = htmlgen.a(href=nextUrl, "Next")
|
|
||||||
result.add(htmlgen.`div`(class = "right",
|
|
||||||
nextTag,
|
|
||||||
lastTag))
|
|
||||||
|
|
||||||
# Numbers
|
# Numbers
|
||||||
var pages = "" # Tags
|
var pages = "" # Tags
|
||||||
for i in 1..totalPages:
|
# 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:
|
if i == c.pageNum:
|
||||||
pages.add(span($(i)))
|
pages.add(span($(i)))
|
||||||
else:
|
else:
|
||||||
|
|
@ -533,14 +631,25 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string =
|
||||||
pageUrl = c.req.makeUri(firstUrl & "/" & $(i))
|
pageUrl = c.req.makeUri(firstUrl & "/" & $(i))
|
||||||
|
|
||||||
pages.add(htmlgen.a(href = pageUrl, $(i)))
|
pages.add(htmlgen.a(href = pageUrl, $(i)))
|
||||||
result.add(htmlgen.`div`(class = "middle",
|
if lastToShow < totalPages: pages.add(span("..."))
|
||||||
pages))
|
result.add(pages)
|
||||||
|
|
||||||
result = htmlgen.`div`(id = "pagenumbers", result)
|
# 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: var TForumData, thrid: int): int =
|
proc gatherTotalPostsByID(c: var TForumData, thrid: int): int =
|
||||||
## Gets the total post count of a thread.
|
## Gets the total post count of a thread.
|
||||||
result = GetValue(db, sql"select count(*) from post where thread = ?", $thrid).parseInt
|
result = getValue(db, sql"select count(*) from post where thread = ?", $thrid).parseInt
|
||||||
|
|
||||||
proc gatherTotalPosts(c: var TForumData) =
|
proc gatherTotalPosts(c: var TForumData) =
|
||||||
if c.totalPosts > 0: return
|
if c.totalPosts > 0: return
|
||||||
|
|
@ -551,13 +660,13 @@ proc gatherTotalPosts(c: var TForumData) =
|
||||||
|
|
||||||
proc getPagesInThread(c: var TForumData): int =
|
proc getPagesInThread(c: var TForumData): int =
|
||||||
c.gatherTotalPosts() # Get total post count
|
c.gatherTotalPosts() # Get total post count
|
||||||
result = ceil(c.totalPosts / postsPerPage).int-1
|
result = ceil(c.totalPosts / PostsPerPage).int-1
|
||||||
|
|
||||||
proc getPagesInThreadByID(c: var TForumData, thrid: int): int =
|
proc getPagesInThreadByID(c: var TForumData, thrid: int): int =
|
||||||
result = ceil(c.gatherTotalPostsByID(thrid) / postsPerPage).int
|
result = ceil(c.gatherTotalPostsByID(thrid) / PostsPerPage).int
|
||||||
|
|
||||||
proc getThreadTitle(thrid: int, pageNum: int): string =
|
proc getThreadTitle(thrid: int, pageNum: int): string =
|
||||||
result = GetValue(db, sql"select name from thread where id = ?", $thrid)
|
result = getValue(db, sql"select name from thread where id = ?", $thrid)
|
||||||
if pageNum notin {0,1}:
|
if pageNum notin {0,1}:
|
||||||
result.add(" - Page " & $pageNum)
|
result.add(" - Page " & $pageNum)
|
||||||
|
|
||||||
|
|
@ -579,7 +688,7 @@ proc genPagenumLocalNav(c: var TForumData, thrid: int): string =
|
||||||
else:
|
else:
|
||||||
inc(i)
|
inc(i)
|
||||||
|
|
||||||
result = htmlgen.`div`(class = "localnums", result)
|
result = htmlgen.span(class = "pages", result)
|
||||||
|
|
||||||
proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool =
|
proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool =
|
||||||
ui.nick = nick
|
ui.nick = nick
|
||||||
|
|
@ -600,12 +709,28 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool =
|
||||||
let lastOnlineDBVal = getValue(db, lastOnlineQuery, uid)
|
let lastOnlineDBVal = getValue(db, lastOnlineQuery, uid)
|
||||||
ui.lastOnline = if lastOnlineDBVal != "": lastOnlineDBVal.parseInt else: -1
|
ui.lastOnline = if lastOnlineDBVal != "": lastOnlineDBVal.parseInt else: -1
|
||||||
ui.email = getValue(db, sql"select email from person where id = ?", uid)
|
ui.email = getValue(db, sql"select email from person where id = ?", uid)
|
||||||
|
ui.ban = getValue(db, sql"select ban from person where id = ?", uid)
|
||||||
|
|
||||||
|
proc genSetUserStatusUrl(c: var TForumData, nick: string, typ: string): string =
|
||||||
|
c.req.makeUri("/setUserStatus?nick=$1&type=$2" % [nick, typ])
|
||||||
|
|
||||||
proc genProfile(c: var TForumData, ui: TUserInfo): string =
|
proc genProfile(c: var TForumData, ui: TUserInfo): string =
|
||||||
result = ""
|
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)))
|
result.add(htmlgen.`div`(id = "avatar", genGravatar(ui.email, 250)))
|
||||||
let t2 = if ui.lastOnline != -1: getGMTime(TTime(ui.lastOnline))
|
let t2 = if ui.lastOnline != -1: getGMTime(Time(ui.lastOnline))
|
||||||
else: getGMTime(GetTime())
|
else: getGMTime(getTime())
|
||||||
|
|
||||||
result.add(htmlgen.`div`(id = "info",
|
result.add(htmlgen.`div`(id = "info",
|
||||||
htmlgen.table(
|
htmlgen.table(
|
||||||
|
|
@ -625,6 +750,41 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string =
|
||||||
th("Last Online"),
|
th("Last Online"),
|
||||||
td(if ui.lastOnline != -1: t2.format("dd/MM/yy HH':'mm 'UTC'")
|
td(if ui.lastOnline != -1: t2.format("dd/MM/yy HH':'mm 'UTC'")
|
||||||
else: "Never")
|
else: "Never")
|
||||||
|
),
|
||||||
|
tr(
|
||||||
|
th("Status"),
|
||||||
|
td(case ui.ban
|
||||||
|
of banReasonDeactivated:
|
||||||
|
"Deactivated"
|
||||||
|
of "":
|
||||||
|
"Active"
|
||||||
|
else:
|
||||||
|
"Banned: " & ui.ban
|
||||||
|
)
|
||||||
|
),
|
||||||
|
tr(
|
||||||
|
th(""),
|
||||||
|
td(if c.isAdmin and ui.ban != banReasonDeactivated:
|
||||||
|
if ui.ban == "":
|
||||||
|
htmlgen.a(
|
||||||
|
href=c.genSetUserStatusUrl(ui.nick, "ban"),
|
||||||
|
"Ban user")
|
||||||
|
else:
|
||||||
|
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "unban"),
|
||||||
|
"Unban user")
|
||||||
|
else: "")
|
||||||
|
),
|
||||||
|
tr(
|
||||||
|
th(""),
|
||||||
|
td(if c.userName == ui.nick or c.isAdmin:
|
||||||
|
if ui.ban == "":
|
||||||
|
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "deactivate"),
|
||||||
|
"Deactivate user")
|
||||||
|
elif ui.ban == banReasonDeactivated:
|
||||||
|
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"),
|
||||||
|
"Activate user")
|
||||||
|
else: ""
|
||||||
|
else: "")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|
@ -651,12 +811,15 @@ template createTFD(): stmt =
|
||||||
if request.cookies.len > 0:
|
if request.cookies.len > 0:
|
||||||
checkLoggedIn(c)
|
checkLoggedIn(c)
|
||||||
|
|
||||||
|
routes:
|
||||||
get "/":
|
get "/":
|
||||||
createTFD()
|
createTFD()
|
||||||
c.isThreadsList = true
|
c.isThreadsList = true
|
||||||
var count = 0
|
var count = 0
|
||||||
resp genMain(c, genThreadsList(c, count),
|
let threadList = genThreadsList(c, count)
|
||||||
|
let data = genMain(c, threadList,
|
||||||
additionalHeaders = genRSSHeaders(c), showRssLinks = true)
|
additionalHeaders = genRSSHeaders(c), showRssLinks = true)
|
||||||
|
resp data
|
||||||
|
|
||||||
get "/threadActivity.xml":
|
get "/threadActivity.xml":
|
||||||
createTFD()
|
createTFD()
|
||||||
|
|
@ -667,17 +830,19 @@ get "/postActivity.xml":
|
||||||
createTFD()
|
createTFD()
|
||||||
resp genPostsRSS(c), "application/atom+xml"
|
resp genPostsRSS(c), "application/atom+xml"
|
||||||
|
|
||||||
get "/t/@threadid/?@page?/?":
|
get "/t/@threadid/?@page?/?@postid?/?":
|
||||||
createTFD()
|
createTFD()
|
||||||
parseInt(@"threadid", c.threadId, -1..1000_000)
|
parseInt(@"threadid", c.threadId, -1..1000_000)
|
||||||
if c.threadid == unselectedThread:
|
|
||||||
# Thread has just beed deleted
|
if c.threadId == unselectedThread:
|
||||||
|
# Thread has just been deleted.
|
||||||
redirect(uri("/"))
|
redirect(uri("/"))
|
||||||
|
|
||||||
if @"page".len > 0:
|
if @"page".len > 0:
|
||||||
parseInt(@"page", c.pageNum, 0..1000_000)
|
parseInt(@"page", c.pageNum, 0..1000_000)
|
||||||
|
if @"postid".len > 0:
|
||||||
|
parseInt(@"postid", c.postId, 0..1000_000)
|
||||||
cond (c.pageNum > 0)
|
cond (c.pageNum > 0)
|
||||||
if (@"postid").len > 0:
|
|
||||||
parseInt(@"postid", c.postId, -1..1000_000)
|
|
||||||
var count = 0
|
var count = 0
|
||||||
var pSubject = getThreadTitle(c.threadid, c.pageNum)
|
var pSubject = getThreadTitle(c.threadid, c.pageNum)
|
||||||
cond validThreadId(c)
|
cond validThreadId(c)
|
||||||
|
|
@ -686,7 +851,7 @@ get "/t/@threadid/?@page?/?":
|
||||||
var title = ""
|
var title = ""
|
||||||
case @"action"
|
case @"action"
|
||||||
of "reply":
|
of "reply":
|
||||||
let subject = GetValue(db,
|
let subject = getValue(db,
|
||||||
sql"select header from post where id = (select max(id) from post where thread = ?)",
|
sql"select header from post where id = (select max(id) from post where thread = ?)",
|
||||||
$c.threadId).prependRe
|
$c.threadId).prependRe
|
||||||
body = genPostsList(c, $c.threadId, count)
|
body = genPostsList(c, $c.threadId, count)
|
||||||
|
|
@ -701,14 +866,15 @@ get "/t/@threadid/?@page?/?":
|
||||||
let content = ||row[1]
|
let content = ||row[1]
|
||||||
body = genFormPost(c, "doedit", "Edit", header, content, true)
|
body = genFormPost(c, "doedit", "Edit", header, content, true)
|
||||||
title = "Editing post"
|
title = "Editing post"
|
||||||
resp c.genMain(body, title & " - Nimrod Forum")
|
else: discard
|
||||||
|
resp c.genMain(body, title & " - Nim Forum")
|
||||||
else:
|
else:
|
||||||
incrementViews(c)
|
incrementViews(c)
|
||||||
let posts = genPostsList(c, $c.threadId, count)
|
let posts = genPostsList(c, $c.threadId, count)
|
||||||
cond count != 0
|
cond count != 0
|
||||||
resp genMain(c, posts, pSubject & " - Nimrod Forum")
|
resp genMain(c, posts, pSubject & " - Nim Forum")
|
||||||
|
|
||||||
get "/page/@page/?":
|
get "/page/?@page?/?":
|
||||||
createTFD()
|
createTFD()
|
||||||
c.isThreadsList = true
|
c.isThreadsList = true
|
||||||
cond (@"page" != "")
|
cond (@"page" != "")
|
||||||
|
|
@ -718,7 +884,7 @@ get "/page/@page/?":
|
||||||
let list = genThreadsList(c, count)
|
let list = genThreadsList(c, count)
|
||||||
if count == 0:
|
if count == 0:
|
||||||
pass()
|
pass()
|
||||||
resp genMain(c, list, "Page " & $c.pageNum & " - Nimrod Forum",
|
resp genMain(c, list, "Page " & $c.pageNum & " - Nim Forum",
|
||||||
genRSSHeaders(c), showRssLinks = true)
|
genRSSHeaders(c), showRssLinks = true)
|
||||||
|
|
||||||
get "/profile/@nick/?":
|
get "/profile/@nick/?":
|
||||||
|
|
@ -727,13 +893,13 @@ get "/profile/@nick/?":
|
||||||
var userinfo: TUserInfo
|
var userinfo: TUserInfo
|
||||||
if gatherUserInfo(c, @"nick", userinfo):
|
if gatherUserInfo(c, @"nick", userinfo):
|
||||||
resp genMain(c, c.genProfile(userinfo),
|
resp genMain(c, c.genProfile(userinfo),
|
||||||
@"nick" & "'s profile - Nimrod Forum")
|
@"nick" & "'s profile - Nim Forum")
|
||||||
else:
|
else:
|
||||||
halt()
|
halt()
|
||||||
|
|
||||||
get "/login/?":
|
get "/login/?":
|
||||||
createTFD()
|
createTFD()
|
||||||
resp genMain(c, genFormLogin(c), "Log in - Nimrod Forum")
|
resp genMain(c, genFormLogin(c), "Log in - Nim Forum")
|
||||||
|
|
||||||
get "/logout/?":
|
get "/logout/?":
|
||||||
createTFD()
|
createTFD()
|
||||||
|
|
@ -742,7 +908,7 @@ get "/logout/?":
|
||||||
|
|
||||||
get "/register/?":
|
get "/register/?":
|
||||||
createTFD()
|
createTFD()
|
||||||
resp genMain(c, genFormRegister(c), "Register - Nimrod Forum")
|
resp genMain(c, genFormRegister(c), "Register - Nim Forum")
|
||||||
|
|
||||||
template readIDs(): stmt =
|
template readIDs(): stmt =
|
||||||
# Retrieve the threadid, postid and pagenum
|
# Retrieve the threadid, postid and pagenum
|
||||||
|
|
@ -760,14 +926,20 @@ template handleError(action: string, topText: string, isEdit: bool): stmt =
|
||||||
body.add genPostPreview(c, @"subject", @"content",
|
body.add genPostPreview(c, @"subject", @"content",
|
||||||
c.userName, $getGMTime(getTime()))
|
c.userName, $getGMTime(getTime()))
|
||||||
body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit)
|
body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit)
|
||||||
resp genMain(c, body(), "Nimrod Forum - Error")
|
resp genMain(c, body(), "Nim Forum - " &
|
||||||
|
(if c.isPreview: "Preview" else: "Error"))
|
||||||
|
|
||||||
post "/dologin":
|
post "/dologin":
|
||||||
createTFD()
|
createTFD()
|
||||||
if login(c, @"name", @"password"):
|
if login(c, @"name", @"password"):
|
||||||
finishLogin()
|
finishLogin()
|
||||||
else:
|
else:
|
||||||
resp c.genMain(genFormLogin(c))
|
c.isThreadsList = true
|
||||||
|
var count = 0
|
||||||
|
let threadList = genThreadsList(c, count)
|
||||||
|
let data = genMain(c, threadList,
|
||||||
|
additionalHeaders = genRSSHeaders(c), showRssLinks = true)
|
||||||
|
resp data
|
||||||
|
|
||||||
post "/doregister":
|
post "/doregister":
|
||||||
createTFD()
|
createTFD()
|
||||||
|
|
@ -801,7 +973,8 @@ post "/doedit":
|
||||||
createTFD()
|
createTFD()
|
||||||
readIDs()
|
readIDs()
|
||||||
if edit(c, c.postId):
|
if edit(c, c.postId):
|
||||||
redirect(c.genThreadUrl(pageNum = $(c.getPagesInThread+1)) & "#" & $c.postId)
|
redirect(c.genThreadUrl(postId = $c.postId,
|
||||||
|
pageNum = $(c.getPagesInThread+1)))
|
||||||
else:
|
else:
|
||||||
body = ""
|
body = ""
|
||||||
handleError("doedit", "Edit", true)
|
handleError("doedit", "Edit", true)
|
||||||
|
|
@ -809,22 +982,130 @@ post "/doedit":
|
||||||
get "/newthread/?":
|
get "/newthread/?":
|
||||||
createTFD()
|
createTFD()
|
||||||
resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false),
|
resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false),
|
||||||
"New Thread - Nimrod Forum")
|
"New Thread - Nim Forum")
|
||||||
|
|
||||||
|
get "/setUserStatus/?":
|
||||||
|
createTFD()
|
||||||
|
cond (@"nick" != "")
|
||||||
|
cond (@"type" != "")
|
||||||
|
var formBody = "<input type=\"hidden\" name=\"nick\" value=\"" &
|
||||||
|
@"nick" & "\">"
|
||||||
|
var del = false
|
||||||
|
var content = ""
|
||||||
|
echo("Got type: ", @"type")
|
||||||
|
case @"type"
|
||||||
|
of "ban":
|
||||||
|
formBody.add "<input type='text' name='reason' />" &
|
||||||
|
"<input type='submit' name='banBtn' value='Ban' />"
|
||||||
|
content =
|
||||||
|
htmlgen.p("Please enter a reason for banning this user:")
|
||||||
|
of "unban":
|
||||||
|
formBody.add "<input type='submit' name='unbanBtn' value='Unban' />"
|
||||||
|
content =
|
||||||
|
htmlgen.p("Are you sure you wish to unban ", htmlgen.b(@"nick"),
|
||||||
|
"?")
|
||||||
|
del = true
|
||||||
|
of "deactivate":
|
||||||
|
formBody.add "<input type='hidden' name='reason' value='" &
|
||||||
|
banReasonDeactivated & "' />" &
|
||||||
|
"<input type='submit' name='actBtn' value='Deactivate' />"
|
||||||
|
content =
|
||||||
|
htmlgen.p("Are you sure you wish to deactivate ", htmlgen.b(@"nick"),
|
||||||
|
"?")
|
||||||
|
of "activate":
|
||||||
|
formBody.add "<input type='submit' name='actBtn' value='Activate' />"
|
||||||
|
content =
|
||||||
|
htmlgen.p("Are you sure you wish to activate ", htmlgen.b(@"nick"),
|
||||||
|
"?")
|
||||||
|
del = true
|
||||||
|
formBody.add "<input type='hidden' name='del' value='" & $del & "'/>"
|
||||||
|
content = htmlgen.form(action = c.req.makeUri("/dosetban"),
|
||||||
|
`method` = "POST", formBody) & content
|
||||||
|
resp genMain(c, content, "Set user status - Nim Forum")
|
||||||
|
|
||||||
|
post "/dosetban":
|
||||||
|
createTFD()
|
||||||
|
cond (@"nick" != "")
|
||||||
|
if not c.isAdmin and @"nick" != c.userName:
|
||||||
|
resp genMain(c, "You cannot ban this user.", "Error - Nim Forum")
|
||||||
|
if @"reason" == "" and @"del" != "true":
|
||||||
|
resp genMain(c, "Invalid ban reason.", "Error - Nim Forum")
|
||||||
|
let result =
|
||||||
|
if @"del" == "true":
|
||||||
|
# Remove the ban.
|
||||||
|
setBan(c, @"nick", "")
|
||||||
|
else:
|
||||||
|
setBan(c, @"nick", @"reason")
|
||||||
|
if result:
|
||||||
|
redirect(c.req.makeUri("/profile/" & @"nick"))
|
||||||
|
else:
|
||||||
|
resp genMain(c, "Failed to change the ban status of user.",
|
||||||
|
"Error - Nim Forum")
|
||||||
|
|
||||||
const licenseRst = slurp("static/license.rst")
|
const licenseRst = slurp("static/license.rst")
|
||||||
get "/license":
|
get "/license":
|
||||||
createTFD()
|
createTFD()
|
||||||
resp genMain(c, rstToHtml(licenseRst), "Content license - Nimrod Forum")
|
resp genMain(c, rstToHtml(licenseRst), "Content license - Nim Forum")
|
||||||
|
|
||||||
|
post "/search/?@page?":
|
||||||
|
cond isFTSAvailable
|
||||||
|
createTFD()
|
||||||
|
c.isThreadsList = true
|
||||||
|
c.noPagenumumNav = true
|
||||||
|
var count = 0
|
||||||
|
var q = @"q"
|
||||||
|
for i in 0 .. q.len-1:
|
||||||
|
if q[i].int < 32: q[i] = ' '
|
||||||
|
elif q[i] == '\'': q[i] = '"'
|
||||||
|
c.search = q.replace("\"",""");
|
||||||
|
if @"page".len > 0:
|
||||||
|
parseInt(@"page", c.pageNum, 0..1000_000)
|
||||||
|
cond (c.pageNum > 0)
|
||||||
|
iterator searchResults(): db_sqlite.TRow {.closure, tags: [FReadDB].} =
|
||||||
|
const queryFT = "fts.sql".slurp.sql
|
||||||
|
for rowFT in fastRows(db, queryFT,
|
||||||
|
[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): stmt =
|
||||||
|
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"
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
docConfig = rstgen.defaultConfig()
|
docConfig = rstgen.defaultConfig()
|
||||||
|
docConfig["doc.smiley_format"] = "/images/smilieys/$1.png"
|
||||||
math.randomize()
|
math.randomize()
|
||||||
db = Open(connection="nimforum.db", user="postgres", password="",
|
db = open(connection="nimforum.db", user="postgres", password="",
|
||||||
database="nimforum")
|
database="nimforum")
|
||||||
|
isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " &
|
||||||
|
"type='table' AND name='post_fts'")).len == 1
|
||||||
var http = true
|
var http = true
|
||||||
if paramCount() > 0:
|
if paramCount() > 0:
|
||||||
if paramStr(1) == "scgi":
|
if paramStr(1) == "scgi":
|
||||||
http = false
|
http = false
|
||||||
run("", port = TPort(9000), http = http)
|
|
||||||
|
#run("", port = TPort(9000), http = http)
|
||||||
|
|
||||||
|
runForever()
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,3 @@
|
||||||
--path:"$nimrod/lib/packages/docutils"
|
--path:"$nimrod/lib/packages/docutils"
|
||||||
|
|
||||||
--path:"$nimrod"
|
--path:"$nimrod"
|
||||||
--path:"../jester"
|
|
||||||
74
fts.sql
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
-- selects just threads,
|
||||||
|
-- those where title doesn't coinside with some of its posts' titles
|
||||||
|
-- by now selects only the threads title (no post snippet)
|
||||||
|
SELECT
|
||||||
|
thread_id,
|
||||||
|
snippet(thread_fts, '<b>', '</b>', '<b>...</b>') AS thread,
|
||||||
|
0 AS post_id,
|
||||||
|
'' AS header,
|
||||||
|
'' AS content,
|
||||||
|
person.name AS author,
|
||||||
|
cdate,
|
||||||
|
author_id,
|
||||||
|
person.email AS email,
|
||||||
|
0 AS what
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
thread_fts.id AS thread_id,
|
||||||
|
post.id AS post_id,
|
||||||
|
post.creation AS cdate,
|
||||||
|
MIN(post.creation) AS cdate,
|
||||||
|
post.author AS author_id
|
||||||
|
FROM thread_fts
|
||||||
|
JOIN post ON post.thread=thread_id
|
||||||
|
WHERE thread_fts MATCH ?
|
||||||
|
GROUP BY thread_id, post_id
|
||||||
|
HAVING thread_id NOT IN (
|
||||||
|
SELECT thread
|
||||||
|
FROM post_fts JOIN post USING(id)
|
||||||
|
WHERE post_fts MATCH ?
|
||||||
|
)
|
||||||
|
LIMIT ? OFFSET (? - 1) * ?
|
||||||
|
)
|
||||||
|
JOIN thread_fts ON thread_fts.id=thread_id
|
||||||
|
JOIN person ON person.id=author_id
|
||||||
|
WHERE thread_fts MATCH ?
|
||||||
|
UNION
|
||||||
|
-- the main query, selects posts
|
||||||
|
SELECT
|
||||||
|
thread.id AS thread_id,
|
||||||
|
thread.name AS thread,
|
||||||
|
post.id AS post_id,
|
||||||
|
CASE what WHEN 1
|
||||||
|
THEN snippet(post_fts, '<b>', '</b>', '...', what)
|
||||||
|
ELSE post_fts.header END AS header,
|
||||||
|
CASE what WHEN 2
|
||||||
|
THEN snippet(post_fts, '**', '**', '...', what, -45)
|
||||||
|
ELSE SUBSTR(post_fts.content, 1, 200) END AS content,
|
||||||
|
person.name AS author,
|
||||||
|
cdate,
|
||||||
|
post.author AS author_id,
|
||||||
|
person.email AS email,
|
||||||
|
what
|
||||||
|
FROM post_fts JOIN (
|
||||||
|
-- inner query, selects ids of matching posts, orders and limits them,
|
||||||
|
-- so snippets only for limited count of posts are created (in outer query)
|
||||||
|
SELECT id, post.creation AS cdate, thread, 1 AS what, post.author AS author
|
||||||
|
FROM post_fts JOIN post USING(id)
|
||||||
|
WHERE post_fts.header MATCH ?
|
||||||
|
GROUP BY post.header
|
||||||
|
HAVING SUBSTR(post.header,1,3)<>'Re:'
|
||||||
|
UNION
|
||||||
|
SELECT id, post.creation AS cdate, thread, 2 AS what, post.author AS author
|
||||||
|
FROM post_fts JOIN post USING(id)
|
||||||
|
WHERE post_fts.content MATCH ?
|
||||||
|
ORDER BY what, cdate DESC
|
||||||
|
LIMIT ? OFFSET (? - 1) * ?
|
||||||
|
) AS post USING(id)
|
||||||
|
JOIN thread ON thread.id=thread
|
||||||
|
JOIN person ON person.id=author
|
||||||
|
WHERE post_fts MATCH ?
|
||||||
|
ORDER BY what ASC, cdate DESC
|
||||||
|
LIMIT 300 -- hardcoded limit just in case
|
||||||
|
;
|
||||||
|
|
||||||
241
main.tmpl
|
|
@ -10,87 +10,170 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>${XmlEncode(title)}</title>
|
<title>${xmlEncode(title)}</title>
|
||||||
<link rel="stylesheet" href="${c.req.makeUri("css/normalize.css", absolute = false)}">
|
<link rel="stylesheet" href="${c.req.makeUri("css/style.css", absolute = false)}">
|
||||||
<link rel="stylesheet" href="${c.req.makeUri("css/style.css", absolute = false)}">${additional_headers}
|
<link rel="shortcut icon" href="${c.req.makeUri("favicon.ico", absolute = false)}">${additional_headers}
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="wrapper">
|
<!--- #head --->
|
||||||
#let frontQuery = c.req.makeUri("/")
|
#let frontQuery = c.req.makeUri("/")
|
||||||
<div id="nimbtn">
|
<header id="head" class="forum">
|
||||||
<a href="${frontQuery}">Forum</a>
|
<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/documentation.html">docs</a>
|
||||||
|
<a href="http://nim-lang.org/learn.html">learn</a>
|
||||||
|
<a href="http://nim-lang.org/download.html">download</a>
|
||||||
|
<a href="${frontQuery}" class="active">forum</a>
|
||||||
|
<a href="http://nim-lang.org/question.html">faq</a>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div id="header">
|
<!--- #neck --->
|
||||||
<span>Nimrod's <a href="http://nimrod-lang.org/">homepage</a></span>
|
<section id="neck">
|
||||||
<span><a href="http://nimrod-lang.org/documentation.html">stable docs</a></span>
|
<div class="wide-layout tall">
|
||||||
<span><a href="http://build.nimrod-lang.org/docs/overview.html">development docs</a></span>
|
<div style="right: 131px;" id="glow-arrow" class="forum"></div>
|
||||||
<span><a href="https://github.com/Araq/Nimrod/issues">github issues</a></span>
|
|
||||||
#if c.loggedIn:
|
|
||||||
<a href="${frontQuery}logout" class="right">Logout</a>
|
|
||||||
#let profileUrl = c.req.makeUri("profile/", false) &
|
|
||||||
# XMLencode(c.username)
|
|
||||||
<span id="welcome"><a href="${profileUrl}">$c.username</a></span>
|
|
||||||
<a href="$profileUrl">${genGravatar(c.email, 26)}</a>
|
|
||||||
#else:
|
|
||||||
<a href="${frontQuery}register" class="right">Register</a>
|
|
||||||
<a href="${frontQuery}login" class="right">Login</a>
|
|
||||||
#end if
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="topbar">
|
|
||||||
${c.genActionMenu}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="get" action="http://www.google.com/search">
|
|
||||||
<table align="right" cellpadding="5pt" cellspacing="5pt"><tr><td>
|
|
||||||
<input type="text" name="q" size="25" maxlength="255" value="" />
|
|
||||||
</td><td>
|
|
||||||
<input type="submit" value="Google Search this forum" />
|
|
||||||
<input type="hidden" name="sitesearch"
|
|
||||||
value="http://forum.nimrod-lang.org" />
|
|
||||||
</td></tr></table>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="content">
|
|
||||||
$content
|
|
||||||
<span style="color:red">$c.errorMsg</span>
|
|
||||||
</div>
|
|
||||||
#if c.req.pathInfo.normalizeUri notin noPageNums:
|
|
||||||
${c.genPagenumNav(stats)}
|
|
||||||
#end if
|
|
||||||
<div id="topbar">
|
|
||||||
${c.genActionMenu}
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!--- #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:
|
#if c.isThreadsList:
|
||||||
|
<div id="talk-info">
|
||||||
|
<div class="info">
|
||||||
|
<div>
|
||||||
${c.genListOnline(stats)}
|
${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
|
#end if
|
||||||
|
|
||||||
#if showRssLinks:
|
</article>
|
||||||
<span id="rss">
|
<div id="sidebar">
|
||||||
<a href="${c.req.makeUri("/threadActivity.xml")}"
|
<div class="title">Search</div>
|
||||||
><img
|
<div class="content">
|
||||||
src="${c.req.makeUri("/images/Feed-icon.svg", absolute = false)}"
|
#if isFTSAvailable:
|
||||||
class="rssfeed"
|
<a href="/search-help" target="_blank" class="searchHelp">?</a>
|
||||||
>Thread activity</a>
|
<form method="post" action="/search" class="searchForm">
|
||||||
<a href="${c.req.makeUri("/postActivity.xml")}"
|
<input type="text" name="q" maxlength="255" value="${c.search}" title="Search this forum" />
|
||||||
><img
|
<input type="submit" value="Search" class="button search">
|
||||||
src="${c.req.makeUri("/images/Feed-icon.svg", absolute = false)}"
|
</form>
|
||||||
class="rssfeed"
|
#else:
|
||||||
>Posts activity</a>
|
<form method="get" action="http://www.google.com/search" target="_blank" class="searchForm">
|
||||||
</span>
|
<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.nimrod-lang.org" />
|
||||||
|
</form>
|
||||||
#end if
|
#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 id="footerPush"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="footer">
|
#else:
|
||||||
<span>Written in <a href="http://nimrod-lang.org/">Nimrod</a> using <a href="https://github.com/dom96/jester">Jester</a></span>
|
<div class="title">Login</div>
|
||||||
<span> | <a href="https://github.com/nimrod-code/nimforum">Fork on Github</a></span>
|
<div class="content">
|
||||||
<span> | User contributions licensed under <a href="http://creativecommons.org/licenses/by-sa/3.0/">cc-wiki</a> with <a href="/license">attribution required</a></span>
|
<form name="login" action="${frontQuery}dologin" method="POST">
|
||||||
<span style="float:right;">Generated in ${int((epochTime()-c.startTime)*1000.0)}ms</span>
|
<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>
|
||||||
|
#if c.errorMsg != "" and c.req.pathInfo.normalizeUri == "/dologin":
|
||||||
|
<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>
|
</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/Araq/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="http://nim-lang.org/blog/">Andreas Rumpf</a> & <a href="https://github.com/Araq/Nimrod/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>
|
||||||
|
|
||||||
|
<script src="${frontQuery}js/arrow.js"></script>
|
||||||
|
<script src="${frontQuery}js/forum.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
#end proc
|
#end proc
|
||||||
|
|
@ -120,7 +203,7 @@
|
||||||
# const postContent = 5
|
# const postContent = 5
|
||||||
# const postId = 6
|
# const postId = 6
|
||||||
# let frontQuery = c.req.makeUri("/")
|
# let frontQuery = c.req.makeUri("/")
|
||||||
# let recent = GetValue(db, sql"""SELECT
|
# let recent = getValue(db, sql"""SELECT
|
||||||
# strftime('%Y-%m-%dT%H:%M:%SZ', (modified)) FROM thread
|
# strftime('%Y-%m-%dT%H:%M:%SZ', (modified)) FROM thread
|
||||||
# ORDER BY modified DESC LIMIT 1""")
|
# ORDER BY modified DESC LIMIT 1""")
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
@ -130,22 +213,22 @@
|
||||||
<link href="${frontQuery}" />
|
<link href="${frontQuery}" />
|
||||||
<id>${frontQuery}</id>
|
<id>${frontQuery}</id>
|
||||||
<updated>${recent}</updated>
|
<updated>${recent}</updated>
|
||||||
# for row in Rows(db, query, 10):
|
# for row in rows(db, query, 10):
|
||||||
<entry>
|
<entry>
|
||||||
<title>${XMLencode(%name)}</title>
|
<title>${xmlEncode(%name)}</title>
|
||||||
<id>urn:entry:${%threadid}</id>
|
<id>urn:entry:${%threadid}</id>
|
||||||
# let url = c.genThreadUrl(threadid = %threadid,
|
# let url = c.genThreadUrl(threadid = %threadid,
|
||||||
# pageNum = $(ceil(parseInt(%postCount) / postsPerPage).int)) &
|
# pageNum = $(ceil(parseInt(%postCount) / PostsPerPage).int)) &
|
||||||
# "#" & %postId
|
# "#" & %postId
|
||||||
<link rel="alternate" type="text/html"
|
<link rel="alternate" type="text/html"
|
||||||
href="${c.req.makeUri(url)}"/>
|
href="${c.req.makeUri(url)}"/>
|
||||||
<published>${%threadDate}</published>
|
<published>${%threadDate}</published>
|
||||||
<updated>${%threadDate}</updated>
|
<updated>${%threadDate}</updated>
|
||||||
<author><name>${XMLEncode(%postAuthor)}</name></author>
|
<author><name>${xmlEncode(%postAuthor)}</name></author>
|
||||||
<content type="html"
|
<content type="html"
|
||||||
>Posts ${%postCount}, ${XMLEncode(%postAuthor)} said:
|
>Posts ${%postCount}, ${xmlEncode(%postAuthor)} said:
|
||||||
<p>
|
<p>
|
||||||
${XMLEncode(rstToHtml(%postContent))}</content>
|
${xmlEncode(rstToHtml(%postContent))}</content>
|
||||||
</entry>
|
</entry>
|
||||||
# end for
|
# end for
|
||||||
</feed>
|
</feed>
|
||||||
|
|
@ -169,7 +252,7 @@ ${XMLEncode(rstToHtml(%postContent))}</content>
|
||||||
# const postHumanDate = 6
|
# const postHumanDate = 6
|
||||||
# const postPosition = 7
|
# const postPosition = 7
|
||||||
# let frontQuery = c.req.makeUri("/")
|
# let frontQuery = c.req.makeUri("/")
|
||||||
# let recent = GetValue(db, sql"""SELECT
|
# let recent = getValue(db, sql"""SELECT
|
||||||
# strftime('%Y-%m-%dT%H:%M:%SZ', creation) FROM post
|
# strftime('%Y-%m-%dT%H:%M:%SZ', creation) FROM post
|
||||||
# ORDER BY creation DESC LIMIT 1""")
|
# ORDER BY creation DESC LIMIT 1""")
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
@ -179,22 +262,22 @@ ${XMLEncode(rstToHtml(%postContent))}</content>
|
||||||
<link href="${frontQuery}" />
|
<link href="${frontQuery}" />
|
||||||
<id>${frontQuery}</id>
|
<id>${frontQuery}</id>
|
||||||
<updated>${recent}</updated>
|
<updated>${recent}</updated>
|
||||||
# for row in Rows(db, query, 10):
|
# for row in rows(db, query, 10):
|
||||||
<entry>
|
<entry>
|
||||||
<title>${XMLencode(%postHeader)}</title>
|
<title>${xmlEncode(%postHeader)}</title>
|
||||||
<id>urn:entry:${%postId}</id>
|
<id>urn:entry:${%postId}</id>
|
||||||
# let url = c.genThreadUrl(threadid = %postThread,
|
# let url = c.genThreadUrl(threadid = %postThread,
|
||||||
# pageNum = $(ceil(parseInt(%postPosition) / postsPerPage).int)) &
|
# pageNum = $(ceil(parseInt(%postPosition) / PostsPerPage).int)) &
|
||||||
# "#" & %postId
|
# "#" & %postId
|
||||||
<link rel="alternate" type="text/html"
|
<link rel="alternate" type="text/html"
|
||||||
href="${c.req.makeUri(url)}"/>
|
href="${c.req.makeUri(url)}"/>
|
||||||
<published>${%postRssDate}</published>
|
<published>${%postRssDate}</published>
|
||||||
<updated>${%postRssDate}</updated>
|
<updated>${%postRssDate}</updated>
|
||||||
<author><name>${XMLEncode(%postAuthor)}</name></author>
|
<author><name>${xmlEncode(%postAuthor)}</name></author>
|
||||||
<content type="html"
|
<content type="html"
|
||||||
>On ${XMLEncode(%postHumanDate)}, ${XMLEncode(%postAuthor)} said:
|
>On ${xmlEncode(%postHumanDate)}, ${xmlEncode(%postAuthor)} said:
|
||||||
<p>
|
<p>
|
||||||
${XMLEncode(rstToHtml(%postContent))}</content>
|
${xmlEncode(rstToHtml(%postContent))}</content>
|
||||||
</entry>
|
</entry>
|
||||||
# end for
|
# end for
|
||||||
</feed>
|
</feed>
|
||||||
|
|
|
||||||
11
nimforum.babel
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Package]
|
||||||
|
name = "nimforum"
|
||||||
|
version = "0.1.0"
|
||||||
|
author = "Dominik Picheta"
|
||||||
|
description = "Nimrod forum"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
bin = "forum"
|
||||||
|
|
||||||
|
[Deps]
|
||||||
|
Requires: "nimrod >= 0.9.2, cairo#head, jester#head, bcrypt#head"
|
||||||
12
public/css/arrow.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function positionGlowArrow() {
|
||||||
|
var headLinks = document.getElementById("head-links");
|
||||||
|
var activeLink = headLinks.getElementsByClassName("active")[0]
|
||||||
|
if (activeLink == undefined || activeLink == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var offset = (headLinks.offsetWidth - activeLink.offsetLeft) - (activeLink.offsetWidth / 2) - 133;
|
||||||
|
var glowArrow = document.getElementById("glow-arrow");
|
||||||
|
glowArrow.style.right = offset + "px";
|
||||||
|
}
|
||||||
5
public/css/forum.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
positionGlowArrow();
|
||||||
|
};
|
||||||
284
public/css/normalize.css
vendored
|
|
@ -1,284 +0,0 @@
|
||||||
/*
|
|
||||||
* HTML5 Boilerplate
|
|
||||||
*
|
|
||||||
* What follows is the result of much research on cross-browser styling.
|
|
||||||
* Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
|
|
||||||
* Kroc Camen, and the H5BP dev community and team.
|
|
||||||
*
|
|
||||||
* Detailed information about this CSS: h5bp.com/css
|
|
||||||
*
|
|
||||||
* ==|== normalize ==========================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
HTML5 display definitions
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; }
|
|
||||||
audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; }
|
|
||||||
audio:not([controls]) { display: none; }
|
|
||||||
[hidden] { display: none; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Base
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units
|
|
||||||
* 2. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g
|
|
||||||
*/
|
|
||||||
|
|
||||||
html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
|
||||||
|
|
||||||
html, button, input, select, textarea { font-family: sans-serif; color: #222; }
|
|
||||||
|
|
||||||
body { margin: 0; font-size: 1em; line-height: 1.4; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Links
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
a { color: #00e; }
|
|
||||||
a:visited { color: #551a8b; }
|
|
||||||
a:hover { color: #06e; }
|
|
||||||
a:focus { outline: thin dotted; }
|
|
||||||
|
|
||||||
/* Improve readability when focused and hovered in all browsers: h5bp.com/h */
|
|
||||||
a:hover, a:active { outline: 0; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Typography
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
abbr[title] { border-bottom: 1px dotted; }
|
|
||||||
|
|
||||||
b, strong { font-weight: bold; }
|
|
||||||
|
|
||||||
blockquote { margin: 1em 40px; }
|
|
||||||
|
|
||||||
dfn { font-style: italic; }
|
|
||||||
|
|
||||||
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
|
|
||||||
|
|
||||||
ins { background: #ff9; color: #000; text-decoration: none; }
|
|
||||||
|
|
||||||
mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
|
|
||||||
|
|
||||||
/* Redeclare monospace font family: h5bp.com/j */
|
|
||||||
pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; }
|
|
||||||
|
|
||||||
/* Improve readability of pre-formatted text in all browsers */
|
|
||||||
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
|
|
||||||
|
|
||||||
q { quotes: none; }
|
|
||||||
q:before, q:after { content: ""; content: none; }
|
|
||||||
|
|
||||||
small { font-size: 85%; }
|
|
||||||
|
|
||||||
/* Position subscript and superscript content without affecting line-height: h5bp.com/k */
|
|
||||||
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
|
|
||||||
sup { top: -0.5em; }
|
|
||||||
sub { bottom: -0.25em; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Lists
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
ul, ol { margin: 1em 0; padding: 0 0 0 40px; }
|
|
||||||
dd { margin: 0 0 0 40px; }
|
|
||||||
nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Embedded content
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. Improve image quality when scaled in IE7: h5bp.com/d
|
|
||||||
* 2. Remove the gap between images and borders on image containers: h5bp.com/i/440
|
|
||||||
*/
|
|
||||||
|
|
||||||
img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Correct overflow not hidden in IE9
|
|
||||||
*/
|
|
||||||
|
|
||||||
svg:not(:root) { overflow: hidden; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Figures
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
figure { margin: 0; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Forms
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
form { margin: 0; }
|
|
||||||
fieldset { border: 0; margin: 0; padding: 0; }
|
|
||||||
|
|
||||||
/* Indicate that 'label' will shift focus to the associated form element */
|
|
||||||
label { cursor: pointer; }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. Correct color not inheriting in IE6/7/8/9
|
|
||||||
* 2. Correct alignment displayed oddly in IE6/7
|
|
||||||
*/
|
|
||||||
|
|
||||||
legend { border: 0; *margin-left: -7px; padding: 0; white-space: normal; }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. Correct font-size not inheriting in all browsers
|
|
||||||
* 2. Remove margins in FF3/4 S5 Chrome
|
|
||||||
* 3. Define consistent vertical alignment display in all browsers
|
|
||||||
*/
|
|
||||||
|
|
||||||
button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet)
|
|
||||||
*/
|
|
||||||
|
|
||||||
button, input { line-height: normal; }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. Display hand cursor for clickable form elements
|
|
||||||
* 2. Allow styling of clickable form elements in iOS
|
|
||||||
* 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6)
|
|
||||||
*/
|
|
||||||
|
|
||||||
button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Re-set default cursor for disabled elements
|
|
||||||
*/
|
|
||||||
|
|
||||||
button[disabled], input[disabled] { cursor: default; }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Consistent box sizing and appearance
|
|
||||||
*/
|
|
||||||
|
|
||||||
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; *width: 13px; *height: 13px; }
|
|
||||||
input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }
|
|
||||||
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Remove inner padding and border in FF3/4: h5bp.com/l
|
|
||||||
*/
|
|
||||||
|
|
||||||
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. Remove default vertical scrollbar in IE6/7/8/9
|
|
||||||
* 2. Allow only vertical resizing
|
|
||||||
*/
|
|
||||||
|
|
||||||
textarea { overflow: auto; vertical-align: top; resize: vertical; }
|
|
||||||
|
|
||||||
/* Colors for form validity */
|
|
||||||
input:valid, textarea:valid { }
|
|
||||||
input:invalid, textarea:invalid { background-color: #f0dddd; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Tables
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
table { border-collapse: collapse; border-spacing: 0; }
|
|
||||||
td { vertical-align: top; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Chrome Frame Prompt
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
.chromeframe { margin: 0.2em 0; background: #ccc; color: black; padding: 0.2em 0; }
|
|
||||||
|
|
||||||
|
|
||||||
/* ==|== primary styles =====================================================
|
|
||||||
Author:
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ==|== media queries ======================================================
|
|
||||||
EXAMPLE Media Query for Responsive Design.
|
|
||||||
This example overrides the primary ('mobile first') styles
|
|
||||||
Modify as content requires.
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
@media only screen and (min-width: 35em) {
|
|
||||||
/* Style adjustments for viewports that meet the condition */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ==|== non-semantic helper classes ========================================
|
|
||||||
Please define your styles before this section.
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/* For image replacement */
|
|
||||||
.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; *line-height: 0; }
|
|
||||||
.ir br { display: none; }
|
|
||||||
|
|
||||||
/* Hide from both screenreaders and browsers: h5bp.com/u */
|
|
||||||
.hidden { display: none !important; visibility: hidden; }
|
|
||||||
|
|
||||||
/* Hide only visually, but have it available for screenreaders: h5bp.com/v */
|
|
||||||
.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
|
|
||||||
|
|
||||||
/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */
|
|
||||||
.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
|
|
||||||
|
|
||||||
/* Hide visually and from screenreaders, but maintain layout */
|
|
||||||
.invisible { visibility: hidden; }
|
|
||||||
|
|
||||||
/* Contain floats: h5bp.com/q */
|
|
||||||
.clearfix:before, .clearfix:after { content: ""; display: table; }
|
|
||||||
.clearfix:after { clear: both; }
|
|
||||||
.clearfix { *zoom: 1; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ==|== print styles =======================================================
|
|
||||||
Print styles.
|
|
||||||
Inlined to avoid required HTTP connection: h5bp.com/r
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
* { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */
|
|
||||||
a, a:visited { text-decoration: underline; }
|
|
||||||
a[href]:after { content: " (" attr(href) ")"; }
|
|
||||||
abbr[title]:after { content: " (" attr(title) ")"; }
|
|
||||||
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */
|
|
||||||
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
|
|
||||||
thead { display: table-header-group; } /* h5bp.com/t */
|
|
||||||
tr, img { page-break-inside: avoid; }
|
|
||||||
img { max-width: 100% !important; }
|
|
||||||
@page { margin: 0.5cm; }
|
|
||||||
p, h2, h3 { orphans: 3; widows: 3; }
|
|
||||||
h2, h3 { page-break-after: avoid; }
|
|
||||||
}
|
|
||||||
BIN
public/images/bg.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
public/images/forum-posts.png
Normal file
|
After Width: | Height: | Size: 206 B |
BIN
public/images/forum-reply.png
Normal file
|
After Width: | Height: | Size: 490 B |
BIN
public/images/forum-views.png
Normal file
|
After Width: | Height: | Size: 424 B |
BIN
public/images/glow-arrow.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
public/images/glow-line.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/images/glow-line2.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/images/head-link.png
Normal file
|
After Width: | Height: | Size: 203 B |
BIN
public/images/head-link_hover.png
Normal file
|
After Width: | Height: | Size: 799 B |
BIN
public/images/head.png
Normal file
|
After Width: | Height: | Size: 171 B |
BIN
public/images/logo.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
public/images/smilieys/icon_cool.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/images/smilieys/icon_e_biggrin.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/images/smilieys/icon_e_confused.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/images/smilieys/icon_e_sad.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/images/smilieys/icon_e_smile.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/images/smilieys/icon_e_surprised.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/images/smilieys/icon_e_wink.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/images/smilieys/icon_exclaim.png
Normal file
|
After Width: | Height: | Size: 897 B |
BIN
public/images/smilieys/icon_mad.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/images/smilieys/icon_neutral.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/smilieys/icon_razz.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
12
public/js/arrow.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function positionGlowArrow() {
|
||||||
|
var headLinks = document.getElementById("head-links");
|
||||||
|
var activeLink = headLinks.getElementsByClassName("active")[0]
|
||||||
|
if (activeLink == undefined || activeLink == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var offset = (headLinks.offsetWidth - activeLink.offsetLeft) - (activeLink.offsetWidth / 2) - 133;
|
||||||
|
var glowArrow = document.getElementById("glow-arrow");
|
||||||
|
glowArrow.style.right = offset + "px";
|
||||||
|
}
|
||||||
5
public/js/forum.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
positionGlowArrow();
|
||||||
|
};
|
||||||
35
static/search-help.rst
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
Full-text search for Nim forum
|
||||||
|
==============================
|
||||||
|
|
||||||
|
Syntax (using *SQLite* dll compiled without *Enhanced Query Syntax* support):
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
- Only alphanumeric characters are searched.
|
||||||
|
- Only full words and words beginnings (e.g. ``Nim*`` for both ``Nimrod`` and ``Nim``) are searched
|
||||||
|
- All words are joined with implicit **AND** operator; there's no explicit one
|
||||||
|
- There's explicit **OR** operator (upper-case) and it has higher priority
|
||||||
|
- Words can be prepended with **-** to be excluded from search
|
||||||
|
- No parentheses support
|
||||||
|
- Quotes for phrases search, e.g. ``"programming language"``
|
||||||
|
- Distances between words/phrases can be specified putting ``NEAR`` or ``NEAR/some_number`` between them
|
||||||
|
|
||||||
|
Syntax - differences in *Enhanced Query Syntax* (should be enabled in *SQLite* dll):
|
||||||
|
------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
- **AND** and **NOT** logical operators available
|
||||||
|
- Precedence of operators is, from highest to lowest: **NOT**, **AND**, **OR**
|
||||||
|
- Parentheses for grouping are supported
|
||||||
|
|
||||||
|
Where search is performed:
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
- **Threads' titles** - these results are outputed first
|
||||||
|
- **Posts' titles** - middle precedence
|
||||||
|
- **Posts' contents** - the latest
|
||||||
|
|
||||||
|
How results are shown:
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- All results are ordered by date (posts' edits don't affect)
|
||||||
|
- Matched tokens in text are marked (bold or dotted underline)
|
||||||
|
- Threads title is the link to the thread and posts title is the link to the post
|
||||||