├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── TODO.txt ├── apache ├── django.wsgi └── httpd.conf ├── forum ├── __init__.py ├── admin.py ├── app_settings.py ├── auth.py ├── bin │ └── create-test-data.py ├── fixtures │ └── testdata.json ├── formatters │ ├── __init__.py │ └── emoticons.py ├── forms.py ├── manage.py ├── models.py ├── moderation.py ├── redis_connection.py ├── sessions │ ├── __init__.py │ └── redis_session_backend.py ├── settings.py ├── static │ └── forum │ │ ├── css │ │ ├── ie.css │ │ └── style.css │ │ ├── img │ │ ├── djangosite.gif │ │ ├── emoticons │ │ │ ├── angry.gif │ │ │ ├── blink.gif │ │ │ ├── grin.gif │ │ │ ├── huh.gif │ │ │ ├── lol.gif │ │ │ ├── ohmy.gif │ │ │ ├── ph34r.gif │ │ │ ├── rolleyes.gif │ │ │ ├── sad.gif │ │ │ ├── smile.gif │ │ │ ├── tongue.gif │ │ │ ├── unsure.gif │ │ │ ├── wacko.gif │ │ │ ├── wink.gif │ │ │ └── wub.gif │ │ ├── icon_alert.gif │ │ ├── icon_exclamation.gif │ │ ├── icon_lock.gif │ │ ├── new_posts.gif │ │ ├── no_new_posts.gif │ │ └── unread_post.gif │ │ └── js │ │ ├── DOMBuilder.dom.min.js │ │ ├── Paginator.js │ │ ├── PostForm.js │ │ ├── Stalk.js │ │ ├── Topic.js │ │ └── jquery-1.2.1.pack.js ├── templates │ ├── 404.html │ ├── 500.html │ ├── base_forum.html │ ├── forum │ │ ├── add_forum.html │ │ ├── add_reply.html │ │ ├── add_section.html │ │ ├── add_topic.html │ │ ├── base.html │ │ ├── delete_forum.html │ │ ├── delete_post.html │ │ ├── delete_section.html │ │ ├── delete_topic.html │ │ ├── edit_forum.html │ │ ├── edit_post.html │ │ ├── edit_section.html │ │ ├── edit_topic.html │ │ ├── edit_user_forum_profile.html │ │ ├── edit_user_forum_settings.html │ │ ├── forum_detail.html │ │ ├── forum_index.html │ │ ├── help │ │ │ ├── basic_formatting_quick.html │ │ │ ├── bbcode_formatting_quick.html │ │ │ ├── emoticons.html │ │ │ └── markdown_formatting_quick.html │ │ ├── new_posts.html │ │ ├── pagination.html │ │ ├── permission_denied.html │ │ ├── search.html │ │ ├── search_results.html │ │ ├── section_detail.html │ │ ├── stalk_users.html │ │ ├── topic_detail.html │ │ ├── topic_post_summary.html │ │ ├── user_profile.html │ │ └── user_topics.html │ └── registration │ │ ├── activate.html │ │ ├── activation_email.txt │ │ ├── activation_email_subject.txt │ │ ├── login.html │ │ ├── logout.html │ │ ├── password_change_done.html │ │ ├── password_change_form.html │ │ ├── password_reset_done.html │ │ ├── password_reset_email.html │ │ ├── password_reset_form.html │ │ ├── registration_complete.html │ │ └── registration_form.html ├── templatetags │ ├── __init__.py │ └── forum_tags.py ├── tests │ ├── __init__.py │ ├── auth.py │ └── models.py ├── urls.py ├── utils │ ├── __init__.py │ ├── dates.py │ └── models.py └── views.py ├── package.json ├── pip-requirements.txt ├── setup.py └── stalk.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | MANIFEST 4 | build/ 5 | dist/ 6 | forum/database.db 7 | node_modules -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Django Forum 2 | ------------ 3 | 4 | Copyright (c) 2007, Jonathan Buchanan 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | recursive-include tests * -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Django Forum 3 | ============ 4 | 5 | This is a basic forum application which is usable by itself as a standalone 6 | project and should eventually be usable as a pluggable application in any 7 | Django project. 8 | 9 | .. contents:: 10 | :local: 11 | :depth: 2 12 | 13 | Features 14 | ======== 15 | 16 | - **Bog-standard layout** - Sections |rarr| Forums |rarr| Topics. 17 | 18 | - **Search** posts and topics - search for keywords by section, forum, 19 | username, post type and date. 20 | 21 | Searching supports quoted phrases and ``+`` and ``-`` modifiers on 22 | keywords and phrases. 23 | 24 | - **Real-time tracking** with `Redis`_ - performing ``UPDATE`` queries on 25 | every single page view to track view counts, active users and user 26 | activity on the forum? No. 27 | 28 | Redis does it in style. 29 | 30 | - **Configurable post formatting** - comes with BBCode and Markdown formatters. 31 | 32 | - **Metaposts** - each topic has regular posts, as you'd expect, and also 33 | metaposts. These are effectively a second thread of conversation for 34 | posts *about* the topic. Why, you say? 35 | 36 | People who want to talk about the topic itself or how it's being 37 | discussed, or just start a good old ding-dong with other users, rather 38 | than taking part in the discussion at hand, have a place to vent instead 39 | of dragging the topic into the realm of the off-topic. 40 | 41 | Moderators have another option other than deleting or hiding posts when 42 | topics start to take a turn for the worse in that direction. 43 | 44 | People who just wanted to read and post in the original topic can 45 | continue to do so *and* still have it out in the metaposts. Win/win 46 | or win/lose - the choice is yours. 47 | 48 | Inspired by my many years with the excellent people of `RLLMUK`_. 49 | 50 | - **Avatar validation** - linked avatars are validated for format, file 51 | size and dimensions. 52 | 53 | Possible Misfeatures 54 | -------------------- 55 | 56 | - **Denormalised up the yazoo** - data such as post counts and last post 57 | information are maintained on all affected objects on every write. 58 | 59 | Trading write complexity and ease of maintenance against fewer, more 60 | simple reads, just because. 61 | 62 | - **No signatures** - it's not a bug, it's a feature. 63 | 64 | .. _`RLLMUK`: http://www.rllmukforum.com 65 | .. |rarr| unicode:: 0x2192 .. rightward arrow 66 | 67 | Installation 68 | ============ 69 | 70 | Dependencies 71 | ------------ 72 | 73 | **Required:** 74 | 75 | - `Python Imaging Library`_ (PIL) is required for validation of user avatars. 76 | - `pytz`_ is required to perform timezone conversions based on the timezone 77 | registered users can choose as part of their forum profile. 78 | 79 | **Required for standalone mode:** 80 | 81 | - `django-registration`_ is used to perform registration and validation of new 82 | users when running as a standalone project - it is assumed that when the forum 83 | is integrated into existing projects, they will already have their own 84 | registration mechanism in place. 85 | 86 | **Required based on settings:** 87 | 88 | - `Redis`_ and `redis-py`_ are required for real-time tracking fun if 89 | ``FORUM_USE_REDIS`` is set to ``True``. For Windows users, 90 | `native Redis binaries`_ are available. 91 | - `python-markdown2`_ is required to use ``forum.formatters.MarkdownFormatter`` 92 | to format posts using `Markdown`_ syntax. 93 | - `postmarkup`_ and `Pygments`_ are required to use 94 | ``forum.formatters.BBCodeFormatter`` to format posts using `BBCode`_ syntax. 95 | 96 | **Others:** 97 | 98 | - `Django Debug Toolbar`_ will be used if available, if the forum is in 99 | standalone mode and ``DEBUG`` is set to ``True``. 100 | 101 | .. _`redis-py`: https://github.com/andymccurdy/redis-py 102 | .. _`native redis binaries`: https://github.com/dmajkic/redis/downloads 103 | .. _`Python Imaging Library`: http://www.pythonware.com/products/pil/ 104 | .. _`pytz`: http://pytz.sourceforge.net/ 105 | .. _`django-registration`: http://code.google.com/p/django-registration/ 106 | .. _`Django Debug Toolbar`: http://robhudson.github.com/django-debug-toolbar/ 107 | .. _`python-markdown2`: http://code.google.com/p/python-markdown2 108 | .. _`Markdown`: http://daringfireball.net/projects/markdown/ 109 | .. _`postmarkup`: http://code.google.com/p/postmarkup/ 110 | .. _`BBCode`: http://en.wikipedia.org/wiki/BBCode 111 | .. _`Pygments`: http://pygments.org 112 | 113 | Standalone Mode 114 | --------------- 115 | 116 | At the time of typing, the codebase comes with a complete development 117 | ``settings.py`` module which can be used to run the forum in standalone 118 | mode. 119 | 120 | It's configured to to use Redis for real-time tracking on the forum and 121 | for session management using ``forum.sessions.redis_session_backend``. 122 | 123 | Pluggable Application Mode 124 | -------------------------- 125 | 126 | **Note: this mode has not yet been fully developed or tested** 127 | 128 | Add ``'forum'`` to your application's ``INSTALLED_APPS`` setting, then run 129 | ``syncdb`` to create its tables. 130 | 131 | Include the forum's URLConf in your project's main URLConf at whatever URL you 132 | like. For example:: 133 | 134 | from django.conf.urls.defaults import * 135 | 136 | urlpatterns = patterns( 137 | (r'^forum/', include('forum.urls')), 138 | ) 139 | 140 | The forum application's URLs are decoupled using Django's `named URL patterns`_ 141 | feature, so it doesn't mind which URL you mount it at. 142 | 143 | .. _`named URL patterns`: http://www.djangoproject.com/documentation/url_dispatch/#naming-url-patterns 144 | 145 | Settings 146 | ======== 147 | 148 | The following settings may be added to your project's settings module to 149 | configure the forum application. 150 | 151 | ``FORUM_STANDALONE`` 152 | 153 | *Default:* ``False`` 154 | 155 | Whether or not the forum is being used in standalone mode. If set to 156 | ``True``, URL configurations for the django.contrib.admin and 157 | django-registration apps will be included in the application's main 158 | URLConf. 159 | 160 | ``FORUM_USE_REDIS`` 161 | 162 | *Default:* ``False`` 163 | 164 | Whether or not the forum should use `Redis`_ to track real-time information 165 | such as topic view counts, active users and user locations on the forum. 166 | 167 | If set to ``False``, these details will not be displayed. 168 | 169 | ``FORUM_REDIS_HOST`` 170 | 171 | *Default:* ``'localhost'`` 172 | 173 | Redis host. 174 | 175 | ``FORUM_REDIS_PORT`` 176 | 177 | *Default:* ``6379`` 178 | 179 | Redis port. 180 | 181 | ``FORUM_REDIS_DB`` 182 | 183 | *Default:* ``0`` 184 | 185 | Redis database number, ``0``-``16``. 186 | 187 | ``FORUM_POST_FORMATTER`` 188 | 189 | *Default:* ``'forum.formatters.PostFormatter'`` 190 | 191 | The Python path to the module to be used to format raw post input. This class 192 | should satisfy the requirements defined below in `Post Formatter Structure`_. 193 | 194 | ``FORUM_DEFAULT_POSTS_PER_PAGE`` 195 | 196 | *Default:* ``20`` 197 | 198 | The number of posts which are displayed by default on any page where posts are 199 | listed - this applies to registered users who do not choose to override the 200 | number of posts per page and to anonymous users. 201 | 202 | ``FORUM_DEFAULT_TOPICS_PER_PAGE`` 203 | 204 | *Default:* ``30`` 205 | 206 | The number of topics which are displayed by default on any page where topics are 207 | listed - this applies to registered users who do not choose to override the 208 | number of topics per page and to anonymous users. 209 | 210 | ``FORUM_MAX_AVATAR_FILESIZE`` 211 | 212 | *Default:* ``512 * 1024`` (512 kB) 213 | 214 | The maximum allowable filesize for user avatars, specified in bytes. To disable 215 | validation of user avatar filesizes, set this setting to ``None``. 216 | 217 | ``FORUM_ALLOWED_AVATAR_FORMATS`` 218 | 219 | *Default:* ``('GIF', 'JPEG', 'PNG')`` 220 | 221 | A tuple of allowed image formats for user avatars. To disable validation of user 222 | avatar image formats, set this setting to ``None``. 223 | 224 | ``FORUM_MAX_AVATAR_DIMENSIONS`` 225 | 226 | *Default:* ``(64, 64)`` 227 | 228 | A two-tuple, (width, height), of maximum allowable dimensions for user avatars. 229 | To disable validation of user avatar dimensions, set this setting to ``None``. 230 | 231 | ``FORUM_FORCE_AVATAR_DIMENSIONS`` 232 | 233 | *Default:* ``True`` 234 | 235 | Whether or not ```` tags created for user avatars should include ``width`` 236 | and ``height`` attributes to force all avatars to be displayed with the 237 | dimensions specified in the ``FORUM_MAX_AVATAR_DIMENSIONS`` setting. 238 | 239 | ``FORUM_EMOTICONS`` 240 | 241 | *Default:* 242 | 243 | :: 244 | 245 | {':angry:': 'angry.gif', 246 | ':blink:': 'blink.gif', 247 | ':D': 'grin.gif', 248 | ':huh:': 'huh.gif', 249 | ':lol:': 'lol.gif', 250 | ':o': 'ohmy.gif', 251 | ':ph34r:': 'ph34r.gif', 252 | ':rolleyes:': 'rolleyes.gif', 253 | ':(': 'sad.gif', 254 | ':)': 'smile.gif', 255 | ':p': 'tongue.gif', 256 | ':unsure:': 'unsure.gif', 257 | ':wacko:': 'wacko.gif', 258 | ';)': 'wink.gif', 259 | ':wub:': 'wub.gif'} 260 | 261 | A ``dict`` mapping emoticon symbols to the filenames of images they 262 | should be replaced with when emoticons are enabled while formatting 263 | posts. Images should be placed in media/img/emticons. 264 | 265 | Post Formatters 266 | =============== 267 | 268 | Post formatting classes are responsible for taking raw input entered by forum 269 | users and transforming and escaping it for display, as well as performing any 270 | other operations which are dependent on the post formatting syntax being used. 271 | 272 | The following post formatting classes are bundled with the forum application: 273 | 274 | - ``forum.formatters.PostFormatter`` 275 | - ``forum.formatters.MarkdownFormatter`` 276 | - ``forum.formatters.BBCodeFormatter`` 277 | 278 | Post Formatter Structure 279 | ------------------------ 280 | 281 | When creating a custom post formatting class, you should subclass 282 | ``forum.formatters.PostFormatter`` and override the following: 283 | 284 | ``QUICK_HELP_TEMPLATE`` 285 | 286 | This class-level attribute should specify the location of a template providing 287 | quick help, suitable for embedding into posting pages. 288 | 289 | ``FULL_HELP_TEMPLATE`` 290 | 291 | This class-level attribute should specify the location of a template file 292 | providing detailed help, suitable for embedding in a standalone page. 293 | 294 | ``format_post_body(body)`` 295 | 296 | This method should accept raw post text input by the user, returning a version 297 | of it which has been transformed and escaped for display. It is important that 298 | the output of this function has been made safe for direct inclusion in 299 | templates, as no further escaping will be performed. 300 | 301 | For example, given the raw post text:: 302 | 303 | [quote]T 304 | 305 | t![/quote] 306 | 307 | ...a BBCode post formatter might return something like:: 308 | 309 |
T
310 | <es>
311 | t!
312 | 313 | ``quote_post(post)`` 314 | 315 | This method should accept a ``Post`` object and return the raw post text for a 316 | a "quoted" version of the post's content. The ``Post`` object itself is passed, 317 | as opposed to just the raw post text, as the quote may wish to include other 318 | details such as the name of the user who made the post, the time the post was 319 | made at, a link back to the quoted post... and so on. 320 | 321 | Note that the raw post text returned by this function will be escaped when it is 322 | displayed to the user for editing, so to avoid double escaping it should *not* 323 | be escaped by this function. 324 | 325 | For example, given a ``Post`` whose raw ``body`` text is:: 326 | 327 | Tt! 328 | 329 | ...a BBCode post formatter might return something like:: 330 | 331 | [quote]Tt![/quote] 332 | 333 | .. _`Redis`: http://redis.io 334 | 335 | MIT License 336 | =========== 337 | 338 | Copyright (c) 2011, Jonathan Buchanan 339 | 340 | Permission is hereby granted, free of charge, to any person obtaining a copy of 341 | this software and associated documentation files (the "Software"), to deal in 342 | the Software without restriction, including without limitation the rights to 343 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 344 | the Software, and to permit persons to whom the Software is furnished to do so, 345 | subject to the following conditions: 346 | 347 | The above copyright notice and this permission notice shall be included in all 348 | copies or substantial portions of the Software. 349 | 350 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 351 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 352 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 353 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 354 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 355 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | ====================== 2 | Django Forum TODO List 3 | ====================== 4 | 5 | :author: Jonathan Buchanan 6 | 7 | Top Priority 8 | ============ 9 | 10 | *Dirty hacks and other broken windows.* 11 | 12 | Make It So, Number One 13 | ====================== 14 | 15 | *Features, fixes and tasks which are ready to be implementated or performed as 16 | and when the time can be found.* 17 | 18 | - Non-standalone mode issues which will need to be resolved: 19 | 20 | - Provide a means of performing certain URL lookups based on whether we're in 21 | standalone mode or not. There's already a ``LOGIN_URL`` setting which we 22 | could use instead of django-registration's ``auth_login`` named URL, but 23 | what about the logout and register URLs? 24 | - I imagine it will be likely that the forum's standalone password 25 | change/reset forms will be used in favour of the admin application's any 26 | time a project is using the application directory template loader (as we 27 | require it to) - it might be necessary to add moving of the standalone 28 | registration templates into the ``/forum/templates/`` directory as one of 29 | the installation steps for using the forum in standalone mode. 30 | 31 | - Create full help templates for the provided post formatters. 32 | - Give the BBCode formatter some love - we're currently using the default set of 33 | tags it provides. Decide which we really need, which are missing and implement 34 | them if need be. 35 | - Post control for moderators - move post to another topic, merge all posts from 36 | one topic into another. 37 | - User control for moderators. 38 | 39 | *Testing.* 40 | 41 | - Write test client tests. 42 | - Testing in non-standalone mode - create a project which itself uses 43 | django-registration to ensure that the forum's set of templates don't take 44 | precedence. If the order of ``INSTALLED_APPS`` is important in this case, 45 | document it in the installation docs. 46 | 47 | For Future Consideration 48 | ======================== 49 | 50 | *Features which require a bit more consideration - as in does the application 51 | really need the added complexity they bring? It has a fairly simple structure 52 | at the moment.* 53 | 54 | *Boring stuff which most forums have:* 55 | 56 | - Subforums. 57 | - Tracking which posts were replied to / using this information to offer a 58 | threaded topic view. 59 | - Poll topics. 60 | 61 | *Possibly less boring stuff:* 62 | 63 | - Up/down voting *everywhere*, starting with users, topics and posts, each of 64 | which should have a configurable lower boundary in user profiles, scores under 65 | which would result in topics, posts or everything from given users being 66 | hidden. Let consensus be your ignore list, if you're mad enough. 67 | - Forum Types, which could affect how everything in a particular forum works. 68 | Examples: 69 | 70 | - Help Forums could have the initial post in each topic displayed on every 71 | page, with some kind of kudos point system in place, where topic starters 72 | can award points for useful answers. Uses of kudos points: 73 | 74 | 1. Total points earned could be displayed in place of Postcounts in user 75 | profiles, and they would actually *mean* something. 76 | 2. An option to view only the starting post and posts which got kudos points 77 | if you just want to see what the useful answers were. 78 | 79 | - Progressive enhancement with JavaScript: 80 | 81 | - Hijack topic and forum pagination. 82 | - Hijack posting of new replies to work on the same page. 83 | - Periodic fetching of new posts when on the last page of a topic. 84 | - Adding a new control to edit replies in place, with preview. 85 | -------------------------------------------------------------------------------- /apache/django.wsgi: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | activate_this = 'C:/virtualenvs/forum/Scripts/activate_this.py' 5 | execfile(activate_this, dict(__file__=activate_this)) 6 | 7 | os.environ['DJANGO_SETTINGS_MODULE'] = 'forum.settings' 8 | 9 | import django.core.handlers.wsgi 10 | application = django.core.handlers.wsgi.WSGIHandler() 11 | -------------------------------------------------------------------------------- /apache/httpd.conf: -------------------------------------------------------------------------------- 1 | WSGIPythonHome C:/virtualenvs/forum/ 2 | 3 | NameVirtualHost *:81 4 | 5 | 6 | ServerName insin.forum 7 | ServerAlias insin.forum 8 | 9 | WSGIScriptAlias / C:/virtualenvs/forum/src/forum/apache/django.wsgi 10 | 11 | Order allow,deny 12 | Allow from all 13 | 14 | 15 | Alias /media/forum/ C:/virtualenvs/forum/src/forum/forum/media/ 16 | 17 | Order allow,deny 18 | Allow from all 19 | 20 | 21 | Alias /admin_media/ C:/virtualenvs/forum/Lib/site-packages/django/contrib/admin/media/ 22 | 23 | Order allow,deny 24 | Allow from all 25 | 26 | -------------------------------------------------------------------------------- /forum/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 1, 0, 'alpha', 0) 2 | 3 | def get_version(): 4 | version = '%s.%s' % (VERSION[0], VERSION[1]) 5 | if VERSION[2]: 6 | version = '%s.%s' % (version, VERSION[2]) 7 | if VERSION[3:] == ('alpha', 0): 8 | version = '%s pre-alpha' % version 9 | else: 10 | if VERSION[3] != 'final': 11 | version = '%s %s %s' % (version, VERSION[3], VERSION[4]) 12 | return version 13 | -------------------------------------------------------------------------------- /forum/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from forum.models import ForumProfile, Section, Forum, Topic, Post, Search 4 | 5 | DENORMALISED_DATA_NOTICE = 'You shouldn\'t need to edit this data manually.' 6 | 7 | class ForumProfileAdmin(admin.ModelAdmin): 8 | list_display = ('user', 'group', 'title', 'location', 9 | 'post_count') 10 | list_filter = ('group',) 11 | fieldsets = ( 12 | (None, { 13 | 'fields': ('user', 'group', 'title', 'location', 'avatar', 14 | 'website'), 15 | }), 16 | ('Board settings', { 17 | 'fields': ('timezone', 'topics_per_page', 'posts_per_page', 18 | 'auto_fast_reply'), 19 | }), 20 | ('Denormalised data', { 21 | 'classes': ('collapse',), 22 | 'description': DENORMALISED_DATA_NOTICE, 23 | 'fields': ('post_count',), 24 | }), 25 | ) 26 | 27 | class SectionAdmin(admin.ModelAdmin): 28 | list_display = ('name', 'order') 29 | 30 | class ForumAdmin(admin.ModelAdmin): 31 | list_display = ('name', 'section', 'description', 'order', 32 | 'topic_count', 'locked', 'hidden') 33 | list_filter = ('section',) 34 | fieldsets = ( 35 | (None, { 36 | 'fields': ('name', 'section', 'description', 'order'), 37 | }), 38 | ('Administration', { 39 | 'fields': ('locked', 'hidden'), 40 | }), 41 | ('Denormalised data', { 42 | 'classes': ('collapse',), 43 | 'description': DENORMALISED_DATA_NOTICE, 44 | 'fields': ('topic_count', 'last_post_at', 'last_topic_id', 45 | 'last_topic_title','last_user_id', 'last_username'), 46 | }), 47 | ) 48 | search_fields = ('name',) 49 | 50 | class TopicAdmin(admin.ModelAdmin): 51 | list_display = ('title', 'forum', 'user', 'started_at', 'post_count', 52 | 'metapost_count', 'last_post_at', 'locked', 'pinned', 53 | 'hidden') 54 | list_filter = ('forum', 'locked', 'pinned', 'hidden') 55 | fieldsets = ( 56 | (None, { 57 | 'fields': ('title', 'forum', 'user', 'description'), 58 | }), 59 | ('Administration', { 60 | 'fields': ('pinned', 'locked', 'hidden'), 61 | }), 62 | ('Denormalised data', { 63 | 'classes': ('collapse',), 64 | 'description': DENORMALISED_DATA_NOTICE, 65 | 'fields': ('post_count', 'metapost_count', 'last_post_at', 66 | 'last_user_id', 'last_username'), 67 | }), 68 | ) 69 | search_fields = ('title',) 70 | 71 | class PostAdmin(admin.ModelAdmin): 72 | list_display = ('__unicode__', 'user', 'topic', 'meta', 'posted_at', 73 | 'edited_at', 'user_ip') 74 | list_filter = ('meta',) 75 | fieldsets = ( 76 | (None, { 77 | 'fields': ('user', 'topic', 'body', 'meta', 'emoticons'), 78 | }), 79 | ('Denormalised data', { 80 | 'classes': ('collapse',), 81 | 'description': DENORMALISED_DATA_NOTICE, 82 | 'fields': ('num_in_topic',), 83 | }), 84 | ) 85 | search_fields = ('body',) 86 | 87 | class SearchAdmin(admin.ModelAdmin): 88 | list_display = ('type', 'user', 'searched_at') 89 | list_filter = ('type',) 90 | 91 | admin.site.register(ForumProfile, ForumProfileAdmin) 92 | admin.site.register(Section, SectionAdmin) 93 | admin.site.register(Forum, ForumAdmin) 94 | admin.site.register(Topic, TopicAdmin) 95 | admin.site.register(Post, PostAdmin) 96 | admin.site.register(Search, SearchAdmin) 97 | -------------------------------------------------------------------------------- /forum/app_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Convenience module for access of application-specific settings, which 3 | enforces default settings when the main settings module does not contain 4 | the appropriate settings. 5 | """ 6 | from django.conf import settings 7 | 8 | STANDALONE = getattr(settings, 'FORUM_STANDALONE', False) 9 | DEFAULT_POSTS_PER_PAGE = getattr(settings, 'FORUM_DEFAULT_POSTS_PER_PAGE', 20) 10 | DEFAULT_TOPICS_PER_PAGE = getattr(settings, 'FORUM_DEFAULT_TOPICS_PER_PAGE', 30) 11 | POST_FORMATTER = getattr(settings, 'FORUM_POST_FORMATTER', 'forum.formatters.PostFormatter') 12 | MAX_AVATAR_FILESIZE = getattr(settings, 'FORUM_MAX_AVATAR_FILESIZE', 512 * 1024) 13 | ALLOWED_AVATAR_FORMATS = getattr(settings, 'FORUM_ALLOWED_AVATAR_FORMATS', ('GIF', 'JPEG', 'PNG')) 14 | MAX_AVATAR_DIMENSIONS = getattr(settings, 'FORUM_MAX_AVATAR_DIMENSIONS', (64, 64)) 15 | FORCE_AVATAR_DIMENSIONS = getattr(settings, 'FORUM_FORCE_AVATAR_DIMENSIONS', True) 16 | 17 | EMOTICONS = getattr(settings, 'FORUM_EMOTICONS', { 18 | ':angry:': 'angry.gif', 19 | ':blink:': 'blink.gif', 20 | ':D': 'grin.gif', 21 | ':huh:': 'huh.gif', 22 | ':lol:': 'lol.gif', 23 | ':o': 'ohmy.gif', 24 | ':ph34r:': 'ph34r.gif', 25 | ':rolleyes:': 'rolleyes.gif', 26 | ':(': 'sad.gif', 27 | ':)': 'smile.gif', 28 | ':p': 'tongue.gif', 29 | ':unsure:': 'unsure.gif', 30 | ':wacko:': 'wacko.gif', 31 | ';)': 'wink.gif', 32 | ':wub:': 'wub.gif', 33 | }) 34 | 35 | USE_REDIS = getattr(settings, 'FORUM_USE_REDIS', False) 36 | REDIS_HOST = getattr(settings, 'FORUM_REDIS_HOST', 'localhost') 37 | REDIS_PORT = getattr(settings, 'FORUM_REDIS_PORT', 6379) 38 | REDIS_DB = getattr(settings, 'FORUM_REDIS_DB', 0) 39 | 40 | USE_NODEJS = getattr(settings, 'FORUM_USE_NODEJS', False) 41 | -------------------------------------------------------------------------------- /forum/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions related to forum user permissions. 3 | """ 4 | from forum.models import ForumProfile 5 | 6 | def is_admin(user): 7 | """ 8 | Shortcut so we don't have to paste this incantation everywhere. 9 | 10 | Also provides a single point of change should we ever modify how 11 | user permissions are determined. 12 | """ 13 | return ForumProfile.objects.get_for_user(user).is_admin() 14 | 15 | def is_moderator(user): 16 | """ 17 | Shortcut so we don't have to paste this incantation everywhere. 18 | 19 | Also provides a single point of change should we ever modify how 20 | user permissions are determined. 21 | """ 22 | return ForumProfile.objects.get_for_user(user).is_moderator() 23 | 24 | def user_can_edit_post(user, post, topic=None): 25 | """ 26 | Returns ``True`` if the given User can edit the given Post, 27 | ``False`` otherwise. 28 | 29 | If the Post's Topic is also given, its ``locked`` status will be 30 | taken into account when determining permissions. 31 | """ 32 | if topic and topic.locked: 33 | return is_moderator(user) 34 | else: 35 | return user.id == post.user_id or is_moderator(user) 36 | 37 | def user_can_edit_topic(user, topic): 38 | """ 39 | Returns ``True`` if the given User can edit the given Topic, 40 | ``False`` otherwise. 41 | """ 42 | if topic.locked: 43 | return is_moderator(user) 44 | else: 45 | return user.id == topic.user_id or is_moderator(user) 46 | 47 | def user_can_edit_user_profile(user, user_to_edit): 48 | """ 49 | Returns ``True`` if the given User can edit the given User's 50 | profile, ``False`` otherwise. 51 | """ 52 | return user.id == user_to_edit.id or is_moderator(user) 53 | 54 | def user_can_view_search_results(user, search): 55 | """ 56 | Returns ``True`` if the given User can view the given search results, 57 | ``False`` otherwise. 58 | """ 59 | return user.id == search.user_id or is_moderator(user) 60 | -------------------------------------------------------------------------------- /forum/bin/create-test-data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Uses the ORM to create test data. 3 | """ 4 | import datetime 5 | import os 6 | import random 7 | 8 | os.environ['DJANGO_SETTINGS_MODULE'] = 'forum.settings' 9 | 10 | from django.contrib.auth.models import User 11 | from django.db import transaction 12 | 13 | from forum.models import Forum, ForumProfile, Post, Section, Topic 14 | 15 | @transaction.commit_on_success 16 | def create_test_data(): 17 | # 3 Users 18 | admin = User.objects.create_user('admin', 'a@a.com', 'admin') 19 | admin.first_name = 'Admin' 20 | admin.last_name = 'User' 21 | admin.is_staff = True 22 | admin.is_superuser = True 23 | admin.save() 24 | ForumProfile.objects.create(user=admin, group=ForumProfile.ADMIN_GROUP) 25 | 26 | moderator = User.objects.create_user('moderator', 'm@m.com', 'moderator') 27 | moderator.first_name = 'Moderator' 28 | moderator.last_name = 'User' 29 | moderator.save() 30 | ForumProfile.objects.create(user=moderator, group=ForumProfile.MODERATOR_GROUP) 31 | 32 | user = User.objects.create_user('user', 'u@u.com', 'user') 33 | user.first_name = 'Test' 34 | user.last_name = 'User' 35 | user.save() 36 | ForumProfile.objects.create(user=user, group=ForumProfile.USER_GROUP) 37 | 38 | users = [admin, moderator, user] 39 | 40 | # 3 Sections 41 | sections = [Section.objects.create(name='Section %s' % i, order=i) \ 42 | for i in xrange(1, 4)] 43 | 44 | # 3 Forums per Section 45 | forums = [] 46 | for section in sections: 47 | forums += [section.forums.create(name='Forum %s' % i, order=i) \ 48 | for i in xrange(1, 4)] 49 | 50 | # 3 Topics per Forum 51 | topics = [] 52 | for forum in forums: 53 | topics += [forum.topics.create(user=users[i-1], title='Topic %s' % i) \ 54 | for i in xrange(1, 4)] 55 | 56 | # 3 Posts per Topic 57 | for topic in topics: 58 | for i in xrange(1, 4): 59 | topic.posts.create(user=topic.user, body='Post %s' % i) 60 | 61 | # 3 Metaposts per Topic 62 | for topic in topics: 63 | for i in xrange(1, 4): 64 | topic.posts.create(user=topic.user, meta=True, body='Metapost %s' % i) 65 | 66 | if __name__ == '__main__': 67 | create_test_data() 68 | -------------------------------------------------------------------------------- /forum/formatters/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.conf import settings 4 | from django.utils.html import escape, linebreaks, urlize 5 | from django.utils.text import normalize_newlines, wrap 6 | 7 | from forum.formatters.emoticons import Emoticons 8 | 9 | quote_post_re = re.compile(r'^', re.MULTILINE) 10 | 11 | class PostFormatter(object): 12 | """ 13 | Base post formatting object. 14 | 15 | If used as a post formatter itself, performs basic formatting, 16 | preserving linebreaks and converting URLs to links. 17 | """ 18 | QUICK_HELP_TEMPLATE = 'forum/help/basic_formatting_quick.html' 19 | FULL_HELP_TEMPLATE = 'forum/help/basic_formatting.html' 20 | 21 | def __init__(self, emoticons=None): 22 | if emoticons is None: emoticons = {} 23 | self.emoticon_processor = Emoticons(emoticons, 24 | base_url='%sforum/img/emoticons/' % settings.STATIC_URL) 25 | 26 | def format_post(self, body, process_emoticons=True): 27 | """ 28 | Formats the given post body, replacing emoticon symbols with 29 | images if ``emoticons`` is ``True``. 30 | """ 31 | if process_emoticons: 32 | return self.emoticon_processor.process(self.format_post_body(body)) 33 | else: 34 | return self.format_post_body(body) 35 | 36 | def format_post_body(self, body): 37 | """ 38 | Formats the given raw post body as HTML. 39 | """ 40 | return linebreaks(urlize(escape(body.strip()))) 41 | 42 | def quote_post(self, post): 43 | """ 44 | Returns a raw post body which quotes the given Post. 45 | """ 46 | return u'%s wrote:\n\n%s\n\n' % ( 47 | escape(post.user.username), 48 | quote_post_re.sub('> ', wrap(normalize_newlines(post.body), 80)), 49 | ) 50 | 51 | class MarkdownFormatter(PostFormatter): 52 | """ 53 | Post formatter which uses Markdown to format posts. 54 | """ 55 | QUICK_HELP_TEMPLATE = 'forum/help/markdown_formatting_quick.html' 56 | FULL_HELP_TEMPLATE = 'forum/help/markdown_formatting.html' 57 | 58 | def __init__(self, *args, **kwargs): 59 | super(MarkdownFormatter, self).__init__(*args, **kwargs) 60 | from markdown2 import Markdown 61 | self.md = Markdown(safe_mode='escape') 62 | 63 | def format_post_body(self, body): 64 | """ 65 | Formats the given raw post body as HTML using Markdown 66 | formatting. 67 | """ 68 | self.md.reset() 69 | return self.md.convert(body).strip() 70 | 71 | def quote_post(self, post): 72 | """ 73 | Returns a raw post body which quotes the given Post using 74 | Markdown syntax. 75 | """ 76 | return u'**%s** [wrote](%s "View quoted post"):\n\n%s\n\n' % ( 77 | escape(post.user.username), 78 | post.get_absolute_url(), 79 | quote_post_re.sub('> ', post.body), 80 | ) 81 | 82 | class BBCodeFormatter(PostFormatter): 83 | """ 84 | Post formatter which uses BBCode syntax to format posts. 85 | """ 86 | QUICK_HELP_TEMPLATE = 'forum/help/bbcode_formatting_quick.html' 87 | FULL_HELP_TEMPLATE = 'forum/help/bbcode_formatting.html' 88 | 89 | def __init__(self, *args, **kwargs): 90 | super(BBCodeFormatter, self).__init__(*args, **kwargs) 91 | import postmarkup 92 | self.pm = postmarkup.create() 93 | 94 | def format_post_body(self, body): 95 | """ 96 | Formats the given raw post body as HTML using BBCode formatting. 97 | """ 98 | return self.pm(body).strip() 99 | 100 | def quote_post(self, post): 101 | """ 102 | Returns a raw post body which quotes the given Post using BBCode 103 | syntax. 104 | """ 105 | return u'[quote="%s"]%s[/quote]' % (escape(post.user.username), post.body) 106 | 107 | def get_post_formatter(): 108 | """ 109 | Creates a post formatting object as specified by current settings. 110 | """ 111 | from django.core import exceptions 112 | from forum import app_settings 113 | try: 114 | dot = app_settings.POST_FORMATTER.rindex('.') 115 | except ValueError: 116 | raise exceptions.ImproperlyConfigured, '%s isn\'t a post formatting module' % app_settings.POST_FORMATTER 117 | modulename, classname = app_settings.POST_FORMATTER[:dot], app_settings.POST_FORMATTER[dot+1:] 118 | try: 119 | mod = __import__(modulename, {}, {}, ['']) 120 | except ImportError, e: 121 | raise exceptions.ImproperlyConfigured, 'Error importing post formatting module %s: "%s"' % (modulename, e) 122 | try: 123 | formatter_class = getattr(mod, classname) 124 | except AttributeError: 125 | raise exceptions.ImproperlyConfigured, 'Post formatting module "%s" does not define a "%s" class' % (modulename, classname) 126 | return formatter_class(emoticons=app_settings.EMOTICONS) 127 | 128 | # For convenience, make a single instance of the currently specified post 129 | # formatter available for reuse. 130 | post_formatter = get_post_formatter() 131 | -------------------------------------------------------------------------------- /forum/formatters/emoticons.py: -------------------------------------------------------------------------------- 1 | """ 2 | Emoticon Replacement 3 | ==================== 4 | 5 | Converts emoticon symbols to images, with the symbols set as their 6 | ``alt`` text. 7 | 8 | Basic usage:: 9 | 10 | >>> em = Emoticons({':p': 'tongue.gif'}) 11 | >>> em.process(u'Cheeky :p') 12 | u'Cheeky :p' 13 | 14 | Example showing usage of all arguments:: 15 | 16 | >>> em = Emoticons({':p': 'cheeky.png'}, 17 | ... base_url='http://localhost/', xhtml=True) 18 | >>> em.process(u'Cheeky :p') 19 | u'Cheeky :p' 20 | 21 | Other tests:: 22 | 23 | >>> em = Emoticons({}) 24 | >>> em.process(u'Cheeky :p') 25 | u'Cheeky :p' 26 | 27 | """ 28 | import re 29 | 30 | class Emoticons: 31 | """ 32 | Replacement of multiple string pairs in one go based on 33 | http://effbot.org/zone/python-replace.htm 34 | """ 35 | def __init__ (self, emoticons, base_url='', xhtml=False): 36 | """ 37 | emoticons 38 | A dict mapping emoticon symbols to image filenames. 39 | 40 | base_url 41 | The base URL to be prepended to image filenames when 42 | generating image URLs. 43 | 44 | xhtml 45 | If ``True``, a closing slash will be added to image tags. 46 | """ 47 | self.emoticons = dict( 48 | [(k, '%s' % (base_url, v, k, 49 | xhtml and ' /' or '')) \ 50 | for k, v in emoticons.items()]) 51 | keys = emoticons.keys() 52 | keys.sort() # lexical order 53 | keys.reverse() # use longest match first 54 | self.pattern = re.compile('|'.join([re.escape(key) for key in keys])) 55 | 56 | def process(self, text): 57 | """ 58 | Returns a version of the given text with emoticon symbols 59 | replaced with HTML for their image equivalents. 60 | 61 | text 62 | The text to be processed. 63 | """ 64 | def repl(match, get=self.emoticons.get): 65 | item = match.group(0) 66 | return get(item, item) 67 | return self.pattern.sub(repl, text) 68 | 69 | if __name__ == '__main__': 70 | import doctest 71 | doctest.testmod() 72 | -------------------------------------------------------------------------------- /forum/forms.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import operator 3 | import urllib 4 | 5 | from django import forms 6 | from django.db.models.query_utils import Q 7 | from django.forms.models import modelform_factory 8 | from django.template.defaultfilters import filesizeformat 9 | from django.utils.text import capfirst, get_text_list, smart_split 10 | 11 | from forum import app_settings 12 | from forum.models import Forum, ForumProfile, Post, Search, Section, Topic 13 | 14 | # Try to import PIL in either of the two ways it can end up installed. 15 | try: 16 | from PIL import ImageFile as PILImageFile 17 | except ImportError: 18 | import ImageFile as PILImageFile 19 | 20 | class AddSectionForm(forms.Form): 21 | """ 22 | Form for adding a new Section - takes and existing Section it should 23 | be inserted before. 24 | """ 25 | name = forms.CharField(max_length=100) 26 | section = forms.ChoiceField(required=False) 27 | 28 | def __init__(self, sections, *args, **kwargs): 29 | super(AddSectionForm, self).__init__(*args, **kwargs) 30 | self.sections = sections 31 | self.fields['section'].choices = [('', '----------')] + \ 32 | [(section.id, section.name) for section in sections] 33 | 34 | def clean_name(self): 35 | """Validates that the section name is unique.""" 36 | for section in self.sections: 37 | if self.cleaned_data['name'] == section.name: 38 | raise forms.ValidationError('A Section with this name already exists.') 39 | return self.cleaned_data['name'] 40 | 41 | class EditSectionForm(forms.ModelForm): 42 | """Form for editing a Section.""" 43 | class Meta: 44 | model = Section 45 | fields = ('name',) 46 | 47 | def clean_name(self): 48 | """Validates that the section name is unique if it has changed.""" 49 | if self.fields['name'].initial != self.cleaned_data['name']: 50 | try: 51 | Section.objects.get(name=self.cleaned_data['name']) 52 | raise forms.ValidationError('A Section with this name already exists.') 53 | except Section.DoesNotExist: 54 | pass 55 | return self.cleaned_data['name'] 56 | 57 | class AddForumForm(forms.Form): 58 | """ 59 | Form for adding a new Forum - takes an existing Forum it should be 60 | inserted before. 61 | """ 62 | name = forms.CharField(max_length=100) 63 | description = forms.CharField(max_length=100, required=False, widget=forms.Textarea()) 64 | forum = forms.ChoiceField(required=False) 65 | 66 | def __init__(self, forums, *args, **kwargs): 67 | super(AddForumForm, self).__init__(*args, **kwargs) 68 | self.fields['forum'].choices = [('', '----------')] + \ 69 | [(forum.id, forum.name) for forum in forums] 70 | 71 | class EditForumForm(forms.ModelForm): 72 | """Form for editing a Forum.""" 73 | class Meta: 74 | model = Forum 75 | fields = ('name', 'description') 76 | 77 | def topic_formfield_callback(field, **kwargs): 78 | """ 79 | Callback for Post form field creation. 80 | 81 | Customises the size of the widgets used to edit topic details. 82 | """ 83 | if field.name in ['title', 'description']: 84 | formfield = field.formfield(**kwargs) 85 | formfield.widget.attrs['size'] = 50 86 | return formfield 87 | else: 88 | return field.formfield(**kwargs) 89 | 90 | class AddTopicForm(forms.ModelForm): 91 | """Form for adding a new Topic.""" 92 | formfield_callback = topic_formfield_callback 93 | 94 | class Meta: 95 | model = Topic 96 | fields = ('title', 'description') 97 | 98 | class EditTopicForm(forms.ModelForm): 99 | """Form for editing a Topic.""" 100 | formfield_callback = topic_formfield_callback 101 | 102 | class Meta: 103 | model = Topic 104 | fields = ('title', 'description', 'pinned', 'locked', 'hidden') 105 | 106 | def __init__(self, moderate, *args, **kwargs): 107 | super(EditTopicForm, self).__init__(*args, **kwargs) 108 | if not moderate: 109 | del self.fields['pinned'] 110 | del self.fields['locked'] 111 | del self.fields['hidden'] 112 | 113 | def post_formfield_callback(field, **kwargs): 114 | """ 115 | Callback for Post form field creation. 116 | 117 | Customises the widget used to edit posts. 118 | """ 119 | if field.name == 'body': 120 | formfield = field.formfield(**kwargs) 121 | formfield.widget.attrs['rows'] = 14 122 | formfield.widget.attrs['cols'] = 70 123 | return formfield 124 | else: 125 | return field.formfield(**kwargs) 126 | 127 | class TopicPostForm(forms.ModelForm): 128 | """Form for the initial Post in a new Topic.""" 129 | formfield_callback = post_formfield_callback 130 | 131 | class Meta: 132 | model = Post 133 | fields = ('body', 'emoticons') 134 | 135 | class ReplyForm(forms.ModelForm): 136 | """Form for a reply Post.""" 137 | formfield_callback = post_formfield_callback 138 | 139 | class Meta: 140 | model = Post 141 | fields = ('body', 'emoticons', 'meta') 142 | 143 | def __init__(self, meta, *args, **kwargs): 144 | super(ReplyForm, self).__init__(*args, **kwargs) 145 | if not meta: 146 | del self.fields['meta'] 147 | 148 | class SearchForm(forms.Form): 149 | """ 150 | Criteria for searching Topics or Posts. 151 | 152 | Creates a QuerySet based on selected criteria. 153 | """ 154 | SEARCH_ALL_FORUMS = 'A' 155 | SEARCH_IN_SECTION = 'S' 156 | SEARCH_IN_FORUM = 'F' 157 | 158 | SEARCH_ALL_POSTS = 'A' 159 | SEARCH_REGULAR_POSTS = 'R' 160 | SEARCH_METAPOSTS = 'M' 161 | SEARCH_POST_TYPE_CHOICES = ( 162 | (SEARCH_ALL_POSTS, 'All Posts'), 163 | (SEARCH_REGULAR_POSTS, 'Regular Posts'), 164 | (SEARCH_METAPOSTS, 'Metaposts'), 165 | ) 166 | 167 | SEARCH_FROM_TODAY = 'T' 168 | SEARCH_ANY_DATE = 'A' 169 | SEARCH_FROM_CHOICES = ( 170 | (SEARCH_FROM_TODAY, 'Today and...'), 171 | (7, '7 days ago and...'), 172 | (30, '30 days ago and...'), 173 | (60, '60 days ago and...'), 174 | (90, '90 days ago and...'), 175 | (180, '180 days ago and...'), 176 | (365, '365 days ago and...'), 177 | (SEARCH_ANY_DATE, 'Any date'), 178 | ) 179 | 180 | SEARCH_OLDER = 'O' 181 | SEARCH_NEWER = 'N' 182 | SEARCH_WHEN_CHOICES = ( 183 | (SEARCH_OLDER, 'Older'), 184 | (SEARCH_NEWER, 'Newer'), 185 | ) 186 | SEARCH_WHEN_LOOKUP = { 187 | SEARCH_OLDER: 'lte', 188 | SEARCH_NEWER: 'gte', 189 | } 190 | 191 | SORT_DESCENDING = 'D' 192 | SORT_ASCENDING = 'A' 193 | SORT_DIRECTION_CHOICES = ( 194 | (SORT_DESCENDING, 'Descending'), 195 | (SORT_ASCENDING, 'Ascending'), 196 | ) 197 | SORT_DIRECTION_FLAG = { 198 | SORT_DESCENDING: '-', 199 | SORT_ASCENDING: '', 200 | } 201 | 202 | USERNAME_LOOKUP = {True: '', False: '__icontains'} 203 | 204 | search_type = forms.ChoiceField(choices=Search.TYPE_CHOICES, initial=Search.POST_SEARCH, widget=forms.RadioSelect) 205 | keywords = forms.CharField() 206 | username = forms.CharField(required=False) 207 | exact_username = forms.BooleanField(required=False, initial=True, label='Match exact username') 208 | post_type = forms.ChoiceField(choices=SEARCH_POST_TYPE_CHOICES, initial=SEARCH_ALL_POSTS, widget=forms.RadioSelect) 209 | search_in = forms.MultipleChoiceField(required=False, initial=[SEARCH_ALL_FORUMS]) 210 | search_from = forms.ChoiceField(choices=SEARCH_FROM_CHOICES) 211 | search_when = forms.ChoiceField(choices=SEARCH_WHEN_CHOICES, initial=SEARCH_OLDER, widget=forms.RadioSelect) 212 | sort_direction = forms.ChoiceField(choices=SORT_DIRECTION_CHOICES, initial=SORT_DESCENDING, widget=forms.RadioSelect) 213 | 214 | def __init__(self, *args, **kwargs): 215 | super(SearchForm, self).__init__(*args, **kwargs) 216 | choices = [(self.SEARCH_ALL_FORUMS, 'All Forums')] 217 | for section, forums in Section.objects.get_forums_by_section(): 218 | choices.append(('%s.%s' % (self.SEARCH_IN_SECTION, section.pk), 219 | section.name)) 220 | choices.extend([('%s.%s' % (self.SEARCH_IN_FORUM, forum.pk), 221 | '|-- %s' % forum.name) \ 222 | for forum in forums]) 223 | self.fields['search_in'].choices = choices 224 | self.fields['search_in'].widget.attrs['size'] = 10 225 | 226 | def clean_keywords(self): 227 | """ 228 | Validates that no search keyword is shorter than 3 characters. 229 | """ 230 | for keyword in smart_split(self.cleaned_data['keywords']): 231 | keyword_len = len(keyword) 232 | if keyword[0] in ('+', '-'): 233 | keyword_len = keyword_len - 1 234 | elif keyword[0] == '"' and keyword[-1] == '"' or \ 235 | keyword[0] == "'" and keyword[-1] == "'": 236 | keyword_len = keyword_len - 2 237 | if keyword_len < 3: 238 | raise forms.ValidationError('Keywords must be a minimun of 3 characters long.') 239 | return self.cleaned_data['keywords'] 240 | 241 | def get_queryset(self): 242 | """ 243 | Creates a ``QuerySet`` based on the search criteria specified in 244 | this form. 245 | 246 | Returns ``None`` if the form doesn't appear to have been 247 | validated. 248 | """ 249 | if not hasattr(self, 'cleaned_data'): 250 | return None 251 | 252 | search_type = self.cleaned_data['search_type'] 253 | filters = [] 254 | 255 | # Calculate certain lookup values based on criteria 256 | search_in = {} 257 | if len(self.cleaned_data['search_in']) and \ 258 | self.SEARCH_ALL_FORUMS not in self.cleaned_data['search_in']: 259 | for item in self.cleaned_data['search_in']: 260 | bits = item.split('.') 261 | search_in.setdefault(bits[0], []).append(bits[1]) 262 | 263 | from_date = None 264 | if self.cleaned_data['search_from'] != self.SEARCH_ANY_DATE: 265 | from_date = datetime.date.today() 266 | if self.cleaned_data['search_from'] != self.SEARCH_FROM_TODAY: 267 | days_ago = int(self.cleaned_data['search_from']) 268 | from_date = from_date - datetime.timedelta(days=days_ago) 269 | if self.cleaned_data['search_when'] == self.SEARCH_OLDER: 270 | # Less-than date searches should compare to midnight on 271 | # the following day. 272 | from_date = from_date + datetime.timedelta(days=1) 273 | 274 | # Some lookup fields which change based on the search type 275 | if search_type == Search.POST_SEARCH: 276 | section_lookup = 'topic__forum__section' 277 | forum_lookup = 'topic__forum' 278 | date_lookup = 'posted_at' 279 | text_lookup = 'body' 280 | # Searching should not give the user access to Posts in 281 | # hidden Topics. 282 | filters.append(Q(topic__hidden=False)) 283 | else: 284 | section_lookup = 'forum__section' 285 | forum_lookup = 'forum' 286 | date_lookup = 'started_at' 287 | text_lookup = 'title' 288 | # Searching should not give the user access to hidden Topics 289 | filters.append(Q(hidden=False)) 290 | 291 | # Create lookup filters 292 | if search_type == Search.POST_SEARCH and \ 293 | self.cleaned_data['post_type'] != self.SEARCH_ALL_POSTS: 294 | meta = self.cleaned_data['post_type'] == self.SEARCH_METAPOSTS 295 | filters.append(Q(meta=meta)) 296 | 297 | if self.SEARCH_IN_SECTION in search_in and \ 298 | self.SEARCH_IN_FORUM in search_in: 299 | filters.append(Q(**{'%s__in' % section_lookup: search_in[self.SEARCH_IN_SECTION]}) | \ 300 | Q(**{'%s__in' % forum_lookup: search_in[self.SEARCH_IN_FORUM]})) 301 | elif self.SEARCH_IN_SECTION in search_in: 302 | filters.append(Q(**{'%s__in' % section_lookup: search_in[self.SEARCH_IN_SECTION]})) 303 | elif self.SEARCH_IN_FORUM in search_in: 304 | filters.append(Q(**{'%s__in' % forum_lookup: search_in[self.SEARCH_IN_FORUM]})) 305 | 306 | if from_date is not None: 307 | lookup_type = self.SEARCH_WHEN_LOOKUP[self.cleaned_data['search_when']] 308 | filters.append(Q(**{'%s__%s' % (date_lookup, lookup_type): from_date})) 309 | 310 | if self.cleaned_data['username']: 311 | lookup_type = self.USERNAME_LOOKUP[self.cleaned_data['exact_username']] 312 | filters.append(Q(**{'user__username%s' % lookup_type: \ 313 | self.cleaned_data['username']})) 314 | 315 | one_of_filters = [] 316 | phrase_filters = [] 317 | for keyword in smart_split(self.cleaned_data['keywords']): 318 | if keyword[0] == '+': 319 | filters.append(Q(**{'%s__icontains' % text_lookup: keyword[1:]})) 320 | elif keyword[0] == '-': 321 | filters.append(~Q(**{'%s__icontains' % text_lookup: keyword[1:]})) 322 | elif keyword[0] == '"' and keyword[-1] == '"' or \ 323 | keyword[0] == "'" and keyword[-1] == "'": 324 | phrase_filters.append(Q(**{'%s__icontains' % text_lookup: keyword[1:-1]})) 325 | else: 326 | one_of_filters.append(Q(**{'%s__icontains' % text_lookup: keyword})) 327 | if one_of_filters: 328 | filters.append(reduce(operator.or_, one_of_filters)) 329 | if phrase_filters: 330 | filters.append(reduce(operator.or_, phrase_filters)) 331 | 332 | # Apply filters and perform ordering 333 | if search_type == Search.POST_SEARCH: 334 | qs = Post.objects.all() 335 | else: 336 | qs = Topic.objects.all() 337 | if len(filters): 338 | qs = qs.filter(reduce(operator.and_, filters)) 339 | sort_direction_flag = \ 340 | self.SORT_DIRECTION_FLAG[self.cleaned_data['sort_direction']] 341 | return qs.order_by('%s%s' % (sort_direction_flag, date_lookup), 342 | '%sid' % sort_direction_flag) 343 | 344 | class ImageURLField(forms.URLField): 345 | """ 346 | A URL field specifically for images, which can validate details 347 | about the filesize, dimensions and format of an image at a given 348 | URL. 349 | 350 | Specifying any of the following arguments will result in the 351 | appropriate validation of image details, retrieved from the URL 352 | specified in this field: 353 | 354 | max/min_filesize 355 | An integer specifying an image filesize limit, in bytes. 356 | 357 | max/min_width 358 | An integer specifying an image width limit, in pixels. 359 | 360 | max/min_height 361 | An integer specifying an image height limit, in pixels. 362 | 363 | image_formats 364 | A list of image formats to be accepted, specified as uppercase 365 | strings. 366 | 367 | For a list of valid image formats, see the "Image File Formats" 368 | section of the `Python Imaging Library Handbook`_. 369 | 370 | .. _`Python Imaging Library Handbook`: http://www.pythonware.com/library/pil/handbook/ 371 | """ 372 | def __init__(self, max_filesize=None, min_filesize=None, max_width=None, 373 | min_width=None, max_height=None, min_height=None, image_formats=None, 374 | *args, **kwargs): 375 | super(ImageURLField, self).__init__(*args, **kwargs) 376 | self.max_filesize, self.min_filesize = max_filesize, min_filesize 377 | self.max_width, self.min_width = max_width, min_width 378 | self.max_height, self.min_height = max_height, min_height 379 | self.image_formats = image_formats 380 | self.validate_image = \ 381 | max_filesize is not None or min_filesize is not None or \ 382 | max_width is not None or min_width is not None or \ 383 | max_height is not None or min_height is not None or \ 384 | image_formats is not None 385 | 386 | def validate(self, value): 387 | super(ImageURLField, self).validate(value) 388 | 389 | if value == '' or not self.validate_image: 390 | return 391 | 392 | try: 393 | filesize, dimensions, format = self._get_image_details(value) 394 | if dimensions is None or format is None: 395 | raise forms.ValidationError( 396 | 'Could not retrieve image details from this URL.') 397 | if self.max_filesize is not None and filesize > self.max_filesize: 398 | raise forms.ValidationError( 399 | 'The image at this URL is %s large - it must be at most %s.' % ( 400 | filesizeformat(filesize), filesizeformat(self.max_filesize))) 401 | if self.min_filesize is not None and filesize < self.min_filesize: 402 | raise forms.ValidationError( 403 | 'The image at this URL is %s large - it must be at least %s.' % ( 404 | filesizeformat(filesize), filesizeformat(self.min_filesize))) 405 | if self.max_width is not None and dimensions[0] > self.max_width: 406 | raise forms.ValidationError( 407 | 'The image at this URL is %s pixels wide - it must be at most %s pixels.' % ( 408 | dimensions[0], self.max_width)) 409 | if self.min_width is not None and dimensions[0] < self.min_width: 410 | raise forms.ValidationError( 411 | 'The image at this URL is %s pixels wide - it must be at least %s pixels.' % ( 412 | dimensions[0], self.min_width)) 413 | if self.max_height is not None and dimensions[1] > self.max_height: 414 | raise forms.ValidationError( 415 | 'The image at this URL is %s pixels high - it must be at most %s pixels.' % ( 416 | dimensions[1], self.max_height)) 417 | if self.min_height is not None and dimensions[1] < self.min_height: 418 | raise forms.ValidationError( 419 | 'The image at this URL is %s pixels high - it must be at least %s pixels.' % ( 420 | dimensions[1], self.min_height)) 421 | if self.image_formats is not None and format not in self.image_formats: 422 | raise forms.ValidationError( 423 | 'The image at this URL is in %s format - %s %s.' % ( 424 | format, 425 | len(self.image_formats) == 1 and 'the only accepted format is' or 'accepted formats are', 426 | get_text_list(self.image_formats))) 427 | except IOError: 428 | raise forms.ValidationError('Could not load an image from this URL.') 429 | return value 430 | 431 | def _get_image_details(self, url): 432 | """ 433 | Retrieves details about the image accessible at the given URL, 434 | returning a 3-tuple of (filesize, image dimensions (width, 435 | height) and image format), or (filesize, ``None``, ``None``) if 436 | image details could not be determined. 437 | 438 | The Python Imaging Library is used to parse the image in chunks 439 | in order to determine its dimension and format details without 440 | having to load the entire image into memory. 441 | 442 | Adapted from http://effbot.org/zone/pil-image-size.htm 443 | """ 444 | file = urllib.urlopen(url) 445 | filesize = file.headers.get('content-length') 446 | if filesize: filesize = int(filesize) 447 | p = PILImageFile.Parser() 448 | while 1: 449 | data = file.read(1024) 450 | if not data: 451 | break 452 | p.feed(data) 453 | if p.image: 454 | return filesize, p.image.size, p.image.format 455 | break 456 | file.close() 457 | return filesize, None, None 458 | 459 | def forum_profile_formfield_callback(field, **kwargs): 460 | """ 461 | Callback for forum profile form field creation. 462 | 463 | Generates an ``ImageURLField`` for the ``avatar`` field and default 464 | fields for all others. 465 | """ 466 | if field.name == 'avatar': 467 | args = { 468 | 'verify_exists': field.validators[-1].verify_exists, # TODO Make nice 469 | 'max_length': field.max_length, 470 | 'required': not field.blank, 471 | 'label': capfirst(field.verbose_name), 472 | 'help_text': field.help_text, 473 | } 474 | if app_settings.MAX_AVATAR_FILESIZE is not None: 475 | args['max_filesize'] = app_settings.MAX_AVATAR_FILESIZE 476 | if app_settings.ALLOWED_AVATAR_FORMATS is not None: 477 | args['image_formats'] = app_settings.ALLOWED_AVATAR_FORMATS 478 | if app_settings.MAX_AVATAR_DIMENSIONS is not None: 479 | args['max_width'] = app_settings.MAX_AVATAR_DIMENSIONS[0] 480 | args['max_height'] = app_settings.MAX_AVATAR_DIMENSIONS[1] 481 | args.update(kwargs) 482 | return ImageURLField(**args) 483 | else: 484 | return field.formfield(**kwargs) 485 | 486 | class UserProfileForm(forms.ModelForm): 487 | """Form for editing the user profile fields in a ForumProfile.""" 488 | formfield_callback = forum_profile_formfield_callback 489 | 490 | class Meta: 491 | model = ForumProfile 492 | fields = ('title', 'location', 'avatar', 'website') 493 | 494 | def __init__(self, can_edit_title, *args, **kwargs): 495 | super(UserProfileForm, self).__init__(*args, **kwargs) 496 | if not can_edit_title: 497 | del self.fields['title'] 498 | 499 | class ForumSettingsForm(forms.ModelForm): 500 | """Form for editing the board setting fields in a ForumProfile.""" 501 | class Meta: 502 | model = ForumProfile 503 | fields = ('timezone', 'topics_per_page', 'posts_per_page', 'auto_fast_reply') 504 | -------------------------------------------------------------------------------- /forum/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | 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(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /forum/moderation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions which perform moderation tasks - this can involve making 3 | multiple, complex changes to the items being moderated. 4 | """ 5 | from forum.models import Post 6 | 7 | def _update_num_in_topic(post, topic): 8 | """ 9 | Updates the ``num_in_topic`` field for Posts in the given Topic 10 | affected by the given Post having its ``meta`` flag changed. 11 | """ 12 | # Decrement num_in_topic for the post's current meta type 13 | Post.objects.update_num_in_topic(topic, post.num_in_topic, increment=False, 14 | meta=not post.meta) 15 | try: 16 | # Find the first prior post of the new meta type by post time - 17 | # fall back on id if times are equal. 18 | previous_post = \ 19 | (topic.posts.filter(meta=post.meta, posted_at__lt=post.posted_at) | \ 20 | topic.posts.filter(meta=post.meta, posted_at=post.posted_at, pk__lt=post.pk)) \ 21 | .order_by('-posted_at', '-id')[0] 22 | post.num_in_topic = previous_post.num_in_topic + 1 23 | increment_from = previous_post.num_in_topic 24 | except IndexError: 25 | # There is no existing, earlier post - make this the first 26 | post.num_in_topic = 1 27 | increment_from = 0 28 | # Increment num_in_topic for the post's new meta type 29 | Post.objects.update_num_in_topic(topic, increment_from, increment=True, 30 | meta=post.meta) 31 | # Save the post to update its meta and num_in_topic attributes 32 | post.save() 33 | 34 | def make_post_not_meta(post, topic, forum): 35 | """ 36 | Performs changes required to turn a metapost into a regular post. 37 | """ 38 | # If this becomes the new last post in its topic, the topic will need 39 | # its last post details updated. 40 | is_last_in_topic = False 41 | if post.posted_at > topic.last_post_at: 42 | is_last_in_topic = True 43 | # If this becomes the new last post in its forum, the forum will need 44 | # its last post details updated. 45 | is_last_in_forum = False 46 | if post.posted_at > forum.last_post_at: 47 | is_last_in_forum = True 48 | # Update num_in_topic for all affected posts 49 | _update_num_in_topic(post, topic) 50 | # Make any changes required to the topic and forum 51 | if is_last_in_topic: 52 | topic.set_last_post(post) 53 | else: 54 | topic.update_post_count(meta=False) 55 | topic.update_post_count(meta=True) 56 | if is_last_in_forum: 57 | forum.set_last_post(post) 58 | 59 | def make_post_meta(post, topic, forum): 60 | """ 61 | Performs changes required to turn a regular post into a metapost. 62 | """ 63 | # If this was the last post in its topic, the topic will need its 64 | # last post details updated. 65 | was_last_in_topic = False 66 | if topic.last_post_at == post.posted_at and \ 67 | topic.last_user_id == post.user_id: 68 | was_last_in_topic = True 69 | # If this was the last post in its forum, the forum will need its 70 | # latest post details updated. 71 | was_last_in_forum = False 72 | if forum.last_topic_id == topic.pk and \ 73 | forum.last_post_at == post.posted_at and \ 74 | forum.last_user_id == post.user_id: 75 | was_last_in_forum = True 76 | # Update num_in_topic for all affected posts 77 | _update_num_in_topic(post, topic) 78 | # Make any changes required to the topic and forum 79 | if was_last_in_topic: 80 | topic.set_last_post() 81 | else: 82 | topic.update_post_count(meta=False) 83 | topic.update_post_count(meta=True) 84 | if was_last_in_forum: 85 | forum.set_last_post() 86 | -------------------------------------------------------------------------------- /forum/redis_connection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Connection and forum API for real-time tracking using Redis. 3 | """ 4 | import datetime 5 | import time 6 | 7 | from django.core import signals 8 | from django.utils.html import escape 9 | 10 | import redis 11 | from forum import app_settings 12 | 13 | r = redis.StrictRedis(app_settings.REDIS_HOST, 14 | app_settings.REDIS_PORT, 15 | app_settings.REDIS_DB) 16 | 17 | TOPIC_ViEWS = 't:%s:v' 18 | TOPIC_TRACKER = 'u:%s:t:%s' 19 | ACTIVE_USERS = 'au' 20 | USER_USERNAME = 'u:%s:un' 21 | USER_LAST_SEEN = 'u:%s:s' 22 | USER_DOING = 'u:%s:d' 23 | 24 | def increment_view_count(topic): 25 | """Increments the view count for a Topic.""" 26 | r.incr(TOPIC_ViEWS % topic.pk) 27 | 28 | def get_view_counts(topic_ids): 29 | """Yields viewcounts for the given Topics.""" 30 | for view_count in r.mget([TOPIC_ViEWS % id for id in topic_ids]): 31 | if view_count: 32 | yield int(view_count) 33 | else: 34 | yield 0 35 | 36 | def update_last_read_time(user, topic): 37 | """ 38 | Sets the last read time for a User in the given Topic, expiring in a 39 | fortnight. 40 | """ 41 | key = TOPIC_TRACKER % (user.pk, topic.pk) 42 | last_read = datetime.datetime.now() 43 | expire_at = last_read + datetime.timedelta(days=14) 44 | r.set(key, int(time.mktime(last_read.timetuple()))) 45 | r.expireat(key, int(time.mktime(expire_at.timetuple()))) 46 | 47 | def get_last_read_time(user, topic_id): 48 | """Gets the last read time for a User in the given Topic.""" 49 | last_read = r.get(TOPIC_TRACKER % (user.pk, topic_id)) 50 | if last_read: 51 | return datetime.datetime.fromtimestamp(int(last_read)) 52 | return None 53 | 54 | def get_last_read_times(user, topics): 55 | """Gets last read times for a User in the given Topics.""" 56 | for last_read in r.mget([TOPIC_TRACKER % (user.pk, t.pk) 57 | for t in topics]): 58 | if last_read: 59 | yield datetime.datetime.fromtimestamp(int(last_read)) 60 | else: 61 | yield None 62 | 63 | def seen_user(user, doing, item=None): 64 | """ 65 | Stores what a User was doing when they were last seen and updates 66 | their last seen time in the active users sorted set. 67 | """ 68 | last_seen = int(time.mktime(datetime.datetime.now().timetuple())) 69 | r.zadd(ACTIVE_USERS, last_seen, user.pk) 70 | r.setnx(USER_USERNAME % user.pk, user.username) 71 | r.set(USER_LAST_SEEN % user.pk, last_seen) 72 | if item: 73 | doing = '%s %s' % ( 74 | doing, item.get_absolute_url(), escape(str(item))) 75 | r.set(USER_DOING % user.pk, doing) 76 | 77 | def get_active_users(minutes_ago=30): 78 | """ 79 | Yields active Users in the last ``minutes_ago`` minutes, returning 80 | 2-tuples of (user_detail_dict, last_seen_time) in most-to-least recent 81 | order by time. 82 | """ 83 | since = datetime.datetime.now() - datetime.timedelta(minutes=minutes_ago) 84 | since_time = int(time.mktime(since.timetuple())) 85 | for user_id, last_seen in reversed(r.zrangebyscore(ACTIVE_USERS, since_time, 86 | 'inf', withscores=True)): 87 | yield ( 88 | {'id': int(user_id), 'username': r.get(USER_USERNAME % user_id)}, 89 | datetime.datetime.fromtimestamp(int(last_seen)), 90 | ) 91 | 92 | def get_last_seen(user): 93 | """ 94 | Returns a 2-tuple of (last_seen, doing), where doing may contain HTML 95 | linking to the relevant place. 96 | """ 97 | last_seen = r.get(USER_LAST_SEEN % user.pk) 98 | if last_seen: 99 | last_seen = datetime.datetime.fromtimestamp(int(last_seen)) 100 | else: 101 | last_seen = user.date_joined 102 | doing = r.get(USER_DOING % user.pk) 103 | return last_seen, doing 104 | -------------------------------------------------------------------------------- /forum/sessions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/sessions/__init__.py -------------------------------------------------------------------------------- /forum/sessions/redis_session_backend.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sessions.backends.base import SessionBase, CreateError 2 | 3 | from forum.redis_connection import r 4 | 5 | class SessionStore(SessionBase): 6 | """ 7 | Implements a Redis session store. 8 | """ 9 | def load(self): 10 | session_data = r.get('session:%s' % self.session_key) 11 | if session_data is not None: 12 | return self.decode(session_data) 13 | self.create() 14 | return {} 15 | 16 | def exists(self, session_key): 17 | if r.exists('session:%s' % session_key): 18 | return True 19 | return False 20 | 21 | def create(self): 22 | while True: 23 | self.session_key = self._get_new_session_key() 24 | try: 25 | self.save(must_create=True) 26 | except CreateError: 27 | continue 28 | self.modified = True 29 | return 30 | 31 | def save(self, must_create=False): 32 | key = 'session:%s' % self.session_key 33 | if must_create and r.exists(key): 34 | raise CreateError 35 | r.set(key, self.encode(self._get_session(no_load=must_create))) 36 | r.expire(key, self.get_expiry_age()) 37 | 38 | def delete(self, session_key=None): 39 | if session_key is None: 40 | if self._session_key is None: 41 | return 42 | session_key = self._session_key 43 | r.delete('session:%s' % session_key) 44 | -------------------------------------------------------------------------------- /forum/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for using the forum application as a standalone project. 2 | 3 | import os 4 | DIRNAME = os.path.dirname(__file__) 5 | 6 | DEBUG = True 7 | TEMPLATE_DEBUG = DEBUG 8 | 9 | ADMINS = ( 10 | # ('Your Name', 'your_email@domain.com'), 11 | ) 12 | 13 | MANAGERS = ADMINS 14 | 15 | INTERNAL_IPS = ('127.0.0.1',) 16 | 17 | DATABASES = { 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 20 | 'NAME': os.path.join(DIRNAME, 'database.db'), # Or path to database file if using sqlite3. 21 | 'USER': '', # Not used with sqlite3. 22 | 'PASSWORD': '', # Not used with sqlite3. 23 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 24 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 25 | } 26 | } 27 | 28 | # Local time zone for this installation. Choices can be found here: 29 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 30 | # although not all choices may be available on all operating systems. 31 | # On Unix systems, a value of None will cause Django to use the same 32 | # timezone as the operating system. 33 | # If running in a Windows environment this must be set to the same as your 34 | # system time zone. 35 | TIME_ZONE = 'Europe/Belfast' 36 | 37 | # Language code for this installation. All choices can be found here: 38 | # http://www.i18nguy.com/unicode/language-identifiers.html 39 | LANGUAGE_CODE = 'en-gb' 40 | 41 | SITE_ID = 1 42 | 43 | # If you set this to False, Django will make some optimizations so as not 44 | # to load the internationalization machinery. 45 | USE_I18N = True 46 | 47 | # If you set this to False, Django will not format dates, numbers and 48 | # calendars according to the current locale 49 | USE_L10N = True 50 | 51 | # Absolute filesystem path to the directory that will hold user-uploaded files. 52 | # Example: "/home/media/media.lawrence.com/media/" 53 | MEDIA_ROOT = '' 54 | 55 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 56 | # trailing slash. 57 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 58 | MEDIA_URL = '' 59 | 60 | # Absolute path to the directory static files should be collected to. 61 | # Don't put anything in this directory yourself; store your static files 62 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 63 | # Example: "/home/media/media.lawrence.com/static/" 64 | STATIC_ROOT = '' 65 | 66 | # URL prefix for static files. 67 | # Example: "http://media.lawrence.com/static/" 68 | STATIC_URL = '/static/' 69 | 70 | # URL prefix for admin static files -- CSS, JavaScript and images. 71 | # Make sure to use a trailing slash. 72 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 73 | ADMIN_MEDIA_PREFIX = '/static/admin/' 74 | 75 | # Additional locations of static files 76 | STATICFILES_DIRS = ( 77 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 78 | # Always use forward slashes, even on Windows. 79 | # Don't forget to use absolute paths, not relative paths. 80 | ) 81 | 82 | # List of finder classes that know how to find static files in 83 | # various locations. 84 | STATICFILES_FINDERS = ( 85 | 'django.contrib.staticfiles.finders.FileSystemFinder', 86 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 87 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 88 | ) 89 | 90 | # Make this unique, and don't share it with anybody. 91 | SECRET_KEY = '4z-(+=l(wkd)1aj+wn)(r%9684uj2589o&uu_w$ids#ww=' 92 | 93 | # List of callables that know how to import templates from various sources. 94 | TEMPLATE_LOADERS = ( 95 | 'django.template.loaders.filesystem.Loader', 96 | 'django.template.loaders.app_directories.Loader', 97 | # 'django.template.loaders.eggs.Loader', 98 | ) 99 | 100 | MIDDLEWARE_CLASSES = [ 101 | 'django.middleware.common.CommonMiddleware', 102 | 'django.contrib.sessions.middleware.SessionMiddleware', 103 | 'django.middleware.csrf.CsrfViewMiddleware', 104 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 105 | 'django.contrib.messages.middleware.MessageMiddleware', 106 | ] 107 | 108 | ROOT_URLCONF = 'forum.urls' 109 | 110 | TEMPLATE_DIRS = ( 111 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 112 | # Always use forward slashes, even on Windows. 113 | # Don't forget to use absolute paths, not relative paths. 114 | os.path.join(DIRNAME, 'templates'), 115 | ) 116 | 117 | INSTALLED_APPS = [ 118 | 'django.contrib.auth', 119 | 'django.contrib.contenttypes', 120 | 'django.contrib.sessions', 121 | 'django.contrib.sites', 122 | 'django.contrib.messages', 123 | 'django.contrib.staticfiles', 124 | 'django.contrib.admin', 125 | 'django.contrib.humanize', 126 | 'registration', 127 | 'forum', 128 | ] 129 | 130 | # A sample logging configuration. The only tangible logging 131 | # performed by this configuration is to send an email to 132 | # the site admins on every HTTP 500 error. 133 | # See http://docs.djangoproject.com/en/dev/topics/logging for 134 | # more details on how to customize your logging configuration. 135 | LOGGING = { 136 | 'version': 1, 137 | 'disable_existing_loggers': False, 138 | 'handlers': { 139 | 'mail_admins': { 140 | 'level': 'ERROR', 141 | 'class': 'django.utils.log.AdminEmailHandler' 142 | } 143 | }, 144 | 'loggers': { 145 | 'django.request': { 146 | 'handlers': ['mail_admins'], 147 | 'level': 'ERROR', 148 | 'propagate': True, 149 | }, 150 | } 151 | } 152 | 153 | if DEBUG: 154 | try: 155 | import debug_toolbar 156 | MIDDLEWARE_CLASSES.append('debug_toolbar.middleware.DebugToolbarMiddleware') 157 | INSTALLED_APPS.append('debug_toolbar') 158 | except ImportError: 159 | pass 160 | 161 | # Auth settings 162 | LOGIN_URL = '/accounts/login/' 163 | LOGIN_REDIRECT_URL = '/' 164 | 165 | # Session settings 166 | SESSION_ENGINE = 'forum.sessions.redis_session_backend' 167 | 168 | # Registration settings 169 | ACCOUNT_ACTIVATION_DAYS = 10 170 | 171 | # Forum settings 172 | FORUM_STANDALONE = True 173 | FORUM_USE_REDIS = True 174 | FORUM_USE_NODEJS = True 175 | FORUM_POST_FORMATTER = 'forum.formatters.BBCodeFormatter' 176 | -------------------------------------------------------------------------------- /forum/static/forum/css/ie.css: -------------------------------------------------------------------------------- 1 | /* Invoke the dark, proprietary art of hasLayout */ 2 | #userbar, div.tools, div.post, .module h2 { zoom: 1; } 3 | 4 | div.postbody { padding-bottom: 1em; } -------------------------------------------------------------------------------- /forum/static/forum/css/style.css: -------------------------------------------------------------------------------- 1 | body { font-family: "Lucida Grande", Tahoma, Arial, Verdana, sans-serif; font-size: 12px; margin: 0; padding: 1em; background-color: #fff; color: #000; } 2 | a img { border: none; } 3 | blockquote { border-left: 2px solid #595979; margin-left: 1em; padding-left: 6px; } 4 | img { vertical-align: text-bottom; } 5 | .clickable { cursor: pointer; } 6 | 7 | /* Tables */ 8 | table { border-collapse: collapse; width: 100%; } 9 | td, th { padding: 4px 6px; border: 1px solid #999; text-align: left; } 10 | th { font-weight: bold; } 11 | td p.description { margin: 4px 0 0 0; } 12 | td.icon, td.posts, td.views { text-align: center; } 13 | td.last-post { white-space: nowrap; } 14 | th.forum-topics { text-align: center; } 15 | a.unread { margin-right: 4px; } 16 | a.unread img { vertical-align: middle; } 17 | 18 | /* Common formatting */ 19 | .odd { background-color: #e9e9f9; } 20 | .even { background-color: #e3e3f3; } 21 | .module { border: 1px solid #ccc; margin-bottom: 1em; background-color: #fff; } 22 | .module-body { padding: 4px 8px; } 23 | .no-margin { margin: 0; } 24 | .module h2, .module caption { overflow: hidden; margin: 0; padding: 2px 8px 3px 8px; font-size: 1em; text-align: left; font-weight: bold; background: #595979 url("../img/module-bg.gif") top left repeat-x; color: #fff; border-bottom: 0; } 25 | .module h2 a { color: #fff; } 26 | .module h2 span.title { float: left; } 27 | .module h2 span.controls { float: right; } 28 | .module h2 span.controls a { color: #ff0; } 29 | .module h2 span.separator { display: none; } 30 | .module h3 { margin: 0 0 8px 0; } 31 | .module h3 span { font-weight: normal; } 32 | .module p.message { text-align: center; margin: 1em; } 33 | .clear { clear: both; } 34 | 35 | /* Navigation elements */ 36 | #userbar { padding: 4px 6px; overflow: hidden; border: 1px solid #ccc; background: #e9e9f9 url("../img/module-bg.gif") top left repeat-x; } 37 | #authenticated { float: left; } 38 | #authenticated a.user { font-weight: bold; } 39 | #user-tools { float: right; } 40 | #breadcrumbs { padding: 6px 0; } 41 | p.description { font-style: italic; } 42 | 43 | div.tools { overflow: hidden; padding: 4px 0; } 44 | div.tools .actions { float: right; } 45 | div.paginator { float: left; } 46 | 47 | h1 { margin: .25em 0 .5em 0; } 48 | 49 | /* Posts */ 50 | div.post { overflow: hidden; padding: 0 10px; } 51 | div.postbody { float: left; width: 76%; clear: both; padding-top: 6px; } 52 | div.postbody ul.post-actions { float: right; margin: 0; padding: 0; } 53 | div.postbody ul.post-actions li { list-style-type: none; float: left; margin-left: 10px; } 54 | p.author { margin: 0 0 .5em 0; } 55 | div.profile { border-left: 1px solid #fff; float: right; width: 22%; margin: 8px 0; } 56 | div.profile dl { margin-top: 0; margin-bottom: 0; } 57 | div.profile dd, div.profile dt { margin-left: 8px; } 58 | div.profile dd.postcount { margin-top: 1em; } 59 | 60 | /* Forms */ 61 | fieldset { padding: 0; margin-left: 0; margin-right: 0; } 62 | .form-row { height: 100%; overflow: hidden; padding: 8px 12px; border-bottom: 1px solid #eee; border-right: 1px solid #eee; } 63 | .form-row img, .form-row input { vertical-align: middle; } 64 | .aligned label { padding: 0 1em 3px 0; float: left; width: 9em; } 65 | .checkbox-row label, label.checkbox, .radio-list label { pading: 0 0 3px 0; float: none; width: auto; } 66 | .radio-list ul { list-style-type: none; margin: 0; padding: 0; } 67 | .radio-list li { padding: 4px 0; margin: 0; } 68 | .form-field { float: left; } 69 | .avatar-field input, .avatar-field img { vertical-align: top; } 70 | .post-preview .body { padding: 0 12px; } 71 | #fast-reply { width: 70%; margin: 1em auto 0 auto; } 72 | .fast-reply-field { padding: 8px 12px; } 73 | .fast-reply-field textarea { width: 98.5%; margin: 0; padding: 3px; } 74 | #fast-reply .buttons { text-align: center; } 75 | #fast-reply .checkbox-row { text-align: center; padding-top: 0; } 76 | .post-body-field { margin-right: 1em; margin-bottom: .5em; } 77 | .quick-help { float: left; } 78 | .quick-help table { width: auto; } 79 | .quick-help ul, .quick-help li { margin: 0 0 0 1em; padding: 0; } 80 | .quick-help blockquote { margin: 0 0 0 2em; } 81 | .emoticon-help { float: left; } 82 | .emoticon-help table { width: auto; } 83 | .emoticon-help th, .emoticon-help td { text-align: center; } 84 | ul.errorlist { margin: 0; padding: 0; } 85 | ul.errorlist li { padding: 4px 8px 4px 25px; margin: 0 0 8px 0; border: 1px solid #f00; color: #fff; background: #f00 url("../img/icon_alert.gif") 5px .3em no-repeat; } 86 | 87 | .col-l { float: left; width: 30%; } 88 | .forum-profile th, .forum-settings th { width: 30%; } 89 | .col-r { float: right; width: 70%; } 90 | .col-r .last-topics-started { margin-left: 1em; } 91 | 92 | #active-users p { margin: 0; } 93 | 94 | #footer { clear: both; margin-top: 1em; padding: 4px 0; } 95 | 96 | #debug { clear: both; } -------------------------------------------------------------------------------- /forum/static/forum/img/djangosite.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/djangosite.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/angry.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/angry.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/blink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/blink.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/grin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/grin.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/huh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/huh.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/lol.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/lol.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/ohmy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/ohmy.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/ph34r.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/ph34r.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/rolleyes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/rolleyes.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/sad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/sad.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/smile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/smile.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/tongue.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/tongue.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/unsure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/unsure.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/wacko.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/wacko.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/wink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/wink.gif -------------------------------------------------------------------------------- /forum/static/forum/img/emoticons/wub.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/emoticons/wub.gif -------------------------------------------------------------------------------- /forum/static/forum/img/icon_alert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/icon_alert.gif -------------------------------------------------------------------------------- /forum/static/forum/img/icon_exclamation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/icon_exclamation.gif -------------------------------------------------------------------------------- /forum/static/forum/img/icon_lock.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/icon_lock.gif -------------------------------------------------------------------------------- /forum/static/forum/img/new_posts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/new_posts.gif -------------------------------------------------------------------------------- /forum/static/forum/img/no_new_posts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/no_new_posts.gif -------------------------------------------------------------------------------- /forum/static/forum/img/unread_post.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/static/forum/img/unread_post.gif -------------------------------------------------------------------------------- /forum/static/forum/js/DOMBuilder.dom.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DOMBuilder 2.0.1 (modes: dom [default]) - https://github.com/insin/DOMBuilder 3 | * MIT licensed 4 | */ 5 | (function(w){function k(a,b){for(var f in b)a[f]=b[f];return a}function o(a){for(var b={},f=0,d=a.length;f",h).html(j).get(0)}else return jQuery("<"+i+">",h).get(0)}:function(){var i={tabindex:"tabIndex"},h={tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength", 15 | cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},j=function(){var e=o.createElement("div");e.setAttribute("className","t");e.innerHTML='s';var c=e.getElementsByTagName("span")[0],g=o.createElement("input");g.value="t";g.setAttribute("type","radio");return{style:/silver/.test(c.getAttribute("style")),getSetAttribute:e.className!="t",radioValue:g.value== 16 | "t"}}(),l,n=function(e,c,g){c!==!1&&(c=h[g]||g,c in e&&(e[c]=!0),e.setAttribute(g,g.toLowerCase()));return g},k={type:function(e,c){if(!j.radioValue&&c=="radio"&&e.nodeName&&e.nodeName.toUpperCase()=="INPUT"){var g=e.value;e.setAttribute("type",c);if(g)e.value=g;return c}},value:function(e,c,g){if(l&&e.nodeName&&e.nodeName.toUpperCase()=="BUTTON")return l(e,c,g);e.value=c}},r=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, 17 | t=/\:|^on/;if(!j.getSetAttribute)i=h,l=k.name=k.title=function(e,c,g){if(e=e.getAttributeNode(g))return e.nodeValue=c},k.width=k.height=function(e,c){if(c==="")return e.setAttribute(name,"auto"),c};if(!j.style)k.style=function(e,c){return e.style.cssText=""+c};return function(e,c){var g=o.createElement(e),j,p;q.call(c,"innerHTML")&&(u(g,c.innerHTML),delete c.innerHTML);for(j in c)if(p=c[j],s[j])g["on"+j]=p;else{var m=g,a=j;if("getAttribute"in m){var b=void 0,a=i[a]||a,b=k[a];if(!b)if(r.test(a))b= 18 | n;else if(l&&a!="className"&&(m.nodeName&&m.nodeName.toUpperCase()=="FORM"||t.test(a)))b=l;p!==void 0&&!(b&&b(m,p,a)!==void 0)&&m.setAttribute(a,""+p)}else a=h[a]||a,p!==void 0&&(m[a]=p)}return g}}();k.addMode({name:"dom",createElement:function(i,h,j){var l=q.call(h,"innerHTML"),i=v(i,h);if(!l)for(var l=0,h=j.length,k;l 0 && page <= pages) { 7 | window.location.href = '?page=' + page 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /forum/static/forum/js/PostForm.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | if ($('#emoticons img').size() > 0) { 3 | $('#emoticons img').addClass('clickable').click(function() { 4 | document.forms[0].elements['body'].value += this.alt + ' ' 5 | }) 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /forum/static/forum/js/Stalk.js: -------------------------------------------------------------------------------- 1 | var dom = DOMBuilder.dom 2 | 3 | function update() { 4 | $.getJSON('http://localhost:8001/?callback=?', function(data) { 5 | $body = $('#stalkUsers') 6 | $body.empty() 7 | $.each(data, function(i, user) { 8 | $body.append( 9 | dom.TR({'class': (i % 2 == 0 ? 'odd' : 'even')} 10 | , dom.TD(dom.A({href: '/user/' + user.id}, user.username)) 11 | , dom.TD(new Date(user.seen * 1000).toString().substring(0, 24)) 12 | , dom.TD({innerHTML: user.doing}) 13 | ) 14 | ) 15 | }) 16 | setTimeout(update, 5000) 17 | }) 18 | } 19 | 20 | $(function() { 21 | update() 22 | }) -------------------------------------------------------------------------------- /forum/static/forum/js/Topic.js: -------------------------------------------------------------------------------- 1 | // Create Fast Reply controls 2 | if (createFastReplyControls) 3 | $(function() { 4 | if ($('#topic-actions-bottom').size() > 0) { 5 | $('#topic-actions-bottom') 6 | .prepend(document.createTextNode(' | ')) 7 | .prepend( 8 | $('Fast Reply').click(function() { 9 | $("#fast-reply").toggle() 10 | }) 11 | ) 12 | } 13 | 14 | if ($("#fast-reply-buttons").size() > 0) { 15 | $("#fast-reply-buttons").append( 16 | $('').click(function() { 17 | $("#fast-reply").toggle() 18 | }) 19 | ) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /forum/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base_forum.html" %} 2 | {% block title %}Page not found{% endblock %} 3 | {% block breadcrumbs %} 4 | Forums 5 | » Page not found 6 | {% endblock %} 7 | {% block content_title %}

Page not found

{% endblock %} 8 | {% block main_content %} 9 |

The page you requested could not be found :(

10 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base_forum.html" %} 2 | {% block title %}Error{% endblock %} 3 | {% block breadcrumbs %} 4 | Forums 5 | » Error 6 | {% endblock %} 7 | {% block content_title %}

Error

{% endblock %} 8 | {% block main_content %} 9 |

An error occurred while processing your request :(

10 |

Hopefully, it's nothing terminal.

11 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/base_forum.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{{ title }}{% endblock %} 5 | 6 | 7 | 8 | 9 | {% block extrahead %}{% endblock %} 10 | 11 | 12 | 13 |
14 | {% block userbar %}{% endblock %} 15 |
16 | 17 | 20 | 21 |
22 | {% block content_title %}{% endblock %} 23 | {% block main_content %}{% endblock %} 24 |
25 | 26 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /forum/templates/forum/add_forum.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block breadcrumbs %} 3 | Forums 4 | » {{ section.name }} 5 | » Add Forum 6 | {% endblock %} 7 | {% block main_content %} 8 |
9 | {% csrf_token %} 10 |
11 |

Forum

12 |
13 | {% if form.name.errors %}{{ form.name.errors.as_ul }}{% endif %} 14 | {{ form.name.label_tag }} 15 |
16 | {{ form.name }} 17 |
18 |
19 |
20 | {% if form.description.errors %}{{ form.description.errors.as_ul }}{% endif %} 21 | {{ form.description.label_tag }} 22 |
23 | {{ form.description }} 24 |
25 |
26 |
27 | {% if form.forum.errors %}{{ form.forum.errors.as_ul }}{% endif %} 28 | 29 |
30 | {{ form.forum }} 31 |

Leave this field blank to insert the new Forum at the end.

32 |
33 |
34 |
35 |
36 | 37 | or 38 | Cancel 39 |
40 |
41 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/add_reply.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block extrahead %} 3 | {{ block.super }} 4 | 5 | {% endblock %} 6 | {% block breadcrumbs %} 7 | Forums 8 | » {{ section.name }} 9 | » {{ forum.name }} 10 | » {{ topic.title }} 11 | {% if meta %}» Metaposts{% endif %} 12 | » Add Reply 13 | {% endblock %} 14 | {% block main_content %} 15 | {% if preview %} 16 |
17 |

Reply Preview

18 |
19 | {{ preview|safe }} 20 |
21 |
22 | {% endif %} 23 |
24 | {% csrf_token %} 25 |
26 |

Reply

27 |
28 | {% if form.body.errors %}{{ form.body.errors.as_ul }}{% endif %} 29 |
30 | {{ form.body }} 31 |
32 | {% emoticon_help %} 33 |
34 |
35 | {% if form.emoticons.errors %}{{ form.emoticons.errors.as_ul }}{% endif %} 36 | 37 |
38 | {% if not meta %} 39 |
40 | {% if form.meta.errors %}{{ form.meta.errors.as_ul }}{% endif %} 41 | 42 |
43 | {% endif %} 44 |
45 |
46 | 47 | 48 | or 49 | Cancel 50 |
51 |
52 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/add_section.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block main_content %} 3 |
4 | {% csrf_token %} 5 |
6 |

Section

7 |
8 | {% if form.name.errors %}{{ form.name.errors.as_ul }}{% endif %} 9 | {{ form.name.label_tag }} 10 |
11 | {{ form.name }} 12 |
13 |
14 |
15 | {% if form.section.errors %}{{ form.section.errors.as_ul }}{% endif %} 16 | 17 |
18 | {{ form.section }} 19 |

Leave this field blank to insert the new Section at the end.

20 |
21 |
22 |
23 |
24 | 25 | or 26 | Cancel 27 |
28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/add_topic.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block extrahead %} 3 | {{ block.super }} 4 | 5 | {% endblock %} 6 | {% block breadcrumbs %} 7 | Forums 8 | » {{ section.name }} 9 | » {{ forum.name }} 10 | » Add Topic 11 | {% endblock %} 12 | {% block main_content %} 13 | {% if preview %} 14 |
15 |

Topic Preview

16 |
17 | {{ preview|safe }} 18 |
19 |
20 | {% endif %} 21 |
22 | {% csrf_token %} 23 |
24 |

Topic

25 |
26 | {% if topic_form.title.errors %}{{ topic_form.title.errors.as_ul }}{% endif %} 27 | {{ topic_form.title.label_tag }} 28 |
29 | {{ topic_form.title }} 30 |
31 |
32 |
33 | {% if topic_form.description.errors %}{{ topic_form.description.errors.as_ul }}{% endif %} 34 | {{ topic_form.description.label_tag }} 35 |
36 | {{ topic_form.description }} 37 |
38 |
39 |
40 |
41 |

Opening Post

42 |
43 | {% if post_form.body.errors %}{{ post_form.body.errors.as_ul }}{% endif %} 44 |
45 | {{ post_form.body }} 46 |
47 | {% emoticon_help %} 48 |
49 |
50 | {% if post_form.emoticons.errors %}{{ post_form.emoticons.errors.as_ul }}{% endif %} 51 | 52 |
53 |
54 |
55 | 56 | 57 | or 58 | Cancel 59 |
60 |
61 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base_forum.html" %} 2 | {% block extrahead %}{% endblock %} 3 | 4 | {% block userbar %} 5 | {% if user.is_authenticated %} 6 |
7 | Logged in as: {{ user.username }} (Log out) 8 |
9 |
10 | View New Posts | 11 | {% if nodejs %} 12 | Stalk Users | 13 | {% endif %} 14 | Search 15 |
16 | {% else %} 17 |
18 | Log in or Sign up 19 |
20 | {% endif %} 21 | {% endblock %} 22 | 23 | {% block breadcrumbs %} 24 | Forums 25 | {% if section %}{% ifnotequal section.name title %}» {{ section.name }}{% endifnotequal %}{% endif %} 26 | {% if forum %}{% ifnotequal forum.name title %}» {{ forum.name }}{% endifnotequal %}{% endif %} 27 | {% if topic %}{% ifnotequal topic.title title %}» {{ topic.title }}{% endifnotequal %}{% endif %} 28 | {% if title %}» {{ title }}{% endif %} 29 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/delete_forum.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block main_content %} 3 |

Are you sure you want to delete the {{ forum.name }} forum?

4 | {% if topic_count %} 5 |

This will result in the deletion of its {{ topic_count }} topic{{ topic_count|pluralize }} and all {{ topic_count|pluralize:"its,their" }} posts.

6 | {% endif %} 7 |
8 | {% csrf_token %} 9 |
10 | 11 | or 12 | Cancel 13 | 14 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/delete_post.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block main_content %} 3 |

Are you sure you want to delete the following post?

4 |
5 |
6 |
7 |
8 |

{% if post.meta %}Metapost {% endif %}#{{ post.num_in_topic }} by {{ post.user_username }}, {{ post.posted_at|post_time:user }}

9 |
10 | {{ post.body_html|safe }} 11 |
12 |
13 |
14 |
15 |
16 | {% if post.user_avatar %} 17 |
18 | {% endif %} 19 |
{{ post.user_username }}
20 | {% if post.user_title %} 21 |
{{ post.user_title }} 22 | {% endif %} 23 |
Posts: {{ post.user_post_count }}
24 |
Joined: {{ post.user_date_joined|joined_date }}
25 | {% if post.user_location %} 26 |
Location: {{ post.user_location }}
27 | {% endif %} 28 | {% if user|is_moderator and post.user_ip %} 29 |
Post IP: {{ post.user_ip }}
30 | {% endif %} 31 |
32 |
33 |
34 |
35 | 36 |
37 | {% csrf_token %} 38 |
39 | 40 | or 41 | Cancel 42 | 43 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/delete_section.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block main_content %} 3 |

Are you sure you want to delete the following section?

4 | 5 |
6 |

{{ section.name }}

7 | {% if forum_list %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for forum in forum_list %} 20 | 21 | 25 | 26 | 31 | {% endfor %} 32 | 33 |
ForumTopicsLast Post
22 | {{ forum.name }} 23 | {% if forum.description %}

{{ forum.description }}

{% endif %} 24 |
{{ forum.topic_count }}{% if forum.last_post_at %} 27 | Last post {{ forum.last_post_at|post_time:user }}
28 | In: {{ forum.last_topic_title }}
29 | By: {{ forum.last_username }} 30 | {% else %}N/A{% endif %}
34 | {% else %} 35 |
36 | This Section does not have any Forums yet. 37 |
38 | {% endif %} 39 |
40 | 41 |
42 | {% csrf_token %} 43 |
44 | 45 | or 46 | Cancel 47 | 48 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/delete_topic.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block main_content %} 3 |

Are you sure you want to delete the following topic?

4 | {% if topic.hidden %} 5 |

Hidden This topic is hidden.

6 | {% endif %} 7 | {% if topic.locked %} 8 |

Locked This topic is locked.

9 | {% endif %} 10 |
11 |

{{ topic.title }}{% if topic.description %}, {{ topic.description }}{% endif %}

12 |
13 |
14 |
15 |

#{{ post.num_in_topic }} by {{ post.user_username }}, {{ post.posted_at|post_time:user }}

16 |
17 | {{ post.body_html|safe }} 18 |
19 |
20 |
21 |
22 |
23 | {% if post.user_avatar %} 24 |
25 | {% endif %} 26 |
{{ post.user_username }}
27 | {% if post.user_title %} 28 |
{{ post.user_title }} 29 | {% endif %} 30 |
Posts: {{ post.user_post_count }}
31 |
Joined: {{ post.user_date_joined|joined_date }}
32 | {% if post.user_location %} 33 |
Location: {{ post.user_location }}
34 | {% endif %} 35 | {% if user|is_moderator and post.user_ip %} 36 |
Post IP: {{ post.user_ip }}
37 | {% endif %} 38 |
39 |
40 |
41 |
42 | 43 |
44 | {% csrf_token %} 45 |
46 | 47 | or 48 | Cancel 49 | 50 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/edit_forum.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block main_content %} 3 |
4 | {% csrf_token %} 5 |
6 |

Forum

7 |
8 | {% if form.name.errors %}{{ form.name.errors.as_ul }}{% endif %} 9 | {{ form.name.label_tag }} 10 |
11 | {{ form.name }} 12 |
13 |
14 |
15 | {% if form.description.errors %}{{ form.description.errors.as_ul }}{% endif %} 16 | {{ form.description.label_tag }} 17 |
18 | {{ form.description }} 19 |
20 |
21 |
22 |
23 | 24 | or 25 | Cancel 26 |
27 |
28 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/edit_post.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block extrahead %} 3 | {{ block.super }} 4 | 5 | {% endblock %} 6 | {% block main_content %} 7 | {% if preview %} 8 |
9 |

Post Preview

10 |
11 | {{ preview|safe }} 12 |
13 |
14 | {% endif %} 15 |
16 | {% csrf_token %} 17 |
18 |

Post

19 |
20 | {% if form.body.errors %}{{ form.body.errors.as_ul }}{% endif %} 21 |
22 | {{ form.body }} 23 |
24 | {% emoticon_help %} 25 |
26 |
27 | {% if form.emoticons.errors %}{{ form.emoticons.errors.as_ul }}{% endif %} 28 | 29 |
30 |
31 | {% if user|is_moderator %} 32 |
33 |

Moderation

34 |
35 | {% if form.meta.errors %}{{ form.meta.errors.as_ul }}{% endif %} 36 | 37 |
38 |
39 | {% endif %} 40 |
41 | 42 | 43 | or 44 | Cancel 45 |
46 |
47 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/edit_section.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block main_content %} 3 |
4 | {% csrf_token %} 5 |
6 |

Section

7 |
8 | {% if form.name.errors %}{{ form.name.errors.as_ul }}{% endif %} 9 | {{ form.name.label_tag }} 10 |
11 | {{ form.name }} 12 |
13 |
14 |
15 |
16 | 17 | or 18 | Cancel 19 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/edit_topic.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block main_content %} 3 | {% if topic.hidden %} 4 |

Hidden This topic is hidden.

5 | {% endif %} 6 | {% if topic.locked %} 7 |

Locked This topic is locked.

8 | {% endif %} 9 |
10 | {% csrf_token %} 11 |
12 |

Topic

13 |
14 | {% if topic_form.title.errors %}{{ form.title.errors.as_ul }}{% endif %} 15 | {{ form.title.label_tag }} 16 |
17 | {{ form.title }} 18 |
19 |
20 |
21 | {% if form.description.errors %}{{ form.description.errors.as_ul }}{% endif %} 22 | {{ form.description.label_tag }} 23 |
24 | {{ form.description }} 25 |
26 |
27 |
28 | {% if user|is_moderator %} 29 |
30 |

Moderation

31 |
32 | {% if form.pinned.errors %}{{ form.pinned.errors.as_ul }}{% endif %} 33 | 34 |
35 |
36 | {% if form.locked.errors %}{{ form.locked.errors.as_ul }}{% endif %} 37 | 38 |
39 |
40 | {% if form.hidden.errors %}{{ form.hidden.errors.as_ul }}{% endif %} 41 | 42 |
43 |
44 | {% endif %} 45 |
46 | 47 | or 48 | Cancel 49 |
50 |
51 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/edit_user_forum_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block breadcrumbs %} 3 | Forums 4 | » Forum Profile for {{ forum_user.username }} 5 | » {{ title }} 6 | {% endblock %} 7 | {% block main_content %} 8 |
9 | {% csrf_token %} 10 |
11 |

Forum Profile

12 | {% if form.title %}
13 | {% if form.title.errors %}{{ form.title.errors.as_ul }}{% endif %} 14 | {{ form.title.label_tag }} 15 |
16 | {{ form.title }} 17 |
18 |
{% endif %} 19 |
20 | {% if form.location.errors %}{{ form.location.errors.as_ul }}{% endif %} 21 | {{ form.location.label_tag }} 22 |
23 | {{ form.location }} 24 |
25 |
26 |
27 | {% if form.avatar.errors %}{{ form.avatar.errors.as_ul }}{% endif %} 28 | {{ form.avatar.label_tag }} 29 |
30 | {{ form.avatar }} 31 | {% if forum_profile.avatar %} 32 | 33 | {% endif %} 34 |
35 |
36 |
37 | {% if form.website.errors %}{{ form.website.errors.as_ul }}{% endif %} 38 | {{ form.website.label_tag }} 39 |
40 | {{ form.website }} 41 |
42 |
43 |
44 |
45 | 46 | or 47 | Cancel 48 |
49 |
50 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/edit_user_forum_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block breadcrumbs %} 3 | Forums 4 | » Forum Profile for {{ user.username }} 5 | » {{ title }} 6 | {% endblock %} 7 | {% block main_content %} 8 |
9 | {% csrf_token %} 10 |
11 |

Forum Settings

12 |
13 | {% if form.timezone.errors %}{{ form.timezone.errors.as_ul }}{% endif %} 14 | {{ form.timezone.label_tag }} 15 |
16 | {{ form.timezone }} 17 |
18 |
19 |
20 | {% if form.topics_per_page.errors %}{{ form.topics_per_page.errors.as_ul }}{% endif %} 21 | {{ form.topics_per_page.label_tag }} 22 |
23 | {{ form.topics_per_page }} 24 |
25 |
26 |
27 | {% if form.posts_per_page.errors %}{{ form.posts_per_page.errors.as_ul }}{% endif %} 28 | {{ form.posts_per_page.label_tag }} 29 |
30 | {{ form.posts_per_page }} 31 |
32 |
33 |
34 | {% if form.auto_fast_reply.errors %}{{ form.auto_fast_reply.errors.as_ul }}{% endif %} 35 | 36 |
37 |
38 |
39 | 40 | or 41 | Cancel 42 |
43 |
44 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/forum_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %}{% load humanize %} 2 | {% block extrahead %} 3 | {{ block.super }} 4 | {% if is_paginated %}{% endif %} 5 | {% endblock %} 6 | {% block main_content %} 7 | {% if forum.description %}

{{ forum.description }}

{% endif %} 8 |
9 | {% if is_paginated %}
{% paginator "Topic" %}
{% endif %} 10 | {% if user.is_authenticated %} 11 |
12 | Add Topic 13 |
14 | {% endif %} 15 |
16 |
17 |

{{ forum.name }}{% if user|is_admin %} - Edit Forum | Delete Forum{% endif %}

18 | {% if topic_list or pinned_topics %} 19 | 20 | 21 | 22 | 23 | 24 | {% if redis %}{% endif %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% if redis %}{% endif %} 33 | 34 | 35 | 36 | 37 | {% if pinned_topics %} 38 | {% for topic in pinned_topics %} 39 | 40 | 41 | 48 | 49 | 50 | {% if redis %}{% endif %} 51 | 52 | {% endfor %} 53 | 54 | 55 | 56 | {% endif %} 57 | {% for topic in topic_list %} 58 | 59 | 60 | 67 | 68 | 69 | {% if redis %}{% endif %} 70 | 71 | {% endfor %} 72 | 73 |
 TopicStarted ByPostsViewsLast Post
{{ topic|topic_status_image }} 42 | {% if topic.hidden %}Hidden{% endif %} 43 | {% if topic.locked %}Locked{% endif %} 44 | {% if topic|has_new_posts %}First Unread Post{% endif %} 45 | Pinned: {{ topic.title }} {{ topic|topic_pagination:posts_per_page }} 46 | {% if topic.description %}

{{ topic.description }}

{% endif %} 47 |
{{ topic.user_username }}{{ topic.post_count|intcomma }}{{ topic.view_count|intcomma }}{{ topic.last_post_at|post_time:user }}
Last Post by {{ topic.last_username }}
Forum Topics
{{ topic|topic_status_image }} 61 | {% if topic.hidden %}Hidden{% endif %} 62 | {% if topic.locked %}Locked{% endif %} 63 | {% if topic|has_new_posts %}First Unread Post{% endif %} 64 | {{ topic.title }} {{ topic|topic_pagination:posts_per_page }} 65 | {% if topic.description %}

{{ topic.description }}

{% endif %} 66 |
{{ topic.user_username }}{{ topic.post_count|intcomma }}{{ topic.view_count|intcomma }}{{ topic.last_post_at|post_time:user }}
Last Post by {{ topic.last_username }}
74 | {% else %} 75 |
76 | This Forum does not have any Topics yet. 77 |
78 | {% endif %} 79 |
80 |
81 | {% if is_paginated %}
{% paginator "Topic" %}
{% endif %} 82 | {% if user.is_authenticated %} 83 |
84 | Add Topic 85 |
86 | {% endif %} 87 |
88 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/forum_index.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block main_content %} 3 | {% if user|is_admin %} 4 |
5 |
6 | Add Section 7 |
8 |
9 | {% endif %} 10 | {% for section,forum_list in section_list %} 11 |
12 |

{{ section.name }}

13 | {% if forum_list %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for forum in forum_list %} 26 | 27 | 31 | 32 | 37 | {% endfor %} 38 | 39 |
ForumTopicsLast Post
28 | {{ forum.name }} 29 | {% if forum.description %}

{{ forum.description }}

{% endif %} 30 |
{{ forum.topic_count }}{% if forum.last_post_at %} 33 | Last post {{ forum.last_post_at|post_time:user }}
34 | In: {{ forum.last_topic_title }}
35 | By: {{ forum.last_username }} 36 | {% else %}N/A{% endif %}
40 | {% else %} 41 |
42 | This Section does not have any Forums yet. 43 |
44 | {% endif %} 45 |
46 | {% endfor %} 47 | 48 | {% if redis and active_users %} 49 |
50 |
51 |

{{ active_users|length }} active user{{ active_users|length|pluralize }} (in the last 30 minutes)

52 |

{% for active_user, last_seen in active_users %} 53 | {{ active_user.username }}{% if not forloop.last %}, {% endif %} 54 | {% endfor %}

55 |
56 |
57 | {% endif %} 58 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/help/basic_formatting_quick.html: -------------------------------------------------------------------------------- 1 |
2 |

Basic Formatting Quick Help

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
You TypeYou See
http://google.comhttp://google.com
17 |
-------------------------------------------------------------------------------- /forum/templates/forum/help/bbcode_formatting_quick.html: -------------------------------------------------------------------------------- 1 |
2 |

BBCode Formatting Quick Help

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
You TypeYou See
[i]italics[/i]italics
[b]bold[/b]bold
[url=http://google.com]Google[/url]Google [google.com]
[list][*]item 1[*]item 2[/list]
  • item 1
  • item 2
[quote]Quoted.[/quote]
Quoted.
33 |
-------------------------------------------------------------------------------- /forum/templates/forum/help/emoticons.html: -------------------------------------------------------------------------------- 1 | {% if emoticons %} 2 | {% load forum_tags %} 3 |
4 | 5 | 6 | 7 | 8 | 9 | {% for row in emoticons.items|partition:"3" %} 10 | {% for symbol,image in row %}{% endfor %} 11 | {% endfor %} 12 | 13 |
Emoticons
{{ image|safe }}
14 |
15 | {% endif %} -------------------------------------------------------------------------------- /forum/templates/forum/help/markdown_formatting_quick.html: -------------------------------------------------------------------------------- 1 |
2 |

Markdown Formatting Quick Help

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
You TypeYou See
*italics*italics
**bold**bold
[Google](http://google.com)Google
* item 1
* item 2
  • item 1
  • item 2
> Quoted.
Quoted.
33 |
-------------------------------------------------------------------------------- /forum/templates/forum/new_posts.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %}{% load humanize %} 2 | {% block extrahead %} 3 | {% if is_paginated %}{% endif %} 4 | {{ block.super }} 5 | {% endblock %} 6 | {% block main_content %} 7 |

Topics with new posts in the last 14 days.

8 | {% if is_paginated %} 9 |
10 |
{% paginator "Topic" %}
11 |
12 | {% endif %} 13 |
14 |

{{ title }}

15 | {% if topic_list %} 16 | {% if redis %} 17 | {% add_view_counts topic_list %} 18 | {% add_last_read_times topic_list user %} 19 | {% endif %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% if redis %}{% endif %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% if redis %}{% endif %} 36 | 37 | 38 | 39 | 40 | {% for topic in topic_list %} 41 | 42 | 43 | 50 | 51 | 52 | 53 | {% if redis %}{% endif %} 54 | 55 | {% endfor %} 56 | 57 |
 TopicIn ForumStarted ByPostsViewsLast Post
{{ topic|topic_status_image }} 44 | {% if topic.hidden %}Hidden{% endif %} 45 | {% if topic.locked %}Locked{% endif %} 46 | {% if topic|has_new_posts %}First Unread Post{% endif %} 47 | {{ topic.title }} {{ topic|topic_pagination:posts_per_page }} 48 | {% if topic.description %}

{{ topic.description }}

{% endif %} 49 |
{{ topic.forum_name }}{{ topic.user_username }}{{ topic.post_count|intcomma }}{{ topic.view_count|intcomma }}{{ topic.last_post_at|post_time:user }}
Last Post by {{ topic.last_username }}
58 | {% else %} 59 |
60 | No posts found. 61 |
62 | {% endif %} 63 |
64 | {% if is_paginated %} 65 |
66 |
{% paginator "Topic" %}
67 |
68 | {% endif %} 69 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/pagination.html: -------------------------------------------------------------------------------- 1 | {{ hits }} {{ what }}{{ hits|pluralize }} 2 | | 3 | Page {{ page }} of {{ pages }} 4 | | 5 | {% if show_first %}1{% if show_first_divider %} …{% endif %}{% endif %} 6 | {% for num in page_numbers %} 7 | {% ifequal num page %} 8 | {{ num }} 9 | {% else %} 10 | {{ num }} 11 | {% endifequal %} 12 | {% endfor %} 13 | {% if show_last %}{% if show_last_divider %}… {% endif %}{{ pages }}{% endif %} -------------------------------------------------------------------------------- /forum/templates/forum/permission_denied.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block main_content %} 3 |

{{ message }}

4 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/search.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block main_content %} 3 |
4 | {% csrf_token %} 5 |
6 |

Search Terms

7 |
8 | {% if form.search_type.errors %}{{ form.search_type.errors.as_ul }}{% endif %} 9 | {{ form.search_type.label_tag }} 10 |
11 | {{ form.search_type }} 12 |
13 |
14 |
15 | {% if form.keywords.errors %}{{ form.keywords.errors.as_ul }}{% endif %} 16 | {{ form.keywords.label_tag }} 17 |
18 | {{ form.keywords }} 19 |
20 |
21 |
22 | {% if form.username.errors %}{{ form.username.errors.as_ul }}{% endif %} 23 | {{ form.username.label_tag }} 24 |
25 | {{ form.username }} 26 | 27 |
28 |
29 |
30 |
31 |

Search Options

32 |
33 | {% if form.search_in.errors %}{{ form.search_in.errors.as_ul }}{% endif %} 34 | {{ form.search_in.label_tag }} 35 |
36 | {{ form.search_in }} 37 |
38 |
39 |
40 | {% if form.post_type.errors %}{{ form.post_type.errors.as_ul }}{% endif %} 41 | {{ form.post_type.label_tag }} 42 |
43 | {{ form.post_type }} 44 |
45 |
46 |
47 | {% if form.search_from.errors %}{{ form.search_from.errors.as_ul }}{% endif %} 48 | {{ form.search_from.label_tag }} 49 |
50 | {{ form.search_from }} 51 |
52 | {{ form.search_when }} 53 |
54 |
55 |
56 |
57 | {% if form.sort_direction.errors %}{{ form.sort_direction.errors.as_ul }}{% endif %} 58 | {{ form.sort_direction.label_tag }} 59 |
60 | {{ form.sort_direction }} 61 |
62 |
63 |
64 |
65 | 66 |
67 |
68 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/search_results.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %}{% load humanize %} 2 | 3 | {% block breadcrumbs %} 4 | Forums 5 | » Search 6 | » {{ title }} 7 | {% endblock %} 8 | 9 | {% block main_content %} 10 | {% if object_list %} 11 |
12 | {% if is_paginated %}
{% paginator object_name %}
{% endif %} 13 |
14 | {% if search.is_post_search %} 15 | {% if redis %} 16 | {% add_topic_view_counts object_list %} 17 | {% endif %} 18 | {% for post in object_list %} 19 |
20 |

{{ post.topic_title }} in {{ post.forum_name }} - {{ post.topic_post_count|intcomma }} Post{{ post.topic_post_count|pluralize }}{% if redis %}, {{ post.topic_view_count|intcomma }} View{{ post.topic_view_count|pluralize }}{% endif %}

21 |
22 |
23 |
24 |

{% if post.meta %}Metapost {% endif %}#{{ post.num_in_topic }} by {{ post.user_username }}, {{ post.posted_at|post_time:user }}

25 |
26 | {{ post.body_html|safe }} 27 |
28 |
29 |
30 |
31 |
32 | {% if post.user_avatar %} 33 |
34 | {% endif %} 35 |
{{ post.user_username }}
36 | {% if post.user_title %} 37 |
{{ post.user_title }}
38 | {% endif %} 39 |
Posts: {{ post.user_post_count }}
40 |
Joined: {{ post.user_date_joined|joined_date }}
41 | {% if post.user_location %} 42 |
Location: {{ post.user_location }}
43 | {% endif %} 44 | {% if user|is_moderator and post.user_ip %} 45 |
Post IP: {{ post.user_ip }}
46 | {% endif %} 47 |
48 |
49 |
50 |
51 | {% endfor %} 52 | {% else %} 53 | {% if redis %} 54 | {% add_view_counts object_list %} 55 | {% add_last_read_times object_list user %} 56 | {% endif %} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {% if redis %}{% endif %} 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {% if redis %}{% endif %} 75 | 76 | 77 | 78 | 79 | {% for topic in object_list %} 80 | 81 | 82 | 89 | 90 | 91 | 92 | 93 | {% if redis %}{% endif %} 94 | 95 | {% endfor %} 96 | 97 |
 TopicIn ForumIn SectionStarted ByPostsViewsLast Post
{{ topic|topic_status_image }} 83 | {% if topic.hidden %}Hidden{% endif %} 84 | {% if topic.locked %}Locked{% endif %} 85 | {% if topic|has_new_posts %}First Unread Post{% endif %} 86 | {{ topic.title }} {{ topic|topic_pagination:posts_per_page }} 87 | {% if topic.description %}

{{ topic.description }}

{% endif %} 88 |
{{ topic.forum_name }}{{ topic.section_name }}{{ topic.user_username }}{{ topic.post_count|intcomma }}{{ topic.view_count|intcomma }}{{ topic.last_post_at|post_time:user }}
Last Post by {{ topic.last_username }}
98 | {% endif %} 99 |
100 | {% if is_paginated %}
{% paginator object_name %}
{% endif %} 101 |
102 | {% else %} 103 |

No Posts Found

104 |

Your search did not return any results.

105 | {% endif %} 106 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/section_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block main_content %} 3 | {% if user|is_admin %} 4 |
5 |
6 | Add Forum 7 |
8 |
9 | {% endif %} 10 |
11 |

{{ section.name }}{% if user|is_admin %} - Edit Section | Delete Section{% endif %}

12 | {% if forum_list %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for forum in forum_list %} 25 | 26 | 30 | 31 | 36 | {% endfor %} 37 | 38 |
ForumTopicsLast Post
27 | {{ forum.name }} 28 | {% if forum.description %}

{{ forum.description }}

{% endif %} 29 |
{{ forum.topic_count }}{% if forum.last_post_at %} 32 | Last post {{ forum.last_post_at|post_time:user }}
33 | In: {{ forum.last_topic_title }}
34 | By: {{ forum.last_username }} 35 | {% else %}N/A{% endif %}
39 | {% else %} 40 |
41 | This Section does not have any Forums yet. 42 |
43 | {% endif %} 44 |
45 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/stalk_users.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{ 2 | 3 | {% block extrahead %} 4 | 5 | {{ block.super }} 6 | 7 | {% endblock %} 8 | 9 | {% block breadcrumbs %} 10 | Forums 11 | » {{ title }} 12 | {% endblock %} 13 | 14 | {% block main_content %} 15 |

User activity updates.

16 |
17 |

{{ title }}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
UserLast SeenDoing
32 |
33 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/topic_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | 3 | {% block extrahead %} 4 | {{ block.super }} 5 | 6 | 7 | {% if is_paginated %}{% endif %} 8 | {% endblock %} 9 | 10 | {% block breadcrumbs %} 11 | Forums 12 | » {{ topic.section_name }} 13 | » {{ topic.forum_name }} 14 | {% if meta %} 15 | » {{ title }} 16 | » Metaposts 17 | {% else %} 18 | » {{ title }} 19 | {% endif %} 20 | {% endblock %} 21 | 22 | {% block main_content %} 23 | {% if topic.hidden %} 24 |

Hidden This topic is hidden.

25 | {% endif %} 26 | {% if topic.locked %} 27 |

Locked This topic is locked.

28 | {% endif %} 29 |
30 | {% if is_paginated %}
{% paginator "Post" %}
{% endif %} 31 | {% if user.is_authenticated %} 32 |
33 | {% if user|can_see_post_actions:topic %}Add Reply |{% endif %} 34 | New Topic 35 |
36 | {% endif %} 37 |
38 |
39 |

40 | {{ topic.title }}{% if topic.description %}, {{ topic.description }}{% endif %} 41 | {% if not meta %} - View Metaposts{% if user|can_edit_topic:topic %} | Edit Topic | Delete Topic{% endif %}{% endif %} 42 |

43 | {% if post_list %} 44 | {% for post in post_list %} 45 |
46 |
47 | {% if user|can_see_post_actions:topic %} 48 | 55 | {% endif %} 56 |
57 |

{% if post.meta %}Metapost {% endif %}#{{ post.num_in_topic }} by {{ post.user_username }}, {{ post.posted_at|post_time:user }}

58 |
59 | {{ post.body_html|safe }} 60 |
61 |
62 |
63 |
64 |
65 | {% if post.user_avatar %} 66 |
67 | {% endif %} 68 |
{{ post.user_username }}
69 | {% if post.user_title %} 70 |
{{ post.user_title }}
71 | {% endif %} 72 |
Posts: {{ post.user_post_count }}
73 |
Joined: {{ post.user_date_joined|joined_date }}
74 | {% if post.user_location %} 75 |
Location: {{ post.user_location }}
76 | {% endif %} 77 | {% if user|is_moderator and post.user_ip %} 78 |
Post IP: {{ post.user_ip }}
79 | {% endif %} 80 |
81 |
82 |
83 | {% endfor %} 84 | {% else %} 85 |

There are no {% if meta %}Meta{% endif %}posts yet.

86 | {% endif %} 87 |
88 |
89 | {% if is_paginated %}
{% paginator "Post" %}
{% endif %} 90 | {% if user.is_authenticated %} 91 |
92 | {% if user|can_see_post_actions:topic %}Add Reply |{% endif %} 93 | New Topic 94 |
95 | {% endif %} 96 |
97 | 98 | {% if user.is_authenticated and user|can_see_post_actions:topic %} 99 | 118 | {% endif %} 119 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/topic_post_summary.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block breadcrumbs %} 3 | Forums 4 | » {{ topic.section_name }} 5 | » {{ topic.forum_name }} 6 | » {{ topic.title }} 7 | » Post Summary 8 | {% endblock %} 9 | {% block main_content %} 10 |
11 |

{{ title }}

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for topic_user in users %} 23 | 24 | 25 | 26 | 27 | {% endfor %} 28 | 29 |
UserPosts
{{ topic_user.username }}{% ifequal topic.user_id topic_user.id %} (Topic Starter){% endifequal %}{{ topic_user.post_count }}
30 |
31 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/user_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %} 2 | {% block main_content %} 3 | {% ifequal user.id forum_user.id %} 4 |
5 |
6 | Change Password 7 |
8 |
9 | {% endifequal %} 10 |
11 |
12 |

{{ title }}{% if user|can_edit_user_profile:forum_user %} - Edit{% endif %}

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {% if redis %} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {% endif %} 53 | 54 |
Avatar{% if forum_profile.avatar %}{% else %}None{% endif %}
Group{{ forum_profile.get_group_display }}
Title{{ forum_profile.title|default:"None" }}
Posts{{ forum_profile.post_count }}
Joined{{ forum_user.date_joined|joined_date }}
Location{% if forum_profile.location %}{{ forum_profile.location }}{% else %}None{% endif %}
Website{% if forum_profile.website %}{{ forum_profile.website }}{% else %}None{% endif %}
Last Seen{{ last_seen|timesince }}
Doing{{ doing|safe }}
55 |
56 | 57 | {% ifequal user.id forum_user.id %} 58 |
59 |

Your Forum Settings - Edit

60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
Timezone{{ forum_profile.timezone|default:"None" }}
Topics Per Page{{ forum_profile.topics_per_page|default:"Default" }}
Posts Per Page{{ forum_profile.posts_per_page|default:"Default" }}
Auto Fast Reply{{ forum_profile.auto_fast_reply|yesno:"Yes,No" }}
80 |
81 | {% endifequal %} 82 |
83 | 84 |
85 |
86 |

Most Recent Topics Started{% if recent_topics %} - View All Topics{% endif %}

87 | {% if recent_topics %} 88 | {% load humanize %} 89 | 90 | 91 | 92 | 93 | 94 | {% if redis %}{% endif %} 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | {% if redis %}{% endif %} 103 | 104 | 105 | 106 | 107 | {% for topic in recent_topics %} 108 | 109 | 115 | 116 | 117 | 118 | {% if redis %}{% endif %} 119 | 120 | {% endfor %} 121 | 122 |
TopicIn ForumStartedPostsViewsLast Post
110 | {% if topic.hidden %}Hidden{% endif %} 111 | {% if topic.locked %}Locked{% endif %} 112 | {{ topic.title }} 113 | {% if topic.description %}

{{ topic.description }}

{% endif %} 114 |
{{ topic.forum_name }}{{ topic.started_at|timesince }} ago{{ topic.post_count|intcomma }}{{ topic.view_count|intcomma }}{{ topic.last_post_at|post_time:user }}
Last Post by {{ topic.last_username }}
123 | {% else %} 124 |
125 | This user has not started any Topics yet. 126 |
127 | {% endif %} 128 |
129 |
130 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/forum/user_topics.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load forum_tags %}{% load humanize %} 2 | {% block extrahead %} 3 | {{ block.super }} 4 | {% if is_paginated %}{% endif %} 5 | {% endblock %} 6 | {% block breadcrumbs %} 7 | Forums 8 | » Forum Profile for {{ forum_user.username }} 9 | » {{ title }} 10 | {% endblock %} 11 | {% block main_content %} 12 | {% if is_paginated %} 13 |
14 |
{% paginator "Topic" %}
15 |
16 | {% endif %} 17 |
18 |

{{ title }}

19 | {% if topic_list %} 20 | {% if redis %} 21 | {% add_view_counts topic_list %} 22 | {% add_last_read_times topic_list user %} 23 | {% endif %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% if redis %}{% endif %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% if redis %}{% endif %} 40 | 41 | 42 | 43 | 44 | {% for topic in topic_list %} 45 | 46 | 47 | 54 | 55 | 56 | 57 | {% if redis %}{% endif %} 58 | 59 | {% endfor %} 60 | 61 |
 TopicIn ForumStartedPostsViewsLast Post
{{ topic|topic_status_image }} 48 | {% if topic.hidden %}Hidden{% endif %} 49 | {% if topic.locked %}Locked{% endif %} 50 | {% if topic|has_new_posts %}First Unread Post{% endif %} 51 | {{ topic.title }} {{ topic|topic_pagination:posts_per_page }} 52 | {% if topic.description %}

{{ topic.description }}

{% endif %} 53 |
{{ topic.forum_name }}{{ topic.started_at|forum_datetime:user }}{{ topic.post_count|intcomma }}{{ topic.view_count|intcomma }}{{ topic.last_post_at|post_time:user }}
Last Post by {{ topic.last_username }}
62 | {% else %} 63 |
64 | This user has not started any Topics yet. 65 |
66 | {% endif %} 67 |
68 | {% if is_paginated %} 69 |
70 |
{% paginator "Topic" %}
71 |
72 | {% endif %} 73 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/registration/activate.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block title %}Account activation{% endblock %} 3 | {% block content_title %}

Account activation

{% endblock %} 4 | {% block main_content %} 5 | {% load humanize %} 6 | {% if account %} 7 |

Thanks for signing up! Now you can log in and start contributing!

8 | {% else %} 9 |

Either your activation link was incorrect, or the activation key for your account has expired; activation keys are only valid for {{ expiration_days|apnumber }} days after registration.

10 | {% endif %} 11 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/registration/activation_email.txt: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 | Someone, hopefully you, signed up for a new account at {{ site.domain|escape }} using this email address. If it was you, and you'd like to activate and use your account, click the link below or copy and paste it into your web browser's address bar: 3 | 4 | {{ site.domain|escape }}accounts/activate/{{ activation_key }}/ 5 | 6 | If you didn't request this, you don't need to do anything; you won't receive any more email from us, and the account will expire automatically in {{ expiration_days|apnumber }} days. 7 | -------------------------------------------------------------------------------- /forum/templates/registration/activation_email_subject.txt: -------------------------------------------------------------------------------- 1 | Your registration at {{ site.name|escape }} -------------------------------------------------------------------------------- /forum/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block title %}Log in{% endblock %} 3 | {% block breadcrumbs %} 4 | Forums 5 | » Log in 6 | {% endblock %} 7 | {% block main_content %} 8 |

If you don't have an account, you can sign up for one.

9 |
10 | {% csrf_token %} 11 |
12 |

Log in

13 |
14 | {% if form.username.errors %}{{ form.username.html_error_list }}{% endif %} 15 | 16 |
17 | {{ form.username }} 18 |
19 |
20 |
21 | {% if form.password.errors %}{{ form.password.html_error_list }}{% endif %} 22 | 23 |
24 | {{ form.password }} 25 |
26 |
27 |
28 | Forgotten your password? You can reset it if you're really stumped. 29 |
30 |
31 |
32 |
33 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/registration/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block title %}Logged out{% endblock %} 3 | {% block breadcrumbs %} 4 | Forums 5 | » Logged out 6 | {% endblock %} 7 | {% block content_title %}

Logged out

{% endblock %} 8 | {% block main_content %} 9 |

You've been logged out.

10 |

Thanks for stopping by; when you come back, don't forget to log in again.

11 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load i18n %} 2 | {% block title %}{% trans "Password change successful" %}{% endblock %} 3 | {% block breadcrumbs %} 4 | {% trans "Forums" %} 5 | » {% trans "Password change successful" %} 6 | {% endblock %} 7 | {% block content_title %}

{% trans "Password change successful" %}

{% endblock %} 8 | {% block main_content %} 9 |

{% trans "Your password was changed." %}

10 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load i18n %} 2 | {% block title %}{% trans "Password change" %}{% endblock %} 3 | {% block breadcrumbs %} 4 | {% trans "Forums" %} 5 | » {% trans "Password change" %} 6 | {% endblock %} 7 | {% block main_content %} 8 |

{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}

9 |
10 | {% csrf_token %} 11 |
12 |

{% trans "Password change" %}

13 |
14 | {% if form.old_password.errors %}{{ form.old_password.html_error_list }}{% endif %} 15 | 16 |
17 | {{ form.old_password }} 18 |
19 |
20 |
21 | {% if form.new_password1.errors %}{{ form.new_password1.html_error_list }}{% endif %} 22 | 23 |
24 | {{ form.new_password1 }} 25 |
26 |
27 |
28 | {% if form.new_password2.errors %}{{ form.new_password2.html_error_list }}{% endif %} 29 | 30 |
31 | {{ form.new_password2 }} 32 |
33 |
34 |
35 |
36 | 37 | or 38 | Cancel 39 |
40 |
41 | 42 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load i18n %} 2 | {% block title %}{% trans "Password reset successful" %}{% endblock %} 3 | {% block breadcrumbs %} 4 | {% trans "Forums" %} 5 | » {% trans "Password reset successful" %} 6 | {% endblock %} 7 | {% block content_title %}

{% trans "Password reset successful" %}

{% endblock %} 8 | {% block main_content %} 9 |

{% trans "We've e-mailed a new password to the e-mail address you submitted. You should be receiving it shortly." %}

10 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% trans "You're receiving this e-mail because you requested a password reset" %} 3 | {% blocktrans %}for your user account at {{ site_name }}{% endblocktrans %}. 4 | 5 | {% blocktrans %}Your new password is: {{ new_password }}{% endblocktrans %} 6 | 7 | {% trans "Feel free to change this password by going to this page:" %} 8 | 9 | http://{{ domain }}/accounts/password/change/ 10 | 11 | {% trans "Your username, in case you've forgotten:" %} {{ user.username }} 12 | 13 | {% trans "Thanks for using our site!" %} 14 | 15 | {% blocktrans %}The {{ site_name }} team{% endblocktrans %} -------------------------------------------------------------------------------- /forum/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load i18n %} 2 | {% block title %}{% trans "Password reset" %}{% endblock %} 3 | {% block breadcrumbs %} 4 | {% trans "Forums" %} 5 | » {% trans "Password reset" %} 6 | {% endblock %} 7 | {% block main_content %} 8 |

{% trans "Forgotten your password? Enter your e-mail address below, and we'll reset your password and e-mail the new one to you." %}

9 |
10 | {% csrf_token %} 11 |
12 |

{% trans "Password reset" %}

13 |
14 | {% if form.email.errors %}{{ form.email.html_error_list }}{% endif %} 15 | 16 |
17 | {{ form.email }} 18 |
19 |
20 |
21 |
22 | 23 | or 24 | Cancel 25 |
26 |
27 | 28 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/registration/registration_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %}{% load i18n %} 2 | {% block title %}{% trans "Registration complete" %}{% endblock %} 3 | {% block breadcrumbs %} 4 | {% trans "Forums" %} 5 | » {% trans "Registration complete" %} 6 | {% endblock %} 7 | {% block content_title %}

{% trans "Registration complete" %}

{% endblock %} 8 | {% block main_content %} 9 | {% load humanize %} 10 |

{% trans "An activation link has been sent to the email address you supplied, along with instructions for activating your account." %}

11 | {% endblock %} -------------------------------------------------------------------------------- /forum/templates/registration/registration_form.html: -------------------------------------------------------------------------------- 1 | {% extends "forum/base.html" %} 2 | {% block title %}Sign up{% endblock %} 3 | {% block breadcrumbs %} 4 | Forums 5 | » Sign up 6 | {% endblock %} 7 | {% block main_content %} 8 |

Fill out the form below (all fields are required) and your account will be created; you'll be sent an email with instructions on how to finish your registration.

9 |
10 | {% csrf_token %} 11 |
12 |

Sign up

13 |
14 | {% if form.username.errors %}{{ form.username.errors.as_ul }}{% endif %} 15 | {{ form.username.label_tag }} 16 |
17 | {{ form.username }} 18 |
19 |
20 |
21 | {% if form.email.errors %}{{ form.email.errors.as_ul }}{% endif %} 22 | {{ form.email.label_tag }} 23 |
24 | {{ form.email }} 25 |
26 |
27 | {% if form.non_field_errors %}{{ form.non_field_errors.as_ul }}{% endif %} 28 |
29 | {% if form.password1.errors %}{{ form.password1.errors.as_ul }}{% endif %} 30 | {{ form.password1.label_tag }} 31 |
32 | {{ form.password1 }} 33 |
34 |
35 |
36 | {% if form.password2.errors %}{{ form.password2.errors.as_ul }}{% endif %} 37 | {{ form.password2.label_tag }} 38 |
39 | {{ form.password2 }} 40 |
41 |
42 |
43 |
44 |
45 | {% endblock %} -------------------------------------------------------------------------------- /forum/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/templatetags/__init__.py -------------------------------------------------------------------------------- /forum/templatetags/forum_tags.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from urlparse import urljoin 3 | 4 | from django import template 5 | from django.conf import settings 6 | from django.template import loader 7 | from django.utils import dateformat 8 | from django.utils.safestring import mark_safe 9 | 10 | from forum import auth 11 | from forum.formatters import post_formatter 12 | from forum.models import Topic, Post 13 | from forum.utils.dates import format_datetime 14 | 15 | register = template.Library() 16 | 17 | ################# 18 | # Template Tags # 19 | ################# 20 | 21 | @register.simple_tag 22 | def add_last_read_times(topics, user): 23 | """ 24 | Adds last read times to the given Topics for the given User. 25 | """ 26 | Topic.objects.add_last_read_times(topics, user) 27 | return mark_safe(u'') 28 | 29 | @register.simple_tag 30 | def add_view_counts(topics): 31 | """ 32 | Adds view count details to the given Topics. 33 | """ 34 | Topic.objects.add_view_counts(topics) 35 | return mark_safe(u'') 36 | 37 | @register.simple_tag 38 | def add_topic_view_counts(posts): 39 | """ 40 | Adds view count details to the given Topics. 41 | """ 42 | Post.objects.add_topic_view_counts(posts) 43 | return mark_safe(u'') 44 | 45 | @register.simple_tag 46 | def emoticon_help(): 47 | """ 48 | Creates a help section for the currently configured set of emoticons. 49 | """ 50 | return mark_safe(loader.render_to_string('forum/help/emoticons.html', { 51 | 'emoticons': post_formatter.emoticon_processor.emoticons 52 | })) 53 | 54 | ################## 55 | # Inclusion Tags # 56 | ################## 57 | 58 | @register.inclusion_tag('forum/pagination.html', takes_context=True) 59 | def paginator(context, what, adjacent_pages=3): 60 | """ 61 | Adds pagination context variables for use in displaying first, 62 | adjacent and last page links, in addition to those created by the 63 | ``object_list`` generic view. 64 | """ 65 | page_numbers = [n for n in \ 66 | range(context['page'] - adjacent_pages, 67 | context['page'] + adjacent_pages + 1) \ 68 | if n > 0 and n <= context['pages']] 69 | show_first = 1 not in page_numbers 70 | show_last = context['pages'] not in page_numbers 71 | return { 72 | 'what': what, 73 | 'hits': context['hits'], 74 | 'page': context['page'], 75 | 'pages': context['pages'], 76 | 'page_numbers': page_numbers, 77 | 'next': context['next'], 78 | 'previous': context['previous'], 79 | 'has_next': context['has_next'], 80 | 'has_previous': context['has_previous'], 81 | 'show_first': show_first, 82 | 'show_first_divider': show_first and page_numbers[0] != 2, 83 | 'show_last': show_last, 84 | 'show_last_divider': show_last and page_numbers[-1] != context['pages'] - 1, 85 | } 86 | 87 | ########### 88 | # Filters # 89 | ########### 90 | 91 | @register.filter 92 | def partition(l, n): 93 | """ 94 | Partitions a list into lists of size ``n``. 95 | 96 | From http://www.djangosnippets.org/snippets/6/ 97 | """ 98 | try: 99 | n = int(n) 100 | thelist = list(l) 101 | except (ValueError, TypeError): 102 | return [l] 103 | lists = [list() for i in range(int(ceil(len(l) / float(n))))] 104 | for i, item in enumerate(l): 105 | lists[i/n].append(item) 106 | return lists 107 | 108 | ########################## 109 | # Authentication Filters # 110 | ########################## 111 | 112 | @register.filter 113 | def can_edit_post(user, post): 114 | return user.is_authenticated() and \ 115 | auth.user_can_edit_post(user, post) 116 | 117 | @register.filter 118 | def can_edit_topic(user, topic): 119 | return user.is_authenticated() and \ 120 | auth.user_can_edit_topic(user, topic) 121 | 122 | @register.filter 123 | def can_edit_user_profile(user, user_to_edit): 124 | return user.is_authenticated() and \ 125 | auth.user_can_edit_user_profile(user, user_to_edit) 126 | 127 | @register.filter 128 | def is_admin(user): 129 | """ 130 | Returns ``True`` if the given user has admin permissions, 131 | ``False`` otherwise. 132 | """ 133 | return user.is_authenticated() and \ 134 | auth.is_admin(user) 135 | 136 | @register.filter 137 | def is_moderator(user): 138 | """ 139 | Returns ``True`` if the given user has moderation permissions, 140 | ``False`` otherwise. 141 | """ 142 | return user.is_authenticated() and \ 143 | auth.is_moderator(user) 144 | 145 | @register.filter 146 | def can_see_post_actions(user, topic): 147 | """ 148 | Returns ``True`` if the given User should be able to see the post 149 | action list for posts in the given topic, ``False`` otherwise. 150 | 151 | This function is used as part of ensuring that moderators have 152 | unrestricted access to locked Topics. 153 | """ 154 | if user.is_authenticated(): 155 | return not topic.locked or auth.is_moderator(user) 156 | else: 157 | return False 158 | 159 | ####################### 160 | # Date / Time Filters # 161 | ####################### 162 | 163 | @register.filter 164 | def forum_datetime(st, user=None): 165 | """ 166 | Formats a general datetime. 167 | """ 168 | return mark_safe(format_datetime(st, user, 'M jS Y', 'H:i A', ', ')) 169 | 170 | @register.filter 171 | def post_time(posted_at, user=None): 172 | """ 173 | Formats a Post time. 174 | """ 175 | return mark_safe(format_datetime(posted_at, user, r'\o\n M jS Y', r'\a\t H:i A')) 176 | 177 | @register.filter 178 | def joined_date(date): 179 | """ 180 | Formats a joined date. 181 | """ 182 | return mark_safe(dateformat.format(date, 'M jS Y')) 183 | 184 | ######################## 185 | # Topic / Post Filters # 186 | ######################## 187 | 188 | @register.filter 189 | def is_first_post(post): 190 | """ 191 | Determines if the given post is the first post in a topic. 192 | """ 193 | return post.num_in_topic == 1 and not post.meta 194 | 195 | @register.filter 196 | def topic_status_image(topic): 197 | """ 198 | Returns HTML for an image representing a topic's status. 199 | """ 200 | if has_new_posts(topic): 201 | src = u'forum/img/new_posts.gif' 202 | description = u'New Posts' 203 | else: 204 | src = u'forum/img/no_new_posts.gif' 205 | description = u'No New Posts' 206 | return mark_safe(u'%s' % ( 207 | urljoin(settings.STATIC_URL, src), description, description)) 208 | 209 | @register.filter 210 | def has_new_posts(topic): 211 | """ 212 | Returns ``True`` if the given topic has new posts for the current 213 | User, based on the presence and value of a ``last_read`` attribute. 214 | """ 215 | if hasattr(topic, 'last_read'): 216 | return topic.last_read is None or topic.last_post_at > topic.last_read 217 | else: 218 | return False 219 | 220 | @register.filter 221 | def topic_pagination(topic, posts_per_page): 222 | """ 223 | Creates topic listing page links for the given topic, with the given 224 | number of posts per page. 225 | 226 | Topics with between 2 and 5 pages will have page links displayed for 227 | each page. 228 | 229 | Topics with more than 5 pages will have page links displayed for the 230 | first page and the last 3 pages. 231 | """ 232 | hits = (topic.post_count - 1) 233 | if hits < 1: 234 | hits = 0 235 | pages = hits // posts_per_page + 1 236 | if pages < 2: 237 | html = u'' 238 | else: 239 | page_link = u'%%s' % \ 240 | topic.get_absolute_url() 241 | if pages < 6: 242 | html = u' '.join([page_link % (page, page) \ 243 | for page in xrange(1, pages + 1)]) 244 | else: 245 | html = u' '.join([page_link % (1 ,1), u'…'] + \ 246 | [page_link % (page, page) \ 247 | for page in xrange(pages - 2, pages + 1)]) 248 | return mark_safe(html) 249 | -------------------------------------------------------------------------------- /forum/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import forum 2 | 3 | from forum.tests.auth import * 4 | from forum.tests.models import * -------------------------------------------------------------------------------- /forum/tests/auth.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | 4 | from forum import auth 5 | from forum.models import Post, Topic 6 | 7 | class AuthTestCase(TestCase): 8 | """ 9 | Tests for the authorisation module. 10 | """ 11 | fixtures = ['testdata.json'] 12 | 13 | def setUp(self): 14 | """ 15 | Retrieves a user from each user group for convenience. 16 | """ 17 | self.admin = User.objects.get(pk=1) 18 | self.moderator = User.objects.get(pk=2) 19 | self.user = User.objects.get(pk=3) 20 | 21 | def test_is_admin(self): 22 | """ 23 | Verifies the check for a user having Administrator privileges. 24 | """ 25 | self.assertTrue(auth.is_admin(self.admin)) 26 | self.assertFalse(auth.is_admin(self.moderator)) 27 | self.assertFalse(auth.is_admin(self.user)) 28 | 29 | def test_is_moderator(self): 30 | """ 31 | Verifies the check for a user having Moderator privileges. 32 | """ 33 | self.assertTrue(auth.is_moderator(self.admin)) 34 | self.assertTrue(auth.is_moderator(self.moderator)) 35 | self.assertFalse(auth.is_moderator(self.user)) 36 | 37 | def test_user_can_edit_post(self): 38 | """ 39 | Verifies the check for a given user being able to edit a given 40 | Post. 41 | 42 | Members of the User group may only edit their own Posts if they 43 | are not in unlocked Topics. 44 | """ 45 | # Post by admin 46 | post = Post.objects.get(pk=1) 47 | topic = post.topic 48 | self.assertTrue(auth.user_can_edit_post(self.admin, post)) 49 | self.assertTrue(auth.user_can_edit_post(self.moderator, post)) 50 | self.assertFalse(auth.user_can_edit_post(self.user, post)) 51 | self.assertTrue(auth.user_can_edit_post(self.admin, post, topic)) 52 | self.assertTrue(auth.user_can_edit_post(self.moderator, post, topic)) 53 | self.assertFalse(auth.user_can_edit_post(self.user, post, topic)) 54 | topic.locked = True 55 | self.assertTrue(auth.user_can_edit_post(self.admin, post, topic)) 56 | self.assertTrue(auth.user_can_edit_post(self.moderator, post, topic)) 57 | self.assertFalse(auth.user_can_edit_post(self.user, post, topic)) 58 | 59 | # Post by moderator 60 | post = Post.objects.get(pk=4) 61 | topic = post.topic 62 | self.assertTrue(auth.user_can_edit_post(self.admin, post)) 63 | self.assertTrue(auth.user_can_edit_post(self.moderator, post)) 64 | self.assertFalse(auth.user_can_edit_post(self.user, post)) 65 | self.assertTrue(auth.user_can_edit_post(self.admin, post, topic)) 66 | self.assertTrue(auth.user_can_edit_post(self.moderator, post, topic)) 67 | self.assertFalse(auth.user_can_edit_post(self.user, post, topic)) 68 | topic.locked = True 69 | self.assertTrue(auth.user_can_edit_post(self.admin, post, topic)) 70 | self.assertTrue(auth.user_can_edit_post(self.moderator, post, topic)) 71 | self.assertFalse(auth.user_can_edit_post(self.user, post, topic)) 72 | 73 | # Post by user 74 | post = Post.objects.get(pk=7) 75 | topic = post.topic 76 | self.assertTrue(auth.user_can_edit_post(self.admin, post)) 77 | self.assertTrue(auth.user_can_edit_post(self.moderator, post)) 78 | self.assertTrue(auth.user_can_edit_post(self.user, post)) 79 | self.assertTrue(auth.user_can_edit_post(self.admin, post, topic)) 80 | self.assertTrue(auth.user_can_edit_post(self.moderator, post, topic)) 81 | self.assertTrue(auth.user_can_edit_post(self.user, post, topic)) 82 | topic.locked = True 83 | self.assertTrue(auth.user_can_edit_post(self.admin, post, topic)) 84 | self.assertTrue(auth.user_can_edit_post(self.moderator, post, topic)) 85 | self.assertFalse(auth.user_can_edit_post(self.user, post, topic)) 86 | 87 | def test_user_can_edit_topic(self): 88 | """ 89 | Verifies the check for a given user being able to edit a given 90 | Topic. 91 | 92 | Members of the User group may only edit their own Topics if they 93 | are not locked. 94 | """ 95 | # Topic creeated by admin 96 | topic = Topic.objects.get(pk=1) 97 | self.assertTrue(auth.user_can_edit_topic(self.admin, topic)) 98 | self.assertTrue(auth.user_can_edit_topic(self.moderator, topic)) 99 | self.assertFalse(auth.user_can_edit_topic(self.user, topic)) 100 | topic.locked = True 101 | self.assertTrue(auth.user_can_edit_topic(self.admin, topic)) 102 | self.assertTrue(auth.user_can_edit_topic(self.moderator, topic)) 103 | self.assertFalse(auth.user_can_edit_topic(self.user, topic)) 104 | 105 | # Topic created by moderator 106 | topic = Topic.objects.get(pk=2) 107 | self.assertTrue(auth.user_can_edit_topic(self.admin, topic)) 108 | self.assertTrue(auth.user_can_edit_topic(self.moderator, topic)) 109 | self.assertFalse(auth.user_can_edit_topic(self.user, topic)) 110 | topic.locked = True 111 | self.assertTrue(auth.user_can_edit_topic(self.admin, topic)) 112 | self.assertTrue(auth.user_can_edit_topic(self.moderator, topic)) 113 | self.assertFalse(auth.user_can_edit_topic(self.user, topic)) 114 | 115 | # Topic created by user 116 | topic = Topic.objects.get(pk=3) 117 | self.assertTrue(auth.user_can_edit_topic(self.admin, topic)) 118 | self.assertTrue(auth.user_can_edit_topic(self.moderator, topic)) 119 | self.assertTrue(auth.user_can_edit_topic(self.user, topic)) 120 | topic.locked = True 121 | self.assertTrue(auth.user_can_edit_topic(self.admin, topic)) 122 | self.assertTrue(auth.user_can_edit_topic(self.moderator, topic)) 123 | self.assertFalse(auth.user_can_edit_topic(self.user, topic)) 124 | 125 | def test_user_can_edit_user_profile(self): 126 | """ 127 | Verifies the check for a given user being able to edit another 128 | given user's public ForumProfile. 129 | 130 | Members of the User group may only edit their own ForumProfile. 131 | """ 132 | self.assertTrue(auth.user_can_edit_user_profile(self.admin, self.admin)) 133 | self.assertTrue(auth.user_can_edit_user_profile(self.moderator, self.admin)) 134 | self.assertFalse(auth.user_can_edit_user_profile(self.user, self.admin)) 135 | 136 | self.assertTrue(auth.user_can_edit_user_profile(self.admin, self.moderator)) 137 | self.assertTrue(auth.user_can_edit_user_profile(self.moderator, self.moderator)) 138 | self.assertFalse(auth.user_can_edit_user_profile(self.user, self.moderator)) 139 | 140 | self.assertTrue(auth.user_can_edit_user_profile(self.admin, self.user)) 141 | self.assertTrue(auth.user_can_edit_user_profile(self.moderator, self.user)) 142 | self.assertTrue(auth.user_can_edit_user_profile(self.user, self.user)) -------------------------------------------------------------------------------- /forum/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.conf import settings 3 | 4 | from forum import app_settings 5 | 6 | urlpatterns = patterns('forum.views', 7 | url(r'^$', 'forum_index', name='forum_index'), 8 | url(r'^search/$', 'search', name='forum_search'), 9 | url(r'^search_results/(?P\d+)/$', 'search_results', name='forum_search_results'), 10 | url(r'^new_posts/$', 'new_posts', name='forum_new_posts'), 11 | url(r'^stalk_users/$', 'stalk_users', name='forum_stalk_users'), 12 | url(r'^add_section/$', 'add_section', name='forum_add_section'), 13 | url(r'^section/(?P\d+)/$', 'section_detail', name='forum_section_detail'), 14 | url(r'^section/(?P\d+)/edit/$', 'edit_section', name='forum_edit_section'), 15 | url(r'^section/(?P\d+)/delete/$', 'delete_section', name='forum_delete_section'), 16 | url(r'^section/(?P\d+)/add_forum/$', 'add_forum', name='forum_add_forum'), 17 | url(r'^forum/(?P\d+)/$', 'forum_detail', name='forum_forum_detail'), 18 | url(r'^forum/(?P\d+)/edit/$', 'edit_forum', name='forum_edit_forum'), 19 | url(r'^forum/(?P\d+)/delete/$', 'delete_forum', name='forum_delete_forum'), 20 | url(r'^forum/(?P\d+)/add_topic/$', 'add_topic', name='forum_add_topic'), 21 | url(r'^topic/(?P\d+)/$', 'topic_detail', {'meta': False}, name='forum_topic_detail'), 22 | url(r'^topic/(?P\d+)/edit/$', 'edit_topic', name='forum_edit_topic'), 23 | url(r'^topic/(?P\d+)/reply/$', 'add_reply', {'meta': False}, name='forum_add_reply'), 24 | url(r'^topic/(?P\d+)/last_post/$', 'redirect_to_last_post', name='forum_redirect_to_last_post'), 25 | url(r'^topic/(?P\d+)/unread_post/$', 'redirect_to_unread_post', name='forum_redirect_to_unread_post'), 26 | url(r'^topic/(?P\d+)/delete/$', 'delete_topic', name='forum_delete_topic'), 27 | url(r'^topic/(?P\d+)/meta/$', 'topic_detail', {'meta': True}, name='forum_topic_meta_detail'), 28 | url(r'^topic/(?P\d+)/meta/reply/$', 'add_reply', {'meta': True}, name='forum_add_meta_reply'), 29 | url(r'^topic/(?P\d+)/summary/$', 'topic_post_summary', name='forum_topic_post_summary'), 30 | url(r'^post/(?P\d+)/$', 'redirect_to_post', name='forum_redirect_to_post'), 31 | url(r'^post/(?P\d+)/edit/$', 'edit_post', name='forum_edit_post'), 32 | url(r'^post/(?P\d+)/quote/$', 'quote_post', name='forum_quote_post'), 33 | url(r'^post/(?P\d+)/delete/$', 'delete_post', name='forum_delete_post'), 34 | url(r'^user/(?P\d+)/$', 'user_profile', name='forum_user_profile'), 35 | url(r'^user/(?P\d+)/topics/$', 'user_topics', name='forum_user_topics'), 36 | url(r'^user/(?P\d+)/edit_profile/$', 'edit_user_forum_profile', name='forum_edit_user_forum_profile'), 37 | url(r'^user/edit_forum_settings/$', 'edit_user_forum_settings', name='forum_edit_user_forum_settings'), 38 | ) 39 | 40 | if app_settings.STANDALONE: 41 | from django.contrib import admin 42 | admin.autodiscover() 43 | urlpatterns += patterns('', 44 | (r'^accounts/', include('registration.backends.default.urls')), 45 | (r'^admin/', include(admin.site.urls)), 46 | ) 47 | 48 | -------------------------------------------------------------------------------- /forum/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insin/forum/59b35bd102da3bdeb0d6bc104de77572158992b3/forum/utils/__init__.py -------------------------------------------------------------------------------- /forum/utils/dates.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.conf import settings 4 | from django.utils import dateformat 5 | 6 | import pytz 7 | from forum.models import ForumProfile 8 | 9 | def user_timezone(dt, user): 10 | """ 11 | Converts the given datetime to the given User's timezone, if they 12 | have one set in their forum profile. 13 | 14 | Adapted from http://www.djangosnippets.org/snippets/183/ 15 | """ 16 | tz = settings.TIME_ZONE 17 | if user.is_authenticated(): 18 | profile = ForumProfile.objects.get_for_user(user) 19 | if profile.timezone: 20 | tz = profile.timezone 21 | try: 22 | result = dt.astimezone(pytz.timezone(tz)) 23 | except ValueError: 24 | # The datetime was stored without timezone info, so use the 25 | # timezone configured in settings. 26 | result = dt.replace(tzinfo=pytz.timezone(settings.TIME_ZONE)) \ 27 | .astimezone(pytz.timezone(tz)) 28 | return result 29 | 30 | def format_datetime(dt, user, date_format, time_format, separator=' '): 31 | """ 32 | Formats a datetime, using ``'Today'`` or ``'Yesterday'`` instead of 33 | the given date format when appropriate. 34 | 35 | If a User is given and they have a timezone set in their profile, 36 | the datetime will be translated to their local time. 37 | """ 38 | if user: 39 | dt = user_timezone(dt, user) 40 | today = user_timezone(datetime.datetime.now(), user).date() 41 | else: 42 | today = datetime.date.today() 43 | date_part = dt.date() 44 | delta = date_part - today 45 | if delta.days == 0: 46 | date = u'Today' 47 | elif delta.days == -1: 48 | date = u'Yesterday' 49 | else: 50 | date = dateformat.format(dt, date_format) 51 | return u'%s%s%s' % (date, separator, 52 | dateformat.time_format(dt.time(), time_format)) 53 | -------------------------------------------------------------------------------- /forum/utils/models.py: -------------------------------------------------------------------------------- 1 | from django.db import connection, transaction 2 | 3 | def update(model_instance, *args): 4 | """ 5 | Updates only specified fields of the given model instance. 6 | """ 7 | opts = model_instance._meta 8 | fields = [opts.get_field(f) for f in args] 9 | db_values = [f.get_db_prep_save(f.pre_save(model_instance, False)) for f in fields] 10 | if db_values: 11 | connection.cursor().execute("UPDATE %s SET %s WHERE %s=%%s" % \ 12 | (connection.ops.quote_name(opts.db_table), 13 | ','.join(['%s=%%s' % connection.ops.quote_name(f.column) for f in fields]), 14 | connection.ops.quote_name(opts.pk.column)), 15 | db_values + opts.pk.get_db_prep_lookup('exact', model_instance.pk)) 16 | transaction.commit_unless_managed() 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forum", 3 | "version": "0.1.0-alpha", 4 | "description": "Node.js component of https://github.com/insin/forum", 5 | "dependencies": { 6 | "redis": ">= 0.6.7" 7 | } 8 | } -------------------------------------------------------------------------------- /pip-requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.3.1 2 | django-debug-toolbar==0.8.5 3 | -e hg+https://bitbucket.org/ubernostrum/django-registration#egg=registration 4 | markdown2==1.0.1.19 5 | pil==1.1.7 6 | postmarkup==1.1.4 7 | Pygments==1.4 8 | pytz==2011k 9 | redis==2.4.10 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from distutils.command.install_data import install_data 3 | from distutils.command.install import INSTALL_SCHEMES 4 | import os 5 | import sys 6 | 7 | class osx_install_data(install_data): 8 | # On MacOS, the platform-specific lib dir is /System/Library/Framework/Python/.../ 9 | # which is wrong. Python 2.5 supplied with MacOS 10.5 has an Apple-specific fix 10 | # for this in distutils.command.install_data#306. It fixes install_lib but not 11 | # install_data, which is why we roll our own install_data class. 12 | 13 | def finalize_options(self): 14 | # By the time finalize_options is called, install.install_lib is set to the 15 | # fixed directory, so we set the installdir to install_lib. The 16 | # install_data class uses ('install_data', 'install_dir') instead. 17 | self.set_undefined_options('install', ('install_lib', 'install_dir')) 18 | install_data.finalize_options(self) 19 | 20 | if sys.platform == "darwin": 21 | cmdclasses = {'install_data': osx_install_data} 22 | else: 23 | cmdclasses = {'install_data': install_data} 24 | 25 | def fullsplit(path, result=None): 26 | """ 27 | Split a pathname into components (the opposite of os.path.join) in a 28 | platform-neutral way. 29 | """ 30 | if result is None: 31 | result = [] 32 | head, tail = os.path.split(path) 33 | if head == '': 34 | return [tail] + result 35 | if head == path: 36 | return result 37 | return fullsplit(head, [tail] + result) 38 | 39 | # Tell distutils to put the data_files in platform-specific installation 40 | # locations. See here for an explanation: 41 | # http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb 42 | for scheme in INSTALL_SCHEMES.values(): 43 | scheme['data'] = scheme['purelib'] 44 | 45 | # Compile the list of packages available, because distutils doesn't have 46 | # an easy way to do this. 47 | packages, data_files = [], [] 48 | root_dir = os.path.dirname(__file__) 49 | if root_dir != '': 50 | os.chdir(root_dir) 51 | app_dir = 'forum' 52 | 53 | for dirpath, dirnames, filenames in os.walk(app_dir): 54 | # Ignore dirnames that start with '.' 55 | for i, dirname in enumerate(dirnames): 56 | if dirname.startswith('.'): del dirnames[i] 57 | if '__init__.py' in filenames: 58 | packages.append('.'.join(fullsplit(dirpath))) 59 | elif filenames: 60 | data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]]) 61 | 62 | # Small hack for working with bdist_wininst. 63 | # See http://mail.python.org/pipermail/distutils-sig/2004-August/004134.html 64 | if len(sys.argv) > 1 and sys.argv[1] == 'bdist_wininst': 65 | for file_info in data_files: 66 | file_info[0] = '\\PURELIB\\%s' % file_info[0] 67 | 68 | # Dynamically calculate the version based on forum.VERSION. 69 | version = __import__('forum').get_version() 70 | 71 | setup( 72 | name = 'forum', 73 | version = version.replace(' ', '-'), 74 | author = 'Jonathan Buchanan', 75 | author_email = 'jonathan.buchanan@gmail.com', 76 | url = 'http://github.com/insin/forum/', 77 | packages = packages, 78 | cmdclass = cmdclasses, 79 | data_files = data_files 80 | ) 81 | -------------------------------------------------------------------------------- /stalk.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , redis = require('redis') 3 | , sys = require('sys') 4 | , querystring = require('querystring') 5 | 6 | var REDIS_HOST = 'localhost' 7 | , REDIS_PORT = 6379 8 | , REDIS_DB = 0 9 | 10 | var client = redis.createClient(REDIS_PORT, REDIS_HOST) 11 | 12 | client.on('error', function (err) { 13 | sys.puts('Redis error ' + err) 14 | }) 15 | 16 | var buffer = new Buffer('{}') 17 | 18 | function update() { 19 | sys.puts('[' + new Date() + '] Updating stalk data') 20 | // Python stores activity scores as seconds since epoch 21 | var since = (new Date().valueOf() - (30 * 60 * 1000)) / 1000 22 | var users = [] 23 | // Get recently active users and when they were last seen 24 | client.zrangebyscore('au', since, '+inf', 'withscores', function(err, active) { 25 | var getKeys = [] 26 | // Iterate back to front to get most recent users first 27 | for (var i = active.length - 2; i >= 0; i -= 2) { 28 | var id = active[i] 29 | users.push({id: id, seen: active[i + 1]}) 30 | getKeys.push('u:' + id + ':un') 31 | getKeys.push('u:' + id + ':d') 32 | } 33 | 34 | if (!users.length) { 35 | buffer = new Buffer(JSON.stringify(users)) 36 | setTimeout(update, 5000) 37 | return 38 | } 39 | 40 | sys.puts(users.length + ' active') 41 | 42 | // Get usernames and what they were last seen doing 43 | client.mget(getKeys, function(err, details) { 44 | var userIndex = 0 45 | for (var i = 0, l = details.length; i < l; i += 2) { 46 | var user = users[userIndex++] 47 | user.username = details[i] 48 | user.doing = details[i + 1] 49 | } 50 | buffer = new Buffer(JSON.stringify(users)) 51 | setTimeout(update, 5000) 52 | }) 53 | }) 54 | } 55 | 56 | var server = http.createServer(function (req, res) { 57 | var callback = require('url').parse(req.url, true).query['callback'] || 'callback' 58 | res.write(callback + '(') 59 | res.write(buffer) 60 | res.end(')') 61 | }) 62 | 63 | server.listen(8001, '127.0.0.1') 64 | sys.puts('Stalking users at http://127.0.0.1:8001/') 65 | 66 | update() 67 | --------------------------------------------------------------------------------