Add Comments and Emoticons modules

This commit is contained in:
Noah 2014-04-06 15:45:43 -07:00
parent 91536e3d05
commit 4bb9b5d687
189 changed files with 1266 additions and 7 deletions

View File

@ -10,6 +10,9 @@ DEBUG = True
# Unique name of your site, e.g. "kirsle.net"
SITE_NAME = "example.com"
# E-mail addresses for site notifications (i.e. new comments).
NOTIFY_ADDRESS = ["root@localhost"]
# Secret key used for session cookie signing. Make this long and hard to guess.
#
# Tips for creating a strong secret key:
@ -36,6 +39,19 @@ REDIS_PORT = 6379
REDIS_DB = 0
REDIS_PREFIX = "rophako:"
# Mail settings
MAIL_METHOD = "smtp" # or "sendmail", not yet implemented
MAIL_SERVER = "localhost"
MAIL_PORT = 25
MAIL_SENDER = "Rophako CMS <no-reply@rophako.kirsle.net>"
# Emoticon theme used for blog posts and comments. Should exist at the URL
# "/static/smileys" from your document root, and have a file named
# "emoticons.json" inside. If you add a custom theme to your private site
# folder, then also change EMOTICON_ROOT_PRIVATE to look there instead.
EMOTICON_THEME = "tango"
EMOTICON_ROOT_PRIVATE = os.path.join(_basedir, "rophako", "www", "static", "smileys")
################################################################################
## Blog Settings ##
################################################################################
@ -63,4 +79,14 @@ PHOTO_TIME_FORMAT = BLOG_TIME_FORMAT
# Photo sizes.
PHOTO_WIDTH_LARGE = 800 # Max width of full size photos.
PHOTO_WIDTH_THUMB = 256 # Max square width of photo thumbnails.
PHOTO_WIDTH_AVATAR = 96 # Square width of photo avatars.
PHOTO_WIDTH_AVATAR = 96 # Square width of photo avatars.
################################################################################
## Comment Settings ##
################################################################################
COMMENT_TIME_FORMAT = "%A, %B %d %Y @ %I:%M %p"
# We use Gravatar for comments if the user provides an e-mail address. Specify
# the URL to a fallback image to use in case they don't have a gravatar.
COMMENT_DEFAULT_AVATAR = ""

View File

@ -19,10 +19,14 @@ from rophako.modules.admin import mod as AdminModule
from rophako.modules.account import mod as AccountModule
from rophako.modules.blog import mod as BlogModule
from rophako.modules.photo import mod as PhotoModule
from rophako.modules.comment import mod as CommentModule
from rophako.modules.emoticons import mod as EmoticonsModule
app.register_blueprint(AdminModule)
app.register_blueprint(AccountModule)
app.register_blueprint(BlogModule)
app.register_blueprint(PhotoModule)
app.register_blueprint(CommentModule)
app.register_blueprint(EmoticonsModule)
# Custom Jinja handler to support custom- and default-template folders for
# rendering templates.
@ -34,6 +38,10 @@ app.jinja_loader = jinja2.ChoiceLoader([
app.jinja_env.globals["csrf_token"] = rophako.utils.generate_csrf_token
app.jinja_env.globals["include_page"] = rophako.utils.include
# Preload the emoticon data.
import rophako.model.emoticons as Emoticons
Emoticons.load_theme()
@app.before_request
def before_request():

View File

@ -117,6 +117,11 @@ def read_json(path):
if not os.path.isfile(path):
raise Exception("Can't read JSON file {}: file not found!".format(path))
# Don't allow any fishy looking paths.
if ".." in path:
logger.error("ERROR: JsonDB tried to read a path with two dots: {}".format(path))
raise Exception()
# Open and lock the file.
fh = codecs.open(path, 'r', 'utf-8')
flock(fh, LOCK_SH)
@ -138,6 +143,11 @@ def write_json(path, data):
"""Write a JSON document."""
path = str(path)
# Don't allow any fishy looking paths.
if ".." in path:
logger.error("ERROR: JsonDB tried to write a path with two dots: {}".format(path))
raise Exception()
logger.debug("JsonDB: WRITE > {}".format(path))
# Open and lock the file.

234
rophako/model/comment.py Normal file
View File

@ -0,0 +1,234 @@
# -*- coding: utf-8 -*-
"""Commenting models."""
from flask import g, url_for
import time
import hashlib
import urllib
import random
import re
import config
import rophako.jsondb as JsonDB
import rophako.model.user as User
import rophako.model.emoticons as Emoticons
from rophako.utils import send_email
from rophako.log import logger
def add_comment(thread, uid, name, subject, message, url, time, ip, image=None):
"""Add a comment to a comment thread.
* uid is 0 if it's a guest post, otherwise the UID of the user.
* name is the commenter's name (if a guest)
* subject is for the e-mails that are sent out
* message is self explanatory.
* url is the URL where the comment can be read.
* time, epoch time of comment.
* ip is the IP address of the commenter.
* image is a Gravatar image URL etc.
"""
# Get the comments for this thread.
comments = get_comments(thread)
# Make up a unique ID for the comment.
cid = random_hash()
while cid in comments:
cid = random_hash()
# Add the comment.
comments[cid] = dict(
uid=uid,
name=name or "Anonymous",
image=image or "",
message=message,
time=time or int(time.time()),
ip=ip,
)
write_comments(thread, comments)
# Get info about the commenter.
if uid > 0:
user = User.get_user(uid=uid)
if user:
name = user["name"]
# Send the e-mail to the site admins.
send_email(
to=config.NOTIFY_ADDRESS,
subject="New comment: {}".format(subject),
message="""{name} has left a comment on: {subject}
{message}
To view this comment, please go to {url}
=====================
This e-mail was automatically generated. Do not reply to it.""".format(
name=name,
subject=subject,
message=message,
url=url,
),
)
# Notify any subscribers.
subs = get_subscribers(thread)
for sub in subs.keys():
# Make the unsubscribe link.
unsub = url_for("comment.unsubscribe", thread=thread, who=sub, _external=True)
send_email(
to=sub,
subject="New Comment: {}".format(subject),
message="""Hello,
You are currently subscribed to the comment thread '{thread}', and somebody has
just added a new comment!
{name} has left a comment on: {subject}
{message}
To view this comment, please go to {url}
=====================
This e-mail was automatically generated. Do not reply to it.
If you wish to unsubscribe from this comment thread, please visit the following
URL: {unsub}""".format(
thread=thread,
name=name,
subject=subject,
message=message,
url=url,
unsub=unsub,
)
)
def delete_comment(thread, cid):
"""Delete a comment from a thread."""
comments = get_comments(thread)
del comments[cid]
write_comments(thread, comments)
def count_comments(thread):
"""Count the comments on a thread."""
comments = get_comments(thread)
return len(comments.keys())
def add_subscriber(thread, email):
"""Add a subscriber to a thread."""
if not "@" in email:
return
# Sanity check: only subscribe to threads that exist.
if not JsonDB.exists("comments/threads/{}".format(thread)):
return
logger.info("Subscribe e-mail {} to thread {}".format(email, thread))
subs = get_subscribers(thread)
subs[email] = int(time.time())
write_subscribers(thread, subs)
def unsubscribe(thread, email):
"""Unsubscribe an e-mail address from a thread.
If `thread` is `*`, the e-mail is unsubscribed from all threads."""
# Which threads to unsubscribe from?
threads = []
if thread == "*":
threads = JsonDB.list_docs("comments/subscribers")
else:
threads = [thread]
# Remove them as a subscriber.
for thread in threads:
if JsonDB.exists("comments/subscribers/{}".format(thread)):
logger.info("Unsubscribe e-mail address {} from comment thread {}".format(email, thread))
db = get_subscribers(thread)
del db[email]
write_subscribers(thread, db)
def format_message(message):
"""HTML sanitize the message and format it for display."""
# We basically want to escape HTML symbols (like what Flask does for us
# automatically), but we want line breaks to translate to literal <br> tags.
message = re.sub(r'&', '&amp;', message)
message = re.sub(r'<', '&lt;', message)
message = re.sub(r'>', '&gt;', message)
message = re.sub(r'"', '&quot;', message)
message = re.sub(r"'", '&apos;', message)
message = re.sub(r'\n', '<br>', message)
message = re.sub(r'\r', '', message)
# Process emoticons.
message = Emoticons.render(message)
return message
def get_comments(thread):
"""Get the comment thread."""
doc = "comments/threads/{}".format(thread)
print doc
if JsonDB.exists(doc):
return JsonDB.get(doc)
print "NOT EXIST"
return {}
def write_comments(thread, comments):
"""Save the comments DB."""
if len(comments.keys()) == 0:
return JsonDB.delete("comments/threads/{}".format(thread))
return JsonDB.commit("comments/threads/{}".format(thread), comments)
def get_subscribers(thread):
"""Get the subscribers to a comment thread."""
doc = "comments/subscribers/{}".format(thread)
if JsonDB.exists(doc):
return JsonDB.get(doc)
return {}
def write_subscribers(thread, subs):
"""Save the subscribers to the DB."""
if len(subs.keys()) == 0:
return JsonDB.delete("comments/subscribers/{}".format(thread))
return JsonDB.commit("comments/subscribers/{}".format(thread), subs)
def random_hash():
"""Get a short random hash to use as the ID for a comment."""
md5 = hashlib.md5()
md5.update(str(random.randint(0, 1000000)))
return md5.hexdigest()
def gravatar(email):
"""Generate a Gravatar link for an email address."""
if "@" in email:
# Default avatar?
default = config.COMMENT_DEFAULT_AVATAR
# Construct the URL.
params = {
"s": "96", # size
}
if default:
params["d"] = default
url = "http://www.gravatar.com/avatar/" + hashlib.md5(email.lower()).hexdigest() + "?"
url += urllib.urlencode(params)
return url
return ""

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""Emoticon models."""
from flask import g, url_for
import os
import codecs
import json
import re
import config
import rophako.jsondb as JsonDB
from rophako.log import logger
_cache = {}
def load_theme():
"""Pre-load and cache the emoticon theme. This happens on startup."""
theme = config.EMOTICON_THEME
global _cache
# Cached?
if _cache:
return _cache
# Only if the theme file exists.
settings = os.path.join(config.EMOTICON_ROOT_PRIVATE, theme, "emoticons.json")
if not os.path.isfile(settings):
logger.error("Failed to load smiley theme {}: not found!")
# Try the default (tango).
theme = "tango"
settings = os.path.join(config.EMOTICON_ROOT_PRIVATE, theme, "emoticons.json")
if os.path.isfile(settings):
logger.info("Falling back to default theme: tango")
else:
# Give up.
return {}
# Read it.
fh = codecs.open(settings, "r", "utf-8")
text = fh.read()
fh.close()
try:
data = json.loads(text)
except Exception, e:
logger.error("Couldn't load JSON from emoticon file: {}".format(e))
data = {}
# Cache and return it.
_cache = data
return data
def render(message):
"""Render the emoticons into a message.
The message should already be stripped of HTML and otherwise be 'safe' to
embed on a web page. The output of this function includes `<img>` tags and
these won't work otherwise."""
# Get the smileys config.
smileys = load_theme()
# Process all smileys.
for img in sorted(smileys["map"]):
for trigger in smileys["map"][img]:
if trigger in message:
# Substitute it.
sub = """<img src="{url}" alt="{trigger}" title="{trigger}">""".format(
url="/static/smileys/{}/{}".format(config.EMOTICON_THEME, img),
trigger=trigger,
)
pattern = r'([^A-Za-z0-9:\-]|^){}([^A-Za-z0-9:\-]|$)'.format(re.escape(trigger))
result = r'\1{}\2'.format(sub)
message = re.sub(pattern, result, message)
return message

View File

@ -9,6 +9,8 @@ import calendar
import rophako.model.user as User
import rophako.model.blog as Blog
import rophako.model.comment as Comment
import rophako.model.emoticons as Emoticons
from rophako.utils import template, pretty_time, login_required
from rophako.log import logger
from config import *
@ -40,6 +42,10 @@ def entry(fid):
post = Blog.get_entry(post_id)
post["post_id"] = post_id
# Render emoticons.
if post["emoticons"]:
post["body"] = Emoticons.render(post["body"])
# Get the author's information.
post["profile"] = User.get_user(uid=post["author"])
post["photo"] = User.get_picture(uid=post["author"])
@ -48,8 +54,8 @@ def entry(fid):
# Pretty-print the time.
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])
# TODO: count the comments for this post
post["comment_count"] = 0
# Count the comments for this post
post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))
g.info["post"] = post
return template("blog/entry.html")
@ -286,6 +292,10 @@ def partial_index():
post["post_id"] = post_id
# Render emoticons.
if post["emoticons"]:
post["body"] = Emoticons.render(post["body"])
# Get the author's information.
post["profile"] = User.get_user(uid=post["author"])
post["photo"] = User.get_picture(uid=post["author"])
@ -293,8 +303,8 @@ def partial_index():
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])
# TODO: count the comments for this post
post["comment_count"] = 0
# Count the comments for this post
post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))
selected.append(post)
g.info["count"] += 1

186
rophako/modules/comment.py Normal file
View File

@ -0,0 +1,186 @@
# -*- coding: utf-8 -*-
"""Endpoints for the commenting subsystem."""
from flask import Blueprint, g, request, redirect, url_for, session, flash
import re
import time
import rophako.model.user as User
import rophako.model.comment as Comment
from rophako.utils import template, pretty_time, login_required, sanitize_name
from rophako.log import logger
from config import *
mod = Blueprint("comment", __name__, url_prefix="/comments")
## TODO: emoticon support
@mod.route("/")
def index():
return template("blog/index.html")
@mod.route("/preview", methods=["POST"])
def preview():
# Get the form fields.
form = get_comment_form(request.form)
# Trap fields.
trap1 = request.form.get("website", "x") != "http://"
trap2 = request.form.get("email", "x") != ""
if trap1 or trap2:
flash("Wanna try that again?")
return redirect(url_for("index"))
# Validate things.
if len(form["message"]) == 0:
flash("You must provide a message with your comment.")
return redirect(form["url"])
# Gravatar.
g.info["gravatar"] = Comment.gravatar(form.get("contact", ""))
g.info["preview"] = Comment.format_message(form.get("message", ""))
g.info.update(form)
return template("comment/preview.html")
@mod.route("/post", methods=["POST"])
def post():
# Get the form fields.
form = get_comment_form(request.form)
thread = sanitize_name(form["thread"])
# Gravatar?
gravatar = Comment.gravatar(form.get("contact"))
# Validate things.
if len(form["message"]) == 0:
flash("You must provide a message with your comment.")
return redirect(form["url"])
Comment.add_comment(
thread=thread,
uid=g.info["session"]["uid"],
ip=request.remote_addr,
time=int(time.time()),
image=gravatar,
name=form["name"],
subject=form["subject"],
message=form["message"],
url=form["url"],
)
# Are we subscribing to the thread?
if form.get("subscribe", "false") == "true":
email = form.get("contact", "")
if "@" in email:
Comment.add_subscriber(thread, email)
flash("You have been subscribed to future comments on this page.")
flash("Your comment has been added!")
return redirect(form["url"])
@mod.route("/delete/<thread>/<cid>")
@login_required
def delete(thread, cid):
"""Delete a comment."""
url = request.args.get("url")
Comment.delete_comment(thread, cid)
flash("Comment deleted!")
return redirect(url or url_for("index"))
@mod.route("/privacy")
def privacy():
"""The privacy policy and global unsubscribe page."""
return template("comment/privacy.html")
@mod.route("/unsubscribe", methods=["GET", "POST"])
def unsubscribe():
"""Unsubscribe an e-mail from a comment thread (or all threads)."""
# This endpoint can be called with either method. For the unsubscribe links
# inside the e-mails, it uses GET. For the global out-opt, it uses POST.
thread, email = None, None
if request.method == "POST":
thread = request.form.get("thread", "")
email = request.form.get("email", "")
# Spam check.
trap1 = request.form.get("url", "x") != "http://"
trap2 = request.form.get("message", "x") != ""
if trap1 or trap2:
flash("Wanna try that again?")
return redirect(url_for("index"))
else:
thread = request.args.get("thread", "")
email = request.args.get("who", "")
# Input validation.
if not thread:
flash("Comment thread not found.")
return redirect(url_for("index"))
if not email:
flash("E-mail address not provided.")
return redirect(url_for("index"))
# Do the unsubscribe. If thread is *, this means a global unsubscribe from
# all threads.
Comment.unsubscribe(thread, email)
g.info["thread"] = thread
g.info["email"] = email
return template("comment/unsubscribed.html")
def partial_index(thread, subject, header=True):
"""Partial template for including the index view of a comment thread."""
comments = Comment.get_comments(thread)
# Sort the comments by most recent on bottom.
sorted_cids = [ x for x in sorted(comments, key=lambda y: comments[y]["time"]) ]
sorted_comments = []
for cid in sorted_cids:
comment = comments[cid]
comment["id"] = cid
# Was the commenter logged in?
if comment["uid"] > 0:
user = User.get_user(uid=comment["uid"])
avatar = User.get_picture(uid=comment["uid"])
comment["name"] = user["name"]
comment["username"] = user["username"]
comment["image"] = avatar
# Add the pretty time.
comment["pretty_time"] = pretty_time(COMMENT_TIME_FORMAT, comment["time"])
# Format the message for display.
comment["formatted_message"] = Comment.format_message(comment["message"])
sorted_comments.append(comment)
g.info["header"] = header
g.info["thread"] = thread
g.info["subject"] = subject
g.info["url"] = request.url
g.info["comments"] = sorted_comments
g.info["photo_url"] = PHOTO_ROOT_PUBLIC
return template("comment/index.inc.html")
def get_comment_form(form):
return dict(
thread = request.form.get("thread", ""),
url = request.form.get("url", ""),
subject = request.form.get("subject", "[No Subject]"),
name = request.form.get("name", ""),
contact = request.form.get("contact", ""),
message = request.form.get("message", ""),
subscribe = request.form.get("subscribe", "false"),
)

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""Endpoints for the commenting subsystem."""
from flask import Blueprint, g
import rophako.model.emoticons as Emoticons
from rophako.utils import template
from rophako.log import logger
from config import *
mod = Blueprint("emoticons", __name__, url_prefix="/emoticons")
@mod.route("/")
def index():
"""List the available emoticons."""
theme = Emoticons.load_theme()
smileys = []
for img in sorted(theme["map"]):
smileys.append({
"img": img,
"triggers": theme["map"][img],
})
g.info["theme"] = EMOTICON_THEME
g.info["theme_name"] = theme["name"]
g.info["smileys"] = smileys
return template("emoticons/index.html")

View File

@ -7,8 +7,10 @@ import datetime
import time
import re
import importlib
import smtplib
from rophako.log import logger
from config import *
def login_required(f):
@ -52,6 +54,29 @@ def template(name, **kwargs):
return html
def send_email(to, subject, message, sender=None):
"""Send an e-mail out."""
if sender is None:
sender = MAIL_SENDER
if type(to) != list:
to = [to]
logger.info("Send email to {}".format(to))
if MAIL_METHOD == "smtp":
# Send mail with SMTP.
for email in to:
server = smtplib.SMTP(MAIL_SERVER, MAIL_PORT)
server.set_debuglevel(1)
msg = """From: {}
To: {}
Subject: {}
{}""".format(sender, email, subject, message)
server.sendmail(sender, email, msg)
server.quit()
def generate_csrf_token():
"""Generator for CSRF tokens."""
if "_csrf" not in session:
@ -82,4 +107,4 @@ def sanitize_name(name):
"""Sanitize a name that may be used in the filesystem.
Only allows numbers, letters, and some symbols."""
return re.sub(r'[^A-Za-z0-9 .-_]+', '', name)
return re.sub(r'[^A-Za-z0-9 .\-_]+', '', name)

View File

@ -67,5 +67,13 @@
{% endif %}
]
</div>
<p>
{% if from != "index" %}
{{ include_page("comment.partial_index",
thread="blog-"+post["post_id"]|string,
subject=post["subject"],
) | safe }}
{% endif %}
{% endmacro %}

View File

@ -27,7 +27,7 @@
<strong>Body:</strong><br>
<textarea cols="80" rows="12" name="body">{{ body }}</textarea><br>
<a href="/emoticons" target="_blank">Emoticon reference</a> (opens in new window)<p>
<a href="{{ url_for('emoticons.index') }}" target="_blank">Emoticon reference</a> (opens in new window)<p>
<strong>Avatar:</strong><br>
<span id="avatar-preview"></span>

View File

@ -0,0 +1,90 @@
{% if header %}
<h1>Comments</h1>
{% endif %}
There {% if comments|length == 1 %}is{% else %}are{% endif %}
{{ comments|length }} comment{% if comments|length != 1 %}s{% endif %}
on this page.<p>
{% for comment in comments %}
<div class="comment">
<div class="comment-author">
{% if comment["image"] and (comment["image"].startswith('http:') or comment["image"].startswith('https:')) %}
<img src="{{ comment['image'] }}" alt="Avatar" width="96" height="96">
{% elif comment["image"] %}
<img src="{{ photo_url }}/{{ comment['image'] }}" alt="Avatar" width="96" height="96">
{% else %}
<img src="/static/avatars/default.png" alt="guest" width="96" height="96">
{% endif %}<br>
<strong>{% if comment['username'] %}{{ comment['username'] }}{% else %}guest{% endif %}</strong>
</div>
<strong>Posted on {{ comment["pretty_time"] }} by {{ comment["name"] }}.</strong><p>
{{ comment["formatted_message"]|safe }}
<div class="clear">
{% if session["login"] %}
[IP: {{ comment["ip"] }} | <a href="{{ url_for('comment.delete', thread=thread, cid=comment['id'], url=url) }}" onclick="return window.confirm('Are you sure?')">Delete</a>]
{% endif %}
</div>
</div><p>
{% endfor %}
<h2>Add a Comment</h2>
<form name="comment" action="{{ url_for('comment.preview') }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<input type="hidden" name="thread" value="{{ thread }}">
<input type="hidden" name="url" value="{{ url }}">
<input type="hidden" name="subject" value="{{ subject }}">
<table border="0" cellspacing="2" cellpadding="2">
<tr>
<td align="left" valign="middle">
Your name:
</td>
<td align="left" valign="middle">
{% if session["login"] %}
<strong>{{ session["name"] }}</strong>
{% else %}
<input type="text" size="40" name="name">
{% endif %}
</td>
</tr>
<tr>
<td align="left" valign="middle">
Your Email:
</td>
<td align="left" valign="middle">
<input type="text" size="40" name="contact"> <small>(optional)</small>
</td>
</tr>
<tr>
<td align="left" valign="top">
Message:
</td>
<td align="left" valign="top">
<textarea cols="40" rows="8" name="message" style="width: 100%"></textarea><br>
<small>You can use <a href="{{ url_for('emoticons.index') }}" target="_blank">emoticons</a>
in your comment. <em>(opens in a new window)</em></small>
</td>
</tr>
<tr>
<td colspan="2" align="left" valign="top">
<label>
<input type="checkbox" name="subscribe" value="true">
Notify me of future comments on this page via e-mail
(<a href="{{ url_for('comment.privacy') }}" target="_blank">Privacy Policy</a>)
</label>
</td>
</tr>
</table><p>
<div style="display: none">
If you can see this, don't touch the following fields.<br>
<input type="text" name="website" value="http://"><br>
<input type="text" name="email" value="">
</div>
<button type="submit">Leave Comment</button>
</form>

View File

@ -0,0 +1,32 @@
{% extends "layout.html" %}
{% block title %}Comment Preview{% endblock %}
{% block content %}
<h1>Comment Preview</h1>
This is a preview of what your comment is going to look like once posted.<p>
<hr><p>
{{ preview|safe }}<p>
<hr><p>
{% if subscribe == "true" and contact %}
You will be subscribed to future comments on this thread. Notification
e-mails will be sent to {{ contact }}.<p>
{% endif %}
<form name="preview" action="{{ url_for('comment.post') }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<input type="hidden" name="thread" value="{{ thread }}">
<input type="hidden" name="url" value="{{ url }}">
<input type="hidden" name="subject" value="{{ subject }}">
<input type="hidden" name="name" value="{{ name }}">
<input type="hidden" name="message" value="{{ message }}">
<input type="hidden" name="contact" value="{{ contact }}">
<input type="hidden" name="subscribe" value="{{ subscribe }}">
<button type="submit">Publish Comment</button>
</form>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "layout.html" %}
{% block title %}Comment Subscriptions{% endblock %}
{% block content %}
<h1>Subscribing to Comments</h1>
When posting a comment on this site, you can optionally subscribe to future
comments on the same page (so you can get an e-mail notification when somebody
answers your questions, for example).<p>
You can unsubscribe from these e-mails in the future by clicking a link in the
e-mail. Or, you can unsubscribe from all comment threads by entering your
e-mail address in the form below.<p>
<h2>Privacy Policy</h2>
<ul>
<li>
Your e-mail address that you use when you post the comment will only be
used for sending you notifications via e-mail when somebody else replies
to the comment thread and for showing a
<a href="http://www.gravatar.com/" target="_blank">Gravatar</a> next to
your comment.
</li>
<li>
Your e-mail will not be visible to anybody else on this site.
</li>
<li>
Your e-mail won't be given to any spammers so you don't need to worry
about junk mail.
</li>
<li>
You can unsubscribe from individual comment threads by using the link
provided in the notification e-mail. You can unsubscribe from ALL
threads by using the form on this page.
</li>
</ul>
<h2>Unsubscribe from All Comment Threads</h2>
<form name="unsubscribe" action="{{ url_for('comment.unsubscribe') }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<input type="hidden" name="thread" value="*">
Enter the e-mail address to be unsubscribed from all threads:<br>
<input type="email" size="40" name="email"><p>
<button type="submit">Unsubscribe</button>
<div style="display: none">
If you can see this, do not touch these fields.<br>
<input type="text" name="url" value="http://"><br>
<input type="text" name="message" value="">
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "layout.html" %}
{% block title %}Comment Subscriptions{% endblock %}
{% block content %}
<h1>You have been unsubscribed</h1>
The e-mail address <strong>{{ email }}</strong> has been unsubscribed
{% if thread == "*" %}
from all comment threads on this site.
{% else %}
from the comment thread "{{ thread }}".
{% endif %}
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "layout.html" %}
{% block title %}Emoticons{% endblock %}
{% block content %}
<h1>Emoticon Theme: {{ theme_name }}</h1>
<table class="table" cellspacing="0" cellpadding="2">
<thead>
<tr>
<th>Emoticon</th>
<th>Trigger Text</th>
</tr>
</thead>
<tbody>
{% for img in smileys %}
<tr>
<td align="center" valign="middle">
<img src="/static/smileys/{{ theme }}/{{ img['img'] }}">
</td>
<td align="left" valign="middle">
{% for trigger in img['triggers'] %}
{{ trigger }}&nbsp;&nbsp;&nbsp;&nbsp;
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

View File

@ -0,0 +1,328 @@
{
"map" : {
"neutral.png" : [
":-|",
":|"
],
"rain.png" : [
"(st)"
],
"mail.png" : [
"(E)",
"(e)"
],
"tongue.png" : [
":-P",
":P",
":-p",
":p"
],
"computer.png" : [
"(co)"
],
"love-over.png" : [
"(U)",
"(u)"
],
"good.png" : [
"(Y)",
"(y)"
],
"in-love.png" : [
"*IN",
"LOVE*"
],
"coffee.png" : [
"(C)",
"(c)"
],
"secret.png" : [
":-X",
":-x"
],
"wilt.png" : [
"|-0"
],
"yin-yang.png" : [
"(%)"
],
"yawn.png" : [
"|-)"
],
"glasses-nerdy.png" : [
"8-|"
],
"laugh.png" : [
":-D",
":D",
":d",
":-d"
],
"skywalker.png" : [
"C:-)",
"c:-)",
"C:)",
"c:)"
],
"car.png" : [
"(au)"
],
"pizza.png" : [
"(pi)"
],
"thunder.png" : [
"(li)"
],
"teeth.png" : [
"8o|"
],
"moon.png" : [
"(S)"
],
"smile-big.png" : [
":-))",
":))"
],
"sarcastic.png" : [
"^o)"
],
"confused.png" : [
":-S",
":S",
":s",
":-s"
],
"film.png" : [
"(~)"
],
"party.png" : [
"<:o)"
],
"turtle.png" : [
"(tu)"
],
"beer.png" : [
"(B)",
"(b)"
],
"clock.png" : [
"(O)",
"(o)"
],
"plate.png" : [
"(pl)"
],
"highfive.png" : [
"(h5)"
],
"angry.png" : [
":-@",
":@",
">:o",
">:O"
],
"rose.png" : [
"(F)",
"(f)"
],
"sick.png" : [
":-!",
":!",
"+o(",
"+O("
],
"victory.png" : [
"*BRAVO*",
":BRAVO:",
":bravo:",
":clapping:"
],
"angel.png" : [
"O:-)",
"O:)",
"(A)",
"(a)"
],
"thinking.png" : [
"*-)"
],
"island.png" : [
"(ip)"
],
"smile.png" : [
":-)",
":)"
],
"rose-dead.png" : [
"(W)",
"(w)"
],
"msn.png" : [
"(M)",
"(m)"
],
"umbrella.png" : [
"(um)"
],
"hug-left.png" : [
"({)"
],
"shock.png" : [
":-O",
":-o",
":O",
":o",
"=-O",
"=-o"
],
"rotfl.png" : [
"*ROFL*",
"*rofl*"
],
"airplane.png" : [
"(ap)"
],
"sun.png" : [
"(#)"
],
"goat.png" : [
"(nah)"
],
"crying.png" : [
":'("
],
"sad.png" : [
":-(",
":("
],
"cat.png" : [
"(@)"
],
"bomb.png" : [
"@="
],
"glasses-cool.png" : [
"(H)",
"(h)"
],
"hypnotized.png" : [
"%)",
"%-)"
],
"girl.png" : [
"(X)",
"(x)"
],
"bowl.png" : [
"(||)"
],
"bad.png" : [
"(N)",
"(n)"
],
"mobile.png" : [
"(mp)"
],
"monkey.png" : [
":-(|)"
],
"love.png" : [
"(L)",
"(l)"
],
"phone.png" : [
"(T)",
"(t)"
],
"quiet.png" : [
":-#"
],
"eyeroll.png" : [
"8-)",
"8)"
],
"vampire.png" : [
":-[",
":["
],
"dog.png" : [
"(&)"
],
"camera.png" : [
"(P)",
"(p)"
],
"dance.png" : [
"*DANCE*",
":dance:"
],
"snail.png" : [
"(sn)"
],
"soccerball.png" : [
"(so)"
],
"star.png" : [
"(*)"
],
"lamp.png" : [
"(I)",
"(i)"
],
"sheep.png" : [
"(bah)"
],
"boy.png" : [
"(Z)",
"(z)"
],
"dont-know.png" : [
":^)"
],
"rainbow.png" : [
"(R)",
"(r)"
],
"coins.png" : [
"(mo)"
],
"hug-right.png" : [
"(})"
],
"embarrassed.png" : [
":-$",
":$"
],
"devil.png" : [
"(6)"
],
"kiss.png" : [
":-*",
"(K)",
"(k)"
],
"brb.png" : [
"(brb)"
],
"present.png" : [
"(G)",
"(g)"
],
"cake.png" : [
"(^)"
],
"drink.png" : [
"(D)",
"(d)"
],
"musical-note.png" : [
"(8)"
],
"wink.png" : [
";-)",
";)"
]
},
"source" : "http://digsbies.org/site/content/project/tango-emoticons-big-pack",
"name" : "Tango"
}

View File

@ -0,0 +1,86 @@
smile.png :-) :) +) =) :smile:
smile-big.png *LOL* :-)) :)) =)) +)) :-))) :))) LOL lol LOL! lol! :lol:
laugh.png :-D :D :d :-d +D =D :biggrin:
wink.png ;-) ;) ^_~ :wink:
shock.png :-O :-o :O :o
tongue.png :-P :P :-p :p +P =P +p =p :-b :b +b =b :tongue:
glasses-cool.png (H) (h)
angry.png :-@ :@ >:o >:O >+O >:o >+o :angry:
embarrassed.png :-$ :$
confused.png :-S :S :s :-s
sad.png :-( :( +( =( :-(( :(( +(( =(( :sad:
crying.png :'(
neutral.png :-| :|
devil.png ]:-> }:-> ]:> }:> >:-] >:] (6) :diablo: *DIABLO*
angel.png O:-) O:) O+) O=) 0:-) 0:) 0+) 0=) (A) (a)
love.png (L) (l)
love-over.png (U) (u)
msn.png (M) (m)
cat.png (@)
dog.png (&)
moon.png (S)
star.png (*)
film.png (~)
musical-note.png (8)
mail.png (E) (e)
rose.png @}->-- @}-:-- @>}--,-`--- (F) (f)
rose-dead.png (W) (w)
clock.png (O) (o)
kiss.png :-* (K) (k)
secret.png :-X
present.png (G) (g)
cake.png (^)
camera.png (P) (p)
lamp.png (I) (i)
coffee.png (C) (c)
phone.png (T) (t)
hug-left.png ({)
hug-right.png (})
beer.png *DRINK* DRINK :drink: (B) (b)
drink.png (D) (d)
boy.png (Z) (z)
girl.png (X) (x)
good.png *THUMBS UP* (Y) (y)
bad.png (N) (n)
vampire.png :-[ :[ ;'> ;-. :blush:
goat.png (nah)
sun.png (#)
rainbow.png (R) (r)
quiet.png :-#
teeth.png 8o|
glasses-nerdy.png 8-|
sarcastic.png ^o)
sick.png :-! :! +o( :-~ ;-~ :(~ +(~ =(~ :bad:
snail.png (sn)
turtle.png (tu)
plate.png (pl)
bowl.png (||)
pizza.png (pi)
soccerball.png (so)
car.png (au)
airplane.png (ap)
umbrella.png (um)
island.png (ip)
computer.png (co)
mobile.png (mp)
brb.png (brb)
rain.png (st)
highfive.png (h5)
coins.png (mo)
sheep.png (bah)
dont-know.png :^)
thinking.png *-)
thunder.png (li)
party.png <:o)
eyeroll.png 8-) 8) B) :COOL: :cool: COOL cool COOL! COOL!! COOL!!!
yawn.png |-)
skywalker.png C:-) c:-) C:) c:)
monkey.png :-(|)
yin-yang.png (%)
wilt.png *TIRED* |-0 :boredom:
bomb.png @=
hypnotized.png %) %-) :-$ :$ :wacko: :WACKO:
rotfl.png *ROFL* :ROFL: :rofl: ROFL ROFL! rofl :-)))) :-))))) :-)))))) :)))) :))))) :)))))) =)))) =))))) =))))))
victory.png *BRAVO* :BRAVO: :bravo: :clapping:
dance.png *DANCE* :dance:
in-love.png *IN LOVE*

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Some files were not shown because too many files have changed in this diff Show More