Commits

Matthew Frazier  committed c828fd0

initial commit

  • Participants

Comments (0)

Files changed (31)

+syntax: glob
+*~
+*.pyc
+*.pyo
+Copyright (c) 2010 Matthew "LeafStorm" Frazier
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+# Makefile for Ryshcate translations
+
+extract:
+	pybabel extract -F babel.cfg -o ryshcate/translations/messages.pot .
+
+update:
+	pybabel update -i ryshcate/translations/messages.pot  -d ryshcate/translations
+
+compile:
+	pybabel compile -d ryshcate/translations
+
+init:
+	pybabel init -i ryshcate/translations/messages.pot -d ryshcate/translations -l $(NEWLANG)
+Ryshcate
+
+Ryshcate is a pastebin. I'll write a manual later. For now, just write a
+config file and 'python run-ryshcate.py YOUR_CONFIG_FILE'. If you have pip
+(which you should), you can install the dependencies in one fell swoop with
+'pip install -r requirements.txt'
+[python: ryshcate/**.py]
+[jinja2: ryshcate/**/templates/**.html]
+extensions=jinja2.ext.autoescape,jinja2.ext.with_

File requirements.txt

+Flask>=0.3.1
+Flask-Babel>=0.5
+Flask-CouchDB>=0.1.1
+Flask-XML-RPC>=0.1.1
+Pygments>=1.0
+speaklater
+WTForms>=0.6

File run-ryshcate.py

+# -*- coding: utf-8 -*-
+"""
+run-ryshcate.py
+===============
+This is a launcher script for Ryshcate.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+import os
+import sys
+from ryshcate import create_app
+
+if len(sys.argv) > 1:
+    config_file = sys.argv[1]
+else:
+    config_file = os.environ.get('RYSHCATE_CONFIG')
+
+if config_file is None:
+    print "Warning: No configuration provided."
+    print "You can provide a config file on the command line or in the "
+    print "RYSHCATE_CONFIG environment variable."
+    print "The app will run, but using the settings in ryshcate.defaults."
+
+app = create_app(config_file)
+
+app.run(debug=os.environ.get('RYSHCATE_DEBUG', True))

File ryshcate/__init__.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate
+========
+Ryshcate is a pastebin using Flask and CouchDB with a variety of extensions.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+
+from flask import Flask
+from ryshcate.database import manager
+from ryshcate.locale import setup_babel
+from ryshcate.utils import TEMPLATE_GLOBALS
+from ryshcate.views.pastebin import pastebin
+from ryshcate.views.system import system
+
+def create_app(config, sync=True):
+    app = Flask(__name__)
+    app.config.from_object('ryshcate.defaults')
+    if isinstance(config, dict):
+        app.config.update(config)
+    elif isinstance(config, basestring):
+        app.config.from_pyfile(config)
+    
+    app.register_module(pastebin)
+    app.register_module(system)
+    
+    manager.setup(app)
+    setup_babel(app)
+    
+    app.jinja_env.globals.update(TEMPLATE_GLOBALS)
+    
+    if sync:
+        manager.sync(app)
+    
+    return app

File ryshcate/api.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate.api
+============
+This module contains the pastebin API, which is powered by XML-RPC.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+from flaskext.xmlrpc import XMLRPCHandler, Fault
+from ryshcate.database import Paste, create_id
+from ryshcate.utils import LEXER_LIST, LEXER_DICT, contains_profanities
+
+def paste_to_struct(paste):
+    return dict(id=paste.id, title=paste.title, code=paste.code,
+                language=paste.language, tags=list(paste.tags),
+                created=paste.created, private=paste.private)
+
+
+handler = XMLRPCHandler('api')
+
+ryshcate = handler.namespace('ryshcate')
+
+@ryshcate.register
+def get_paste(id):
+    """
+    Returns the paste with the given ID. Pastes are structs with the members
+    id (string), title (string), code (string), language (string), tags (lis
+    of strings), created (date/time), and private (boolean). It will raise
+    a fault with the code ``not_found`` if the paste does not exist.
+    """
+    paste = Paste.load(id)
+    if paste is None:
+        raise Fault('not_found', 'The paste with ID %s does not exist.' % id)
+    return paste_to_struct(paste)
+
+
+@ryshcate.register
+def all_languages():
+    """
+    This lists all of the available languages. It is returned as a list of
+    structs with the members name and id.
+    """
+    return [dict(id=id, name=name) for (id, name) in LEXER_LIST]
+
+
+@ryshcate.register
+def public_pastes():
+    """
+    This returns a listing of all public pastes. It returns a list of structs
+    that have the same members as the one for `get_paste`.
+    """
+    pastes = Paste.public()
+    return [paste_to_struct(paste) for paste in pastes]
+
+
+@ryshcate.register
+def list_tags():
+    """
+    This lists all the tags that have public pastes. It returns a list of
+    structs with the members name and count, count being the number of pastes
+    therein.
+    """
+    tag_counts = Paste.tag_counts()
+    return [dict(name=t.key, count=t.value) for t in tag_counts]
+
+
+@ryshcate.register
+def list_languages():
+    """
+    This lists all the languages that have public pastes. It returns a list of
+    structs with the members id, name, and count.
+    """
+    language_counts = Paste.language_counts()
+    return [dict(id=l.key, name=LEXER_DICT[l.key], count=l.value)
+            for l in language_counts]
+
+
+@ryshcate.register
+def pastes_by_tag(tag):
+    """
+    This returns a list of pastes that have a given tag. The format of the
+    return value is identical to the one for `public_pastes`.
+    """
+    pastes = Paste.tagged[tag]
+    return [paste_to_struct(paste) for paste in pastes]
+
+
+@ryshcate.register
+def pastes_by_language(language):
+    """
+    This returns a list of pastes for the given language. The format of the
+    return value is identical to the one for `public_pastes`.
+    """
+    pastes = Paste.by_language[language]
+    return [paste_to_struct(paste) for paste in pastes]
+
+
+@ryshcate.register
+def new_paste(title, code, language, tags=None, private=False):
+    """
+    This creates a new paste.
+    
+    :param title: The title of the paste as a string.
+    :param code: The paste's code as a string.
+    :param language: The id of the language. (See `list_languages` for a
+                     list.)
+    :param tags: Tags to apply, as a list of strings. (Optional, defaults to
+                 an empty list.)
+    :param private: Whether the paste should be private or not. (Optional,
+                    defaults to `False`.
+    """
+    # Validate title
+    if contains_profanities(title):
+        raise Fault('profane', 'Title may not contain profanities.')
+    if len(title) > 64:
+        raise Fault('length', 'Title is too long.')
+    # Validate code
+    if contains_profanities(code):
+        raise Fault('profane', 'Code may not contain profanities.')
+    # Validate language
+    if language not in LEXER_DICT:
+        raise Fault('unknown_language', 'That language cannot be pasted.')
+    # Validate tags
+    if tags is None:
+        tags = []
+    if any(contains_profanities(tag) for tag in tags):
+        raise Fault('profane', 'Tags may not contain profanities.')
+    
+    paste = Paste(title=title, code=code, language=language, tags=tags,
+                  private=private, id=create_id())
+    paste.store()
+    return paste_to_struct(paste)

File ryshcate/database.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate.database
+=================
+This has the DB models, views, and the like used by Ryshcate.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+import datetime
+import uuid
+from flaskext.couchdb import (CouchDBManager, Document, ViewField, TextField,
+                              BooleanField, DateTimeField, ListField)
+from couchdb.client import Row
+
+manager = CouchDBManager(auto_sync=False)
+
+
+def create_id():
+    return hex(uuid.uuid4().time)[2:-1]
+
+
+class Paste(Document):
+    title = TextField()
+    code = TextField()
+    language = TextField()
+    created = DateTimeField(default=datetime.datetime.utcnow)
+    tags = ListField(TextField())
+    private = BooleanField(default=False)
+    
+    public = ViewField('pastes', '''\
+        function (doc) {
+            if (!doc.private) {
+                emit(doc.created, doc);
+            };
+        }''')
+    by_language = ViewField('pastes', '''\
+        function (doc) {
+            if (!doc.private) {
+                emit(doc.language, doc);
+            };
+        }''')
+    tagged = ViewField('pastes', '''\
+        function (doc) {
+            if (!doc.private) {
+                doc.tags.forEach(function (tag) {
+                    emit(tag, doc);
+                });
+            };
+        }''')
+    tag_counts = ViewField('pastes', '''\
+        function (doc) {
+            if (!doc.private) {
+                doc.tags.forEach(function (tag) {
+                    emit(tag, 1);
+                });
+            };
+        }''', '''\
+        function (keys, values, rereduce) {
+            return sum(values);
+        }''', group=True, wrapper=Row)
+    language_counts = ViewField('pastes', '''\
+        function (doc) {
+            if (!doc.private) {
+                emit(doc.language, 1);
+            };
+        }''', '''\
+        function (keys, values, rereduce) {
+            return sum(values);
+        }''', group=True, wrapper=Row)
+
+manager.add_document(Paste)

File ryshcate/defaults.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate.defaults
+=================
+This just has the default settings for Ryshcate. You should probably change
+most of them.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+
+#: Whether to run the application in debug mode or not.
+DEBUG = False
+
+#: Cryptographic secret key. Used for sessions. (It isn't that much of a
+#: liability in this case because the session only holds settings, but still.)
+SECRET_KEY = 'v8F7iI0qGH7NkCa8ds5oNp7tXGc=WvpWYmk6EmNEoQkeDVEUWB6ub/E='
+
+#: What to call the session cookie.
+SESSION_COOKIE_NAME = 'ryshcate_settings'
+
+#: The database server URL.
+COUCHDB_SERVER = 'http://localhost:5984/'
+
+#: The actual database name to use.
+COUCHDB_DATABASE = 'ryshcate'
+
+#: The default language. 'en' for English is a good bet until there are more
+#: translations.
+BABEL_DEFAULT_LANGUAGE = 'en'
+
+#: The default time zone. This should probably be UTC.
+BABEL_DEFAULT_TIMEZONE = 'UTC'
+
+#: The default style to use for Pygments if the user hasn't set one.
+DEFAULT_PYGMENTS_STYLE = 'autumn'
+
+#: The site title to include in the header.
+SITE_TITLE = u"Ryshcate"
+
+#: Copyright text to print in the footer.
+SITE_COPYRIGHT = u"(Please change the SITE_COPYRIGHT setting for your \
+organization.)"
+
+#: Whether to include "Powered by Ryshcate" in the footer. (Please do!)
+SITE_POWERED_BY = True
+
+#: Any extra links you want in the site's navigation bar. This should be a
+#: list of (title, url) tuples.
+SITE_EXTRA_LINKS = []
+
+#: "Message of the Day". This is displayed at the top of the "New Paste" page
+#: if set, and you can use it to briefly describe your site.
+SITE_MOTD = u''

File ryshcate/forms.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate.forms
+==============
+Forms for submission and settings, and utilities for dealing with them.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+from ryshcate.locale import _, __, common_timezones, TRANSLATED_LANGUAGES
+from ryshcate.utils import contains_profanities, LEXER_LIST, get_all_styles
+from wtforms import (Form, BooleanField, TextField, TextAreaField,
+                     SelectField, ValidationError, validators as val)
+from wtforms.fields import Field
+from wtforms.widgets import TextInput
+
+### Custom field
+
+class TagListField(Field):
+    widget = TextInput()
+
+    def _value(self):
+        if self.data:
+            return u', '.join(self.data)
+        else:
+            return u''
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            rawdata = valuelist[0].replace(';', ',')
+            self.data = [x.strip() for x in rawdata.split(',')]
+        else:
+            self.data = []
+    
+    @staticmethod
+    def _remove_duplicates(seq):
+        """
+        Removes duplicate tags. It normalizes tags to lowercase.
+        """
+        s = set()
+        for item in seq:
+            tag = item.lower()
+            if tag not in s:
+                s.add(tag)
+                yield tag
+
+
+### Validators
+
+length_msg = __(u"This field must not be longer than %(max)d characters")
+
+def required():
+    return val.Required(message=__(u"This field is required"))
+
+
+def clean(form, field):
+    if isinstance(field.data, basestring):
+        profane = contains_profanities(field.data)
+    else:
+        profane = any(contains_profanities(i) for i in field.data)
+    if profane:
+        raise ValidationError(_(u"This field may not contain profanities"))
+
+
+### Forms
+
+class CreatePasteForm(Form):
+    title = TextField(__(u'Title'), [required(),
+                      val.Length(max=64, message=length_msg), clean],
+                      description=__(u'A title for the paste.'))
+    code = TextAreaField(__(u'Code'), [required(), clean],
+                         description=__(u'The paste\'s source code.'))
+    language = SelectField(__(u'Language'), [required()], default='text',
+                           choices=LEXER_LIST,
+                           description=__(u'The language this paste is in.'))
+    tags = TagListField(__(u'Tags'), [required(), clean],
+                        description=__(u'A comma-separated list of tags.'))
+    private = BooleanField(__(u'Private'), default=False,
+                           description=__(u'Check this if the paste should '
+                           'not be listed publicly.'))
+
+
+class SettingsForm(Form):
+    locale = SelectField(
+        __(u'Locale'), [required()], choices=TRANSLATED_LANGUAGES,
+        description=__(u'The language to use on the site.')
+    )
+    style = SelectField(
+        __(u'Pygments Style'), [required()],
+        choices=[(s, s) for s in get_all_styles()],
+        description=__(u'The color scheme to use for highlighted code.')
+    )
+    timezone = SelectField(
+        __(u'Timezone'), [required()], default='UTC',
+        choices=[(tz, tz.replace('_', ' ')) for tz in common_timezones],
+        description=__(u'The timezone to display times in.')
+    )

File ryshcate/locale.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate.locale
+===============
+Tools for localizing languages and dates/times.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+from flask import session
+from flaskext.babel import (Babel, format_date, format_time, format_datetime,
+                            gettext, _)
+from pytz import common_timezones
+from speaklater import make_lazy_string
+
+TRANSLATED_LANGUAGES = [
+    ('en', u'English')
+]
+
+
+def lazy_gettext(string, **variables):
+    """
+    Like :func:`gettext` but the string returned is lazy which means
+    it will be translated when it is used as an actual string.
+
+    Example::
+
+        hello = lazy_gettext(u'Hello World')
+
+        @app.route('/')
+        def index():
+            return unicode(hello)
+    """
+    return make_lazy_string(gettext, string, **variables)
+
+__ = lazy_gettext
+
+
+def get_user_locale():
+    return session.get('locale')
+
+
+def get_user_timezone():
+    return session.get('timezone')
+
+
+def setup_babel(app):
+    babel_inst = Babel(app)
+    babel_inst.localeselector(get_user_locale)
+    babel_inst.timezoneselector(get_user_timezone)

File ryshcate/static/style.css

+/* Page layout */
+body {
+    color: black;
+    background-color: white;
+    font-family: 'Liberation Serif', 'Georgia', serif;
+    font-size: 1.1em;
+}
+.container {
+    width: 700px;
+    margin: 20px auto;
+}
+header {
+    margin-bottom: 20px;
+    text-align: center;
+    font-size: 1.4em;
+    color: #430;
+}
+header h1 {
+    margin-bottom: 0px;
+    padding-bottom: 0px;
+}
+footer {
+    margin-top: 20px;
+    text-align: right;
+    font-size: 0.9em;
+    font-color: #444;
+}
+nav {
+    text-align: right;
+}
+nav a {
+    text-decoration: none;
+}
+
+/* Text formatting */
+a {
+    color: #972;
+}
+a:visited {
+    color: #641;
+}
+code, pre, textarea.code {
+    font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono',
+                 'Bitstream Vera Sans Mono', monospace !important;
+}
+pre {
+    line-height: 1.25;
+}
+span.fielddesc {
+    color: #888;
+    font-size: smaller;
+}
+
+textarea.code {
+    width: 100%;
+    font-size: 0.9em;
+}
+
+/* Highlights */
+.highlighttable {
+    font-size: 0.8em;
+    border: 1px solid black;
+    width: 100%;
+}
+.highlighttable td.linenos {
+    padding-right: 12px;
+    background-color: #ddb;
+}
+
+
+/* Flashes */
+section#flashes {
+    margin: 8px auto;
+}
+p.flash {
+    margin: 2px;
+    padding: 4px;
+    text-align: center;
+    -webkit-border-radius: 5px;
+    -moz-border-radius: 5px;
+    border-radius: 5px;
+}
+.messageflash {
+    background-color: #ff9;
+}
+.errorflash {
+    background-color: #fcc;
+}
+.successflash {
+    background-color: #cfc;
+}

File ryshcate/templates/_helpers.html

+{# _helpers.html -- Helper tags for other templates
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}
+
+{% macro link_to(endpoint, text) -%}
+<a href="{{ url_for(endpoint, **kwargs) }}">{{ text|safe }}</a>
+{%- endmacro %}
+
+{% macro form_tag(endpoint, method='post') -%}
+<form action="{{ url_for(endpoint, **kwargs) }}" 
+      method="{{ method }}">
+  {{ caller () }}
+</form>
+{%- endmacro %}
+
+{% macro submit(name) -%}
+<input type="submit" value="{{ name }}" />
+{%- endmacro %}
+
+{% macro desc(field) -%}
+<span class="fielddesc">({{ field.description }})</span>
+{%- endmacro %}

File ryshcate/templates/_pastes.html

+{# _pastes.html -- Paste-related helpers
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}
+{% from "_helpers.html" import link_to %}
+
+{% macro list_pastes(pastes) -%}
+<ul class=pastes>
+{%- for paste in pastes %}
+    <li>
+        <strong>{{ link_to('show_paste', paste.title, id=paste.id) }}</strong>
+        [{{ link_to('by_language', LEXERS[paste.language], language=paste.language) }}]<br>
+        {% trans time=paste.created|datetimeformat %}Created {{ time }}{% endtrans %};
+        {% trans %}Tags{% endtrans %}:
+        {% for tag in paste.tags %}{{ link_to('pastes_tagged', tag, tag=tag) }}{% if not loop.last %}, {% endif %}{% endfor %}
+    </li>
+{%- else %}
+    <li><em>{% trans %}No pastes found in this category{% endtrans %}</em></li>
+{%- endfor %}
+</ul>
+{%- endmacro %}

File ryshcate/templates/all.html

+{# all.html -- A list of all the pastes
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}{% extends "layout.html" %}
+{% from "_helpers.html" import link_to %}
+{% from "_pastes.html" import list_pastes %}
+
+{% set title = _("All Pastes") %}
+
+{% block body %}
+<h2>{% trans %}All Pastes{% endtrans %}</h2>
+
+{{ list_pastes(pastes) }}
+
+{% endblock body %}

File ryshcate/templates/api_info.html

+{# api_info.html -- Describes the API
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}{% extends "layout.html" %}
+
+{% set title = _("API Info") %}
+
+{% block body %}
+<h2>{% trans %}API Info{% endtrans %}</h2>
+
+<p>{% trans %}The API is accessible at the URL:{% endtrans %}</p>
+
+<pre>{{ url }}</pre>
+
+<p>{% trans %}You can access it with any XML-RPC client library.{% endtrans %}</p>
+
+{% endblock %}

File ryshcate/templates/by_language.html

+{# by_language.html -- Shows all pastes in a particular language
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}{% extends "layout.html" %}
+{% from "_helpers.html" import link_to %}
+{% from "_pastes.html" import list_pastes %}
+
+{% set title = _("%(language)s Pastes", language=LEXERS[language]) %}
+
+{% block body %}
+<h2>{{ _("%(language)s Pastes", language=LEXERS[language]) }}</h2>
+
+{{ list_pastes(pastes) }}
+
+{% endblock body %}

File ryshcate/templates/languages.html

+{# languages.html -- displays pastes by language
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}{% extends "layout.html" %}
+{% from "_helpers.html" import link_to %}
+
+{% set title = _("Pastes by Language") %}
+
+{% block body %}
+<h2>{% trans %}Pastes by Language{% endtrans %}</h2>
+
+<ul>
+{%- for lang in languages %}
+    <li>
+        {{ link_to('by_language', LEXERS[lang.key], language=lang.key) }}
+        ({% trans count=lang.value %}{{ count }} paste{% pluralize %}{{ count }} pastes{% endtrans %})
+    </li>
+{%- else %}
+    <li><em>{% trans %}No pastes are available in any language{% endtrans %}</em></li>
+{%- endfor %}
+
+{% endblock body %}

File ryshcate/templates/layout.html

+{# layout.html -- Master page layout for Ryshcate
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}<!doctype html>
+{% set nav_links = [
+    (_('New'), url_for('pastebin.new_paste')),
+    (_('All'), url_for('pastebin.all_pastes')),
+    (_('Languages'), url_for('pastebin.list_languages')),
+    (_('Tags'), url_for('pastebin.list_tags')),
+    (_('Settings'), url_for('system.settings')),
+    (_('API'), url_for('system.api_info'))
+] %}
+<html>
+<head>
+    <title>{{ site_config('title', 'Ryshcate') }}: {{ title }}</title>
+    <meta charset="utf-8" />
+    <!--[if lt IE 9]>
+    <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
+    <![endif]-->
+    <link rel="stylesheet" href="{{ url_for('.static', filename='style.css') }}" />
+    <style type="text/css">
+{{ get_styles() }}
+    </style>
+    {% block head %}{% endblock head %}
+</head>
+<body>
+<div class="container">
+    <header>
+        <h1>{{ site_config('title', 'Ryshcate') }}</h1>
+    </header>
+    
+    <nav>
+        {% if page_nav_links is not defined %}{% set page_nav_links = [] %}{% endif -%}
+        {% for (title, url) in chain(nav_links, page_nav_links, site_config('extra_links', [])) -%}
+        <a href="{{ url }}">{{ title }}</a>{% if not loop.last %} | {% endif %}
+        {% endfor %}
+    </nav>
+    
+    {% with flashes = get_flashed_messages(with_categories=True) -%}
+    {% if flashes -%}
+    <section id="flashes">
+    {%- for category, message in flashes %}
+        <p class="flash {{category}}flash">
+            {{ message }}
+        </p>
+    {%- endfor %}
+    </section>
+    {%- endif %}
+    {%- endwith %}
+    
+    <div class="content">
+        {% block body %}{% endblock body %}
+    </div>
+    
+    <footer>
+        <p>{{ site_config('copyright') }}
+        {%- if site_config('powered_by') %} | {{ _("Powered by Ryshcate") }}{% endif %}</p>
+    </footer>
+</div>
+</body>

File ryshcate/templates/new.html

+{# new.html -- New paste creation form
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}{% extends "layout.html" %}
+{% from "_helpers.html" import form_tag, submit, desc %}
+
+{% set title = _("New Paste") %}
+
+{% block body %}
+
+{%- with motd=site_config('motd', None) %}{% if motd %}
+<p>{{ motd }}</p>
+{%- endif %}{% endwith %}
+
+<h2>{% trans %}New Paste{% endtrans %}</h2>
+
+{% call form_tag('pastebin.new_paste') %}
+<p>
+    {{ form.title.label }}: {{ form.title(maxlength=64) }} {{ desc(form.title) }}
+</p>
+
+<p>
+    {{ form.code.label }}: {{ desc(form.code) }}<br>
+    {{ form.code(rows=20, class='code') }}
+</p>
+
+<p>
+    {{ form.language.label }}: {{ form.language() }} {{ desc(form.language) }}
+</p>
+
+<p>
+    {{ form.tags.label }}: {{ form.tags() }} {{ desc(form.tags) }}
+</p>
+
+<p>
+    {{ form.private() }} {{ form.private.label }} {{ desc(form.private) }}
+</p>
+
+<p>{{ submit(_('Create Paste')) }}</p>
+
+{% endcall %}
+
+{% endblock body %}

File ryshcate/templates/settings.html

+{# settings.html -- User settings page
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}{% extends "layout.html" %}
+{% from "_helpers.html" import form_tag, submit, desc %}
+
+{% set title = _('Settings') %}
+
+{% block body %}
+<h2>{% trans %}Settings{% endtrans %}</h2>
+
+{% call form_tag('system.settings') %}
+
+<p>{{ form.locale.label }}: {{ form.locale() }} {{ desc(form.locale) }}</p>
+
+<p>{{ form.timezone.label }}: {{ form.timezone() }} {{ desc(form.timezone) }}</p>
+
+<p>{{ form.style.label }}: {{ form.style() }} {{ desc(form.style) }}</p>
+
+<p>{{ submit(_('Save settings')) }}</p>
+{% endcall %}
+
+{% call form_tag('system.settings') %}
+<input type=hidden name=clear value=true />
+
+<p>{{ submit(_('Clear settings')) }}</p>
+{% endcall %}
+
+{% endblock %}

File ryshcate/templates/show.html

+{# show.html -- Shows a given paste
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}{% extends "layout.html" %}
+{% from "_helpers.html" import link_to %}
+
+{% set title = paste.title %}
+
+{% block body %}
+<h2>{{ paste.title }} [{{ LEXERS[paste.language] }}]</h2>
+
+<p>
+    {% trans %}Tags{% endtrans %}:
+    {% for tag in paste.tags %}{{ link_to('pastebin.pastes_tagged', tag, tag=tag) }}{% if not loop.last %}, {% endif %}{% endfor %}
+</p>
+
+<p>
+    {% trans time=paste.created|datetimeformat %}Created {{ time }}{% endtrans %}
+</p>
+
+{{ highlighted }}
+
+{% endblock body %}

File ryshcate/templates/tagged.html

+{# tagged.html -- Shows all pastes with a particular tag
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}{% extends "layout.html" %}
+{% from "_helpers.html" import link_to %}
+{% from "_pastes.html" import list_pastes %}
+
+{% set title = _("Pastes tagged %(tag)s", tag=tag) %}
+
+{% block body %}
+<h2>{{ _("Pastes tagged %(tag)s", tag=tag) }}</h2>
+
+{{ list_pastes(pastes) }}
+
+{% endblock body %}

File ryshcate/templates/tags.html

+{# tags.html -- Lists all available tags
+   (C) 2010 Matthew "LeafStorm" Frazier
+   Part of Ryshcate, see LICENSE for details
+#}{% extends "layout.html" %}
+{% from "_helpers.html" import link_to %}
+
+{% set title = _("All Tags") %}
+
+{% block body %}
+<h2>{% trans %}All Tags{% endtrans %}</h2>
+
+<ul>
+{%- for tag in tags %}
+    <li>
+        {{ link_to('pastes_tagged', tag.key, tag=tag.key) }}
+        ({% trans count=tag.value %}{{ count }} paste{% pluralize %}{{ count }} pastes{% endtrans %})
+    </li>
+{%- else %}
+    <li><em>{% trans %}No pastes have been tagged{% endtrans %}</em></li>
+{%- endfor %}
+
+{% endblock body %}

File ryshcate/translations/messages.pot

+# Translations template for PROJECT.
+# Copyright (C) 2010 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2010-06-17 22:16-0400\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.5\n"
+
+#: ryshcate/forms.py:62
+msgid "This field may not contain profanities"
+msgstr ""
+
+#: ryshcate/utils.py:34
+#, python-format
+msgid "Error in the %(field)s field - %(error)s"
+msgstr ""
+
+#: ryshcate/templates/_pastes.html:13 ryshcate/templates/show.html:18
+#, python-format
+msgid "Created %(time)s"
+msgstr ""
+
+#: ryshcate/templates/_pastes.html:14 ryshcate/templates/layout.html:9
+#: ryshcate/templates/show.html:13
+msgid "Tags"
+msgstr ""
+
+#: ryshcate/templates/_pastes.html:18
+msgid "No pastes found in this category"
+msgstr ""
+
+#: ryshcate/templates/all.html:8 ryshcate/templates/all.html:11
+msgid "All Pastes"
+msgstr ""
+
+#: ryshcate/templates/api_info.html:6 ryshcate/templates/api_info.html:9
+msgid "API Info"
+msgstr ""
+
+#: ryshcate/templates/api_info.html:11
+msgid "The API is accessible at the URL:"
+msgstr ""
+
+#: ryshcate/templates/api_info.html:15
+msgid "You can access it with any XML-RPC client library."
+msgstr ""
+
+#: ryshcate/templates/by_language.html:8 ryshcate/templates/by_language.html:11
+#, python-format
+msgid "%(language)s Pastes"
+msgstr ""
+
+#: ryshcate/templates/languages.html:7 ryshcate/templates/languages.html:10
+msgid "Pastes by Language"
+msgstr ""
+
+#: ryshcate/templates/languages.html:16 ryshcate/templates/tags.html:16
+#, python-format
+msgid "%(count)s paste"
+msgid_plural "%(count)s pastes"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ryshcate/templates/languages.html:19
+msgid "No pastes are available in any language"
+msgstr ""
+
+#: ryshcate/templates/layout.html:6
+msgid "New"
+msgstr ""
+
+#: ryshcate/templates/layout.html:7
+msgid "All"
+msgstr ""
+
+#: ryshcate/templates/layout.html:8
+msgid "Languages"
+msgstr ""
+
+#: ryshcate/templates/layout.html:10 ryshcate/templates/settings.html:7
+#: ryshcate/templates/settings.html:10
+msgid "Settings"
+msgstr ""
+
+#: ryshcate/templates/layout.html:11
+msgid "API"
+msgstr ""
+
+#: ryshcate/templates/layout.html:57
+msgid "Powered by Ryshcate"
+msgstr ""
+
+#: ryshcate/templates/new.html:7 ryshcate/templates/new.html:15
+msgid "New Paste"
+msgstr ""
+
+#: ryshcate/templates/new.html:39
+msgid "Create Paste"
+msgstr ""
+
+#: ryshcate/templates/settings.html:20
+msgid "Save settings"
+msgstr ""
+
+#: ryshcate/templates/settings.html:26
+msgid "Clear settings"
+msgstr ""
+
+#: ryshcate/templates/tagged.html:8 ryshcate/templates/tagged.html:11
+#, python-format
+msgid "Pastes tagged %(tag)s"
+msgstr ""
+
+#: ryshcate/templates/tags.html:7 ryshcate/templates/tags.html:10
+msgid "All Tags"
+msgstr ""
+
+#: ryshcate/templates/tags.html:19
+msgid "No pastes have been tagged"
+msgstr ""
+
+#: ryshcate/views/pastebin.py:33
+msgid "Paste successfully created."
+msgstr ""
+
+#: ryshcate/views/pastebin.py:35
+msgid "Since this paste is private, write down the URL so you can find it again!"
+msgstr ""
+
+#: ryshcate/views/system.py:27
+msgid "Settings cleared"
+msgstr ""
+
+#: ryshcate/views/system.py:44
+msgid "Settings saved"
+msgstr ""
+

File ryshcate/utils.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate.utils
+==============
+This contains some utilities used by the rest of Ryshcate.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+import re
+from flask import Markup, flash, current_app, session
+from itertools import chain
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_lexer_by_name, get_all_lexers, ClassNotFound
+from pygments.styles import get_all_styles
+from ryshcate.locale import _
+
+### Form helpers
+
+BACKWARDS_PROFANITIES = set('nmad hctib tihs kcuf tnuc koog reggin hssa'
+                            .split())
+
+reverse = lambda s: ''.join(reversed(s))
+
+def contains_profanities(text):
+    return any(reverse(p) in text for p in BACKWARDS_PROFANITIES)
+
+
+def flash_errors(form):
+    for field, errors in form.errors.items():
+        for error in errors:
+            flash(_(u"Error in the %(field)s field - %(error)s",
+                field=getattr(form, field).label.text, error=error
+            ), 'error')
+
+
+### Highlighting helpers
+
+def highlight_code(code, lexer, formatter=HtmlFormatter, **formatter_opts):
+    fmter = formatter(style=user_pygments_style(), **formatter_opts)
+    if isinstance(lexer, basestring):
+        lexer = get_lexer_by_name(lexer)
+    return Markup(highlight(code, lexer, fmter))
+
+
+def get_style_css(style, prefix='.highlight', **formatter_opts):
+    fmter = HtmlFormatter(style=style, **formatter_opts)
+    return fmter.get_style_defs(prefix)
+
+
+LEXER_LIST = list(sorted((
+    ((lexer[1][0] if 'bf' not in lexer[1] else 'bf'),
+    lexer[0].replace('uck', '---'))
+    for lexer in get_all_lexers()
+), key=lambda l: l[0].lower()))
+
+LEXER_DICT = dict(LEXER_LIST)
+
+
+### Template helpers
+
+def site_config(name, default=None):
+    if not isinstance(name, basestring):
+        raise TypeError("Configuration value name must be a string")
+    key = 'SITE_' + name.upper()
+    return current_app.config.get(key, default)
+
+
+def user_pygments_style():
+    default = current_app.config.get('DEFAULT_PYGMENTS_STYLE', 'default')
+    return session.get('style', default)
+
+
+def get_pygments_css():
+    return get_style_css(user_pygments_style(), linenos='table')
+
+
+TEMPLATE_GLOBALS = dict(site_config=site_config,
+                        pygments_style=user_pygments_style,
+                        get_styles=get_pygments_css,
+                        chain=chain,
+                        LEXERS=LEXER_DICT)

File ryshcate/views/__init__.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate.views
+==============
+This is just a placeholder for the views package.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""

File ryshcate/views/pastebin.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate.views.pastebin
+=======================
+This module contains the views that work *directly* with creating and viewing
+pastes.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+from flask import (Module, request, session, render_template, flash, redirect,
+                   url_for)
+from ryshcate.database import Paste, create_id
+from ryshcate.forms import CreatePasteForm
+from ryshcate.locale import _
+from ryshcate.utils import flash_errors, highlight_code
+
+pastebin = Module(__name__)
+
+@pastebin.route('/', methods=['GET', 'POST'])
+def new_paste():
+    form = CreatePasteForm(request.form)
+    if request.method == 'POST' and form.validate():
+        paste = Paste(
+            id=create_id(),
+            title=form.title.data,
+            code=form.code.data,
+            language=form.language.data,
+            tags=form.tags.data,
+            private=form.private.data
+        )
+        paste.store()
+        flash(_("Paste successfully created."), 'success')
+        if paste.private:
+            flash(_("Since this paste is private, write down the URL so you "
+                    "can find it again!"), 'message')
+        return redirect(url_for('pastebin.show_paste', id=paste.id))
+    flash_errors(form)
+    return render_template('new.html', form=form)
+
+
+@pastebin.route('/paste/<id>')
+def show_paste(id):
+    paste = Paste.load(id)
+    if paste is None:
+        abort(404)
+    highlighted = highlight_code(paste.code, paste.language, linenos='table')
+    return render_template('show.html', paste=paste, highlighted=highlighted)
+
+
+@pastebin.route('/all')
+def all_pastes():
+    pastes = Paste.public()
+    return render_template('all.html', pastes=pastes)
+
+
+@pastebin.route('/language/')
+def list_languages():
+    languages = Paste.language_counts()
+    return render_template('languages.html', languages=languages)
+
+
+@pastebin.route('/language/<language>')
+def by_language(language):
+    pastes = Paste.by_language[language]
+    return render_template('by_language.html', language=language,
+                           pastes=pastes)
+
+
+@pastebin.route('/tag/')
+def list_tags():
+    tags = Paste.tag_counts()
+    return render_template('tags.html', tags=tags)
+
+
+@pastebin.route('/tag/<tag>')
+def pastes_tagged(tag):
+    pastes = Paste.tagged[tag]
+    return render_template('tagged.html', tag=tag, pastes=pastes)

File ryshcate/views/system.py

+# -*- coding: utf-8 -*-
+"""
+ryshcate.views.system
+=====================
+This module contains views that perform system support functions like changing
+the user's settings.
+
+:copyright: 2010 Matthew "LeafStorm" Frazier
+:license:   MIT/X11, see LICENSE for details
+"""
+from flask import (Module, Response, request, session, current_app, flash,
+                   redirect, url_for, render_template)
+from ryshcate.api import handler
+from ryshcate.forms import SettingsForm
+from ryshcate.locale import _
+from ryshcate.utils import get_style_css
+
+system = Module(__name__)
+
+handler.connect(system, '/api')
+
+
+@system.route('/settings', methods=['GET', 'POST'])
+def settings():
+    if request.method == 'POST' and request.form.get('clear'):
+        session.clear()
+        flash(_(u"Settings cleared"), 'success')
+        return redirect(url_for('settings'))
+    
+    cfg = current_app.config
+    locale = session.get('locale', cfg.get('BABEL_DEFAULT_LANGUAGE', 'en'))
+    timezone = session.get('timezone', cfg.get('BABEL_DEFAULT_TIMEZONE',
+                           'UTC'))
+    style = session.get('style', cfg.get('DEFAULT_PYGMENTS_STYLE', 'default'))
+    del cfg
+    
+    form = SettingsForm(request.form, locale=locale, style=style,
+                        timezone=timezone)
+    if request.method == 'POST' and form.validate():
+        session['locale'] = form.locale.data
+        session['timezone'] = form.timezone.data
+        session['style'] = form.style.data
+        session.permanent = True
+        flash(_(u"Settings saved"), 'success')
+    return render_template('settings.html', form=form)
+
+
+@system.route('/api', methods=['GET'])
+def api_info():
+    api_url = url_for('system.api', _external=True)
+    return render_template('api_info.html', url=api_url)