Commits

Gregory Petukhov committed 1bfda6c

Refactoring. Add templates

Comments (0)

Files changed (54)

 
 # Sphinx
 docs/_build/
+
+# Demo
+demo/.env
+demo/var/db
+demo/static/static
-PyBB - is Django application which implements forum function, i.e., 
-it allows users of your site to create discussion threads also known as topics.
+============================
+PyBB - Python Bulletin Board
+============================
 
-PyBB is quite unstable. If some feture exists in rev.A then it could be
-dropped in rev.B.
+Pybb is a django forum application. For now PyBB is unstable.
+If some feture exists in rev.A then it could be dropped in rev.B.
 
-Current implementation does not have templates. You can find working set
-of templates here: https://bitbucket.org/lorien/pep8/src/dddba1eb4931/templates/pybb/
+
+Contacts
+========
 
 Send all questions to lorien@lorien.name
 
-NB: Use version from bitbucket, tarball on pypi is not up to date.
+
+Installation/Download
+=====================
+Most recent version is on bitbucket. Package on pypi.python.org
+is updated from time to time.
+
+
+How to install demo
+===================
+
+There is a demo project in the demo directory.
+
+To install it do:
+* cd demo
+* ./buildenv.sh # that creates virtualenv and install dependencies
+* ./manage.py syncdb --migrate
+* ./generate.py # that creates random data
+* ./manage.py runserver
+
+Note that in this demo django-account is used for login/signup. But
+in your project you can use any login/signup application.
+
+
+Templates
+=========
+
+PyBB is shipped with default templates which you can use as basis for
+development your own templates. PyBB is not shipped with any stylesheets,
+you can find some CSS in demo project.

demo/__init__.py

Empty file added.
+#!/bin/sh
+ENV=.env
+
+echo Creating virtual environment
+virtualenv --no-site-packages $ENV
+
+echo Installing PIP inside virtual environment
+$ENV/bin/easy_install pip
+
+echo Installing dependencies into virtual environment
+$ENV/bin/pip install -r ./requirements.txt
+
+echo Creating var directory
+mkdir -p var
+#!.env/bin/python
+# -*- coding: utf-8 -*-
+"""
+Generate sample data for pybb demo
+"""
+
+import sys
+from datetime import date, datetime, time, timedelta
+from random import randint, choice
+from common.system import setup_django
+from common.sample import random_string, random_text
+setup_django(__file__)
+
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.db import connection
+
+from pybb.models import Category, Topic, Post, Forum, Topic, Post
+
+def track_queries(action, cache=[0]):
+    from django.db import connection
+
+    if action == 'start':
+        cache[0] = len(connection.queries)
+
+    if action == 'stop':
+        print '--'
+        for item in connection.queries[cache[0]:]:
+            print item['time'], item['sql']
+        print 'Query count: %d' % (len(connection.queries) - cache[0])
+
+
+def main():
+    Site.objects.filter(pk=1).update(domain='localhost:8000', name='localhost:8000')
+    return 
+
+    # Delete old data
+    User.objects.all().delete()
+    Category.objects.all().delete()
+
+    print 'Creating superuser'
+    admin = User.objects.create_user('foob', 'foob@foob.com', 'foob') 
+    admin.is_superuser = True
+    admin.is_staff = True
+    admin.save()
+
+    print 'Creating users'
+    users = []
+    for x in xrange(2):
+        users.append(User.objects.create_user('noob%d' % x, 'noob%d@noob.com' %x, 'noob'))
+
+    for x in xrange(2):
+        cat = Category.objects.create(name=random_text(3))
+        print 'Category %s' % cat
+        for x in xrange(2):
+            forum = Forum.objects.create(category=cat, name=random_text(3))
+            print 'Forum %s' % forum
+            for x in xrange(2):
+                topic = Topic.objects.create(forum=forum, name=random_text(3),
+                                             user=choice(users))
+                print 'Topic %s' % topic
+                for x in xrange(20):
+                    post = Post.objects.create(
+                        topic=topic, user=topic.user if not x else choice(users),
+                        markup='markdown', body=random_text(30))
+
+
+if __name__ == '__main__':
+    main()
+#!.env/bin/python
+from django.core.management import execute_manager
+import imp
+try:
+    imp.find_module('settings') # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__)
+    sys.exit(1)
+
+import settings
+
+if __name__ == "__main__":
+    execute_manager(settings)

demo/requirements.txt

+django
+django-common
+markdown
+pytils
+south
+#pybb
+-e hg+http://bitbucket.org/lorien/pybb#egg=pybb
+django-account
+django-urlauth
+import os.path
+
+ROOT = os.path.dirname(os.path.realpath(__file__))
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    # ('Your Name', 'your_email@example.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': os.path.join(ROOT, 'var', 'db'),                      # Or path to database file if using sqlite3.
+        'USER': '',                      # Not used with sqlite3.
+        'PASSWORD': '',                  # Not used with sqlite3.
+        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
+        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
+    }
+}
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/home/media/media.lawrence.com/media/"
+MEDIA_ROOT = os.path.join(ROOT, 'static')
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
+MEDIA_URL = '/static/'
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/home/media/media.lawrence.com/static/"
+STATIC_ROOT = os.path.join(ROOT, 'static', 'static')
+
+# URL prefix for static files.
+# Example: "http://media.lawrence.com/static/"
+STATIC_URL = '/static/static/'
+
+# URL prefix for admin static files -- CSS, JavaScript and images.
+# Make sure to use a trailing slash.
+# Examples: "http://foo.com/static/admin/", "/static/admin/".
+ADMIN_MEDIA_PREFIX = '/static/static/admin/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+    # Put strings here, like "/home/html/static" or "C:/www/django/static".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+    'django.contrib.staticfiles.finders.FileSystemFinder',
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+#    'django.contrib.staticfiles.finders.DefaultStorageFinder',
+)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'd_og7fr@ytm8g^h_wh+%zu(wz(9=*is4oz))mggt=^%gx1537y'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.Loader',
+    'django.template.loaders.app_directories.Loader',
+#     'django.template.loaders.eggs.Loader',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'urlauth.middleware.AuthKeyMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'pybb.middleware.PybbMiddleware',
+)
+
+ROOT_URLCONF = 'urls'
+
+TEMPLATE_DIRS = (
+    os.path.join(ROOT, 'templates'),
+)
+
+INSTALLED_APPS = (
+    # django
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'django.contrib.admin',
+    # 3rd party apps
+    'south',
+    'pybb',
+    'account',
+    'urlauth',
+)
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'class': 'django.utils.log.AdminEmailHandler'
+        }
+    },
+    'loggers': {
+        'django.request': {
+            'handlers': ['mail_admins'],
+            'level': 'ERROR',
+            'propagate': True,
+        },
+    }
+}
+
+# PyBB configuration
+from pybb.settings import *
+
+# django-account configuration
+from account.settings import *
+ACCOUNT_ACTIVATION_REQUIRED = False
+LOGIN_REDIRECT_URL = '/'
+
+# django-urlauth configuration
+from urlauth.settings import *

demo/static/css/blogs.css

+.tag-cloud {
+}
+
+    .tag-cloud .size-1 {
+        font-size: 0.9em;
+    }
+
+    .tag-cloud .size-2 {
+        font-size: 1.2em;
+    }
+
+    .tag-cloud .size-3 {
+        font-size: 1.4em;
+    }
+
+    .tag-cloud .size-4 {
+        font-size: 1.6em;
+    }
+
+.feedzilla-counters .counter {
+    float: left;
+    padding-right: 1em;
+}

demo/static/css/forum.css

+.category-list h2, .forum-details h2, .topic-details h2, .post-form-page h2 {
+    background: #d02c2c url('../img/header-bg.jpg') no-repeat;
+    -webkit-border-radius: 0.5em;
+    -moz-border-radius: 0.5em;
+    padding: 0.4em 0.7em;
+    color: white;
+}
+
+.category-list h2 a, .forum-details h2 a, .topic-details h2 a, .post-form-page h2 {
+    color: white;
+    text-decoration: none;
+}
+
+.category-details {
+    margin-bottom: 1em;
+}
+
+.forum-list, .topic-list {
+    margin-bottom: 1em;
+    margin-left: 0.5em;
+    width: 100%;
+}
+
+    td.post-count, th.post-count {
+        width: 5em;
+        text-align: center;
+    }
+
+    td.recent-info, th-recent-info {
+        width: 10em;
+    }
+
+    td.feed, th.feed {
+        width: 2em;
+        text-align: center;
+    }
+
+.topic-control-bar {
+    margin: 1em 0;
+}
+
+ul.post-list {
+    margin-left: 0;
+}
+
+.post-details {
+    -webkit-border-radius: 0.7em;
+    -moz-border-radius: 0.7em;
+    border: 1px solid #999;
+    margin-bottom: 1.5em;
+}
+
+    .post-details .info {
+        float: left;
+        width: 30%;
+    }
+
+    .post-details .content {
+        float: left;
+        width: 70%;
+    }
+
+    .post-details .inner {
+        margin: 0.7em;
+    }
+
+    .post-details .signature {
+        margin-top: 1em;
+        font-size: 0.8em;
+        color: #999;
+    }
+
+    .post-details .signature hr {
+        border: none;
+        border-top: 1px solid #bbb;
+        width: 30%;
+        margin: 0;
+        margin-bottom: 0.2em;
+    }
+
+    .post-details .post-related {
+        text-align: right;
+    }
+
+    .post-details .update-date {
+        font-size: 0.9em;
+        color: #999;
+        margin-top: 0.5em;
+    }
+
+.preview-box {
+    border: 1px dashed #999;
+    padding: 0.5em;
+    margin-bottom: 1em;
+}
+
+.post-form input, .post-form textarea {
+    width: 90%;
+}

demo/static/css/reset.css

+/* http://meyerweb.com/eric/tools/css/reset/ 
+   v2.0 | 20110126
+   License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed, 
+figure, figcaption, footer, header, hgroup, 
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	font-size: 100%;
+	font: inherit;
+	vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, 
+footer, header, hgroup, menu, nav, section {
+	display: block;
+}
+body {
+	line-height: 1;
+}
+ol, ul {
+	list-style: none;
+}
+blockquote, q {
+	quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+	content: '';
+	content: none;
+}
+table {
+	border-collapse: collapse;
+	border-spacing: 0;
+}

demo/static/css/style.css

+body { 
+  font: 0.8em/1.5 "Arial", sans-serif; 
+  color: #303030; 
+  text-align: center; 
+}
+
+a {
+    color:#b41818;
+}
+
+a:link {
+    color:#b41818;
+}
+
+a:hover {
+    color:#b41818;
+    text-decoration: none;
+}
+
+h1, h2, h3, h4, h5, h6 {
+    font-weight: normal;
+    margin-bottom: 0.7em;
+}
+
+h1 {
+    font-size:170%;
+}
+
+h2 {
+    font-size:150%;
+}
+
+h3 {
+    font-size:130%;
+}
+
+h4 {
+    font-size: 110%;
+    font-weight: bold;
+}
+
+.sidebar-column h3 {
+    border-bottom: 1px solid #ddd;
+}
+
+.width-section { 
+  width:960px; 
+  margin: 0 auto 1em; 
+  text-align: left; 
+}
+
+#head-section {
+    background: #303030;
+    padding-top: 1em;
+    background: white url('../img/site-bg.jpg');
+}
+
+    #head-section .inside {
+        position: relative;
+    }
+
+    #head-banner {
+        float: right;
+        width: 50%;
+    }
+  
+    #logo {
+        font-family: 'Trebuchet MS', 'Geneva CE', lucida, sans-serif;
+        float: left;
+        font-size: 2.3em;
+        width: 50%;
+    }
+        #logo a{
+            text-decoration: none;
+            color: #fff;
+            border-left: 0.3em solid #b41818;  
+            padding: 0 0 0.25em 0.5em;
+        }
+
+        #logo a span {
+            color: #676767;
+            font-weight: bold;
+        }
+          
+        #logo a:hover span {
+            color: #fff;
+        }
+      
+
+.clear {
+    clear: both;
+}
+
+.menu-bar {
+    margin-top: 0.2em;
+    padding-bottom: 0.5em;
+    font-size: 1.2em;
+    color: #cacaca;
+}
+    .menu-bar li a {
+        color: #cacaca;
+        text-decoration: none;
+    }
+
+    .menu-bar a:hover {
+        color: #fff;
+    }
+
+    .menu-bar li {
+        display: inline;
+    }
+
+
+#main-menu {
+    float: left;
+    width: 70%;
+}
+
+    #main-menu li {
+        padding-right: 0.8em;
+    }
+
+#profile-menu {
+    float: left;
+    width: 30%;
+    text-align: right;
+}
+
+    #profile-menu li {
+        padding-left: 0.8em;
+    }
+
+
+#crumbs {
+    margin-bottom: 1em;
+}
+
+    #crumbs li {
+        display: inline;
+    }
+
+#site-message-list {
+    margin-bottom: 1em;
+}
+
+    .site-message-details {
+        margin: 0.5em 0;
+        padding: 1em;
+        background-color: #cfc;
+        -webkit-border-radius: 0.5em;
+        -moz-border-radius: 0.5em;
+    }
+
+#center-column {
+    float: left;
+    width: 600px;
+}
+
+#right-column {
+    float: left;
+    width: 320px;
+    padding-left: 40px;
+    background-color: white;
+}
+
+h1.title, h2.title {
+    background: #d02c2c url('../img/header-bg.jpg') no-repeat;
+    -webkit-border-radius: 0.5em;
+    -moz-border-radius: 0.5em;
+    color: white;
+    padding: 0.4em 0.7em;
+}
+
+    h1.title a, h2.title a {
+        color: white;
+    }
+
+
+.styled-content {
+}
+
+    .styled-content p {
+        margin-bottom: 1em;
+    }
+
+    .styled-content pre {
+        background-color: #f7e98e;
+        padding: 0.5em;
+        font-size:110%;
+    }
+
+    .styled-content pre {
+        background-color: #f7e98e;
+        padding: 0.5em;
+        font-size:110%;
+    }
+
+    .styled-content code {
+        font-family: monospace;
+        font-size: 120%;
+        padding: 0.1em 0.5em;
+        background-color: #f7e98e;
+    }
+    .styled-content dt {
+        font-weight: bold;
+    }
+
+    .styled-content dd {
+        margin-left: 1em;
+    }
+
+    .styled-content ul {
+        margin-left: 1em;
+        margin-bottom: 1em;
+    }
+
+    .styled-content th {
+        font-weight: bold;
+    }
+    
+    .styled-content td, .styled-content th {
+        padding: 0.2em;
+        padding-right: 1em;
+    }
+
+    .styled-content strong {
+        font-weight: bold;
+    }
+    
+    .styled-content blockquote {
+        border: 1px dashed #999;
+        padding: 0.5em;
+        background-color: #f5f5f5;
+    }
+
+
+.infobox {
+    margin-bottom: 1em;
+}
+
+    .infobox h3 {
+        border-left: 0.3em solid #d43838;  
+        padding: 0.5em;
+        padding-left: 0.7em;
+        background-color: #ffefd5;
+        font-weight: bold;
+        color: #333;
+        position: relative;
+        left: -4px;
+    }
+
+#footer-section {
+    background: #303030;
+    color: #b1b1b1;
+    position: relative;
+}
+
+    #footer-section a{
+        color: #b1b1b1;
+    }
+
+    #footer-section .inside {
+        padding: 1em;
+    }
+
+    #footer-section .width-section {
+        margin-bottom: 0;
+    }
+
+    #footer-section .credits {
+        float: right;
+    }
+
+    #footer-section .copyrights {
+        float: left;
+    }
+
+    #footer-section .counters {
+        position: absolute;
+        left: 45%;
+        text-align: center;
+        top: 5px;
+    }
+
+.pagination {
+    font-size: 1.5em;
+    margin-bottom: 0.5em;
+}
+
+    .pagination a.active {
+        color: black;
+        text-decoration: none;
+    }
+
+.fixed {
+    position: fixed;
+}
+
+.global-nav {
+    position: fixed;
+    bottom: 0;
+    left: 0em;
+    font-weight: bold;
+    width: 100%;
+}
+
+    .global-nav .inside {
+        width: 960px; 
+        margin: 0 auto;
+        text-align: left;
+    }
+
+    .global-nav a {
+        font-size: 1.7em;
+        background-color: #666;
+        color: white;
+        padding: 0.2em 0.5em;
+        text-decoration: none;
+    }
+
+form {}
+
+    form p {
+        margin-bottom: 1em;
+    }
+
+    input, label, textarea, select {
+        display: block;
+    }
+
+    input, textarea {
+        /*width: 90%;*/
+        border: 1px solid #333;
+        padding-left: 0.2em;
+    }
+
+    input.inline {
+        display: inline;
+    }
+
+    label {
+        font-weight: bold;
+    }
+    
+    body input[type="submit"], body input[type="checkbox"],
+    body input[type="radio"], body input[type="button"] {
+        width: inherit;
+    }
+
+    input[type="file"] {
+        border: none;
+    }
+
+ul.errorlist {
+    color: red;
+    margin-left: 0;
+}
+
+.auth-provider-list {
+    margin-bottom: 1em;
+}
+
+    .auth-provider-list a {
+        font-size: 1.4em;
+        padding-right: 1em;
+    }
+
+.openid-input {
+    background: no-repeat 0 2px url(/static/img/openid_logo.png);
+    padding-left: 17px;
+}
+
+.user-list {}
+
+    .user-short-details {
+        float: left;
+        width: 94px;
+        text-align: center;
+    }
+
+    .user-short-details a {
+        color: #303030;
+    }
+
+ul.object-list {
+    margin-left: 0;
+}
+
+.object-details {
+    margin-bottom: 2em;
+    margin-left: 0.5em;
+}
+
+    .object-details .meta {
+        margin-bottom: 1em;
+        border: 1px solid #ccc;
+        background-color: #fcfcfc;
+        padding: 0.7em;
+        -webkit-border-radius: 0.5em;
+        -moz-border-radius: 0.5em;
+        margin-left: -0.4em;
+    }
+
+    .object-details .meta-display {
+        font-size: 1.4em;
+        margin-bottom: 0.5em;
+    }
+
+    .object-details h2 {
+        margin-left: -0.4em;
+    }
+
+    .object-details .share {
+        background: no-repeat url(/static/img/thumb_up.png);
+        padding-left: 20px;
+    }
+
+    .object-details .meta .row2 {
+        margin-top: 0.5em;
+    }
+
+    .object-details .meta .row3 {
+        margin-top: 0.5em;
+    }
+
+    .object-details .meta .author {
+        background: no-repeat url(/static/img/user.png);
+        padding-left: 20px;
+        padding-right: 10px;
+    }
+
+    .object-details .meta .date {
+        background: no-repeat url(/static/img/time.png);
+        padding-left: 20px;
+    }
+
+    .object-details .meta .tags {
+        background: no-repeat url(/static/img/tag_blue.png);
+        padding-left: 20px;
+    }
+
+.addthis_toolbox {
+}
+
+ul.list li {
+    margin-left: 0.3em;
+    list-style-type: circle;
+    list-style-position: inside;
+}
+
+.sidebar-share {
+    margin-bottom: 1em;
+}
+
+    .sidebar-share .label {
+        float: left;
+        margin-right: 0.5em;
+    }
+
+
+.section-list {
+}
+
+    .section-item {
+        float: left;
+        width: 48%;
+        margin-bottom: 1em;
+    }
+
+    .section-list .first {
+        margin-right: 2%;
+        clear: left;
+    }
+
+    .section-list .last {
+        margin-left: 2%;
+    }
+
+    .section-item h3 {
+        border-left: 0.3em solid #d43838;  
+        padding: 0.5em;
+        padding-left: 0.7em;
+        background-color: #ffefd5;
+        font-weight: bold;
+        color: #333;
+    }
+
+    .section-item h3 a {
+        color: #333;
+        text-decoration: none;
+    }
+
+    .section-item .date {
+        font-size: 0.8em;
+        color: #666;
+    }
+
+    .section-item .event {
+        padding: 0 0.5em;
+        color: #666;
+        font-weight: bold;
+    }
+
+.user-details {}
+
+    .user-details .main {
+        float: left;
+        width: 100px;
+    }
+
+    .user-details .details {
+        margin-left: 100px;
+    }
+
+.link-item {
+    font-size: 1.1em;
+}
+
+.link-item .hostname {
+    color: #999;
+    padding-left: 0.5em;
+}
+
+.video-list {
+}
+
+    .video-item td {
+        padding-left: 0.4em;
+    }
+
+    .video-item .date {
+    }
+
+    .video-item .author {
+    }
+
+    .video-list tr.dark {
+        background-color: #f5f5f5;
+    }
+
+.video-details .details {
+    margin-bottom: 1em;
+}
+
+.mini-pagination {
+    font-size: 0.9em;
+    font-style: italic;
+    margin-left: 1em;
+}
+
+.document-toc li ul {
+    margin-bottom: 0;
+}
+
+.copyrights-details {
+    font-size: 0.8em;
+    padding: 1em;
+    background-color: #eee;
+    color: #666;
+}
+
+.copyrights-details a {
+    color: #666;
+}
+
+#exchange-section {
+    opacity: 0.7;
+}
+
+    #exchange-section .inside {
+        margin: 1em;
+    }
+
+.tip-body {
+    display: none;
+}
+
+.edit-icon {
+    padding-left: 0.5em;
+    position: relative;
+    top: 0.1em;
+}
+
+.infobox h3 .feed-icon {
+    float: right;
+}
+
+.information {
+    margin-top: 1em;
+    font-size: 1.2em;
+}
+
+.book-item {
+    margin-bottom: 2em;
+}
+
+    .book-item .promote {
+        float: left;
+        width: 150px;
+    }
+
+    .book-item .cover {
+        text-align: center;
+    }
+
+    .book-item .cover img {
+        border: 1px solid #999;
+        padding: 3px;
+    }
+
+    .book-item .details {
+        float: left;
+        width: 450px;
+    }
+
+    .book-item dt {
+        clear: left;
+        float: left;
+        width: 7em;
+    }
+    
+    .book-item dd {
+        display: block;
+    }
+
+.book-details {
+}
+    
+    .book-details .promote {
+        width: 250px;
+    }
+
+    .book-details .cover {
+        text-align: left;
+    }
+    
+    .book-details .details {
+        width: 350px;
+    }
+
+    .book-details .description {
+        margin-top: 1em;
+    }
+
+.count-buttons a {
+    float: left;
+}
+
+.facebook-button {
+    position: relative;
+    left: -10px;
+    margin-right: -10px;
+}
+
+.simple-buttons {
+    position: relative;
+    top: 1px;
+}
+
+.addthis_toolbox iframe {
+    height: 25px !important;
+    margin-bottom: -5px;
+}
+
+.event-list {
+}
+    
+    .event-item {
+        margin-bottom: 2em;
+    }
+
+    .event-item .promo {
+        float: left;
+        width: 250px;
+    }
+
+    .event-item .details {
+        margin-left: 250px;
+    }
+
+    .event-item dt {
+        clear: right;
+        float: left;
+        width: 7em;
+    }
+
+    .event-item dd {
+        display: block;
+    }
+
+.event-post-list {
+}
+
+    .event-post-item {
+        margin-bottom: 1em;
+    }
+
+    .event-post-item h1 {
+        margin-bottom: 0.1em;
+    }
+
+    .event-post-item h3 {
+        margin-bottom: 0.1em;
+    }
+
+    .event-post-item .meta {
+        margin: 0.5em 0;
+    }

demo/static/img/feed-icon-small.png

Added
New image

demo/static/img/site-bg.jpg

Added
New image

demo/templates/base.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <!-- SEO -->
+        <title>{% block title %}PyBB Demo{% endblock %}</title>
+        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+        <meta name="description" content="{% block meta_description %}Demonstration of PyBB - django forum application{% endblock %}" />
+        <meta name="keywords" content="{% block meta_keywords %}python, django, forum, pybb, demo{% endblock %}" />
+        <meta name="robots" content="all,follow" />
+        <link rel="shortcut icon" href="{{ MEDIA_URL }}favicon.ico" />
+        
+        <!-- Styles -->
+        <link href="{{ MEDIA_URL }}css/reset.css" type="text/css" rel="stylesheet" media="screen,projection" />
+        <link href="{{ MEDIA_URL }}css/style.css" type="text/css" rel="stylesheet" media="screen,projection" />
+        {% block extra_css %}{% endblock %}
+
+        <!-- Scripts -->
+        <script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.4.3.min.js"></script>
+        {% block extra_js %}{% endblock %}
+
+        {% block head_extra %}{% endblock %}
+    </head>
+<body>
+</body>
+    <a name="top"></a>
+    <div id="head-section">
+        <div class="width-section">
+            <div class="inside">
+                <div id="logo">
+                    <a href="/">PyBB<span>.Demo</span></a>
+                </div>
+                <div id="head-banner">
+                </div>
+                <div class="clear"></div>
+                <div class="menu-bar">
+                    <ul id="main-menu">
+                        <li><a href="{% url pybb_index %}">Форум</a></li>
+                        <li><a href="{% url pybb_user_list %}">Пользователи</a></li>
+                    </ul>
+                    <ul id="profile-menu">
+                        {% if user.is_authenticated %}
+                        <li><a href="{% url pybb_profile_edit %}">Ваш профиль</a> ({{ user }})</li>
+                        <li><a href="{% url auth_logout %}">Выйти</a></li>
+                        {% else %}
+                        <li><a href="{% url auth_login %}">Войти</a></li> /
+                        <li><a href="{% url registration_register %}">Регистрация</a></li>
+                        {% endif %}
+                    </ul>
+                    <div class="clear"></div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div id="info-section">
+        <div class="width-section">
+            <ul id="crumbs">
+                {% block crumbs %}{% endblock %}
+            </ul>
+            {% if messages %}
+            <div id="site-message-list">
+                {% for message in messages %}
+                <div class="site-message-details">
+                    {{ message }}
+                </div>
+                {% endfor %}
+            </div>
+            {% endif %}
+        </div>
+    </div>
+    <div id="content-section">
+        <div class="width-section">
+            <div id="center-column" class="styled-content">
+                {% block content %}{% endblock %}
+            </div>
+            <div id="right-column">
+                <div class="sidebar-share">
+                </div>
+                {% block sidebar %}{% endblock %}
+            </div>
+            <div class="clear"></div>
+        </div>
+    </div>
+    <div id="footer-section" class="styled-content">
+        <div class="width-section">
+            <div class="inside">
+                <div class="copyrights">
+                    2011 &copy; <a href="#">pep8.ru</a>
+                </div>
+                <table class="credits">
+                    <tr>
+                        <td>
+                            <div>Вебмастер: <a href="http://lorien.name">Grigoriy Petukhov</a></div>
+                            <br/>
+                        </td>
+                        <td>
+                            <a href="http://www.djangoproject.com/"><img src="http://media.djangoproject.com/img/badges/djangosite80x15.gif" border="0" alt="A Django site." title="A Django site." /></a>
+                        </td>
+                    </tr>
+                </table>
+                <div class="clear"></div>
+            </div>
+        </div>
+    </div>
+</html>
+from django.conf.urls.defaults import patterns, include, url
+from django.contrib import admin
+from django.conf import settings
+from django.views.generic.simple import redirect_to
+import django.views.static
+
+admin.autodiscover()
+
+urlpatterns = patterns('',
+    url('^forum/', include('pybb.urls')),
+    url(r'^admin/', include(admin.site.urls)),
+    url(r'^%s(?P<path>.*)$' % settings.MEDIA_URL.lstrip('/'),
+        django.views.static.serve, {'document_root': settings.MEDIA_ROOT}),
+    url(r'^account/', include('account.urls')),
+    url(r'^$', redirect_to, {'url': '/forum'}),
+)

docs/dependencies.rst

-.. _dependencies:
-
-Dependencies
-============
-
-* django
-* beautifulsoup
-* markdown
-* pytils (optional)
-* south (optional)
-* database driver (depends on what database server you use). In most cases it will be mysql or postgres driver (python-mysql and python-psycopg2 packages in Debian).
-
-You can install these software via pip or easy_install. In case of pip you can use this `dependency list <http://bitbucket.org/lorien/pybb/raw/c79338ff15c3/dependencies.pip.txt>`_ with ``-r`` option

docs/features.rst

-.. _features:
-
-Feature list
-============
-
-* Each category, forum, topic and post has its own permanent url
-* Bbcode and markdown support
-* Code blocks are highlighted with highlightjs
-* Simple moderator system
-* Email subscription on topic
-* Topic could be sticked or closed
-* Plain text urls are converted to active links
-* Each user has profile with individual forum related settings
-* Gravatar support
-* Unread topics are marked
-* Search (via google)
-* i18n (translatable interface)
-* File attachments to the post
-* AJAX preview of new post content
   * Add ``pybb`` to ``INSTALLED_APPS``
   * Add ``from pybb.settings import *`` line
   * Add ``pybb.middleware.PybbMiddleware`` to ``MIDDLEWARE_CLASSES``
-    * Add ``url('^forum/', include('pybb.urls'))`` to ``urls.py`` file
-    * Run command ``manage.py migrate`` if you installed `south <http://south.aeracode.org>`_ (recommended) or ``./manage.py syncdb`` (if south is not installed)
-    * Symlink or copy pybb static files to "%MEDIA_ROOT%/pybb"
+* Add ``url('^forum/', include('pybb.urls'))`` to ``urls.py`` file
+* Run command ``manage.py migrate`` if you installed `south <http://south.aeracode.org>`_ (recommended) or ``./manage.py syncdb`` (if south is not installed)
+* Symlink or copy pybb static files to "%MEDIA_ROOT%/pybb"
 
 
 Dependencies

docs/installation.rst

-.. _installation:
-
-Installation
-============
-
-If you like to use pybb as standalone application then may be you have to look on `other forum engines <http://code.djangoproject.com/wiki/ForumAppsComparison>`_. If you still want standalone pybb installation then setup django project with working signin/signup functions and then come back to this howto.
- 
-
-Integrating PyBB into existing django project
----------------------------------------------
-
-* Install ``pybb dependencies``_ and pybb itself. You can ensure that all dependencies were installed by running ``manage.py shell`` and then ``import pybb.models``. If no exception was raised then you may proceed.
-* Edit settings.py
-  * Add ``pybb`` to ``INSTALLED_APPS``
-  * Add ``from pybb.settings import *`` line
-  * Add ``pybb.middleware.PybbMiddleware`` to ``MIDDLEWARE_CLASSES``
-* Add ``url('', include('pybb.urls'))`` to ``urls.py`` file
-* Run command ``manage.py migrate`` if you installed `south <http://south.aeracode.org>`_ (recommended) or ``./manage.py syncdb`` (if south is not installed)
-* Symlink or copy pybb static files to %MEDIA_ROOT%/pybb. You can use ``./manage.py pybb_install`` command.
-
-.. _pybb dependencies: _dependencies

pybb/management/commands/pybb_manage_supermoderator.py

+from optparse import make_option
+
+from django.core.management.base import BaseCommand, CommandError
+from django.contrib.auth.models import User
+
+from pybb.models import Forum
+
+class Command(BaseCommand):
+    help = 'Set and remove moderator to all forums'
+    args = '{add|del} username'
+
+    def handle(self, *args, **kwargs):
+        if len(args) != 2:
+            raise CommandError("Enter action {add|del} and username")
+        action, username = args
+        assert action in ('add', 'del')
+        user = User.objects.get(username=username)
+        forums = Forum.objects.all()
+        for forum in forums:
+            forum.moderators.remove(user)
+            if action == 'add':
+                forum.moderators.add(user)

pybb/management/commands/pybb_rebuild_counters.py

+from datetime import datetime, timedelta
+from optparse import make_option
+
+from django.core.management.base import BaseCommand, CommandError
+from django.db.models import Count, Sum
+from django.contrib.auth.models import User
+from django.conf import settings
+
+from pybb.models import Profile, Topic, Forum
+
+def track_queries(action, cache=[0]):
+    from django.db import connection
+
+    if action == 'start':
+        cache[0] = len(connection.queries)
+
+    if action == 'stop':
+        print '--'
+        for item in connection.queries[cache[0]:]:
+            print item['time'], item['sql']
+        print 'Query count: %d' % (len(connection.queries) - cache[0])
+
+
+class Command(BaseCommand):
+    help = 'Rebuild various pybb counters'
+
+    def handle(self, *args, **kwargs):
+    
+        self.stdout.write('Calculating profile post count\n')
+        for profile in Profile.objects.annotate(_count=Count('user__pybb_posts')):
+            if profile.post_count != profile._count:
+                profile.post_count = profile._count
+                profile.save()
+
+        self.stdout.write('Calculating topic post count\n')
+        for topic in Topic.objects.annotate(_count=Count('posts')):
+            if topic.post_count != topic._count:
+                topic.post_count = topic._count
+                self.save()
+
+        self.stdout.write('Calculating forum post count\n')
+        for forum in Forum.objects.all():
+            post_count = forum.topics.aggregate(value=Sum('post_count'))['value']
+            if forum.post_count != post_count:
+                forum.post_count = post_count

pybb/management/commands/supermoderator.py

-from optparse import make_option
-
-from django.core.management.base import BaseCommand, CommandError
-from django.contrib.auth.models import User
-
-from pybb.models import Forum
-
-class Command(BaseCommand):
-    help = 'Set and remove moderator to all forums'
-    args = '{add|del} username'
-
-    def handle(self, *args, **kwargs):
-        if len(args) != 2:
-            raise CommandError("Enter action {add|del} and username")
-        action, username = args
-        assert action in ('add', 'del')
-        user = User.objects.get(username=username)
-        forums = Forum.objects.all()
-        for forum in forums:
-            forum.moderators.remove(user)
-            if action == 'add':
-                forum.moderators.add(user)
     def __unicode__(self):
         return self.name
 
-    def update_post_count(self):
-        self.post_count = Topic.objects.filter(forum=self).aggregate(
-                                Sum("post_count"))['post_count__sum'] or 0
-        self.save()
-
     def get_absolute_url(self):
         return reverse('pybb_forum_details', args=[self.id])
 
         return reverse('pybb_topic_details', args=[self.id])
 
     def save(self, *args, **kwargs):
-        if self.id is None:
+        if self.pk is None:
             self.created = datetime.now()
         super(Topic, self).save(*args, **kwargs)
 
-    def update_post_count(self):
-        self.post_count = self.posts.count()
-        self.save()
-
 
 class RenderableItem(models.Model):
     """
             self.created = now
         self.render()
 
-        new = self.pk is None
-
         super(Post, self).save(*args, **kwargs)
 
-        if new:
-            self.topic.updated = now
-            self.topic.last_post = self
-            self.topic.update_post_count()
-            self.topic.forum.updated = now
-            self.topic.forum.last_post = self
-            self.topic.forum.update_post_count()
-
     def get_absolute_url(self):
         return reverse('pybb_post_details', args=[self.id])
 
     def delete(self, *args, **kwargs):
         # Change `last_post` of the forum which the deleted post belongs to
-        # This needs to avaid whole forum deletion due to ForeignKey dependency
+        # This needs to avoid whole forum deletion due to ForeignKey dependency
         forum = self.topic.forum
         if forum.last_post == self:
             try:
 
         super(Post, self).delete(*args, **kwargs)
 
-        if self.topic.pk:
-            # If topic was not deleted
-            self.topic.update_post_count()
-        self.topic.forum.update_post_count()
+        #if self.topic.pk:
+            ## If topic was not deleted
+            #self.topic.update_post_count()
+        #self.topic.forum.update_post_count()
 
 
 BAN_STATUS = (
 
 PYBB_ATTACHMENT_UPLOAD_TO = join('pybb_upload', 'attachments')
 PYBB_DEFAULT_AVATAR_URL = 'pybb/img/anonymous.gif'
+PYBB_REBUILD_PRIMARY_PERIOD = 600
+from datetime import datetime
+
 from django.db.models.signals import post_save
 from django.contrib.auth.models import User
 
 from pybb.models import Post, Topic, Profile, ReadTracking
 
 
-def post_saved(instance, **kwargs):
-    notify_topic_subscribers(instance)
+def post_saved(instance, created, **kwargs):
 
-    profile = instance.user.pybb_profile
-    profile.post_count = instance.user.pybb_posts.count()
-    profile.save()
+    if created:
+        notify_topic_subscribers(instance)
 
+        now = datetime.now()
 
-def topic_saved(instance, **kwargs):
-    forum = instance.forum
-    forum.topic_count = forum.topics.count()
-    forum.save()
+        instance.topic.updated = now
+        instance.topic.last_post = instance
+        instance.topic.post_count += 1
+        instance.topic.save()
+
+        forum = instance.topic.forum
+        forum.updated = now
+        forum.last_post = instance
+        forum.post_count += 1
+        forum.save()
+
+        profile = instance.user.pybb_profile
+        profile.post_count += 1
+        profile.save()
+
+
+#def topic_saved(instance, **kwargs):
+    #pass
 
 
 def user_saved(instance, created, **kwargs):
 
 
 post_save.connect(post_saved, sender=Post)
-post_save.connect(topic_saved, sender=Topic)
+#post_save.connect(topic_saved, sender=Topic)
 post_save.connect(user_saved, sender=User)

pybb/templates/pybb/_category_row.html

+<li class="category-item">
+    <h2 class="name"><a href="{{ category.get_absolute_url }}">{{ category }}</a></h2>
+    <table class="forum-list">
+        <thead>
+            <tr>
+                <th class="name">Название</th>
+                <th class="feed">Фид</th>
+                <th class="post-count">Ответы</th>
+                <th class="recent-info">Последний ответ</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for forum in category.cached_forums %}
+            {% include 'pybb/_forum_row.html' %}
+            {% endfor %}
+        </tbody>
+        </table>
+    </table>
+</li>

pybb/templates/pybb/_forum_row.html

+{% load pybb_tags %}
+
+<tr class="forum-item">
+    <td class="title">
+        {% if forum|pybb_forum_unread:user %}<span class="unread"></span>{% endif %}
+        <span class="name"><a href="{{ forum.get_absolute_url }}">{{ forum }}</a></span>
+        <div class="description">{{ forum.description|safe }}</div>
+    </td>
+    <td class="feed"><a href="{% url pybb_feed_forum_byid_topic forum.pk %}"><img src="{{ MEDIA_URL }}img/feed-icon-small.png" /></a></td>
+    <td class="post-count">{{ forum.post_count }}</td>
+    <td class="recent-info">
+        {% if forum.updated %}<a href="{{ forum.last_post.get_absolute_url }}">{% pybb_time forum.updated %}</a>{% endif %}
+    </td>
+</tr>
+</tr>

pybb/templates/pybb/_post_row.html

+{% load i18n %}
+{% load pybb_tags %}
+
+<div class="post-details post-{% cycle 'odd' 'even' %}" id="post-{{ post.id }}">
+    <a name="post-{{ post.id }}"></a>
+    <div class="info">
+        <div class="inner">
+            <div class="avatar">
+                <a href="{{ post.user.pybb_profile.get_absolute_url }}">
+                <img src="{{ post.user|pybb_avatar_url }}" alt="" />
+                </a>
+            </div>
+            <div class="author">{{ post.user|pybb_profile_link }}</div>
+            {% if user.is_superuser %}
+                <div class="updated">{{ post.user_ip }}</div>
+            {% endif %}
+            <div class="bottom">
+                <a class="permalink" href="{{ post.get_absolute_url }}">#</a>
+                <span class="updated">{% pybb_time post.created %}</span>
+            </div>
+
+            {% if post.user.pybb_profile.ban_status %}
+                <div{% if post.user.pybb_profile.is_banned %}
+                title="{% trans 'Expire' %} {{ post.user.pybb_profile.ban_till }}"{% endif %}
+                class="banned">
+                    {{ post.user.pybb_profile.get_ban_status_display }}
+                </div>
+            {% endif %}
+
+            <div class="post-controls">
+                {% if moderator or post|pybb_posted_by:user %}
+                <a href="{% url pybb_post_edit post.id %}">{% trans "Edit" %}</a>
+                {% endif %}
+                {% if moderator or post|pybb_equal_to:last_post %}
+                {% if moderator or post.user|pybb_equal_to:user %}
+                / <a onclick="pybb_delete_post('{% url pybb_post_delete post.id %}',
+                    'post-{{ post.id }}', '{% trans 'Delete post?' %}'); return false;"
+                    href="{% url pybb_post_delete post.id %}">{% trans "Delete" %}</a>
+                {% endif %}
+                {% endif %}
+            </div>
+        </div>
+    </div>
+    <div class="content">
+        <div class="inner">
+            <!-- Post's content -->
+            {% pybb_render_post post %}
+
+            <!-- Last modification date -->
+            {% if post.updated %}
+            <div class="update-date">{% trans "Edited" %} {% pybb_time post.updated %}</div>
+            {% endif %}
+
+            <!-- Attachments -->
+            {% if post.attachment_cache %}
+            {% for attach in post.attachment_cache %}
+            <br/>
+            {% trans "Attachment" %}: <a href="{{ attach.get_absolute_url }}">{{ attach.name }}</a> ({{ attach.size_display }})
+            {% endfor %}
+            {% endif %}
+
+            <!-- Signature -->
+            {% if not user.is_authenticated or user.pybb_profile.show_signatures %}
+            {% if post.user.pybb_profile.signature %}
+            <div class="signature">
+                <hr/>
+                {{ post.user.pybb_profile.signature_html|safe }}
+            </div>
+            {% endif %}
+            {% endif %}
+
+            <div class="post-related">
+                <a href="{% url pybb_post_add topic.id %}?quote_id={{ post.id }}">{% trans "quote" %}</a>
+            </div>
+        </div>
+    </div>
+    <div class="clear"></div>
+</div>

pybb/templates/pybb/_topic_mini_pagination.html

+{% load i18n %}
+{% if is_paginated %}
+{% trans 'Page' %}: {{ pagination|safe }}
+{% endif %}

pybb/templates/pybb/base.html

+{% extends "base.html" %}
+{% load i18n pybb_tags %}
+
+{% block title %}
+    {% block pybb_title %}PyBB Powered Forum{% endblock %}</title>
+{% endblock %}
+
+{% block extra_css %}
+    <link href="{{ MEDIA_URL }}css/forum.css" type="text/css" rel="stylesheet" />
+    {% block pybb_styles_extra %}{% endblock %}
+{% endblock %}
+
+{% block head_extra %}
+    <link rel="alternate" type="application/atom+xml" href="{% url pybb_feed_post %}" title="{% trans "Latest posts on forum" %}" />
+    <link rel="alternate" type="application/atom+xml" href="{% url pybb_feed_topic %}" title="{% trans "Latest topics on forum" %}" />
+    {% block pybb_head_extra %}{% endblock %}
+{% endblock %}
+
+{% block extra_js %}
+    {% block pybb_scripts_extra %}{% endblock %}
+{% endblock %}
+
+{% block crumbs %}
+    {% block pybb_crumbs %}{% endblock %}
+{% endblock %}
+
+{% block content %}
+    {% block pybb_content %}{% endblock %}
+{% endblock %}
+
+{% block sidebar %}
+<div class="infobox">
+    <h3>New topics</h3>
+    <ul>
+        {% pybb_load_last_topics as topics %}
+        {% for topic in topics %}
+        <li><a href="{{ topic.get_absolute_url }}">{{ topic }}</a> - {% pybb_time topic.created %}</li>
+        {% endfor %}
+    </ul>
+</div>
+{% endblock %}

pybb/templates/pybb/category_details.html

+{% extends 'pybb/base.html' %}
+{% load pybb_tags i18n %}
+
+{% block pybb_title %}{{ category }}{% endblock %}
+
+{% block pybb_crumbs %}
+<a href="">Начало</a> &raquo;
+<a href="{% url pybb_index %}">Форум</a>
+{% endblock %}
+
+{% block pybb_content %}
+<ul class="category-list">
+    {% include "pybb/_category_row.html" %}
+</ul>
+{% endblock %}

pybb/templates/pybb/feeds/forum_post_description.html

+{% load pybb_tags %}{% pybb_render_post obj.head %}

pybb/templates/pybb/feeds/forum_post_title.html

+{{ obj.name|safe }} @ {{ obj.created|date }}

pybb/templates/pybb/feeds/posts_description.html

+{% load pybb_tags %}{% pybb_render_post obj %}

pybb/templates/pybb/feeds/posts_title.html

+{{ obj.user }}: {{ obj.topic|safe }} @ {{ obj.topic.forum|safe }} 

pybb/templates/pybb/feeds/topics_description.html

+{% load pybb_tags %}{% pybb_render_post obj.head %}

pybb/templates/pybb/feeds/topics_title.html

+{{ obj.head.user }}: {{ obj|safe }} @ {{ obj.forum|safe }} 

pybb/templates/pybb/forum_details.html

+{% extends 'pybb/base.html' %}
+{% load pybb_tags %}
+{% load i18n %}
+
+{% block title %}
+{{ forum }}
+{% endblock %}
+
+{% block pybb_crumbs %}
+<a href="">Начало</a> &raquo;
+<a href="{% url pybb_index %}">Форум</a> &raquo;
+<a href="{{ forum.category.get_absolute_url }}">{{ forum.category }}</a>
+{% endblock %}
+
+{% block pybb_content %}
+<div class="forum-details">
+    <h2 class="name">{{ forum }}</h2>
+    <table class="topic-list">
+        <thead>
+            <tr>
+                <th>Название</th>
+                <th>Ответы</th>
+                <th>Последний ответ</th>
+            </tr>
+        </thead>
+        {% for topic in page.object_list %}
+        <tr class="topic-item">
+            <td class="title">
+                {% if topic.sticky %}<span class="sticky"></span>{% endif %}
+                {% if topic|pybb_topic_unread:user %}<span class="unread"></span>{% endif %}
+                {% if topic.closed %}{% trans "Closed" %}:{% endif %}
+                {% if topic.sticky %}{% trans "Important" %}:{% endif %}
+                <a href="{{ topic.get_absolute_url }}">{{ topic }}</a>
+                <div class="mini-pagination">{% pybb_topic_mini_pagination topic %}</div>
+            </td>
+            <td class="post-count">{{ topic.post_count }}</td>
+            <td class="recent-info">{% if topic.updated %}<a href="{{ topic.last_post.get_absolute_url }}">{% pybb_time topic.updated %}</a>{% endif %}</td>
+        </tr>
+        {% endfor %}
+    </table>
+</div>
+
+{% with _('Topics') as label %}
+{% include "pybb/pagination.html" %}
+{% endwith %}
+
+{% if not user.pybb_profile.is_banned %}
+    <div class="controls">
+        <a href="{% url pybb_topic_add forum.id %}">{% trans "New topic" %}</a>
+    </div>
+{% endif %}
+
+{% endblock %}

pybb/templates/pybb/index.html

+{% extends 'pybb/base.html' %}
+
+{% block pybb_title %}Форумы{% endblock %}
+
+{% block pybb_crumbs %}
+<a href="/">Начало</a> &raquo;
+<a href="{% url pybb_index %}">Форум</a>
+{% endblock %}
+
+{% block pybb_content %}
+<ul class="category-list">
+    {% for category in cats %}{% include "pybb/_category_row.html" %}{% endfor %}
+</ul>
+{% endblock %}

pybb/templates/pybb/last_topics.html

+{% load i18n pybb_tags %}
+
+<ul class="tablist">
+    <li class="tablist-row">
+        <div class="header">
+            {% if category %}
+            {% blocktrans %}Last topics in category {{ category }}{% endblocktrans %}
+            {% else %}
+                {% if forum %}
+                {% trans "Last updated topics" %}
+                {% else %}
+                {% trans "Last topics" %}
+                {% endif %}
+            {% endif %}
+        </div>
+
+        <ul class="tablist-inner">
+            {% for topic in last_topics %}
+            <li class="tablist-inner-row">
+            {% if topic|pybb_topic_unread:user %}<span class="unread"></span>{% endif %}
+            <a href="{{ topic.get_absolute_url }}">{{ topic }}</a>
+            &rarr;
+            <span class="author">{{ topic.user|pybb_profile_link }}</span>
+            &rarr;
+            <span class="updated">{% pybb_time topic.created %}</span>
+            </li>
+            {% endfor %}
+        </ul>
+    </div>
+</div>

pybb/templates/pybb/pagination.html

+{% load i18n %}
+<div class="pagination">
+
+    <script type="text/javascript">
+        function jumpto() {
+            var userInput=prompt('{% trans "Enter page number" %}','{{ page.number }}');
+            if (userInput != '' && userInput != null) {
+                location.href = '?page='+userInput;
+            }
+        }
+    </script>
+
+    {% comment %}
+    <span class="summary">
+        {{ label }}
+        {{ page.start_index }}&mdash;{{ page.end_index }}
+        {% trans "of" %} {{ page.paginator.count }}
+    </span>
+    {% endcomment %}
+
+    {% if page.has_other_pages %}
+        {#&bull;#}
+
+        <span class="pages-of" onclick="jumpto(); return false;" title="{% trans 'Go to page...' %}">
+            {% trans "Page" %} {{ page.number }}
+            {% trans "of" %} {{ page.paginator.num_pages }}
+        </span>
+
+        &bull;
+
+        {% if 1 < page.paginator.frame_start_page %}
+        {% if page.first_page_url %}
+        <a href="{{ page.first_page_url }}">1</a>
+        <span>&#133;</span>
+        {% endif %}
+        {% endif %}
+
+        {% for xpage, xurl in page.paginator.frame %}
+        {% ifequal page.number xpage %}
+        <span class="current">{{ page.number }}</span>
+        {% else %}
+        <a href="{{ xurl }}">{{ xpage }}</a>
+        {% endifequal %}
+        {% endfor %}
+
+        {% if page.paginator.num_pages > page.paginator.frame_end_page %}
+        {% if page.last_page_url %}
+        <span>&#133;</span>
+        <a href="{{ page.last_page_url }}">{{ page.paginator.num_pages }}</a>
+        {% endif %}
+        {% endif %}
+
+    {% endif %}
+