Browse Source

Merge pull request #144 from rafalp/develop

Merge 0.3 into master branch
Rafał Pitoń 12 years ago
parent
commit
a28a0052bb
260 changed files with 14882 additions and 4649 deletions
  1. 4 0
      .gitignore
  2. 3 1
      README.md
  3. 4 0
      cron.txt
  4. 23 1
      deployment/settings.py
  5. 9 0
      deployment/urls.py
  6. 87 0
      heartbeat.py
  7. 1 1
      misago/__init__.py
  8. 14 0
      misago/acl/permissions/privatethreads.py
  9. 114 0
      misago/acl/permissions/reports.py
  10. 37 0
      misago/acl/permissions/search.py
  11. 90 30
      misago/acl/permissions/threads.py
  12. 4 4
      misago/apps/admin/bans/views.py
  13. 0 51
      misago/apps/admin/clients/forms.py
  14. 0 110
      misago/apps/admin/clients/views.py
  15. 1 1
      misago/apps/admin/forumroles/forms.py
  16. 3 3
      misago/apps/admin/forumroles/views.py
  17. 86 27
      misago/apps/admin/forums/forms.py
  18. 125 99
      misago/apps/admin/forums/views.py
  19. 0 1
      misago/apps/admin/home.py
  20. 1 1
      misago/apps/admin/newsletters/forms.py
  21. 0 1
      misago/apps/admin/online/forms.py
  22. 0 2
      misago/apps/admin/online/views.py
  23. 1 1
      misago/apps/admin/pruneusers/forms.py
  24. 2 2
      misago/apps/admin/pruneusers/views.py
  25. 15 1
      misago/apps/admin/ranks/forms.py
  26. 27 13
      misago/apps/admin/ranks/views.py
  27. 1 1
      misago/apps/admin/roles/forms.py
  28. 3 3
      misago/apps/admin/roles/views.py
  29. 63 75
      misago/apps/admin/sections/forums.py
  30. 52 52
      misago/apps/admin/sections/overview.py
  31. 61 61
      misago/apps/admin/sections/perms.py
  32. 13 43
      misago/apps/admin/sections/system.py
  33. 151 151
      misago/apps/admin/sections/users.py
  34. 2 0
      misago/apps/admin/settings/views.py
  35. 19 23
      misago/apps/admin/users/views.py
  36. 10 41
      misago/apps/admin/widgets.py
  37. 9 4
      misago/apps/alerts.py
  38. 35 15
      misago/apps/index.py
  39. 7 1
      misago/apps/newthreads.py
  40. 8 2
      misago/apps/popularthreads.py
  41. 20 0
      misago/apps/privatethreads/delete.py
  42. 1 1
      misago/apps/privatethreads/forms.py
  43. 11 11
      misago/apps/privatethreads/jumps.py
  44. 11 2
      misago/apps/privatethreads/list.py
  45. 12 10
      misago/apps/privatethreads/mixins.py
  46. 5 0
      misago/apps/privatethreads/posting.py
  47. 27 0
      misago/apps/privatethreads/search.py
  48. 6 6
      misago/apps/privatethreads/thread.py
  49. 18 6
      misago/apps/privatethreads/urls.py
  50. 5 1
      misago/apps/profiles/decorators.py
  51. 2 2
      misago/apps/profiles/followers/urls.py
  52. 7 1
      misago/apps/profiles/followers/views.py
  53. 2 2
      misago/apps/profiles/follows/urls.py
  54. 8 2
      misago/apps/profiles/follows/views.py
  55. 2 2
      misago/apps/profiles/posts/urls.py
  56. 8 1
      misago/apps/profiles/posts/views.py
  57. 0 1
      misago/apps/profiles/template.py
  58. 2 2
      misago/apps/profiles/threads/urls.py
  59. 7 1
      misago/apps/profiles/threads/views.py
  60. 2 2
      misago/apps/profiles/urls.py
  61. 19 4
      misago/apps/profiles/views.py
  62. 15 0
      misago/apps/reports/changelog.py
  63. 37 0
      misago/apps/reports/delete.py
  64. 5 0
      misago/apps/reports/details.py
  65. 30 0
      misago/apps/reports/forms.py
  66. 29 0
      misago/apps/reports/jumps.py
  67. 116 0
      misago/apps/reports/list.py
  68. 7 1
      misago/apps/reports/mixins.py
  69. 44 0
      misago/apps/reports/posting.py
  70. 25 0
      misago/apps/reports/search.py
  71. 56 0
      misago/apps/reports/thread.py
  72. 33 3
      misago/apps/reports/urls.py
  73. 0 0
      misago/apps/search/__init__.py
  74. 75 0
      misago/apps/search/forms.py
  75. 7 0
      misago/apps/search/urls.py
  76. 125 0
      misago/apps/search/views.py
  77. 1 1
      misago/apps/signin/forms.py
  78. 8 0
      misago/apps/signin/views.py
  79. 20 0
      misago/apps/threads/delete.py
  80. 8 0
      misago/apps/threads/jumps.py
  81. 12 5
      misago/apps/threads/list.py
  82. 6 6
      misago/apps/threads/thread.py
  83. 13 6
      misago/apps/threads/urls.py
  84. 8 6
      misago/apps/threadtype/base.py
  85. 1 2
      misago/apps/threadtype/changelog.py
  86. 106 18
      misago/apps/threadtype/delete.py
  87. 120 2
      misago/apps/threadtype/jumps.py
  88. 2 26
      misago/apps/threadtype/list/forms.py
  89. 88 50
      misago/apps/threadtype/list/moderation.py
  90. 4 1
      misago/apps/threadtype/list/views.py
  91. 26 4
      misago/apps/threadtype/mixins.py
  92. 26 4
      misago/apps/threadtype/posting/base.py
  93. 3 11
      misago/apps/threadtype/posting/editreply.py
  94. 7 11
      misago/apps/threadtype/posting/editthread.py
  95. 6 3
      misago/apps/threadtype/posting/forms.py
  96. 29 38
      misago/apps/threadtype/posting/newreply.py
  97. 4 3
      misago/apps/threadtype/posting/newthread.py
  98. 3 2
      misago/apps/threadtype/thread/forms.py
  99. 3 2
      misago/apps/threadtype/thread/moderation/forms.py
  100. 16 15
      misago/apps/threadtype/thread/moderation/posts.py
  101. 51 24
      misago/apps/threadtype/thread/moderation/thread.py
  102. 21 8
      misago/apps/threadtype/thread/views.py
  103. 4 0
      misago/apps/usercp/avatar/views.py
  104. 2 2
      misago/apps/usercp/credentials/forms.py
  105. 2 2
      misago/apps/watchedthreads/urls.py
  106. 9 1
      misago/apps/watchedthreads/views.py
  107. 2 1
      misago/auth.py
  108. 21 3
      misago/context_processors.py
  109. 1 3
      misago/dbsettings.py
  110. 2 1
      misago/fixtures/accountssetings.py
  111. 10 2
      misago/fixtures/aclmonitor.py
  112. 11 2
      misago/fixtures/bansmonitor.py
  113. 12 2
      misago/fixtures/forums.py
  114. 2 0
      misago/fixtures/forumsroles.py
  115. 14 0
      misago/fixtures/onlinemonitor.py
  116. 13 0
      misago/fixtures/reportsmonitor.py
  117. 17 0
      misago/fixtures/signingsettings.py
  118. 2 2
      misago/fixtures/threadssettings.py
  119. 17 0
      misago/fixtures/userroles.py
  120. 11 8
      misago/fixtures/usersmonitor.py
  121. BIN
      misago/locale/pl/LC_MESSAGES/django.mo
  122. 1901 1315
      misago/locale/pl/LC_MESSAGES/django.po
  123. 2 1
      misago/management/commands/clearattempts.py
  124. 9 0
      misago/management/commands/clearmonitor.py
  125. 13 0
      misago/management/commands/countreports.py
  126. 0 289
      misago/management/commands/migratefrom01.py
  127. 20 5
      misago/management/commands/pruneforums.py
  128. 1 2
      misago/management/commands/rebuildacls.py
  129. 22 0
      misago/management/commands/reparseposts.py
  130. 6 3
      misago/management/commands/updateranking.py
  131. 20 0
      misago/markdown/extensions/bbcodes.py
  132. 37 0
      misago/markdown/extensions/cleanlinks.py
  133. 194 0
      misago/markdown/extensions/emoji.py
  134. 8 9
      misago/markdown/extensions/magiclinks.py
  135. 32 0
      misago/markdown/extensions/shorthandimgs.py
  136. 1 1
      misago/markdown/extensions/strikethrough.py
  137. 24 43
      misago/markdown/factory.py
  138. 51 0
      misago/markdown/parsers.py
  139. 1 1
      misago/middleware/heartbeat.py
  140. 17 0
      misago/middleware/mailsqueue.py
  141. 1 3
      misago/middleware/session.py
  142. 12 15
      misago/middleware/theme.py
  143. 9 4
      misago/middleware/user.py
  144. 396 0
      misago/migrations/0002_auto__del_field_session_hidden.py
  145. 415 0
      misago/migrations/0003_auto__add_field_checkpoint_old_forum__add_field_checkpoint_old_forum_n.py
  146. 400 0
      misago/migrations/0004_auto__add_field_checkpoint_deleted.py
  147. 401 0
      misago/migrations/0005_auto__add_field_forum_pruned_archive.py
  148. 405 0
      misago/migrations/0006_auto.py
  149. 394 0
      misago/migrations/0007_removethemeadjustments.py
  150. 397 0
      misago/migrations/0008_auto__add_field_thread_report_for.py
  151. 404 0
      misago/migrations/0009_auto__chg_field_thread_report_for__add_field_monitoritem_type.py
  152. 396 0
      misago/migrations/0010_auto__del_field_checkpoint_post__del_field_monitoritem_value__add_fiel.py
  153. 394 0
      misago/migrations/0011_auto__del_field_thread_merges__del_field_post_merge.py
  154. 396 0
      misago/migrations/0012_auto__add_field_post_current_date.py
  155. 391 0
      misago/migrations/0013_set_posts_current_date.py
  156. 391 0
      misago/migrations/0014_auto__del_field_post_edit_date.py
  157. 388 0
      misago/migrations/0015_remove_users_reported.py
  158. 0 1
      misago/models/__init__.py
  159. 14 21
      misago/models/checkpointmodel.py
  160. 74 8
      misago/models/forummodel.py
  161. 1 1
      misago/models/forumreadmodel.py
  162. 4 0
      misago/models/karmamodel.py
  163. 18 1
      misago/models/monitoritemmodel.py
  164. 37 39
      misago/models/postmodel.py
  165. 7 6
      misago/models/rankmodel.py
  166. 0 1
      misago/models/sessionmodel.py
  167. 0 25
      misago/models/themeadjustmentmodel.py
  168. 92 15
      misago/models/threadmodel.py
  169. 17 10
      misago/models/usermodel.py
  170. 18 2
      misago/monitor.py
  171. 62 0
      misago/onlines.py
  172. 3 3
      misago/readstrackers.py
  173. 0 0
      misago/search.py
  174. 18 0
      misago/search_indexes.py
  175. 33 25
      misago/sessions.py
  176. 28 3
      misago/settings_base.py
  177. 2 1
      misago/signals.py
  178. 11 1
      misago/templatetags/datetime.py
  179. 10 1
      misago/templatetags/utils.py
  180. 1 1
      misago/tests/__init__.py
  181. 14 3
      misago/tests/user_manager_create_user.py
  182. 61 8
      misago/theme.py
  183. 10 3
      misago/urls.py
  184. 43 8
      misago/utils/datesformats.py
  185. 19 2
      misago/utils/fixtures.py
  186. 41 6
      misago/utils/pagination.py
  187. 7 3
      misago/utils/strings.py
  188. 24 0
      misago/utils/urls.py
  189. 5 2
      misago/utils/views.py
  190. 3 1
      requirements.txt
  191. 177 120
      static/cranefly/css/cranefly.css
  192. 3 0
      static/cranefly/css/cranefly.less
  193. 57 20
      static/cranefly/css/cranefly/category.less
  194. 252 115
      static/cranefly/css/cranefly/forum.less
  195. 107 111
      static/cranefly/css/cranefly/forummap.less
  196. 23 0
      static/cranefly/css/cranefly/header.less
  197. 100 62
      static/cranefly/css/cranefly/index.less
  198. 1 1
      static/cranefly/css/cranefly/karmas.less
  199. 72 88
      static/cranefly/css/cranefly/markdown.less
  200. 5 1
      static/cranefly/css/cranefly/messages.less
  201. 158 2
      static/cranefly/css/cranefly/navbar.less
  202. 79 0
      static/cranefly/css/cranefly/report.less
  203. 54 0
      static/cranefly/css/cranefly/reports.less
  204. 18 11
      static/cranefly/css/cranefly/scaffolding.less
  205. 63 0
      static/cranefly/css/cranefly/search.less
  206. 71 16
      static/cranefly/css/cranefly/thread.less
  207. 20 28
      static/cranefly/css/cranefly/watchedthreads.less
  208. 12 3
      static/cranefly/css/variables.less
  209. BIN
      static/cranefly/img/img_broken.jpg
  210. 194 173
      static/cranefly/js/cranefly.js
  211. 166 89
      static/cranefly/js/editor.js
  212. 1 1
      templates/_email/base.html
  213. 9 0
      templates/_email/report_reply_notification.html
  214. 10 0
      templates/_email/report_reply_notification.txt
  215. 1 1
      templates/admin/admin/list.html
  216. 5 5
      templates/admin/bans/list.html
  217. 1 1
      templates/admin/base.html
  218. 1 1
      templates/admin/ranks/list.html
  219. 4 1
      templates/admin/settings/settings.html
  220. 12 5
      templates/cranefly/alerts.html
  221. 5 1
      templates/cranefly/base.html
  222. 63 63
      templates/cranefly/category.html
  223. 5 4
      templates/cranefly/editor.html
  224. 18 61
      templates/cranefly/forum_map.html
  225. 92 66
      templates/cranefly/index.html
  226. 72 42
      templates/cranefly/layout.html
  227. 32 0
      templates/cranefly/macros.html
  228. 48 50
      templates/cranefly/new_threads.html
  229. 49 50
      templates/cranefly/popular_threads.html
  230. 1 1
      templates/cranefly/private_threads/changelog.html
  231. 65 76
      templates/cranefly/private_threads/list.html
  232. 5 3
      templates/cranefly/private_threads/posting.html
  233. 96 59
      templates/cranefly/private_threads/thread.html
  234. 5 2
      templates/cranefly/profiles/details.html
  235. 5 2
      templates/cranefly/profiles/list.html
  236. 7 7
      templates/cranefly/profiles/profile.html
  237. 81 0
      templates/cranefly/reports/changelog.html
  238. 95 0
      templates/cranefly/reports/changelog_diff.html
  239. 35 0
      templates/cranefly/reports/details.html
  240. 187 0
      templates/cranefly/reports/list.html
  241. 177 0
      templates/cranefly/reports/posting.html
  242. 415 0
      templates/cranefly/reports/thread.html
  243. 6 0
      templates/cranefly/search/error.html
  244. 17 0
      templates/cranefly/search/home.html
  245. 21 0
      templates/cranefly/search/layout.html
  246. 48 0
      templates/cranefly/search/results.html
  247. 2 4
      templates/cranefly/threads/changelog.html
  248. 1 3
      templates/cranefly/threads/changelog_diff.html
  249. 1 3
      templates/cranefly/threads/details.html
  250. 5 7
      templates/cranefly/threads/karmas.html
  251. 128 136
      templates/cranefly/threads/list.html
  252. 7 4
      templates/cranefly/threads/merge.html
  253. 1 3
      templates/cranefly/threads/move_posts.html
  254. 1 3
      templates/cranefly/threads/move_thread.html
  255. 1 3
      templates/cranefly/threads/move_threads.html
  256. 5 5
      templates/cranefly/threads/posting.html
  257. 1 3
      templates/cranefly/threads/split.html
  258. 99 68
      templates/cranefly/threads/thread.html
  259. 67 13
      templates/cranefly/watched.html
  260. 3 0
      templates/search/indexes/misago/post_text.txt

+ 4 - 0
.gitignore

@@ -172,13 +172,16 @@ coffin/**
 debug_toolbar/**
 debug_toolbar/**
 dev/**
 dev/**
 django/**
 django/**
+haystack/**
 jinja2/**
 jinja2/**
 markdown/**
 markdown/**
 mptt/**
 mptt/**
 pytz/**
 pytz/**
 recaptcha/**
 recaptcha/**
 south/**
 south/**
+whoosh/**
 custom/**
 custom/**
+searchindex/**
 static/avatars/protoss
 static/avatars/protoss
 static/avatars/terran
 static/avatars/terran
 static/avatars/zerg
 static/avatars/zerg
@@ -190,6 +193,7 @@ yaml/**
 dev-manage.py
 dev-manage.py
 path.py
 path.py
 !templates/debug_toolbar/panels/acl.html
 !templates/debug_toolbar/panels/acl.html
+static/emojis/**
 templates/debug_toolbar/**
 templates/debug_toolbar/**
 
 
 
 

+ 3 - 1
README.md

@@ -23,6 +23,7 @@ Dependencies
 * [Django Debug Toolbar](https://github.com/django-debug-toolbar/django-debug-toolbar)
 * [Django Debug Toolbar](https://github.com/django-debug-toolbar/django-debug-toolbar)
 * [Django-MPTT](https://github.com/django-mptt/django-mptt)
 * [Django-MPTT](https://github.com/django-mptt/django-mptt)
 * [Coffin](https://github.com/coffin/coffin)
 * [Coffin](https://github.com/coffin/coffin)
+* [Django Haystack 2](http://haystacksearch.org/)
 * [Jinja2](https://github.com/mitsuhiko/jinja2)
 * [Jinja2](https://github.com/mitsuhiko/jinja2)
 * [Markdown](http://pypi.python.org/pypi/Markdown)
 * [Markdown](http://pypi.python.org/pypi/Markdown)
 * [path](http://pypi.python.org/pypi/path.py)
 * [path](http://pypi.python.org/pypi/path.py)
@@ -32,6 +33,7 @@ Dependencies
 * [South](http://south.aeracode.org)
 * [South](http://south.aeracode.org)
 * [Unidecode](http://pypi.python.org/pypi/Unidecode)
 * [Unidecode](http://pypi.python.org/pypi/Unidecode)
 
 
+You will also need search engine to provide search functionality. If you don't have one, [Whoosh 2](https://pypi.python.org/pypi/Whoosh/) is pure Python search engine that's easy to setup.
 
 
 Installation
 Installation
 ------------
 ------------
@@ -92,7 +94,7 @@ Updating
 
 
 You can use the `updatemisago` command to update your forums database to latest version _unless_ you are updating from `0.1` which is incompatibile with `0.2` and later releases.
 You can use the `updatemisago` command to update your forums database to latest version _unless_ you are updating from `0.1` which is incompatibile with `0.2` and later releases.
 
 
-If you want to move data from `0.1` to `0.2`, install `0.2` to new database, then add a connection to the `0.1` database in your settings.py and name it "deprecated". When you are ready, use the `migratefrom01` management command to move data from the `0.1` database over to `0.2`.
+Support for migrations from `0.1` has been dropped with `0.3` release.
 
 
 
 
 Contributing
 Contributing

+ 4 - 0
cron.txt

@@ -8,3 +8,7 @@
 5 3 * * * python $HOME/misago/manage.py syncdeltas
 5 3 * * * python $HOME/misago/manage.py syncdeltas
 10 3 * * * python $HOME/misago/manage.py updateranking
 10 3 * * * python $HOME/misago/manage.py updateranking
 25 3 * * * python $HOME/misago/manage.py updatethreadranking
 25 3 * * * python $HOME/misago/manage.py updatethreadranking
+*/30 * * * * python $HOME/misago/manage.py countreports
+* */2 * * * python $HOME/misago/manage.py update_index --age=2
+# Uncomment next line for heartbeat cron
+#*/3 * * * * python $HOME/misago/heartbeat.py deployment.settings --log=heartbeats.txt

+ 23 - 1
deployment/settings.py

@@ -41,12 +41,24 @@ DATABASES = {
 # Misago is EXTREMELY data hungry
 # Misago is EXTREMELY data hungry
 # If you don't set any cache, it will BRUTALISE your database and memory
 # If you don't set any cache, it will BRUTALISE your database and memory
 # In production ALWAYS use cache
 # In production ALWAYS use cache
+# For reference read following document:
+# https://docs.djangoproject.com/en/dev/topics/cache/
 CACHES = {
 CACHES = {
     'default': {
     'default': {
         'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
         'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
     }
     }
 }
 }
 
 
+# Search engine
+# Misago relies on 3rd party search engines to index and search your forum content
+# Read following for information on configurating search:
+# http://django-haystack.readthedocs.org/en/latest/tutorial.html#modify-your-settings-py
+HAYSTACK_CONNECTIONS = {
+    'default': {
+        'ENGINE': 'haystack.backends.whoosh_backend.',
+    },
+}
+
 # Cookies configuration
 # Cookies configuration
 COOKIES_DOMAIN = '192.168.33.10' # E.g. a cookie domain for "www.mysite.com" or "forum.mysite.com" is ".mysite.com"
 COOKIES_DOMAIN = '192.168.33.10' # E.g. a cookie domain for "www.mysite.com" or "forum.mysite.com" is ".mysite.com"
 COOKIES_PATH = '/'
 COOKIES_PATH = '/'
@@ -121,7 +133,6 @@ EMAIL_SUBJECT_PREFIX = '[Misago]'
 # If DEBUG_MODE is on, all emails will be sent to this address instead of real recipient.
 # If DEBUG_MODE is on, all emails will be sent to this address instead of real recipient.
 CATCH_ALL_EMAIL_ADDRESS = ''
 CATCH_ALL_EMAIL_ADDRESS = ''
 
 
-
 # Directories with templates
 # Directories with templates
 TEMPLATE_DIRS = (
 TEMPLATE_DIRS = (
     # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
     # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
@@ -136,6 +147,15 @@ INSTALLED_THEMES = (
     'admin', # Admin theme always last
     'admin', # Admin theme always last
 )
 )
 
 
+# Enable mobile subdomain for mobile stuff
+MOBILE_SUBDOMAIN = ''
+
+# Templates used by mobile version
+MOBILE_TEMPLATES = ''
+
+# Name of root urls configuration
+ROOT_URLCONF = 'deployment.urls'
+
 # Python dotted path to the WSGI application used by Django's runserver.
 # Python dotted path to the WSGI application used by Django's runserver.
 WSGI_APPLICATION = 'deployment.wsgi.application'
 WSGI_APPLICATION = 'deployment.wsgi.application'
 
 
@@ -150,3 +170,5 @@ if 'test' in sys.argv:
     DATABASES['default'] = {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'db4testing'}
     DATABASES['default'] = {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'db4testing'}
     CACHES['default'] = {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
     CACHES['default'] = {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
     SKIP_SOUTH_TESTS = True
     SKIP_SOUTH_TESTS = True
+    MEDIA_URL = "http://media.domain.com/"
+    HAYSTACK_CONNECTIONS = {'default': {'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',},}

+ 9 - 0
deployment/urls.py

@@ -0,0 +1,9 @@
+from misago.urls import *
+
+# Your deployment urls configuration
+# This configuration already contains Misago urls configuration
+# If you want to add 3rd party apps urls to your Misago deployment
+# Uncomment bottom lines and use them to register custom url's
+# urlpatterns += patterns('',
+#    (r'^', include('somewhere.urls')),
+#)

+ 87 - 0
heartbeat.py

@@ -0,0 +1,87 @@
+#!/usr/bin/python
+import os, sys
+import urllib2
+from time import gmtime, strftime, time
+try:
+    from argparse import OptionParser
+except ImportError:
+    from optparse import OptionParser
+
+
+def log_entry(logfile, response=None):
+    if response and response.getcode() == 200:
+        if response.time > 1:
+            stopwatch = '%ss' % round(response.time, 3)
+        else:
+            stopwatch = '%sms' % int(response.time * 1000)
+        msg = 'OK! HTTP 200 after %s' % stopwatch
+    else:
+        msg = 'FAIL!'
+
+    print msg
+
+    if logfile:
+        lf = open(logfile, 'a+')
+        lf.write('%s: ' % strftime("%a, %d %b %Y %X GMT", gmtime()))
+        lf.write('%s\n' % msg)
+        lf.close()
+
+
+def heartbeat():
+    # Change chdir to current file loation, then add it to pythonpath
+    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+    os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+    # Parse options
+    parser = OptionParser()
+    parser.add_option("--timeout", dest="timeout", default=60, type="int",
+                      help="Number of seconds after which heartbeat timeouts.")
+    parser.add_option("--path", dest="pypath",
+                      help="Add extra entry to python-path.")
+    parser.add_option("--log", dest="logfile",
+                      help="Log responses to file.", metavar="FILE")
+
+    (options, argv) = parser.parse_args(sys.argv)
+
+    # Set extra pythonpath?
+    if options.pypath:
+        sys.path.insert(0, options.pypath)
+
+    # Validate timeout
+    if options.timeout < 5 or options.timeout > 300:
+        raise ValueError("Timeout cannot be lower than 5 seconds and greater than 5 minutes (300 seconds).")
+
+    try:
+        # Read Misago settings
+        settings = __import__(argv[1]).settings
+        BOARD_ADDRESS = settings.BOARD_ADDRESS
+        HEARTBEAT_PATH = settings.HEARTBEAT_PATH
+
+        # Validate
+        if not BOARD_ADDRESS:
+            raise ValueError('"BOARD_ADDRESS" setting is not set.')
+        if not HEARTBEAT_PATH:
+            raise ValueError('"HEARTBEAT_PATH" setting is not set.')
+
+        request_url = '%s/%s' % (BOARD_ADDRESS, HEARTBEAT_PATH)
+
+        # Send and handle request
+        try:
+            stopwatch = time()
+            response = urllib2.urlopen(request_url, timeout=options.timeout)
+            body = response.read()
+            response.close()
+            response.time = time() - stopwatch
+            log_entry(options.logfile, response)
+        except urllib2.URLError:
+            log_entry(options.logfile)
+    except IndexError:
+        raise ValueError("You have to specify name of Misago's settings module used by your forum.")
+    except ImportError:
+        raise ValueError('"%s" could not be imported.' % argv[1])
+    except AttributeError as e:
+        raise ValueError('"%s" is not correct settings module.' % argv[1])
+
+
+if __name__ == '__main__':
+    heartbeat()

+ 1 - 1
misago/__init__.py

@@ -1 +1 @@
-__version__ = "0.2.0 DEV"
+__version__ = "0.3.0 DEV"

+ 14 - 0
misago/acl/permissions/privatethreads.py

@@ -14,6 +14,12 @@ def make_form(request, role, form):
         form.base_fields['private_thread_attachments_limit'] = forms.IntegerField(min_value=0, initial=3, required=False)
         form.base_fields['private_thread_attachments_limit'] = forms.IntegerField(min_value=0, initial=3, required=False)
         form.base_fields['can_invite_ignoring'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.base_fields['can_invite_ignoring'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.base_fields['private_threads_mod'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.base_fields['private_threads_mod'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_delete_checkpoints'] = forms.TypedChoiceField(choices=(
+                                                                                     (0, _("No")),
+                                                                                     (1, _("Yes, soft-delete")),
+                                                                                     (2, _("Yes, hard-delete")),
+                                                                                     ), coerce=int)
+
         form.layout.append((
         form.layout.append((
                             _("Private Threads"),
                             _("Private Threads"),
                             (
                             (
@@ -24,6 +30,7 @@ def make_form(request, role, form):
                              ('private_thread_attachments_limit', {'label': _("Max. number of attachments per post")}),
                              ('private_thread_attachments_limit', {'label': _("Max. number of attachments per post")}),
                              ('can_invite_ignoring', {'label': _("Can invite users that ignore him")}),
                              ('can_invite_ignoring', {'label': _("Can invite users that ignore him")}),
                              ('private_threads_mod', {'label': _("Can moderate threads"), 'help_text': _("Makes user with this role Private Threads moderator capable of closing, deleting and editing all private threads he participates in at will.")}),
                              ('private_threads_mod', {'label': _("Can moderate threads"), 'help_text': _("Makes user with this role Private Threads moderator capable of closing, deleting and editing all private threads he participates in at will.")}),
+                             ('can_delete_checkpoints', {'label': _("Can delete checkpoints")}),
                              ),
                              ),
                             ))
                             ))
 
 
@@ -52,6 +59,8 @@ def build(acl, roles):
     acl.private_threads.acl['private_thread_attachments_limit'] = False
     acl.private_threads.acl['private_thread_attachments_limit'] = False
     acl.private_threads.acl['can_invite_ignoring'] = False
     acl.private_threads.acl['can_invite_ignoring'] = False
     acl.private_threads.acl['private_threads_mod'] = False
     acl.private_threads.acl['private_threads_mod'] = False
+    acl.private_threads.acl['can_delete_checkpoints'] = 0
+    acl.private_threads.acl['can_see_deleted_checkpoints'] = False
 
 
     for role in roles:
     for role in roles:
         for perm, value in acl.private_threads.acl.items():
         for perm, value in acl.private_threads.acl.items():
@@ -94,6 +103,8 @@ def cleanup(acl, perms, forums):
                               'can_delete_polls': 0,
                               'can_delete_polls': 0,
                               'can_delete_attachments': False,
                               'can_delete_attachments': False,
                               'can_invite_ignoring': False,
                               'can_invite_ignoring': False,
+                              'can_delete_checkpoints': 0,
+                              'can_see_deleted_checkpoints': False,
                              }
                              }
 
 
     for perm in perms:
     for perm in perms:
@@ -120,5 +131,8 @@ def cleanup(acl, perms, forums):
                 acl.threads.acl[forum]['can_delete_threads'] = 2
                 acl.threads.acl[forum]['can_delete_threads'] = 2
                 acl.threads.acl[forum]['can_delete_posts'] = 2
                 acl.threads.acl[forum]['can_delete_posts'] = 2
                 acl.threads.acl[forum]['can_delete_attachments'] = True
                 acl.threads.acl[forum]['can_delete_attachments'] = True
+                acl.threads.acl[forum]['can_see_deleted_checkpoints'] = True
+            if perm['can_delete_checkpoints'] > acl.threads.acl[forum]['can_delete_checkpoints']:
+                acl.threads.acl[forum]['can_delete_checkpoints'] = perm['can_delete_checkpoints']
         except KeyError:
         except KeyError:
             pass
             pass

+ 114 - 0
misago/acl/permissions/reports.py

@@ -0,0 +1,114 @@
+from django.utils.translation import ugettext_lazy as _
+from django import forms
+from misago.acl.builder import BaseACL
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.forms import YesNoSwitch
+from misago.models import Forum
+
+def make_form(request, role, form):
+    if role.special != 'guest':
+        form.base_fields['can_report_content'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_handle_reports'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_mod_reports_discussions'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_delete_reports'] = forms.TypedChoiceField(choices=(
+                                                                                 (0, _("No")),
+                                                                                 (1, _("Yes, soft-delete")),
+                                                                                 (2, _("Yes, hard-delete")),
+                                                                                 ), coerce=int)
+
+        form.layout.append((
+                            _("Reporting Content"),
+                            (
+                             ('can_report_content', {'label': _("Can report content")}),
+                             ('can_handle_reports', {'label': _("Can handle reports")}),
+                             ('can_mod_reports_discussions', {'label': _("Can moderate reports discussions")}),
+                             ('can_delete_reports', {'label': _("Can delete reports")}),
+                             ),
+                            ))
+
+
+class ReportsACL(BaseACL):
+    def can_report(self):
+        return self.acl['can_report_content']
+
+    def allow_report(self):
+        if not self.acl['can_report_content']:
+            raise ACLError403(_("You don't have permission to report posts."))
+
+    def can_handle(self):
+        return self.acl['can_handle_reports']
+        
+    def is_mod(self):
+        return self.acl['can_mod_reports_discussions']
+        
+    def can_delete(self):
+        return self.acl['can_delete_reports']
+
+
+def build(acl, roles):
+    acl.reports = ReportsACL()
+    acl.reports.acl['can_report_content'] = False
+    acl.reports.acl['can_handle_reports'] = False
+    acl.reports.acl['can_mod_reports_discussions'] = False
+    acl.reports.acl['can_delete_reports'] = False
+
+    for role in roles:
+        for perm, value in acl.reports.acl.items():
+            if perm in role and role[perm] > value:
+                acl.reports.acl[perm] = role[perm]
+
+
+def cleanup(acl, perms, forums):
+    forum = Forum.objects.special_pk('reports')
+    acl.threads.acl[forum] = {
+                              'can_read_threads': 2,
+                              'can_start_threads': 0,
+                              'can_edit_own_threads': True,
+                              'can_soft_delete_own_threads': False,
+                              'can_write_posts': 2,
+                              'can_edit_own_posts': True,
+                              'can_soft_delete_own_posts': True,
+                              'can_upvote_posts': False,
+                              'can_downvote_posts': False,
+                              'can_see_posts_scores': 0,
+                              'can_see_votes': False,
+                              'can_make_polls': False,
+                              'can_vote_in_polls': False,
+                              'can_see_poll_votes': False,
+                              'can_see_attachments': True,
+                              'can_upload_attachments': True,
+                              'can_download_attachments': True,
+                              'attachment_size': 256,
+                              'attachment_limit': 12,
+                              'can_approve': False,
+                              'can_edit_labels': False,
+                              'can_see_changelog': True,
+                              'can_pin_threads': 0,
+                              'can_edit_threads_posts': False,
+                              'can_move_threads_posts': False,
+                              'can_close_threads': False,
+                              'can_protect_posts': False,
+                              'can_delete_threads': 0,
+                              'can_delete_posts': 0,
+                              'can_delete_polls': 0,
+                              'can_delete_attachments': False,
+                              'can_delete_checkpoints': 0,
+                              'can_see_deleted_checkpoints': False,
+                             }
+
+    for perm in perms:
+        try:
+            if perm['can_handle_reports'] and forum not in acl.forums.acl['can_see']:
+                acl.forums.acl['can_see'].append(forum)
+                acl.forums.acl['can_browse'].append(forum)
+                acl.threads.acl[forum]['can_pin_threads'] = 2
+            if perm['can_mod_reports_discussions']:
+                acl.threads.acl[forum]['can_edit_threads_posts'] = True
+                acl.threads.acl[forum]['can_delete_posts'] = 2
+                acl.threads.acl[forum]['can_delete_attachments'] = True
+                acl.threads.acl[forum]['can_delete_checkpoints'] = 2
+                acl.threads.acl[forum]['can_see_deleted_checkpoints'] = True
+            if perm['can_delete_reports'] > acl.threads.acl[forum]['can_delete_threads']:
+                acl.threads.acl[forum]['can_delete_threads'] = perm['can_delete_reports']
+        except KeyError:
+            pass

+ 37 - 0
misago/acl/permissions/search.py

@@ -0,0 +1,37 @@
+from django.utils.translation import ugettext_lazy as _
+from django import forms
+from misago.acl.builder import BaseACL
+from misago.forms import YesNoSwitch
+
+def make_form(request, role, form):
+    form.base_fields['can_search_forums'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['search_cooldown'] = forms.IntegerField(initial=25, min_value=0)
+    form.layout.append((_("Searching"),
+                        (
+                         ('can_search_forums', {'label': _("Can search community")}),
+                         ('search_cooldown', {'label': _("Minimum delay between searches"), 'help_text': _("Forum search can be resources intensive operation, and so its usually good idea to limit frequency of searches by requiring members to wait certain number of seconds before they can perform next search. Enter 0 to disable this requirement.")}),
+                         )
+                        ))
+
+
+class SearchACL(BaseACL):
+    def can_search(self):
+        return self.acl['can_search_forums']
+
+    def search_cooldown(self):
+        return self.acl['search_cooldown']
+
+
+def build(acl, roles):
+    acl.search = SearchACL()
+    acl.search.acl['can_search_forums'] = False
+    acl.search.acl['search_cooldown'] = 25
+
+    for role in roles:
+        try:
+            if role['can_search_forums']:
+                acl.search.acl['can_search_forums'] = True
+            if role['search_cooldown'] < acl.search.acl['search_cooldown']:
+                acl.search.acl['search_cooldown'] = role['search_cooldown']
+        except KeyError:
+            pass

+ 90 - 30
misago/acl/permissions/threads.py

@@ -46,30 +46,36 @@ def make_forum_form(request, role, form):
     form.base_fields['can_edit_labels'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_edit_labels'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_see_changelog'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_see_changelog'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_pin_threads'] = forms.TypedChoiceField(choices=(
     form.base_fields['can_pin_threads'] = forms.TypedChoiceField(choices=(
-                                                                 (0, _("No")),
-                                                                 (1, _("Yes, to stickies")),
-                                                                 (2, _("Yes, to announcements")),
-                                                                 ), coerce=int)
+                                                                          (0, _("No")),
+                                                                          (1, _("Yes, to stickies")),
+                                                                          (2, _("Yes, to announcements")),
+                                                                          ), coerce=int)
     form.base_fields['can_edit_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_edit_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_move_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_move_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_close_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_close_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_protect_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_protect_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_delete_threads'] = forms.TypedChoiceField(choices=(
     form.base_fields['can_delete_threads'] = forms.TypedChoiceField(choices=(
-                                                                    (0, _("No")),
-                                                                    (1, _("Yes, soft-delete")),
-                                                                    (2, _("Yes, hard-delete")),
-                                                                    ), coerce=int)
+                                                                             (0, _("No")),
+                                                                             (1, _("Yes, soft-delete")),
+                                                                             (2, _("Yes, hard-delete")),
+                                                                             ), coerce=int)
     form.base_fields['can_delete_posts'] = forms.TypedChoiceField(choices=(
     form.base_fields['can_delete_posts'] = forms.TypedChoiceField(choices=(
-                                                                  (0, _("No")),
-                                                                  (1, _("Yes, soft-delete")),
-                                                                  (2, _("Yes, hard-delete")),
-                                                                   ), coerce=int)
+                                                                           (0, _("No")),
+                                                                           (1, _("Yes, soft-delete")),
+                                                                           (2, _("Yes, hard-delete")),
+                                                                           ), coerce=int)
     form.base_fields['can_delete_polls'] = forms.TypedChoiceField(choices=(
     form.base_fields['can_delete_polls'] = forms.TypedChoiceField(choices=(
-                                                                  (0, _("No")),
-                                                                  (1, _("Yes, soft-delete")),
-                                                                  (2, _("Yes, hard-delete")),
-                                                                  ), coerce=int)
+                                                                           (0, _("No")),
+                                                                           (1, _("Yes, soft-delete")),
+                                                                           (2, _("Yes, hard-delete")),
+                                                                           ), coerce=int)
     form.base_fields['can_delete_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
     form.base_fields['can_delete_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_delete_checkpoints'] = forms.TypedChoiceField(choices=(
+                                                                                 (0, _("No")),
+                                                                                 (1, _("Yes, soft-delete")),
+                                                                                 (2, _("Yes, hard-delete")),
+                                                                                 ), coerce=int)
+    form.base_fields['can_see_deleted_checkpoints'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
 
 
     form.layout.append((
     form.layout.append((
                         _("Threads"),
                         _("Threads"),
@@ -130,6 +136,8 @@ def make_forum_form(request, role, form):
                          ('can_delete_posts', {'label': _("Can delete posts")}),
                          ('can_delete_posts', {'label': _("Can delete posts")}),
                          ('can_delete_polls', {'label': _("Can delete polls")}),
                          ('can_delete_polls', {'label': _("Can delete polls")}),
                          ('can_delete_attachments', {'label': _("Can delete attachments")}),
                          ('can_delete_attachments', {'label': _("Can delete attachments")}),
+                         ('can_delete_checkpoints', {'label': _("Can delete checkpoints")}),
+                         ('can_see_deleted_checkpoints', {'label': _("Can see deleted checkpoints")}),
                         ),
                         ),
                        ),)
                        ),)
 
 
@@ -170,9 +178,9 @@ class ThreadsACL(BaseACL):
             forum_role = self.acl[forum.pk]
             forum_role = self.acl[forum.pk]
             if not forum_role['can_approve']:
             if not forum_role['can_approve']:
                 if request.user.is_authenticated():
                 if request.user.is_authenticated():
-                    queryset = queryset.filter(Q(moderated=0) | Q(start_poster=request.user))
+                    queryset = queryset.filter(Q(moderated=False) | Q(start_poster=request.user))
                 else:
                 else:
-                    queryset = queryset.filter(moderated=0)
+                    queryset = queryset.filter(moderated=False)
             if forum_role['can_read_threads'] == 1:
             if forum_role['can_read_threads'] == 1:
                 queryset = queryset.filter(Q(weight=2) | Q(start_poster_id=request.user.id))
                 queryset = queryset.filter(Q(weight=2) | Q(start_poster_id=request.user.id))
             if not forum_role['can_delete_threads']:
             if not forum_role['can_delete_threads']:
@@ -363,9 +371,9 @@ class ThreadsACL(BaseACL):
         except KeyError:
         except KeyError:
             return False
             return False
 
 
-    def can_mod_posts(self, thread):
+    def can_mod_posts(self, forum):
         try:
         try:
-            forum_role = self.acl[thread.forum.pk]
+            forum_role = self.acl[forum.pk]
             return (
             return (
                     forum_role['can_edit_threads_posts']
                     forum_role['can_edit_threads_posts']
                     or forum_role['can_move_threads_posts']
                     or forum_role['can_move_threads_posts']
@@ -407,9 +415,11 @@ class ThreadsACL(BaseACL):
     def can_delete_thread(self, user, forum, thread, post):
     def can_delete_thread(self, user, forum, thread, post):
         try:
         try:
             forum_role = self.acl[forum.pk]
             forum_role = self.acl[forum.pk]
+            if post.pk != thread.start_post_id:
+                return False
             if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
             if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
                 return False
                 return False
-            if post.protected and not forum_role['can_protect_posts']:
+            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_threads']:
                 return False
                 return False
             if forum_role['can_delete_threads']:
             if forum_role['can_delete_threads']:
                 return forum_role['can_delete_threads']
                 return forum_role['can_delete_threads']
@@ -427,11 +437,11 @@ class ThreadsACL(BaseACL):
                     raise ACLError403(_("You don't have permission to delete threads in closed forum."))
                     raise ACLError403(_("You don't have permission to delete threads in closed forum."))
                 if thread.closed:
                 if thread.closed:
                     raise ACLError403(_("This thread is closed, you cannot delete it."))
                     raise ACLError403(_("This thread is closed, you cannot delete it."))
-            if post.protected and not forum_role['can_protect_posts']:
+            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_threads']:
                 raise ACLError403(_("This post is protected, you cannot delete it."))
                 raise ACLError403(_("This post is protected, you cannot delete it."))
-            if delete and forum_role['can_delete_threads'] < 2:
-                raise ACLError403(_("You cannot hard delete this thread."))
-            if not (forum_role['can_delete_threads'] or (thread.start_poster_id == user.pk and forum_role['can_soft_delete_own_threads'])):
+            if not (forum_role['can_delete_threads'] == 2 or
+                    (not delete and (forum_role['can_delete_threads'] == 1 or 
+                    (thread.start_poster_id == user.pk and forum_role['can_soft_delete_own_threads'])))):
                 raise ACLError403(_("You don't have permission to delete this thread."))
                 raise ACLError403(_("You don't have permission to delete this thread."))
             if thread.deleted and not delete:
             if thread.deleted and not delete:
                 raise ACLError403(_("This thread is already deleted."))
                 raise ACLError403(_("This thread is already deleted."))
@@ -441,9 +451,11 @@ class ThreadsACL(BaseACL):
     def can_delete_post(self, user, forum, thread, post):
     def can_delete_post(self, user, forum, thread, post):
         try:
         try:
             forum_role = self.acl[forum.pk]
             forum_role = self.acl[forum.pk]
+            if post.pk == thread.start_post_id:
+                return False
             if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
             if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
                 return False
                 return False
-            if post.protected and not forum_role['can_protect_posts']:
+            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_posts']:
                 return False
                 return False
             if forum_role['can_delete_posts']:
             if forum_role['can_delete_posts']:
                 return forum_role['can_delete_posts']
                 return forum_role['can_delete_posts']
@@ -461,11 +473,11 @@ class ThreadsACL(BaseACL):
                     raise ACLError403(_("You don't have permission to delete posts in closed forum."))
                     raise ACLError403(_("You don't have permission to delete posts in closed forum."))
                 if thread.closed:
                 if thread.closed:
                     raise ACLError403(_("This thread is closed, you cannot delete its posts."))
                     raise ACLError403(_("This thread is closed, you cannot delete its posts."))
-            if post.protected and not forum_role['can_protect_posts']:
+            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_posts']:
                 raise ACLError403(_("This post is protected, you cannot delete it."))
                 raise ACLError403(_("This post is protected, you cannot delete it."))
-            if delete and forum_role['can_delete_posts'] < 2:
-                raise ACLError403(_("You cannot hard delete this post."))
-            if not (forum_role['can_delete_posts'] or (post.user_id == user.pk and forum_role['can_soft_delete_own_posts'])):
+            if not (forum_role['can_delete_posts'] == 2 or
+                    (not delete and (forum_role['can_delete_posts'] == 1 or 
+                    (post.user_id == user.pk and forum_role['can_soft_delete_own_posts'])))):
                 raise ACLError403(_("You don't have permission to delete this post."))
                 raise ACLError403(_("You don't have permission to delete this post."))
             if post.deleted and not delete:
             if post.deleted and not delete:
                 raise ACLError403(_("This post is already deleted."))
                 raise ACLError403(_("This post is already deleted."))
@@ -546,6 +558,51 @@ class ThreadsACL(BaseACL):
         except KeyError:
         except KeyError:
             raise ACLError403(_("You don't have permission to see who voted on this post."))
             raise ACLError403(_("You don't have permission to see who voted on this post."))
 
 
+    def can_see_all_checkpoints(self, forum):
+        try:
+            return self.acl[forum.pk]['can_see_deleted_checkpoints']
+        except KeyError:
+            raise False
+
+    def can_delete_checkpoint(self, forum):
+        try:
+            return self.acl[forum.pk]['can_delete_checkpoints']
+        except KeyError:
+            raise False
+
+    def allow_checkpoint_view(self, forum, checkpoint):
+        if checkpoint.deleted:
+            try:
+                forum_role = self.acl[forum.pk]
+                if not forum_role['can_see_deleted_checkpoints']:
+                    raise ACLError403(_("Selected checkpoint could not be found."))
+            except KeyError:
+                raise ACLError403(_("Selected checkpoint could not be found."))
+
+    def allow_checkpoint_hide(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_delete_checkpoints']:
+                raise ACLError403(_("You cannot hide checkpoints!"))
+        except KeyError:
+            raise ACLError403(_("You cannot hide checkpoints!"))
+
+    def allow_checkpoint_delete(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if forum_role['can_delete_checkpoints'] != 2:
+                raise ACLError403(_("You cannot delete checkpoints!"))
+        except KeyError:
+            raise ACLError403(_("You cannot delete checkpoints!"))
+
+    def allow_checkpoint_show(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_delete_checkpoints']:
+                raise ACLError403(_("You cannot show checkpoints!"))
+        except KeyError:
+            raise ACLError403(_("You cannot show checkpoints!"))
+
 
 
 def build_forums(acl, perms, forums, forum_roles):
 def build_forums(acl, perms, forums, forum_roles):
     acl.threads = ThreadsACL()
     acl.threads = ThreadsACL()
@@ -582,7 +639,10 @@ def build_forums(acl, perms, forums, forum_roles):
                      'can_delete_posts': 0,
                      'can_delete_posts': 0,
                      'can_delete_polls': 0,
                      'can_delete_polls': 0,
                      'can_delete_attachments': False,
                      'can_delete_attachments': False,
+                     'can_see_deleted_checkpoints': False,
+                     'can_delete_checkpoints': 0,
                      }
                      }
+
         for perm in perms:
         for perm in perms:
             try:
             try:
                 role = forum_roles[perm['forums'][forum.pk]]
                 role = forum_roles[perm['forums'][forum.pk]]

+ 4 - 4
misago/apps/admin/bans/views.py

@@ -58,7 +58,7 @@ class List(ListWidget):
 
 
     def action_delete(self, items, checked):
     def action_delete(self, items, checked):
         Ban.objects.filter(id__in=checked).delete()
         Ban.objects.filter(id__in=checked).delete()
-        self.request.monitor['bans_version'] = int(self.request.monitor['bans_version']) + 1
+        self.request.monitor.increase('bans_version')
         return Message(_('Selected bans have been lifted successfully.'), 'success'), reverse('admin_bans')
         return Message(_('Selected bans have been lifted successfully.'), 'success'), reverse('admin_bans')
 
 
 
 
@@ -87,7 +87,7 @@ class New(FormWidget):
                       expires=form.cleaned_data['expires']
                       expires=form.cleaned_data['expires']
                      )
                      )
         new_ban.save(force_insert=True)
         new_ban.save(force_insert=True)
-        self.request.monitor['bans_version'] = int(self.request.monitor['bans_version']) + 1
+        self.request.monitor.increase('bans_version')
         return new_ban, Message(_('New Ban has been set.'), 'success')
         return new_ban, Message(_('New Ban has been set.'), 'success')
 
 
 
 
@@ -126,7 +126,7 @@ class Edit(FormWidget):
         target.reason_admin = form.cleaned_data['reason_admin']
         target.reason_admin = form.cleaned_data['reason_admin']
         target.expires = form.cleaned_data['expires']
         target.expires = form.cleaned_data['expires']
         target.save(force_update=True)
         target.save(force_update=True)
-        self.request.monitor['bans_version'] = int(self.request.monitor['bans_version']) + 1
+        self.request.monitor.increase('bans_version')
         return target, Message(_('Changes in ban have been saved.'), 'success')
         return target, Message(_('Changes in ban have been saved.'), 'success')
 
 
 
 
@@ -141,7 +141,7 @@ class Delete(ButtonWidget):
 
 
     def action(self, target):
     def action(self, target):
         target.delete()
         target.delete()
-        self.request.monitor['bans_version'] = int(self.request.monitor['bans_version']) + 1
+        self.request.monitor.increase('bans_version')
         if target.test == 0:
         if target.test == 0:
             return Message(_('E-mail and username Ban "%(ban)s" has been lifted.') % {'ban': target.ban}, 'success'), False
             return Message(_('E-mail and username Ban "%(ban)s" has been lifted.') % {'ban': target.ban}, 'success'), False
         if target.test == 1:
         if target.test == 1:

+ 0 - 51
misago/apps/admin/clients/forms.py

@@ -1,51 +0,0 @@
-from django.conf import settings
-from django.core.exceptions import ValidationError
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.forms import Form
-from misago.models import ThemeAdjustment
-from misago.validators import validate_sluggable
-
-available_themes = []
-for theme in settings.INSTALLED_THEMES[0:-1]:
-    available_themes.append((theme, theme))
-
-
-class ThemeAdjustmentForm(Form):
-    theme = forms.ChoiceField(choices=available_themes, required=False)
-    useragents = forms.CharField(widget=forms.Textarea, required=False)
-    
-    layout = (
-              (
-               _("Theme Adjustment"),
-               (
-                ('theme', {'label': _("Theme"), 'help_text': _("Select theme that is to replace default one.")}),
-                ('useragents', {'label': _("UserAgent Strings"), 'help_text': _("Enter UserAgent strings for which selected theme has to replace default one. Each string has to be entered in new line. This is case insensitive")}),
-                ),
-               ),
-              )
-
-    def __init__(self, adjustment=None, *args, **kwargs):
-        self.request = kwargs['request']
-        if adjustment:
-            self.adjustment = adjustment
-        else:
-            self.adjustment = ThemeAdjustment()
-        super(ThemeAdjustmentForm, self).__init__(*args, **kwargs)
-        
-    def clean_theme(self):
-        self.adjustment.theme = self.cleaned_data['theme']
-        self.adjustment.full_clean()
-        return self.cleaned_data['theme']
-
-    def clean_useragents(self):
-        agents_raw = self.cleaned_data['useragents'].strip().lower().splitlines()
-        agents = []
-        for line in agents_raw:
-            line = line.strip()
-            if line and not line in agents:
-                agents.append(line)
-        self.cleaned_data['useragents'] = agents
-        if not agents:
-            raise ValidationError(_("You have to enter at least one UserAgent."))
-        return self.cleaned_data['useragents']

+ 0 - 110
misago/apps/admin/clients/views.py

@@ -1,110 +0,0 @@
-from django.core.urlresolvers import reverse as django_reverse
-from django import forms
-from django.utils.translation import ugettext as _
-from misago.admin import site
-from misago.apps.admin.widgets import *
-from misago.forms import Form
-from misago.models import ThemeAdjustment
-from misago.utils.strings import slugify
-from misago.apps.admin.clients.forms import ThemeAdjustmentForm
-
-def reverse(route, target=None):
-    if target:
-        return django_reverse(route, kwargs={'target': target.pk, 'slug': slugify(target.theme)})
-    return django_reverse(route)
-
-
-"""
-Views
-"""
-class List(ListWidget):
-    admin = site.get_action('clients')
-    id = 'list'
-    columns = (
-               ('theme', _("Theme")),
-               )
-    nothing_checked_message = _('You have to check at least one adjustment.')
-    actions = (
-               ('delete', _("Delete selected adjustments"), _("Are you sure you want to delete selected theme adjustments?")),
-               )
-
-    def get_item_actions(self, item):
-        return (
-                self.action('pencil', _("Edit Adjustment"), reverse('admin_clients_edit', item)),
-                self.action('remove', _("Delete Adjustment"), reverse('admin_clients_delete', item), post=True, prompt=_("Are you sure you want to delete this adjustment?")),
-                )
-
-    def action_delete(self, items, checked):
-        ThemeAdjustment.objects.filter(id__in=checked).delete()
-        return Message(_('Selected adjustment have been deleted successfully.'), 'success'), reverse('admin_clients')
-
-
-class New(FormWidget):
-    admin = site.get_action('clients')
-    id = 'new'
-    fallback = 'admin_clients'
-    form = ThemeAdjustmentForm
-    submit_button = _("Set Adjustment")
-
-    def get_form_instance(self, form, model, initial, post=False):
-        if post:
-            return form(model, self.request.POST, request=self.request, initial=self.get_initial_data(model))
-        return form(model, request=self.request, initial=self.get_initial_data(model))
-    
-    def get_new_url(self, model):
-        return reverse('admin_clients_new')
-
-    def get_edit_url(self, model):
-        return reverse('admin_clients_edit', model)
-
-    def submit_form(self, form, target):
-        new_adjustment = ThemeAdjustment.objects.create(
-                                                        theme=form.cleaned_data['theme'],
-                                                        useragents='\r\n'.join(form.cleaned_data['useragents']),
-                                                        )
-        return new_adjustment, Message(_('New adjustment has been created.'), 'success')
-
-
-class Edit(FormWidget):
-    admin = site.get_action('clients')
-    id = 'edit'
-    name = _("Edit Adjustment")
-    fallback = 'admin_clients'
-    form = ThemeAdjustmentForm
-    target_name = 'theme'
-    notfound_message = _('Requested adjustment could not be found.')
-    submit_fallback = True
-
-    def get_url(self, model):
-        return reverse('admin_clients_edit', model)
-
-    def get_edit_url(self, model):
-        return self.get_url(model)
-
-    def get_form_instance(self, form, model, initial, post=False):
-        if post:
-            return form(model, self.request.POST, request=self.request, initial=self.get_initial_data(model))
-        return form(model, request=self.request, initial=self.get_initial_data(model))
-    
-    def get_initial_data(self, model):
-        return {
-                'theme': model.theme,
-                'useragents': model.useragents,
-                }
-
-    def submit_form(self, form, target):
-        target.theme = form.cleaned_data['theme']
-        target.useragents = '\r\n'.join(form.cleaned_data['useragents'])
-        target.save(force_update=True)
-        return target, Message(_('Adjustment using theme "%(name)s" has been saved.') % {'name': target.theme}, 'success')
-
-
-class Delete(ButtonWidget):
-    admin = site.get_action('clients')
-    id = 'delete'
-    fallback = 'admin_clients'
-    notfound_message = _('Requested adjustment could not be found.')
-
-    def action(self, target):
-        target.delete()
-        return Message(_('Adjustment using theme "%(name)s" has been deleted.') % {'name': target.theme}, 'success'), False

+ 1 - 1
misago/apps/admin/forumroles/forms.py

@@ -5,7 +5,7 @@ from misago.validators import validate_sluggable
 
 
 class ForumRoleForm(Form):
 class ForumRoleForm(Form):
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                         _("Role name must be sluggable."),
+                                                                         _("Role name must contain alphanumeric characters."),
                                                                          _("Role name is too long.")
                                                                          _("Role name is too long.")
                                                                          )])
                                                                          )])
 
 

+ 3 - 3
misago/apps/admin/forumroles/views.py

@@ -40,7 +40,7 @@ class List(ListWidget):
                 )
                 )
 
 
     def action_delete(self, items, checked):
     def action_delete(self, items, checked):
-        self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
+        self.request.monitor.increase('acl_version')
         Role.objects.filter(id__in=checked).delete()
         Role.objects.filter(id__in=checked).delete()
         return Message(_('Selected forum roles have been deleted successfully.'), 'success'), reverse('admin_roles_forums')
         return Message(_('Selected forum roles have been deleted successfully.'), 'success'), reverse('admin_roles_forums')
 
 
@@ -127,7 +127,7 @@ class ACL(FormWidget):
             raw_acl[perm] = form.cleaned_data[perm]
             raw_acl[perm] = form.cleaned_data[perm]
         target.permissions = raw_acl
         target.permissions = raw_acl
         target.save(force_update=True)
         target.save(force_update=True)
-        self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
+        self.request.monitor.increase('acl_version')
 
 
         return target, Message(_('Forum Role "%(name)s" permissions have been changed.') % {'name': self.original_name}, 'success')
         return target, Message(_('Forum Role "%(name)s" permissions have been changed.') % {'name': self.original_name}, 'success')
 
 
@@ -140,5 +140,5 @@ class Delete(ButtonWidget):
 
 
     def action(self, target):
     def action(self, target):
         target.delete()
         target.delete()
-        self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
+        self.request.monitor.increase('acl_version')
         return Message(_('Forum Role "%(name)s" has been deleted.') % {'name': _(target.name)}, 'success'), False
         return Message(_('Forum Role "%(name)s" has been deleted.') % {'name': _(target.name)}, 'success'), False

+ 86 - 27
misago/apps/admin/forums/forms.py

@@ -5,11 +5,81 @@ from misago.forms import Form, YesNoSwitch
 from misago.models import Forum
 from misago.models import Forum
 from misago.validators import validate_sluggable
 from misago.validators import validate_sluggable
 
 
-class CategoryForm(Form):
+class CleanAttrsMixin(object):
+    def clean_attrs(self):
+        clean = []
+        data = self.cleaned_data['attrs'].strip().split()
+        for i in data:
+            i = i.strip()
+            if not i in clean:
+                clean.append(i)
+        return ' '.join(clean)
+
+
+class NewNodeForm(Form, CleanAttrsMixin):
     parent = False
     parent = False
     perms = False
     perms = False
+    role = forms.ChoiceField(choices=(
+                                      ('category', _("Category")),
+                                      ('forum', _("Forum")),
+                                      ('redirect', _("Redirection")),
+                                      ))
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Category name must be sluggable."),
+                                                                          _("Category name must contain alphanumeric characters."),
+                                                                          _("Category name is too long.")
+                                                                          )])
+    redirect = forms.URLField(max_length=255, required=False)
+    description = forms.CharField(widget=forms.Textarea, required=False)
+    closed = forms.BooleanField(widget=YesNoSwitch, required=False)
+    attrs = forms.CharField(max_length=255, required=False)
+    show_details = forms.BooleanField(widget=YesNoSwitch, required=False, initial=True)
+    style = forms.CharField(max_length=255, required=False)
+
+    layout = (
+              (
+               _("Basic Options"),
+               (
+                ('parent', {'label': _("Node Parent")}),
+                ('perms', {'label': _("Copy Permissions from")}),
+                ('role', {'label': _("Node Type"), 'help_text': _("Each Node has specific role in forums tree. This role cannot be changed after node is created.")}),
+                ('name', {'label': _("Node Name")}),
+                ('description', {'label': _("Node Description")}),
+                ('redirect', {'label': _("Redirect URL"), 'help_text': _("Redirection nodes require you to specify URL they will redirect users to upon click.")}),
+                ('closed', {'label': _("Closed Node")}),
+                ),
+              ),
+              (
+               _("Display Options"),
+               (
+                ('attrs', {'label': _("Node Attributes"), 'help_text': _('Custom templates can check nodes for predefined attributes that will change way they are rendered.')}),
+                ('show_details', {'label': _("Show Subforums Details"), 'help_text': _('Allows you to prevent this node subforums from displaying statistics, last post data, etc. ect. on forums lists.')}),
+                ('style', {'label': _("Node Style"), 'help_text': _('You can add custom CSS classess to this node, to change way it looks on board index.')}),
+                ),
+              ),
+             )
+
+    def finalize_form(self):
+        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(include_self=True), level_indicator=u'- - ')
+        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
+
+    def clean(self):
+        cleaned_data = super(NewNodeForm, self).clean()
+        node_role = cleaned_data['role']
+
+        if node_role != 'category' and cleaned_data['parent'].special == 'root':
+            raise forms.ValidationError(_("Only categories can use Root Category as their parent."))
+        if node_role == 'redirect' and not cleaned_data['redirect']:
+            raise forms.ValidationError(_("You have to define redirection URL"))
+
+        return cleaned_data
+
+
+
+class CategoryForm(Form, CleanAttrsMixin):
+    parent = False
+    perms = False
+    name = forms.CharField(max_length=255, validators=[validate_sluggable(
+                                                                          _("Category name must contain alphanumeric characters."),
                                                                           _("Category name is too long.")
                                                                           _("Category name is too long.")
                                                                           )])
                                                                           )])
     description = forms.CharField(widget=forms.Textarea, required=False)
     description = forms.CharField(widget=forms.Textarea, required=False)
@@ -40,24 +110,15 @@ class CategoryForm(Form):
              )
              )
 
 
     def finalize_form(self):
     def finalize_form(self):
-        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(include_self=True), level_indicator=u'- - ')
         self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
         self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
 
 
-    def clean_attrs(self):
-        clean = []
-        data = self.cleaned_data['attrs'].strip().split()
-        for i in data:
-            i = i.strip()
-            if not i in clean:
-                clean.append(i)
-        return ' '.join(clean)
-
 
 
-class ForumForm(Form):
+class ForumForm(Form, CleanAttrsMixin):
     parent = False
     parent = False
     perms = False
     perms = False
+    pruned_archive = False
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Forum name must be sluggable."),
+                                                                          _("Forum name must contain alphanumeric characters."),
                                                                           _("Forum name is too long.")
                                                                           _("Forum name is too long.")
                                                                           )])
                                                                           )])
     description = forms.CharField(widget=forms.Textarea, required=False)
     description = forms.CharField(widget=forms.Textarea, required=False)
@@ -84,12 +145,13 @@ class ForumForm(Form):
                (
                (
                 ('prune_start', {'label': _("Delete threads with first post older than"), 'help_text': _('Enter number of days since thread start after which thread will be deleted or zero to don\'t delete threads.')}),
                 ('prune_start', {'label': _("Delete threads with first post older than"), 'help_text': _('Enter number of days since thread start after which thread will be deleted or zero to don\'t delete threads.')}),
                 ('prune_last', {'label': _("Delete threads with last post older than"), 'help_text': _('Enter number of days since since last reply in thread after which thread will be deleted or zero to don\'t delete threads.')}),
                 ('prune_last', {'label': _("Delete threads with last post older than"), 'help_text': _('Enter number of days since since last reply in thread after which thread will be deleted or zero to don\'t delete threads.')}),
+                ('pruned_archive', {'label': _("Archive pruned threads?"), 'help_text': _('If you want, you can archive pruned threads in other forum instead of deleting them.')})
                 ),
                 ),
                ),
                ),
               (
               (
                _("Display Options"),
                _("Display Options"),
                (
                (
-                ('attrs', {'label': _("Subforums List Attributes"), 'help_text': _('Custom templates can check forums for predefined attributes that will change way subforums lists are rendered.')}),
+                ('attrs', {'label': _("Forum Attributes"), 'help_text': _('Custom templates can check forums for predefined attributes that will change way subforums lists are rendered.')}),
                 ('show_details', {'label': _("Show Subforums Details"), 'help_text': _("Allows you to prevent this forum's subforums from displaying statistics, last post data, etc. ect. on subforums list.")}),
                 ('show_details', {'label': _("Show Subforums Details"), 'help_text': _("Allows you to prevent this forum's subforums from displaying statistics, last post data, etc. ect. on subforums list.")}),
                 ('style', {'label': _("Forum Style"), 'help_text': _('You can add custom CSS classess to this forum to change way it looks on forums lists.')}),
                 ('style', {'label': _("Forum Style"), 'help_text': _('You can add custom CSS classess to this forum to change way it looks on forums lists.')}),
                 ),
                 ),
@@ -97,24 +159,21 @@ class ForumForm(Form):
               )
               )
 
 
     def finalize_form(self):
     def finalize_form(self):
-        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ')
         self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
         self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
+        self.fields['pruned_archive'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't archive pruned threads"))
 
 
-    def clean_attrs(self):
-        clean = []
-        data = self.cleaned_data['attrs'].strip().split()
-        for i in data:
-            i = i.strip()
-            if not i in clean:
-                clean.append(i)
-        return ' '.join(clean)
+    def clean_pruned_archive(self):
+        data = self.cleaned_data['pruned_archive']
+        if data and data.pk == self.target_forum.pk:
+            raise forms.ValidationError(_("Forum cannot be its own archive."))
+        return data
 
 
 
 
-class RedirectForm(Form):
+class RedirectForm(Form, CleanAttrsMixin):
     parent = False
     parent = False
     perms = False
     perms = False
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Redirect name must be sluggable."),
+                                                                          _("Redirect name must contain alphanumeric characters."),
                                                                           _("Redirect name is too long.")
                                                                           _("Redirect name is too long.")
                                                                           )])
                                                                           )])
     description = forms.CharField(widget=forms.Textarea, required=False)
     description = forms.CharField(widget=forms.Textarea, required=False)
@@ -135,13 +194,13 @@ class RedirectForm(Form):
               (
               (
                _("Display Options"),
                _("Display Options"),
                (
                (
+                ('attrs', {'label': _("Forum Attributes"), 'help_text': _('Custom templates can check forums for predefined attributes that will change way subforums lists are rendered.')}),
                 ('style', {'label': _("Redirect Style"), 'help_text': _('You can add custom CSS classess to this redirect to change way it looks on forums lists.')}),
                 ('style', {'label': _("Redirect Style"), 'help_text': _('You can add custom CSS classess to this redirect to change way it looks on forums lists.')}),
                 ),
                 ),
                ),
                ),
               )
               )
 
 
     def finalize_form(self):
     def finalize_form(self):
-        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ')
         self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
         self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
 
 
 
 

+ 125 - 99
misago/apps/admin/forums/views.py

@@ -1,13 +1,16 @@
 import copy
 import copy
-from django.core.urlresolvers import reverse as django_reverse
+from urlparse import urlparse
+from django.core.urlresolvers import resolve, reverse as django_reverse
 from django.db.models import Q
 from django.db.models import Q
+from django.http import Http404
+from django.shortcuts import redirect
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from mptt.forms import TreeNodeChoiceField
 from mptt.forms import TreeNodeChoiceField
 from misago.admin import site
 from misago.admin import site
 from misago.apps.admin.widgets import *
 from misago.apps.admin.widgets import *
 from misago.models import Forum
 from misago.models import Forum
 from misago.utils.strings import slugify
 from misago.utils.strings import slugify
-from misago.apps.admin.forums.forms import CategoryForm, ForumForm, RedirectForm, DeleteForm
+from misago.apps.admin.forums.forms import NewNodeForm, CategoryForm, ForumForm, RedirectForm, DeleteForm
 
 
 def reverse(route, target=None):
 def reverse(route, target=None):
     if target:
     if target:
@@ -26,8 +29,8 @@ class List(ListWidget):
                )
                )
     nothing_checked_message = _('You have to select at least one forum.')
     nothing_checked_message = _('You have to select at least one forum.')
     actions = (
     actions = (
-               ('resync', _("Resynchronise forums")),
-               ('prune', _("Prune forums"), _("Are you sure you want to delete all content from selected forums?")),
+               ('resync_fast', _("Resynchronize forums (fast)")),
+               ('resync', _("Resynchronize forums")),
                )
                )
     empty_message = _('No forums are currently defined.')
     empty_message = _('No forums are currently defined.')
 
 
@@ -61,125 +64,142 @@ class List(ListWidget):
                 self.action('remove', _("Delete Redirect"), reverse('admin_forums_delete', item)),
                 self.action('remove', _("Delete Redirect"), reverse('admin_forums_delete', item)),
                 )
                 )
 
 
-    def action_resync(self, items, checked):
-        return Message(_('Selected forums have been resynchronised successfully.'), 'success'), reverse('admin_forums')
-
-    def action_prune(self, items, checked):
-        return Message(_('Selected forums have been pruned successfully.'), 'success'), reverse('admin_forums')
+    def action_resync_fast(self, items, checked):
+        for forum in items:
+            if forum.pk in checked:
+                forum.sync()
+                forum.save(force_update=True)
+        return Message(_('Selected forums have been resynchronized successfully.'), 'success'), reverse('admin_forums')
 
 
-
-class NewCategory(FormWidget):
+    def action_resync(self, items, checked):
+        clean_checked = []
+        for item in items:
+            if item.pk in checked and item.type == 'forum':
+                clean_checked.append(item.pk)
+        if not clean_checked:
+            return Message(_('Only forums can be resynchronized.'), 'error'), reverse('admin_forums')
+        self.request.session['sync_forums'] = clean_checked
+        return Message('Meh', 'success'), django_reverse('admin_forums_resync')
+
+
+def resync_forums(request, forum=0, progress=0):
+    progress = int(progress)
+    forums = request.session.get('sync_forums')
+    if not forums:
+        request.messages.set_flash(Message(_('No forums to resynchronize.')), 'info', 'forums')
+        return redirect(reverse('admin_forums'))
+    try:
+        if not forum:
+            forum = request.session['sync_forums'].pop()
+        forum = Forum.objects.get(id=forum)
+    except Forum.DoesNotExist:
+        del request.session['sync_forums']
+        request.messages.set_flash(Message(_('Forum for resynchronization does not exist.')), 'error', 'forums')
+        return redirect(reverse('admin_forums'))
+
+    # Sync 50 threads
+    threads_total = forum.thread_set.count()
+    for thread in forum.thread_set.all()[progress:(progress+1)]:
+        thread.sync()
+        thread.save(force_update=True)
+        progress += 1
+
+    if not threads_total:
+        return redirect(django_reverse('admin_forums_resync'))
+
+    # Render Progress
+    response = request.theme.render_to_response('processing.html', {
+            'task_name': _('Resynchronizing Forums'),
+            'target_name': forum.name,
+            'message': _('Resynchronized %(progress)s from %(total)s threads') % {'progress': progress, 'total': threads_total},
+            'progress': progress * 100 / threads_total,
+            'cancel_url': reverse('admin_forums'),
+        }, context_instance=RequestContext(request));
+
+    # Redirect where to?
+    if progress >= threads_total:
+        forum.sync()
+        forum.save(force_update=True)
+        response['refresh'] = '2;url=%s' % django_reverse('admin_forums_resync')
+    else:
+        response['refresh'] = '2;url=%s' % django_reverse('admin_forums_resync', kwargs={'forum': forum.pk, 'progress': progress})
+    return response
+
+
+class NewNode(FormWidget):
     admin = site.get_action('forums')
     admin = site.get_action('forums')
-    id = 'new_category'
+    id = 'new'
     fallback = 'admin_forums'
     fallback = 'admin_forums'
-    form = CategoryForm
-    submit_button = _("Save Category")
+    form = NewNodeForm
+    submit_button = _("Save Node")
 
 
     def get_new_url(self, model):
     def get_new_url(self, model):
-        return reverse('admin_forums_new_category')
+        return reverse('admin_forums_new')
 
 
     def get_edit_url(self, model):
     def get_edit_url(self, model):
         return reverse('admin_forums_edit', model)
         return reverse('admin_forums_edit', model)
 
 
-    def submit_form(self, form, target):
-        new_forum = Forum(
-                          name=form.cleaned_data['name'],
-                          slug=slugify(form.cleaned_data['name']),
-                          type='category',
-                          attrs=form.cleaned_data['attrs'],
-                          show_details=form.cleaned_data['show_details'],
-                          style=form.cleaned_data['style'],
-                          closed=form.cleaned_data['closed'],
-                          )
-        new_forum.set_description(form.cleaned_data['description'])
-        new_forum.insert_at(form.cleaned_data['parent'], position='last-child', save=True)
-        Forum.objects.populate_tree(True)
-
-        if form.cleaned_data['perms']:
-            new_forum.copy_permissions(form.cleaned_data['perms'])
-            self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
-
-        return new_forum, Message(_('New Category has been created.'), 'success')
-
-
-class NewForum(FormWidget):
-    admin = site.get_action('forums')
-    id = 'new_forum'
-    fallback = 'admin_forums'
-    form = ForumForm
-    submit_button = _("Save Forum")
-
-    def get_new_url(self, model):
-        return reverse('admin_forums_new_forum')
-
-    def get_edit_url(self, model):
-        return reverse('admin_forums_edit', model)
+    def get_initial_data(self, model):
+        print 'CALL!'
+        if not self.request.session.get('forums_admin_preffs'):
+            print 'NO PATTERN!'
+            return {}
+
+        ref = self.request.META.get('HTTP_REFERER')
+        if ref:
+            parsed = urlparse(self.request.META.get('HTTP_REFERER'));
+            try:
+                link = resolve(parsed.path)
+                if not link.url_name == 'admin_forums_new':
+                    return {}
+            except Http404:
+                return {}
+        try:
+            init = self.request.session.get('forums_admin_preffs')
+            del self.request.session['forums_admin_preffs']
+            return {
+                'parent': Forum.objects.get(id=init['parent']),
+                'perms': Forum.objects.get(id=init['perms']) if init['perms'] else None,
+                'role': init['role'],
+            }
+        except (KeyError, Forum.DoesNotExist):
+            return {}
 
 
     def submit_form(self, form, target):
     def submit_form(self, form, target):
         new_forum = Forum(
         new_forum = Forum(
                           name=form.cleaned_data['name'],
                           name=form.cleaned_data['name'],
                           slug=slugify(form.cleaned_data['name']),
                           slug=slugify(form.cleaned_data['name']),
-                          type='forum',
+                          type=form.cleaned_data['role'],
                           attrs=form.cleaned_data['attrs'],
                           attrs=form.cleaned_data['attrs'],
-                          show_details=form.cleaned_data['show_details'],
                           style=form.cleaned_data['style'],
                           style=form.cleaned_data['style'],
-                          closed=form.cleaned_data['closed'],
-                          prune_start=form.cleaned_data['prune_start'],
-                          prune_last=form.cleaned_data['prune_last'],
                           )
                           )
         new_forum.set_description(form.cleaned_data['description'])
         new_forum.set_description(form.cleaned_data['description'])
-        new_forum.insert_at(form.cleaned_data['parent'], position='last-child', save=True)
-        Forum.objects.populate_tree(True)
-
-        if form.cleaned_data['perms']:
-            new_forum.copy_permissions(form.cleaned_data['perms'])
-            self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
-
-        return new_forum, Message(_('New Forum has been created.'), 'success')
-
-    def __call__(self, request):
-        if self.admin.model.objects.get(special='root').get_descendants().count() == 0:
-            request.messages.set_flash(Message(_("You have to create at least one category before you will be able to create forums.")), 'error', self.admin.id)
-            return redirect(self.get_fallback_url())
-        return super(NewForum, self).__call__(request)
-
-
-class NewRedirect(FormWidget):
-    admin = site.get_action('forums')
-    id = 'new_redirect'
-    fallback = 'admin_forums'
-    form = RedirectForm
-    submit_button = _("Save Forum")
 
 
-    def get_new_url(self, model):
-        return reverse('admin_forums_new_redirect')
-
-    def get_edit_url(self, model):
-        return reverse('admin_forums_edit', model)
+        if form.cleaned_data['role'] == 'redirect':
+            new_forum.redirect = form.cleaned_data['redirect']
+        else:
+            new_forum.closed = form.cleaned_data['closed']
+            new_forum.show_details = form.cleaned_data['show_details']
 
 
-    def submit_form(self, form, target):
-        new_forum = Forum(
-                          name=form.cleaned_data['name'],
-                          slug=slugify(form.cleaned_data['name']),
-                          redirect=form.cleaned_data['redirect'],
-                          style=form.cleaned_data['style'],
-                          type='redirect',
-                          )
-        new_forum.set_description(form.cleaned_data['description'])
         new_forum.insert_at(form.cleaned_data['parent'], position='last-child', save=True)
         new_forum.insert_at(form.cleaned_data['parent'], position='last-child', save=True)
         Forum.objects.populate_tree(True)
         Forum.objects.populate_tree(True)
 
 
         if form.cleaned_data['perms']:
         if form.cleaned_data['perms']:
             new_forum.copy_permissions(form.cleaned_data['perms'])
             new_forum.copy_permissions(form.cleaned_data['perms'])
-            self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
+            self.request.monitor.increase('acl_version')
 
 
-        return new_forum, Message(_('New Redirect has been created.'), 'success')
+        self.request.session['forums_admin_preffs'] = {
+            'parent': form.cleaned_data['parent'].pk,
+            'perms': form.cleaned_data['perms'].pk if form.cleaned_data['perms'] else None,
+            'role': form.cleaned_data['role'],
+        }
 
 
-    def __call__(self, request):
-        if self.admin.model.objects.get(special='root').get_descendants().count() == 0:
-            request.messages.set_flash(Message(_("You have to create at least one category before you will be able to create redirects.")), 'error', self.admin.id)
-            return redirect(self.get_fallback_url())
-        return super(NewRedirect, self).__call__(request)
+        if form.cleaned_data['role'] == 'category':
+            return new_forum, Message(_('New Category has been created.'), 'success')
+        if form.cleaned_data['role'] == 'forum':
+            return new_forum, Message(_('New Forum has been created.'), 'success')
+        if form.cleaned_data['role'] == 'redirect':
+            return new_forum, Message(_('New Redirect has been created.'), 'success')
 
 
 
 
 class Up(ButtonWidget):
 class Up(ButtonWidget):
@@ -239,6 +259,7 @@ class Edit(FormWidget):
         form_inst = super(Edit, self).get_form_instance(form, target, initial, post)
         form_inst = super(Edit, self).get_form_instance(form, target, initial, post)
         valid_targets = Forum.objects.get(special='root').get_descendants(include_self=target.type == 'category').exclude(Q(lft__gte=target.lft) & Q(rght__lte=target.rght))
         valid_targets = Forum.objects.get(special='root').get_descendants(include_self=target.type == 'category').exclude(Q(lft__gte=target.lft) & Q(rght__lte=target.rght))
         form_inst.fields['parent'] = TreeNodeChoiceField(queryset=valid_targets, level_indicator=u'- - ')
         form_inst.fields['parent'] = TreeNodeChoiceField(queryset=valid_targets, level_indicator=u'- - ')
+        form_inst.target_forum = target
         return form_inst
         return form_inst
 
 
     def get_initial_data(self, model):
     def get_initial_data(self, model):
@@ -259,6 +280,7 @@ class Edit(FormWidget):
         if model.type == 'forum':
         if model.type == 'forum':
             initial['prune_start'] = model.prune_start
             initial['prune_start'] = model.prune_start
             initial['prune_last'] = model.prune_last
             initial['prune_last'] = model.prune_last
+            initial['pruned_archive'] = model.pruned_archive
 
 
         return initial
         return initial
 
 
@@ -277,10 +299,11 @@ class Edit(FormWidget):
         if target.type == 'forum':
         if target.type == 'forum':
             target.prune_start = form.cleaned_data['prune_start']
             target.prune_start = form.cleaned_data['prune_start']
             target.prune_last = form.cleaned_data['prune_last']
             target.prune_last = form.cleaned_data['prune_last']
+            target.pruned_archive = form.cleaned_data['pruned_archive']
 
 
         if form.cleaned_data['parent'].pk != target.parent.pk:
         if form.cleaned_data['parent'].pk != target.parent.pk:
             target.move_to(form.cleaned_data['parent'], 'last-child')
             target.move_to(form.cleaned_data['parent'], 'last-child')
-            self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
+            self.request.monitor.increase('acl_version')
 
 
         target.save(force_update=True)
         target.save(force_update=True)
         Forum.objects.populate_tree(True)
         Forum.objects.populate_tree(True)
@@ -289,7 +312,10 @@ class Edit(FormWidget):
             target.copy_permissions(form.cleaned_data['perms'])
             target.copy_permissions(form.cleaned_data['perms'])
 
 
         if form.cleaned_data['parent'].pk != target.parent.pk or form.cleaned_data['perms']:
         if form.cleaned_data['parent'].pk != target.parent.pk or form.cleaned_data['perms']:
-            self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
+            self.request.monitor.increase('acl_version')
+
+        if self.original_name != target.name:
+            target.sync_name()
 
 
         return target, Message(_('Changes in forum "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
         return target, Message(_('Changes in forum "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
 
 
@@ -344,5 +370,5 @@ class Delete(FormWidget):
                 Forum.objects.get(id=child.pk).delete()
                 Forum.objects.get(id=child.pk).delete()
         Forum.objects.get(id=target.pk).delete()
         Forum.objects.get(id=target.pk).delete()
         Forum.objects.populate_tree(True)
         Forum.objects.populate_tree(True)
-        self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
+        self.request.monitor.increase('acl_version')
         return target, Message(_('Forum "%(name)s" has been deleted.') % {'name': self.original_name}, 'success')
         return target, Message(_('Forum "%(name)s" has been deleted.') % {'name': self.original_name}, 'success')

+ 0 - 1
misago/apps/admin/home.py

@@ -2,7 +2,6 @@ from django.template import RequestContext
 from misago.models import Session
 from misago.models import Session
 
 
 def home(request):
 def home(request):
-    print 'BAZONGA!'
     return request.theme.render_to_response('home.html', {
     return request.theme.render_to_response('home.html', {
         'users': request.monitor['users'],
         'users': request.monitor['users'],
         'users_inactive': request.monitor['users_inactive'],
         'users_inactive': request.monitor['users_inactive'],

+ 1 - 1
misago/apps/admin/newsletters/forms.py

@@ -7,7 +7,7 @@ from misago.validators import validate_sluggable
 
 
 class NewsletterForm(Form):
 class NewsletterForm(Form):
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Newsletter name must be sluggable."),
+                                                                          _("Newsletter name must contain alphanumeric characters."),
                                                                           _("Newsletter name is too long.")
                                                                           _("Newsletter name is too long.")
                                                                           )])
                                                                           )])
     step_size = forms.IntegerField(initial=300, min_value=1)
     step_size = forms.IntegerField(initial=300, min_value=1)

+ 0 - 1
misago/apps/admin/online/forms.py

@@ -9,7 +9,6 @@ class SearchSessionsForm(Form):
     type = forms.ChoiceField(choices=(
     type = forms.ChoiceField(choices=(
                                       ('all', _("All types")),
                                       ('all', _("All types")),
                                       ('registered', _("Registered Members Sessions")),
                                       ('registered', _("Registered Members Sessions")),
-                                      ('hidden', _("Hidden Sessions")),
                                       ('guest', _("Guests Sessions")),
                                       ('guest', _("Guests Sessions")),
                                       ('crawler', _("Crawler Sessions")),
                                       ('crawler', _("Crawler Sessions")),
                                       ), required=False)
                                       ), required=False)

+ 0 - 2
misago/apps/admin/online/views.py

@@ -30,8 +30,6 @@ class List(ListWidget):
             model = model.filter(agent__icontains=filters['useragent'])
             model = model.filter(agent__icontains=filters['useragent'])
         if filters['type'] == 'registered':
         if filters['type'] == 'registered':
             model = model.filter(user__isnull=False)
             model = model.filter(user__isnull=False)
-        if filters['type'] == 'hidden':
-            model = model.filter(hidden=True)
         if filters['type'] == 'guest':
         if filters['type'] == 'guest':
             model = model.filter(user__isnull=True)
             model = model.filter(user__isnull=True)
         if filters['type'] == 'crawler':
         if filters['type'] == 'crawler':

+ 1 - 1
misago/apps/admin/pruneusers/forms.py

@@ -5,7 +5,7 @@ from misago.validators import validate_sluggable
 
 
 class PolicyForm(Form):
 class PolicyForm(Form):
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Policy name must be sluggable."),
+                                                                          _("Policy name must contain alphanumeric characters."),
                                                                           _("Policy name is too long.")
                                                                           _("Policy name is too long.")
                                                                           )])
                                                                           )])
     email = forms.CharField(max_length=255, required=False)
     email = forms.CharField(max_length=255, required=False)

+ 2 - 2
misago/apps/admin/pruneusers/views.py

@@ -41,7 +41,7 @@ class List(ListWidget):
         if not self.request.user.is_god():
         if not self.request.user.is_god():
             return Message(_('Only system administrators can delete pruning policies.'), 'error'), reverse('admin_prune_users')
             return Message(_('Only system administrators can delete pruning policies.'), 'error'), reverse('admin_prune_users')
 
 
-        Policy.objects.filter(id__in=checked).delete()
+        PruningPolicy.objects.filter(id__in=checked).delete()
         return Message(_('Selected pruning policies have been deleted successfully.'), 'success'), reverse('admin_prune_users')
         return Message(_('Selected pruning policies have been deleted successfully.'), 'success'), reverse('admin_prune_users')
 
 
 
 
@@ -59,7 +59,7 @@ class New(FormWidget):
         return reverse('admin_prune_users_edit', model)
         return reverse('admin_prune_users_edit', model)
 
 
     def submit_form(self, form, target):
     def submit_form(self, form, target):
-        new_policy = Policy(
+        new_policy = PruningPolicy(
                       name=form.cleaned_data['name'],
                       name=form.cleaned_data['name'],
                       email=form.cleaned_data['email'],
                       email=form.cleaned_data['email'],
                       posts=form.cleaned_data['posts'],
                       posts=form.cleaned_data['posts'],

+ 15 - 1
misago/apps/admin/ranks/forms.py

@@ -2,11 +2,12 @@ from django.core.validators import RegexValidator
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from django import forms
 from django import forms
 from misago.forms import Form, YesNoSwitch
 from misago.forms import Form, YesNoSwitch
+from misago.models import Role
 from misago.validators import validate_sluggable
 from misago.validators import validate_sluggable
 
 
 class RankForm(Form):
 class RankForm(Form):
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
     name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Rank name must be sluggable."),
+                                                                          _("Rank name must contain alphanumeric characters."),
                                                                           _("Rank name is too long.")
                                                                           _("Rank name is too long.")
                                                                           )])
                                                                           )])
     description = forms.CharField(widget=forms.Textarea, required=False)
     description = forms.CharField(widget=forms.Textarea, required=False)
@@ -16,6 +17,7 @@ class RankForm(Form):
     as_tab = forms.BooleanField(widget=YesNoSwitch, required=False)
     as_tab = forms.BooleanField(widget=YesNoSwitch, required=False)
     on_index = forms.BooleanField(widget=YesNoSwitch, required=False)
     on_index = forms.BooleanField(widget=YesNoSwitch, required=False)
     criteria = forms.CharField(max_length=255, initial='0', validators=[RegexValidator(regex='^(\d+)(%?)$', message=_('This is incorrect rank match rule.'))], required=False)
     criteria = forms.CharField(max_length=255, initial='0', validators=[RegexValidator(regex='^(\d+)(%?)$', message=_('This is incorrect rank match rule.'))], required=False)
+    roles = False
 
 
     layout = (
     layout = (
               (
               (
@@ -28,6 +30,12 @@ class RankForm(Form):
                 )
                 )
                ),
                ),
               (
               (
+               _("Rank Roles"),
+               (
+                ('roles', {'label': _("Rank Roles"), 'help_text': _("You can grant users with this rank extra roles to serve either as rewards or signs of trust to active members.")}),
+                )
+               ),
+              (
                _("Rank Looks"),
                _("Rank Looks"),
                (
                (
                 ('title', {'label': _("Rank Title"), 'help_text': _("Short description of rank's bearer role in your community.")}),
                 ('title', {'label': _("Rank Title"), 'help_text': _("Short description of rank's bearer role in your community.")}),
@@ -42,3 +50,9 @@ class RankForm(Form):
                 ),
                 ),
                ),
                ),
               )
               )
+
+    def finalize_form(self):
+        if self.request.user.is_god():
+            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.order_by('name').all(), required=False)
+        else:
+            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.filter(protected__exact=False).order_by('name').all(), required=False)

+ 27 - 13
misago/apps/admin/ranks/views.py

@@ -83,18 +83,20 @@ class New(FormWidget):
         position = 0
         position = 0
         last_rank = Rank.objects.latest('order')
         last_rank = Rank.objects.latest('order')
         new_rank = Rank(
         new_rank = Rank(
-                      name=form.cleaned_data['name'],
-                      slug=slugify(form.cleaned_data['name']),
-                      description=form.cleaned_data['description'],
-                      style=form.cleaned_data['style'],
-                      title=form.cleaned_data['title'],
-                      special=form.cleaned_data['special'],
-                      as_tab=form.cleaned_data['as_tab'],
-                      on_index=form.cleaned_data['on_index'],
-                      order=(last_rank.order + 1 if last_rank else 0),
-                      criteria=form.cleaned_data['criteria']
-                     )
+                        name=form.cleaned_data['name'],
+                        slug=slugify(form.cleaned_data['name']),
+                        description=form.cleaned_data['description'],
+                        style=form.cleaned_data['style'],
+                        title=form.cleaned_data['title'],
+                        special=form.cleaned_data['special'],
+                        as_tab=form.cleaned_data['as_tab'],
+                        on_index=form.cleaned_data['on_index'],
+                        order=(last_rank.order + 1 if last_rank else 0),
+                        criteria=form.cleaned_data['criteria']
+                        )  
         new_rank.save(force_insert=True)
         new_rank.save(force_insert=True)
+        for role in form.cleaned_data['roles']:
+            new_rank.roles.add(role)
         return new_rank, Message(_('New Rank has been created.'), 'success')
         return new_rank, Message(_('New Rank has been created.'), 'success')
 
 
 
 
@@ -106,6 +108,7 @@ class Edit(FormWidget):
     form = RankForm
     form = RankForm
     target_name = 'name'
     target_name = 'name'
     notfound_message = _('Requested Rank could not be found.')
     notfound_message = _('Requested Rank could not be found.')
+    translate_target_name = True
     submit_fallback = True
     submit_fallback = True
 
 
     def get_url(self, model):
     def get_url(self, model):
@@ -123,7 +126,8 @@ class Edit(FormWidget):
                 'special': model.special,
                 'special': model.special,
                 'as_tab': model.as_tab,
                 'as_tab': model.as_tab,
                 'on_index': model.on_index,
                 'on_index': model.on_index,
-                'criteria': model.criteria
+                'criteria': model.criteria,
+                'roles': model.roles.all(),
                 }
                 }
 
 
     def submit_form(self, form, target):
     def submit_form(self, form, target):
@@ -137,6 +141,16 @@ class Edit(FormWidget):
         target.on_index = form.cleaned_data['on_index']
         target.on_index = form.cleaned_data['on_index']
         target.criteria = form.cleaned_data['criteria']
         target.criteria = form.cleaned_data['criteria']
         target.save(force_update=True)
         target.save(force_update=True)
+
+        if self.request.user.is_god():
+            target.roles.clear()
+        else:
+            target.roles.remove(*target.roles.filter(protected=False))
+        for role in form.cleaned_data['roles']:
+            target.roles.add(role)
+
+        target.user_set.update(acl_key=None)
+
         return target, Message(_('Changes in rank "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
         return target, Message(_('Changes in rank "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
 
 
 
 
@@ -148,4 +162,4 @@ class Delete(ButtonWidget):
 
 
     def action(self, target):
     def action(self, target):
         target.delete()
         target.delete()
-        return Message(_('Rank "%(name)s" has been deleted.') % {'name': target.name}, 'success'), False
+        return Message(_('Rank "%(name)s" has been deleted.') % {'name': _(target.name)}, 'success'), False

+ 1 - 1
misago/apps/admin/roles/forms.py

@@ -5,7 +5,7 @@ from misago.validators import validate_sluggable
 
 
 class RoleForm(Form):
 class RoleForm(Form):
     name = forms.CharField(max_length=255,validators=[validate_sluggable(
     name = forms.CharField(max_length=255,validators=[validate_sluggable(
-                                                                         _("Role name must be sluggable."),
+                                                                         _("Role name must contain alphanumeric characters."),
                                                                          _("Role name is too long.")
                                                                          _("Role name is too long.")
                                                                          )])
                                                                          )])
     protected = forms.BooleanField(widget=YesNoSwitch,required=False)
     protected = forms.BooleanField(widget=YesNoSwitch,required=False)

+ 3 - 3
misago/apps/admin/roles/views.py

@@ -43,7 +43,7 @@ class List(ListWidget):
 
 
     def action_delete(self, items, checked):
     def action_delete(self, items, checked):
         for item in items:
         for item in items:
-            if unicode(item.pk) in checked:
+            if item.pk in checked:
                 if item.special:
                 if item.special:
                     return Message(_('You cannot delete system roles.'), 'error'), reverse('admin_roles')
                     return Message(_('You cannot delete system roles.'), 'error'), reverse('admin_roles')
                 if item.protected and not self.request.user.is_god():
                 if item.protected and not self.request.user.is_god():
@@ -110,7 +110,7 @@ class Edit(FormWidget):
         if self.request.user.is_god():
         if self.request.user.is_god():
             target.protected = form.cleaned_data['protected']
             target.protected = form.cleaned_data['protected']
         target.save(force_update=True)
         target.save(force_update=True)
-        self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
+        self.request.monitor.increase('acl_version')
         return target, Message(_('Changes in role "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
         return target, Message(_('Changes in role "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
 
 
 
 
@@ -226,7 +226,7 @@ class ACL(FormWidget):
             raw_acl[perm] = form.cleaned_data[perm]
             raw_acl[perm] = form.cleaned_data[perm]
         target.permissions = raw_acl
         target.permissions = raw_acl
         target.save(force_update=True)
         target.save(force_update=True)
-        self.request.monitor['acl_version'] = int(self.request.monitor['acl_version']) + 1
+        self.request.monitor.increase('acl_version')
         
         
         return target, Message(_('Role "%(name)s" permissions have been changed.') % {'name': self.original_name}, 'success')
         return target, Message(_('Role "%(name)s" permissions have been changed.') % {'name': self.original_name}, 'success')
 
 

+ 63 - 75
misago/apps/admin/sections/forums.py

@@ -4,93 +4,81 @@ from misago.admin import AdminAction
 from misago.models import Forum
 from misago.models import Forum
 
 
 ADMIN_ACTIONS = (
 ADMIN_ACTIONS = (
-   AdminAction(
-               section='forums',
-               id='forums',
-               name=_("Forums List"),
-               help=_("Create, edit and delete forums."),
-               icon='comment',
-               model=Forum,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Forums List"),
-                         'help': _("All existing forums"),
-                         'route': 'admin_forums'
-                         },
-                        {
-                         'id': 'new_category',
-                         'name': _("New Category"),
-                         'help': _("Create new category"),
-                         'route': 'admin_forums_new_category'
-                         },
-                        {
-                         'id': 'new_forum',
-                         'name': _("New Forum"),
-                         'help': _("Create new forum"),
-                         'route': 'admin_forums_new_forum'
-                         },
-                        {
-                         'id': 'new_redirect',
-                         'name': _("New Redirect"),
-                         'help': _("Create new redirect"),
-                         'route': 'admin_forums_new_redirect'
-                         },
-                        ],
-               route='admin_forums',
-               urlpatterns=patterns('misago.apps.admin.forums.views',
+    AdminAction(
+                section='forums',
+                id='forums',
+                name=_("Forums List"),
+                help=_("Create, edit and delete forums."),
+                icon='comment',
+                model=Forum,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Forums List"),
+                          'help': _("All existing forums"),
+                          'route': 'admin_forums'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("New Node"),
+                          'help': _("Create new forums tree node"),
+                          'route': 'admin_forums_new'
+                          },
+                         ],
+                route='admin_forums',
+                urlpatterns=patterns('misago.apps.admin.forums.views',
                         url(r'^$', 'List', name='admin_forums'),
                         url(r'^$', 'List', name='admin_forums'),
-                        url(r'^new/category/$', 'NewCategory', name='admin_forums_new_category'),
-                        url(r'^new/forum/$', 'NewForum', name='admin_forums_new_forum'),
-                        url(r'^new/redirect/$', 'NewRedirect', name='admin_forums_new_redirect'),
+                        url(r'^sync/$', 'resync_forums', name='admin_forums_resync'),
+                        url(r'^sync/(?P<forum>\d+)/(?P<progress>\d+)/$', 'resync_forums', name='admin_forums_resync'),
+                        url(r'^new/$', 'NewNode', name='admin_forums_new'),
                         url(r'^up/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Up', name='admin_forums_up'),
                         url(r'^up/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Up', name='admin_forums_up'),
                         url(r'^down/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Down', name='admin_forums_down'),
                         url(r'^down/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Down', name='admin_forums_down'),
                         url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_forums_edit'),
                         url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_forums_edit'),
                         url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_forums_delete'),
                         url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_forums_delete'),
                     ),
                     ),
-               ),
-   AdminAction(
-               section='forums',
-               id='labels',
-               name=_("Thread Labels"),
-               help=_("Thread Labels allow you to group threads together within forums."),
-               icon='tags',
-               route='admin_forums_labels',
-               urlpatterns=patterns('misago.apps.admin.index',
+                ),
+    AdminAction(
+                section='forums',
+                id='labels',
+                name=_("Thread Labels"),
+                help=_("Thread Labels allow you to group threads together within forums."),
+                icon='tags',
+                route='admin_forums_labels',
+                urlpatterns=patterns('misago.apps.admin.index',
                         url(r'^$', 'todo', name='admin_forums_labels'),
                         url(r'^$', 'todo', name='admin_forums_labels'),
                     ),
                     ),
-               ),
-   AdminAction(
-               section='forums',
-               id='badwords',
-               name=_("Words Filter"),
-               help=_("Forbid usage of words in messages"),
-               icon='volume-off',
-               route='admin_forums_badwords',
-               urlpatterns=patterns('misago.apps.admin.index',
+                ),
+    AdminAction(
+                section='forums',
+                id='badwords',
+                name=_("Words Filter"),
+                help=_("Forbid usage of words in messages"),
+                icon='volume-off',
+                route='admin_forums_badwords',
+                urlpatterns=patterns('misago.apps.admin.index',
                         url(r'^$', 'todo', name='admin_forums_badwords'),
                         url(r'^$', 'todo', name='admin_forums_badwords'),
                     ),
                     ),
-               ),
-   AdminAction(
-               section='forums',
-               id='tests',
-               name=_("Tests"),
-               help=_("Tests that new messages have to pass"),
-               icon='filter',
-               route='admin_forums_tests',
-               urlpatterns=patterns('misago.apps.admin.index',
+                ),
+    AdminAction(
+                section='forums',
+                id='tests',
+                name=_("Tests"),
+                help=_("Tests that new messages have to pass"),
+                icon='filter',
+                route='admin_forums_tests',
+                urlpatterns=patterns('misago.apps.admin.index',
                         url(r'^$', 'todo', name='admin_forums_tests'),
                         url(r'^$', 'todo', name='admin_forums_tests'),
                     ),
                     ),
-               ),
-   AdminAction(
-               section='forums',
-               id='attachments',
-               name=_("Attachments"),
-               help=_("Manage allowed attachment types."),
-               icon='download-alt',
-               route='admin_forums_attachments',
-               urlpatterns=patterns('misago.apps.admin.index',
+                ),
+    AdminAction(
+                section='forums',
+                id='attachments',
+                name=_("Attachments"),
+                help=_("Manage allowed attachment types."),
+                icon='download-alt',
+                route='admin_forums_attachments',
+                urlpatterns=patterns('misago.apps.admin.index',
                         url(r'^$', 'todo', name='admin_forums_attachments'),
                         url(r'^$', 'todo', name='admin_forums_attachments'),
                     ),
                     ),
-               ),
+                ),
 )
 )

+ 52 - 52
misago/apps/admin/sections/overview.py

@@ -4,68 +4,68 @@ from misago.admin import AdminAction
 from misago.models import Session, User
 from misago.models import Session, User
 
 
 ADMIN_ACTIONS = (
 ADMIN_ACTIONS = (
-   AdminAction(
-               section='overview',
-               id='index',
-               name=_("Home"),
-               help=_("Your forums right now"),
-               icon='home',
-               route='admin_home',
-               urlpatterns=patterns('misago.apps.admin.index',
+    AdminAction(
+                section='overview',
+                id='index',
+                name=_("Home"),
+                help=_("Your forums right now"),
+                icon='home',
+                route='admin_home',
+                urlpatterns=patterns('misago.apps.admin.index',
                         url(r'^$', 'index', name='admin_home'),
                         url(r'^$', 'index', name='admin_home'),
                     ),
                     ),
-               ),
+                ),
     AdminAction(
     AdminAction(
-               section='overview',
-               id='stats',
-               name=_("Stats"),
-               help=_("Create Statistics Reports"),
-               icon='signal',
-               route='admin_stats',
-               urlpatterns=patterns('misago.apps.admin.stats.views',
+                section='overview',
+                id='stats',
+                name=_("Stats"),
+                help=_("Create Statistics Reports"),
+                icon='signal',
+                route='admin_stats',
+                urlpatterns=patterns('misago.apps.admin.stats.views',
                         url(r'^$', 'form', name='admin_stats'),
                         url(r'^$', 'form', name='admin_stats'),
                         url(r'^(?P<model>[a-z0-9]+)/(?P<date_start>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])/(?P<date_end>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])/(?P<precision>\w+)$', 'graph', name='admin_stats_graph'),
                         url(r'^(?P<model>[a-z0-9]+)/(?P<date_start>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])/(?P<date_end>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])/(?P<precision>\w+)$', 'graph', name='admin_stats_graph'),
                     ),
                     ),
-               ),
+                ),
     AdminAction(
     AdminAction(
-               section='overview',
-               id='online',
-               name=_("Online"),
-               help=_("See who is currently online on forums."),
-               icon='fire',
-               model=Session,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Browse Users"),
-                         'help': _("Browse all registered user accounts"),
-                         'route': 'admin_online'
-                         },
-                        ],
-               route='admin_online',
-               urlpatterns=patterns('misago.apps.admin.online.views',
+                section='overview',
+                id='online',
+                name=_("Online"),
+                help=_("See who is currently online on forums."),
+                icon='fire',
+                model=Session,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Browse Users"),
+                          'help': _("Browse all registered user accounts"),
+                          'route': 'admin_online'
+                          },
+                         ],
+                route='admin_online',
+                urlpatterns=patterns('misago.apps.admin.online.views',
                         url(r'^$', 'List', name='admin_online'),
                         url(r'^$', 'List', name='admin_online'),
-                        url(r'^(?P<page>\d+)/$', 'List', name='admin_online'),
+                        url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'List', name='admin_online'),
                     ),
                     ),
-               ),
+                ),
     AdminAction(
     AdminAction(
-               section='overview',
-               id='team',
-               name=_("Forum Team"),
-               help=_("List of all forum team members"),
-               icon='user',
-               model=User,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Forum Team Members"),
-                         'help': _("List of all forum team members"),
-                         'route': 'admin_team'
-                         },
-                        ],
-               route='admin_team',
-               urlpatterns=patterns('misago.apps.admin.team',
+                section='overview',
+                id='team',
+                name=_("Forum Team"),
+                help=_("List of all forum team members"),
+                icon='user',
+                model=User,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Forum Team Members"),
+                          'help': _("List of all forum team members"),
+                          'route': 'admin_team'
+                          },
+                         ],
+                route='admin_team',
+                urlpatterns=patterns('misago.apps.admin.team',
                         url(r'^$', 'List', name='admin_team'),
                         url(r'^$', 'List', name='admin_team'),
                     ),
                     ),
-               ),
+                ),
 )
 )

+ 61 - 61
misago/apps/admin/sections/perms.py

@@ -4,65 +4,65 @@ from misago.admin import AdminAction
 from misago.models import ForumRole, Role
 from misago.models import ForumRole, Role
 
 
 ADMIN_ACTIONS = (
 ADMIN_ACTIONS = (
-   AdminAction(
-               section='perms',
-               id='roles',
-               name=_("User Roles"),
-               help=_("Manage User Roles"),
-               icon='th-large',
-               model=Role,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Browse Roles"),
-                         'help': _("Browse all existing roles"),
-                         'route': 'admin_roles'
-                         },
-                        {
-                         'id': 'new',
-                         'name': _("Add Role"),
-                         'help': _("Create new role"),
-                         'route': 'admin_roles_new'
-                         },
-                        ],
-               route='admin_roles',
-               urlpatterns=patterns('misago.apps.admin.roles.views',
-                        url(r'^$', 'List', name='admin_roles'),
-                        url(r'^new/$', 'New', name='admin_roles_new'),
-                        url(r'^forums/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Forums', name='admin_roles_masks'),
-                        url(r'^acl/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'ACL', name='admin_roles_acl'),
-                        url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_roles_edit'),
-                        url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_roles_delete'),
-                    ),
-               ),
-   AdminAction(
-               section='perms',
-               id='roles_forums',
-               name=_("Forum Roles"),
-               help=_("Manage Forum Roles"),
-               icon='th-list',
-               model=ForumRole,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Browse Roles"),
-                         'help': _("Browse all existing roles"),
-                         'route': 'admin_roles_forums'
-                         },
-                        {
-                         'id': 'new',
-                         'name': _("Add Role"),
-                         'help': _("Create new role"),
-                         'route': 'admin_roles_forums_new'
-                         },
-                        ],
-               route='admin_roles_forums',
-               urlpatterns=patterns('misago.apps.admin.forumroles.views',
-                        url(r'^$', 'List', name='admin_roles_forums'),
-                        url(r'^new/$', 'New', name='admin_roles_forums_new'),
-                        url(r'^acl/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'ACL', name='admin_roles_forums_acl'),
-                        url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_roles_forums_edit'),
-                        url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_roles_forums_delete'),
-                    ),
-               ),
+    AdminAction(
+                section='perms',
+                id='roles',
+                name=_("User Roles"),
+                help=_("Manage User Roles"),
+                icon='th-large',
+                model=Role,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Browse Roles"),
+                          'help': _("Browse all existing roles"),
+                          'route': 'admin_roles'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Add Role"),
+                          'help': _("Create new role"),
+                          'route': 'admin_roles_new'
+                          },
+                         ],
+                route='admin_roles',
+                urlpatterns=patterns('misago.apps.admin.roles.views',
+                         url(r'^$', 'List', name='admin_roles'),
+                         url(r'^new/$', 'New', name='admin_roles_new'),
+                         url(r'^forums/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Forums', name='admin_roles_masks'),
+                         url(r'^acl/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'ACL', name='admin_roles_acl'),
+                         url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_roles_edit'),
+                         url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_roles_delete'),
+                     ),
+                ),
+    AdminAction(
+                section='perms',
+                id='roles_forums',
+                name=_("Forum Roles"),
+                help=_("Manage Forum Roles"),
+                icon='th-list',
+                model=ForumRole,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Browse Roles"),
+                          'help': _("Browse all existing roles"),
+                          'route': 'admin_roles_forums'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Add Role"),
+                          'help': _("Create new role"),
+                          'route': 'admin_roles_forums_new'
+                          },
+                         ],
+                route='admin_roles_forums',
+                urlpatterns=patterns('misago.apps.admin.forumroles.views',
+                         url(r'^$', 'List', name='admin_roles_forums'),
+                         url(r'^new/$', 'New', name='admin_roles_forums_new'),
+                         url(r'^acl/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'ACL', name='admin_roles_forums_acl'),
+                         url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_roles_forums_edit'),
+                         url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_roles_forums_delete'),
+                     ),
+                ),
 )
 )

+ 13 - 43
misago/apps/admin/sections/system.py

@@ -1,49 +1,19 @@
 from django.conf.urls import patterns, include, url
 from django.conf.urls import patterns, include, url
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.admin import AdminAction
 from misago.admin import AdminAction
-from misago.models import ThemeAdjustment
 
 
 ADMIN_ACTIONS = (
 ADMIN_ACTIONS = (
-   AdminAction(
-               section='system',
-               id='settings',
-               name=_("Settings"),
-               help=_("Change your forum configuration"),
-               icon='wrench',
-               route='admin_settings',
-               urlpatterns=patterns('misago.apps.admin.settings.views',
-                        url(r'^$', 'settings', name='admin_settings'),
-                        url(r'^search/$', 'settings_search', name='admin_settings_search'),
-                        url(r'^(?P<group_slug>([a-z0-9]|-)+)-(?P<group_id>\d+)/$', 'settings', name='admin_settings')
-                    ),
-               ),
-   AdminAction(
-               section='system',
-               id='clients',
-               name=_("Clients"),
-               help=_("Adjust presentation layer to clients"),
-               icon='tint',
-               model=ThemeAdjustment,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Browse Clients"),
-                         'help': _("Browse all existing clients"),
-                         'route': 'admin_clients'
-                         },
-                        {
-                         'id': 'new',
-                         'name': _("Add New Adjustment"),
-                         'help': _("Create new client adjustment"),
-                         'route': 'admin_clients_new'
-                         },
-                        ],
-               route='admin_clients',
-               urlpatterns=patterns('misago.apps.admin.clients.views',
-                        url(r'^$', 'List', name='admin_clients'),
-                        url(r'^new/$', 'New', name='admin_clients_new'),
-                        url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_clients_edit'),
-                        url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_clients_delete'),
-                    ),
-               ),
+    AdminAction(
+                section='system',
+                id='settings',
+                name=_("Settings"),
+                help=_("Change your forum configuration"),
+                icon='wrench',
+                route='admin_settings',
+                urlpatterns=patterns('misago.apps.admin.settings.views',
+                         url(r'^$', 'settings', name='admin_settings'),
+                         url(r'^search/$', 'settings_search', name='admin_settings_search'),
+                         url(r'^(?P<group_slug>([a-z0-9]|-)+)-(?P<group_id>\d+)/$', 'settings', name='admin_settings')
+                     ),
+                ),
 )
 )

+ 151 - 151
misago/apps/admin/sections/users.py

@@ -4,155 +4,155 @@ from misago.admin import AdminAction
 from misago.models import Ban, Newsletter, PruningPolicy, Rank, User
 from misago.models import Ban, Newsletter, PruningPolicy, Rank, User
 
 
 ADMIN_ACTIONS = (
 ADMIN_ACTIONS = (
-   AdminAction(
-               section='users',
-               id='users',
-               name=_("Users List"),
-               help=_("Search and browse users"),
-               icon='user',
-               model=User,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Browse Users"),
-                         'help': _("Browse all registered user accounts"),
-                         'route': 'admin_users'
-                         },
-                        {
-                         'id': 'new',
-                         'name': _("Add User"),
-                         'help': _("Create new user account"),
-                         'route': 'admin_users_new'
-                         },
-                        ],
-               route='admin_users',
-               urlpatterns=patterns('misago.apps.admin.users.views',
-                        url(r'^$', 'List', name='admin_users'),
-                        url(r'^(?P<page>\d+)/$', 'List', name='admin_users'),
-                        url(r'^inactive/$', 'inactive', name='admin_users_inactive'),
-                        url(r'^new/$', 'New', name='admin_users_new'),
-                        url(r'^edit/(?P<slug>[a-z0-9]+)-(?P<target>\d+)/$', 'Edit', name='admin_users_edit'),
-                        url(r'^delete/(?P<slug>[a-z0-9]+)-(?P<target>\d+)/$', 'Delete', name='admin_users_delete'),
-                    ),
-               ),
-   AdminAction(
-               section='users',
-               id='ranks',
-               name=_("Ranks"),
-               help=_("Administrate User Ranks"),
-               icon='star',
-               model=Rank,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Browse Ranks"),
-                         'help': _("Browse all existing ranks"),
-                         'route': 'admin_ranks'
-                         },
-                        {
-                         'id': 'new',
-                         'name': _("Add Rank"),
-                         'help': _("Create new rank"),
-                         'route': 'admin_ranks_new'
-                         },
-                        ],
-               route='admin_ranks',
-               urlpatterns=patterns('misago.apps.admin.ranks.views',
-                        url(r'^$', 'List', name='admin_ranks'),
-                        url(r'^new/$', 'New', name='admin_ranks_new'),
-                        url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_ranks_edit'),
-                        url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_ranks_delete'),
-                    ),
-               ),
-   AdminAction(
-               section='users',
-               id='bans',
-               name=_("Bans"),
-               help=_("Ban or unban users from forums."),
-               icon='lock',
-               model=Ban,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Browse Bans"),
-                         'help': _("Browse all existing bans"),
-                         'route': 'admin_bans'
-                         },
-                        {
-                         'id': 'new',
-                         'name': _("Set Ban"),
-                         'help': _("Set new Ban"),
-                         'route': 'admin_bans_new'
-                         },
-                        ],
-               route='admin_bans',
-               urlpatterns=patterns('misago.apps.admin.bans.views',
-                        url(r'^$', 'List', name='admin_bans'),
-                        url(r'^(?P<page>\d+)/$', 'List', name='admin_bans'),
-                        url(r'^new/$', 'New', name='admin_bans_new'),
-                        url(r'^edit/(?P<target>\d+)/$', 'Edit', name='admin_bans_edit'),
-                        url(r'^delete/(?P<target>\d+)/$', 'Delete', name='admin_bans_delete'),
-                    ),
-               ),
-   AdminAction(
-               section='users',
-               id='prune_users',
-               name=_("Prune Users"),
-               help=_("Delete multiple Users"),
-               icon='remove',
-               model=PruningPolicy,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Pruning Policies"),
-                         'help': _("Browse all existing pruning policies"),
-                         'route': 'admin_prune_users'
-                         },
-                        {
-                         'id': 'new',
-                         'name': _("Set New Policy"),
-                         'help': _("Set new pruning policy"),
-                         'route': 'admin_prune_users_new'
-                         },
-                        ],
-               route='admin_prune_users',
-               urlpatterns=patterns('misago.apps.admin.pruneusers.views',
-                        url(r'^$', 'List', name='admin_prune_users'),
-                        url(r'^new/$', 'New', name='admin_prune_users_new'),
-                        url(r'^edit/(?P<target>\d+)/$', 'Edit', name='admin_prune_users_edit'),
-                        url(r'^delete/(?P<target>\d+)/$', 'Delete', name='admin_prune_users_delete'),
-                        url(r'^apply/(?P<target>\d+)/$', 'Apply', name='admin_prune_users_apply'),
-                    ),
-               ),
-   AdminAction(
-               section='users',
-               id='newsletters',
-               name=_("Newsletters"),
-               help=_("Manage and send Newsletters"),
-               icon='envelope',
-               model=Newsletter,
-               actions=[
-                        {
-                         'id': 'list',
-                         'name': _("Browse Newsletters"),
-                         'help': _("Browse all existing Newsletters"),
-                         'route': 'admin_newsletters'
-                         },
-                        {
-                         'id': 'new',
-                         'name': _("New Newsletter"),
-                         'help': _("Create new Newsletter"),
-                         'route': 'admin_newsletters_new'
-                         },
-                        ],
-               route='admin_newsletters',
-               urlpatterns=patterns('misago.apps.admin.newsletters.views',
-                        url(r'^$', 'List', name='admin_newsletters'),
-                        url(r'^(?P<page>\d+)/$', 'List', name='admin_newsletters'),
-                        url(r'^new/$', 'New', name='admin_newsletters_new'),
-                        url(r'^send/(?P<target>\d+)/(?P<token>[a-zA-Z0-9]+)/$', 'send', name='admin_newsletters_send'),
-                        url(r'^edit/(?P<target>\d+)/$', 'Edit', name='admin_newsletters_edit'),
-                        url(r'^delete/(?P<target>\d+)/$', 'Delete', name='admin_newsletters_delete'),
-                    ),
-               ),
+    AdminAction(
+                section='users',
+                id='users',
+                name=_("Users List"),
+                help=_("Search and browse users"),
+                icon='user',
+                model=User,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Browse Users"),
+                          'help': _("Browse all registered user accounts"),
+                          'route': 'admin_users'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Add User"),
+                          'help': _("Create new user account"),
+                          'route': 'admin_users_new'
+                          },
+                         ],
+                route='admin_users',
+                urlpatterns=patterns('misago.apps.admin.users.views',
+                         url(r'^$', 'List', name='admin_users'),
+                         url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'List', name='admin_users'),
+                         url(r'^inactive/$', 'inactive', name='admin_users_inactive'),
+                         url(r'^new/$', 'New', name='admin_users_new'),
+                         url(r'^edit/(?P<slug>[a-z0-9]+)-(?P<target>\d+)/$', 'Edit', name='admin_users_edit'),
+                         url(r'^delete/(?P<slug>[a-z0-9]+)-(?P<target>\d+)/$', 'Delete', name='admin_users_delete'),
+                     ),
+                ),
+    AdminAction(
+                section='users',
+                id='ranks',
+                name=_("Ranks"),
+                help=_("Administrate User Ranks"),
+                icon='star',
+                model=Rank,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Browse Ranks"),
+                          'help': _("Browse all existing ranks"),
+                          'route': 'admin_ranks'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Add Rank"),
+                          'help': _("Create new rank"),
+                          'route': 'admin_ranks_new'
+                          },
+                         ],
+                route='admin_ranks',
+                urlpatterns=patterns('misago.apps.admin.ranks.views',
+                         url(r'^$', 'List', name='admin_ranks'),
+                         url(r'^new/$', 'New', name='admin_ranks_new'),
+                         url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_ranks_edit'),
+                         url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_ranks_delete'),
+                     ),
+                ),
+    AdminAction(
+                section='users',
+                id='bans',
+                name=_("Bans"),
+                help=_("Ban or unban users from forums."),
+                icon='lock',
+                model=Ban,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Browse Bans"),
+                          'help': _("Browse all existing bans"),
+                          'route': 'admin_bans'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Set Ban"),
+                          'help': _("Set new Ban"),
+                          'route': 'admin_bans_new'
+                          },
+                         ],
+                route='admin_bans',
+                urlpatterns=patterns('misago.apps.admin.bans.views',
+                         url(r'^$', 'List', name='admin_bans'),
+                         url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'List', name='admin_bans'),
+                         url(r'^new/$', 'New', name='admin_bans_new'),
+                         url(r'^edit/(?P<target>\d+)/$', 'Edit', name='admin_bans_edit'),
+                         url(r'^delete/(?P<target>\d+)/$', 'Delete', name='admin_bans_delete'),
+                     ),
+                ),
+    AdminAction(
+                section='users',
+                id='prune_users',
+                name=_("Prune Users"),
+                help=_("Delete multiple Users"),
+                icon='remove',
+                model=PruningPolicy,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Pruning Policies"),
+                          'help': _("Browse all existing pruning policies"),
+                          'route': 'admin_prune_users'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Set New Policy"),
+                          'help': _("Set new pruning policy"),
+                          'route': 'admin_prune_users_new'
+                          },
+                         ],
+                route='admin_prune_users',
+                urlpatterns=patterns('misago.apps.admin.pruneusers.views',
+                         url(r'^$', 'List', name='admin_prune_users'),
+                         url(r'^new/$', 'New', name='admin_prune_users_new'),
+                         url(r'^edit/(?P<target>\d+)/$', 'Edit', name='admin_prune_users_edit'),
+                         url(r'^delete/(?P<target>\d+)/$', 'Delete', name='admin_prune_users_delete'),
+                         url(r'^apply/(?P<target>\d+)/$', 'Apply', name='admin_prune_users_apply'),
+                     ),
+                ),
+    AdminAction(
+                section='users',
+                id='newsletters',
+                name=_("Newsletters"),
+                help=_("Manage and send Newsletters"),
+                icon='envelope',
+                model=Newsletter,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Browse Newsletters"),
+                          'help': _("Browse all existing Newsletters"),
+                          'route': 'admin_newsletters'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("New Newsletter"),
+                          'help': _("Create new Newsletter"),
+                          'route': 'admin_newsletters_new'
+                          },
+                         ],
+                route='admin_newsletters',
+                urlpatterns=patterns('misago.apps.admin.newsletters.views',
+                         url(r'^$', 'List', name='admin_newsletters'),
+                         url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'List', name='admin_newsletters'),
+                         url(r'^new/$', 'New', name='admin_newsletters_new'),
+                         url(r'^send/(?P<target>\d+)/(?P<token>[a-zA-Z0-9]+)/$', 'send', name='admin_newsletters_send'),
+                         url(r'^edit/(?P<target>\d+)/$', 'Edit', name='admin_newsletters_edit'),
+                         url(r'^delete/(?P<target>\d+)/$', 'Delete', name='admin_newsletters_delete'),
+                     ),
+                ),
 )
 )

+ 2 - 0
misago/apps/admin/settings/views.py

@@ -1,3 +1,4 @@
+from django.core.cache import cache
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
@@ -46,6 +47,7 @@ def settings(request, group_id=None, group_slug=None):
         if form.is_valid():
         if form.is_valid():
             for setting in form.cleaned_data.keys():
             for setting in form.cleaned_data.keys():
                 request.settings[setting] = form.cleaned_data[setting]
                 request.settings[setting] = form.cleaned_data[setting]
+            cache.delete('settings')
             request.messages.set_flash(Message(_('Configuration has been changed.')), 'success', 'admin_settings')
             request.messages.set_flash(Message(_('Configuration has been changed.')), 'success', 'admin_settings')
             return redirect(reverse('admin_settings', kwargs={
             return redirect(reverse('admin_settings', kwargs={
                                                        'group_id': active_group.pk,
                                                        'group_id': active_group.pk,

+ 19 - 23
misago/apps/admin/users/views.py

@@ -92,8 +92,8 @@ class List(ListWidget):
 
 
     def action_activate(self, items, checked):
     def action_activate(self, items, checked):
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked and user.activation > 0:
-                self.request.monitor['users_inactive'] = int(self.request.monitor['users_inactive']) - 1
+            if user.pk in checked and user.activation > 0:
+                self.request.monitor.decrease('users_inactive')
                 user.activation = user.ACTIVATION_NONE
                 user.activation = user.ACTIVATION_NONE
                 user.save(force_update=True)
                 user.save(force_update=True)
                 user.email_user(
                 user.email_user(
@@ -107,13 +107,13 @@ class List(ListWidget):
     def action_deactivate(self, items, checked):
     def action_deactivate(self, items, checked):
         # First loop - check for errors
         # First loop - check for errors
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 if user.is_protected() and not self.request.user.is_god():
                 if user.is_protected() and not self.request.user.is_god():
                     return Message(_('You cannot force validation of protected members e-mails.'), 'error'), reverse('admin_users')
                     return Message(_('You cannot force validation of protected members e-mails.'), 'error'), reverse('admin_users')
 
 
         # Second loop - reset passwords
         # Second loop - reset passwords
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 user.activation = user.ACTIVATION_USER
                 user.activation = user.ACTIVATION_USER
                 user.token = token = random_string(12)
                 user.token = token = random_string(12)
                 user.save(force_update=True)
                 user.save(force_update=True)
@@ -128,13 +128,13 @@ class List(ListWidget):
     def action_remove_av(self, items, checked):
     def action_remove_av(self, items, checked):
         # First loop - check for errors
         # First loop - check for errors
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 if user.is_protected() and not self.request.user.is_god():
                 if user.is_protected() and not self.request.user.is_god():
                     return Message(_('You cannot remove and block protected members avatars.'), 'error'), reverse('admin_users')
                     return Message(_('You cannot remove and block protected members avatars.'), 'error'), reverse('admin_users')
 
 
         # Second loop - reset passwords
         # Second loop - reset passwords
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 user.lock_avatar()
                 user.lock_avatar()
                 user.save(force_update=True)
                 user.save(force_update=True)
 
 
@@ -143,13 +143,13 @@ class List(ListWidget):
     def action_remove_sig(self, items, checked):
     def action_remove_sig(self, items, checked):
         # First loop - check for errors
         # First loop - check for errors
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 if user.is_protected() and not self.request.user.is_god():
                 if user.is_protected() and not self.request.user.is_god():
                     return Message(_('You cannot remove and block protected members signatures.'), 'error'), reverse('admin_users')
                     return Message(_('You cannot remove and block protected members signatures.'), 'error'), reverse('admin_users')
 
 
         # Second loop - reset passwords
         # Second loop - reset passwords
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 user.signature_ban = True
                 user.signature_ban = True
                 user.signature = ''
                 user.signature = ''
                 user.signature_preparsed = ''
                 user.signature_preparsed = ''
@@ -159,7 +159,7 @@ class List(ListWidget):
 
 
     def action_remove_locks(self, items, checked):
     def action_remove_locks(self, items, checked):
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 user.default_avatar(self.request.settings)
                 user.default_avatar(self.request.settings)
                 user.avatar_ban = False
                 user.avatar_ban = False
                 user.signature_ban = False
                 user.signature_ban = False
@@ -170,13 +170,13 @@ class List(ListWidget):
     def action_reset(self, items, checked):
     def action_reset(self, items, checked):
         # First loop - check for errors
         # First loop - check for errors
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 if user.is_protected() and not self.request.user.is_god():
                 if user.is_protected() and not self.request.user.is_god():
                     return Message(_('You cannot reset protected members passwords.'), 'error'), reverse('admin_users')
                     return Message(_('You cannot reset protected members passwords.'), 'error'), reverse('admin_users')
 
 
         # Second loop - reset passwords
         # Second loop - reset passwords
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 new_password = random_string(8)
                 new_password = random_string(8)
                 user.set_password(new_password)
                 user.set_password(new_password)
                 user.save(force_update=True)
                 user.save(force_update=True)
@@ -193,14 +193,14 @@ class List(ListWidget):
 
 
     def action_delete_content(self, items, checked):
     def action_delete_content(self, items, checked):
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 if user.pk == self.request.user.id:
                 if user.pk == self.request.user.id:
                     return Message(_('You cannot delete yourself.'), 'error'), reverse('admin_users')
                     return Message(_('You cannot delete yourself.'), 'error'), reverse('admin_users')
                 if user.is_protected():
                 if user.is_protected():
                     return Message(_('You cannot delete protected members.'), 'error'), reverse('admin_users')
                     return Message(_('You cannot delete protected members.'), 'error'), reverse('admin_users')
 
 
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 user.delete_content()
                 user.delete_content()
                 user.delete()
                 user.delete()
 
 
@@ -213,14 +213,14 @@ class List(ListWidget):
 
 
     def action_delete(self, items, checked):
     def action_delete(self, items, checked):
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 if user.pk == self.request.user.id:
                 if user.pk == self.request.user.id:
                     return Message(_('You cannot delete yourself.'), 'error'), reverse('admin_users')
                     return Message(_('You cannot delete yourself.'), 'error'), reverse('admin_users')
                 if user.is_protected():
                 if user.is_protected():
                     return Message(_('You cannot delete protected members.'), 'error'), reverse('admin_users')
                     return Message(_('You cannot delete protected members.'), 'error'), reverse('admin_users')
 
 
         for user in items:
         for user in items:
-            if unicode(user.pk) in checked:
+            if user.pk in checked:
                 user.delete()
                 user.delete()
 
 
         User.objects.resync_monitor(self.request.monitor)
         User.objects.resync_monitor(self.request.monitor)
@@ -338,14 +338,10 @@ class Edit(FormWidget):
         # Update user roles
         # Update user roles
         if self.request.user.is_god():
         if self.request.user.is_god():
             target.roles.clear()
             target.roles.clear()
-            for role in form.cleaned_data['roles']:
-                target.roles.add(role)
         else:
         else:
-            for role in target.roles.all():
-                if not role.protected:
-                    target.roles.remove(role)
-            for role in form.cleaned_data['roles']:
-                target.roles.add(role)
+            target.roles.remove(*target.roles.filter(protected=False))
+        for role in form.cleaned_data['roles']:
+            target.roles.add(role)
 
 
         target.make_acl_key(True)
         target.make_acl_key(True)
         target.save(force_update=True)
         target.save(force_update=True)
@@ -369,6 +365,6 @@ class Delete(ButtonWidget):
 
 
 
 
 def inactive(request):
 def inactive(request):
-    token = 'list_filter.users.User'
+    token = 'list_filter_users.User'
     request.session[token] = {'activation': [1, 2, 3]}
     request.session[token] = {'activation': [1, 2, 3]}
     return redirect(reverse('admin_users'))
     return redirect(reverse('admin_users'))

+ 10 - 41
misago/apps/admin/widgets.py

@@ -1,6 +1,7 @@
 from django import forms
 from django import forms
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
+from django.http import Http404
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
@@ -8,6 +9,7 @@ from jinja2 import TemplateNotFound
 import math
 import math
 from misago.forms import Form, FormLayout, FormFields, FormFieldsets
 from misago.forms import Form, FormLayout, FormFields, FormFieldsets
 from misago.messages import Message
 from misago.messages import Message
+from misago.utils.pagination import make_pagination
 
 
 """
 """
 Class widgets
 Class widgets
@@ -207,46 +209,10 @@ class ListWidget(BaseWidget):
         return reverse(self.admin.get_action_attr(self.id, 'route'), kwargs={'page': page})
         return reverse(self.admin.get_action_attr(self.id, 'route'), kwargs={'page': page})
 
 
     def get_pagination(self, total, page):
     def get_pagination(self, total, page):
-        """
-        Return list pagination.
-        A list with three values:
-        - Offset for ORM slicing
-        - Length of slice
-        - no. of prev page (or -1 for first page)
-        - no. of next page (or -1 for last page)
-        - Current page
-        - Pages total
-        """
         if not self.pagination or total < 0:
         if not self.pagination or total < 0:
             # Dont do anything if we are not paging
             # Dont do anything if we are not paging
             return None
             return None
-
-        # Set basic pagination, use either Session cache or new page value
-        pagination = {'start': 0, 'stop': 0, 'prev':-1, 'next':-1}
-        if self.request.session.get(self.get_token('pagination')):
-            pagination['start'] = self.request.session.get(self.get_token('pagination'))
-        page = int(page)
-        if page > 0:
-            pagination['start'] = (page - 1) * self.pagination
-
-        # Set page and total stat
-        pagination['page'] = int(pagination['start'] / self.pagination) + 1
-        pagination['total'] = int(math.ceil(total / float(self.pagination)))
-
-        # Fix too large offset
-        if pagination['start'] > total:
-            pagination['start'] = 0
-
-        # Allow prev/next?
-        if total > self.pagination:
-            if pagination['page'] > 1:
-                pagination['prev'] = pagination['page'] - 1
-            if pagination['page'] < pagination['total']:
-                pagination['next'] = pagination['page'] + 1
-
-        # Set stop offset
-        pagination['stop'] = pagination['start'] + self.pagination
-        return pagination
+        return make_pagination(page, total, self.pagination)
 
 
     def get_items(self):
     def get_items(self):
         if self.request.session.get(self.get_token('filter')):
         if self.request.session.get(self.get_token('filter')):
@@ -271,7 +237,10 @@ class ListWidget(BaseWidget):
 
 
         # Set sorting and paginating
         # Set sorting and paginating
         sorting_method = self.get_sorting()
         sorting_method = self.get_sorting()
-        paginating_method = self.get_pagination(items_total, page)
+        try:
+            paginating_method = self.get_pagination(items_total, page)
+        except Http404:
+            return redirect(self.get_url())
 
 
         # List items
         # List items
         items = self.get_items()
         items = self.get_items()
@@ -360,7 +329,7 @@ class ListWidget(BaseWidget):
                 if list_form.is_valid():
                 if list_form.is_valid():
                     try:
                     try:
                         form_action = getattr(self, 'action_' + list_form.cleaned_data['list_action'])
                         form_action = getattr(self, 'action_' + list_form.cleaned_data['list_action'])
-                        message, redirect_url = form_action(items, list_form.cleaned_data['list_items'])
+                        message, redirect_url = form_action(items, [int(x) for x in list_form.cleaned_data['list_items']])
                         if redirect_url:
                         if redirect_url:
                             request.messages.set_flash(message, message.type, self.admin.id)
                             request.messages.set_flash(message, message.type, self.admin.id)
                             return redirect(redirect_url)
                             return redirect(redirect_url)
@@ -426,8 +395,8 @@ class FormWidget(BaseWidget):
 
 
     def get_form_instance(self, form, target, initial, post=False):
     def get_form_instance(self, form, target, initial, post=False):
         if post:
         if post:
-            return form(self.request.POST, request=self.request, initial=self.get_initial_data(target))
-        return form(request=self.request, initial=self.get_initial_data(target))
+            return form(self.request.POST, request=self.request, initial=initial)
+        return form(request=self.request, initial=initial)
 
 
     def get_layout(self, form, model):
     def get_layout(self, form, model):
         if self.layout:
         if self.layout:

+ 9 - 4
misago/apps/alerts.py

@@ -1,24 +1,29 @@
+from copy import deepcopy
+from datetime import timedelta
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils import timezone
 from django.utils import timezone
+from django.utils.timezone import localtime
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.decorators import block_guest
 from misago.decorators import block_guest
 
 
 @block_guest
 @block_guest
 def alerts(request):
 def alerts(request):
-    now = timezone.now()
+    now = localtime(timezone.now())
+    yesterday = now - timedelta(days=1)
     alerts = {}
     alerts = {}
     if not request.user.alerts_date:
     if not request.user.alerts_date:
         request.user.alerts_date = request.user.join_date
         request.user.alerts_date = request.user.join_date
 
 
     for alert in request.user.alert_set.order_by('-id'):
     for alert in request.user.alert_set.order_by('-id'):
         alert.new = alert.date > request.user.alerts_date
         alert.new = alert.date > request.user.alerts_date
-        diff = now - alert.date
-        if diff.days <= 0:
+        alert_date = localtime(deepcopy(alert.date))
+        diff = now - alert_date
+        if now.date() == alert_date.date():
             try:
             try:
                 alerts['today'].append(alert)
                 alerts['today'].append(alert)
             except KeyError:
             except KeyError:
                 alerts['today'] = [alert]
                 alerts['today'] = [alert]
-        elif diff.days <= 1:
+        elif yesterday.date() == alert_date.date():
             try:
             try:
                 alerts['yesterday'].append(alert)
                 alerts['yesterday'].append(alert)
             except KeyError:
             except KeyError:

+ 35 - 15
misago/apps/index.py

@@ -12,12 +12,15 @@ def index(request):
         popular_threads = cache.get('thread_ranking_%s' % request.user.make_acl_key(), 'nada')
         popular_threads = cache.get('thread_ranking_%s' % request.user.make_acl_key(), 'nada')
         if popular_threads == 'nada':
         if popular_threads == 'nada':
             popular_threads = []
             popular_threads = []
-            for thread in Thread.objects.filter(moderated=False).filter(deleted=False).filter(forum__in=Forum.objects.readable_forums(request.acl)).prefetch_related('forum').order_by('-score')[:request.settings['thread_ranking_size']]:
+            for thread in Thread.objects.filter(moderated=False).filter(deleted=False).filter(forum__in=Forum.objects.readable_forums(request.acl)).prefetch_related('forum').order_by('-score', '-last')[:request.settings['thread_ranking_size']]:
                 thread.forum_name = thread.forum.name
                 thread.forum_name = thread.forum.name
                 thread.forum_slug = thread.forum.slug
                 thread.forum_slug = thread.forum.slug
                 popular_threads.append(thread)
                 popular_threads.append(thread)
             cache.set('thread_ranking_%s' % request.user.make_acl_key(), popular_threads, 60 * request.settings['thread_ranking_refresh'])
             cache.set('thread_ranking_%s' % request.user.make_acl_key(), popular_threads, 60 * request.settings['thread_ranking_refresh'])
 
 
+    # Users online
+    users_online = request.onlines.stats(request)
+
     # Ranks online
     # Ranks online
     ranks_list = cache.get('ranks_online', 'nada')
     ranks_list = cache.get('ranks_online', 'nada')
     if ranks_list == 'nada':
     if ranks_list == 'nada':
@@ -25,31 +28,39 @@ def index(request):
         ranks_list = []
         ranks_list = []
         users_list = []
         users_list = []
         for rank in Rank.objects.filter(on_index=True).order_by('order'):
         for rank in Rank.objects.filter(on_index=True).order_by('order'):
-            rank_entry = {'id':rank.id, 'name': rank.name, 'style': rank.style, 'title': rank.title, 'online': []}
+            rank_entry = {
+                          'id':rank.id,
+                          'name': rank.name,
+                          'slug': rank.slug if rank.as_tab else '',
+                          'style': rank.style,
+                          'title': rank.title,
+                          'online': [],
+                          'pks': [],
+                         }
             ranks_list.append(rank_entry)
             ranks_list.append(rank_entry)
             ranks_dict[rank.pk] = rank_entry
             ranks_dict[rank.pk] = rank_entry
         if ranks_dict:
         if ranks_dict:
-            for session in Session.objects.select_related('user').filter(rank__in=ranks_dict.keys()).filter(last__gte=timezone.now() - timedelta(minutes=10)).filter(user__isnull=False):
+            for session in Session.objects.select_related('user').filter(rank__in=ranks_dict.keys()).filter(last__gte=timezone.now() - timedelta(seconds=request.settings['online_counting_frequency'])).filter(user__isnull=False):
                 if not session.user_id in users_list:
                 if not session.user_id in users_list:
                     ranks_dict[session.user.rank_id]['online'].append(session.user)
                     ranks_dict[session.user.rank_id]['online'].append(session.user)
+                    ranks_dict[session.user.rank_id]['pks'].append(session.user.pk)
                     users_list.append(session.user_id)
                     users_list.append(session.user_id)
             # Assert we are on list
             # Assert we are on list
             if (request.user.is_authenticated() and request.user.rank_id in ranks_dict.keys()
             if (request.user.is_authenticated() and request.user.rank_id in ranks_dict.keys()
-                and not request.user.id in users_list):
+                and not request.user.pk in users_list):
                     ranks_dict[request.user.rank_id]['online'].append(request.user)
                     ranks_dict[request.user.rank_id]['online'].append(request.user)
+                    ranks_dict[request.user.rank_id]['pks'].append(request.user.pk)
+                    users_list.append(request.user.pk)
+            cache.set('team_users_online', users_list, request.settings['online_counting_frequency'])
             del ranks_dict
             del ranks_dict
             del users_list
             del users_list
-        cache.set('ranks_online', ranks_list, 300)
-
-    # Users online
-    users_online = cache.get('users_online', 'nada')
-    if users_online == 'nada':
-        users_online = Session.objects.filter(matched=True).filter(crawler__isnull=True).filter(last__gte=timezone.now() - timedelta(seconds=300)).count()
-        cache.set('users_online', users_online, 300)
-    if not users_online and not request.user.is_crawler():
-        # Cheatey trick to make sure we'll never display
-        # zero users online to human client
-        users_online = 1
+        cache.set('ranks_online', ranks_list, request.settings['online_counting_frequency'])
+    elif request.user.is_authenticated():
+        for rank in ranks_list:
+            if rank['id'] == request.user.rank_id and not request.user.pk in rank['pks']:
+                rank['online'].append(request.user)
+                rank['pks'].append(request.user.pk)
+                break
 
 
     # Load reads tracker and build forums list
     # Load reads tracker and build forums list
     reads_tracker = ForumsTracker(request.user)
     reads_tracker = ForumsTracker(request.user)
@@ -65,5 +76,14 @@ def index(request):
                                              'ranks_online': ranks_list,
                                              'ranks_online': ranks_list,
                                              'users_online': users_online,
                                              'users_online': users_online,
                                              'popular_threads': popular_threads,
                                              'popular_threads': popular_threads,
+                                             'hook_above_forum_home': u'',
+                                             'hook_below_forum_home': u'',
+                                             'hook_above_home_forums_list': u'',
+                                             'hook_below_home_forums_list': u'',
+                                             'hook_above_home_sidepanel': u'',
+                                             'hook_after_home_sidepanel_ranks_online': u'',
+                                             'hook_after_home_sidepanel_popular_threads': u'',
+                                             'hook_after_home_sidepanel_forum_stats': u'',
+                                             'hook_below_home_sidepanel': u'',
                                              },
                                              },
                                             context_instance=RequestContext(request));
                                             context_instance=RequestContext(request));

+ 7 - 1
misago/apps/newthreads.py

@@ -1,4 +1,7 @@
 from datetime import timedelta
 from datetime import timedelta
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils import timezone
 from django.utils import timezone
 from misago.models import Forum, Thread
 from misago.models import Forum, Thread
@@ -7,7 +10,10 @@ from misago.utils.pagination import make_pagination
 def new_threads(request, page=0):
 def new_threads(request, page=0):
     queryset = Thread.objects.filter(forum_id__in=Forum.objects.readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).filter(start__gte=(timezone.now() - timedelta(days=2)))
     queryset = Thread.objects.filter(forum_id__in=Forum.objects.readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).filter(start__gte=(timezone.now() - timedelta(days=2)))
     items_total = queryset.count();
     items_total = queryset.count();
-    pagination = make_pagination(page, items_total, 30)
+    try:
+        pagination = make_pagination(page, items_total, 30)
+    except Http404:
+        return redirect(reverse('new_threads'))
 
 
     queryset = queryset.order_by('-start').prefetch_related('forum')[pagination['start']:pagination['stop']];
     queryset = queryset.order_by('-start').prefetch_related('forum')[pagination['start']:pagination['stop']];
     if request.settings['avatars_on_threads_list']:
     if request.settings['avatars_on_threads_list']:

+ 8 - 2
misago/apps/popularthreads.py

@@ -1,4 +1,7 @@
 from datetime import timedelta
 from datetime import timedelta
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils import timezone
 from django.utils import timezone
 from misago.models import Forum, Thread
 from misago.models import Forum, Thread
@@ -7,9 +10,12 @@ from misago.utils.pagination import make_pagination
 def popular_threads(request, page=0):
 def popular_threads(request, page=0):
     queryset = Thread.objects.filter(forum_id__in=Forum.objects.readable_forums(request.acl)).filter(deleted=False).filter(moderated=False)
     queryset = Thread.objects.filter(forum_id__in=Forum.objects.readable_forums(request.acl)).filter(deleted=False).filter(moderated=False)
     items_total = queryset.count();
     items_total = queryset.count();
-    pagination = make_pagination(page, items_total, 30)
+    try:
+        pagination = make_pagination(page, items_total, 30)
+    except Http404:
+        return redirect(reverse('popular_threads'))
 
 
-    queryset = queryset.order_by('-score').prefetch_related('forum')[pagination['start']:pagination['stop']];
+    queryset = queryset.order_by('-score', '-last').prefetch_related('forum')[pagination['start']:pagination['stop']];
     if request.settings['avatars_on_threads_list']:
     if request.settings['avatars_on_threads_list']:
         queryset = queryset.prefetch_related('start_poster', 'last_poster')
         queryset = queryset.prefetch_related('start_poster', 'last_poster')
 
 

+ 20 - 0
misago/apps/privatethreads/delete.py

@@ -9,9 +9,29 @@ class HideThreadView(HideThreadBaseView, TypeMixin):
     pass
     pass
 
 
 
 
+class ShowThreadView(ShowThreadBaseView, TypeMixin):
+    pass
+
+
 class DeleteReplyView(DeleteReplyBaseView, TypeMixin):
 class DeleteReplyView(DeleteReplyBaseView, TypeMixin):
     pass
     pass
 
 
 
 
 class HideReplyView(HideReplyBaseView, TypeMixin):
 class HideReplyView(HideReplyBaseView, TypeMixin):
+    pass
+
+
+class ShowReplyView(ShowReplyBaseView, TypeMixin):
+    pass
+
+
+class DeleteCheckpointView(DeleteCheckpointBaseView, TypeMixin):
+    pass
+
+
+class HideCheckpointView(HideCheckpointBaseView, TypeMixin):
+    pass
+
+
+class ShowCheckpointView(ShowCheckpointBaseView, TypeMixin):
     pass
     pass

+ 1 - 1
misago/apps/privatethreads/forms.py

@@ -28,7 +28,7 @@ class InviteUsersMixin(object):
                     if not user.acl(self.request).private_threads.can_participate():
                     if not user.acl(self.request).private_threads.can_participate():
                         raise forms.ValidationError(_('%(user)s cannot participate in private threads.') % {'user': user.username})
                         raise forms.ValidationError(_('%(user)s cannot participate in private threads.') % {'user': user.username})
                     if (not self.request.acl.private_threads.can_invite_ignoring() and
                     if (not self.request.acl.private_threads.can_invite_ignoring() and
-                        not user.allow_pd_invite(self.request.user)):
+                            not user.allow_pd_invite(self.request.user)):
                         raise forms.ValidationError(_('%(user)s restricts who can invite him to private threads.') % {'user': user.username})
                         raise forms.ValidationError(_('%(user)s restricts who can invite him to private threads.') % {'user': user.username})
                     self.invite_users.append(user)
                     self.invite_users.append(user)
                 except User.DoesNotExist:
                 except User.DoesNotExist:

+ 11 - 11
misago/apps/privatethreads/jumps.py

@@ -37,11 +37,15 @@ class UnwatchEmailThreadView(UnwatchEmailThreadBaseView, TypeMixin):
     pass
     pass
 
 
 
 
-class UpvotePostView(UpvotePostBaseView, TypeMixin):
+class FirstReportedView(FirstReportedBaseView, TypeMixin):
     pass
     pass
 
 
 
 
-class DownvotePostView(DownvotePostBaseView, TypeMixin):
+class ReportPostView(ReportPostBaseView, TypeMixin):
+    pass
+
+
+class ShowPostReportView(ShowPostReportBaseView, TypeMixin):
     pass
     pass
 
 
 
 
@@ -59,7 +63,7 @@ class InviteUserView(JumpView, TypeMixin):
                     self.request.messages.set_flash(Message(_('You cannot add yourself to this thread.')), 'error', 'threads')
                     self.request.messages.set_flash(Message(_('You cannot add yourself to this thread.')), 'error', 'threads')
                 else:
                 else:
                     self.request.messages.set_flash(Message(_('%(user)s is already participating in this thread.') % {'user': user.username}), 'info', 'threads')
                     self.request.messages.set_flash(Message(_('%(user)s is already participating in this thread.') % {'user': user.username}), 'info', 'threads')
-            if not acl.private_threads.can_participate():
+            elif not acl.private_threads.can_participate():
                     self.request.messages.set_flash(Message(_('%(user)s cannot participate in private threads.') % {'user': user.username}), 'info', 'threads')
                     self.request.messages.set_flash(Message(_('%(user)s cannot participate in private threads.') % {'user': user.username}), 'info', 'threads')
             elif (not self.request.acl.private_threads.can_invite_ignoring() and
             elif (not self.request.acl.private_threads.can_invite_ignoring() and
                     not user.allow_pd_invite(self.request.user)):
                     not user.allow_pd_invite(self.request.user)):
@@ -69,13 +73,11 @@ class InviteUserView(JumpView, TypeMixin):
                 user.sync_pds = True
                 user.sync_pds = True
                 user.save(force_update=True)
                 user.save(force_update=True)
                 user.email_user(self.request, 'private_thread_invite', _("You've been invited to private thread \"%(thread)s\" by %(user)s") % {'thread': self.thread.name, 'user': self.request.user.username}, {'author': self.request.user, 'thread': self.thread})
                 user.email_user(self.request, 'private_thread_invite', _("You've been invited to private thread \"%(thread)s\" by %(user)s") % {'thread': self.thread.name, 'user': self.request.user.username}, {'author': self.request.user, 'thread': self.thread})
-                self.thread.last_post.set_checkpoint(self.request, 'invited', user)
-                self.thread.last_post.save(force_update=True)
+                self.thread.set_checkpoint(self.request, 'invited', user)
                 self.request.messages.set_flash(Message(_('%(user)s has been added to this thread.') % {'user': user.username}), 'success', 'threads')
                 self.request.messages.set_flash(Message(_('%(user)s has been added to this thread.') % {'user': user.username}), 'success', 'threads')
-            return self.retreat_redirect()
         except User.DoesNotExist:
         except User.DoesNotExist:
             self.request.messages.set_flash(Message(_('User with requested username could not be found.')), 'error', 'threads')
             self.request.messages.set_flash(Message(_('User with requested username could not be found.')), 'error', 'threads')
-            return self.retreat_redirect()
+        return self.retreat_redirect()
 
 
 
 
 class RemoveUserView(JumpView, TypeMixin):
 class RemoveUserView(JumpView, TypeMixin):
@@ -99,15 +101,13 @@ class RemoveUserView(JumpView, TypeMixin):
                 return self.threads_list_redirect()
                 return self.threads_list_redirect()
             # Nope, see if we removed ourselves
             # Nope, see if we removed ourselves
             if user.pk == self.request.user.pk:
             if user.pk == self.request.user.pk:
-                self.thread.last_post.set_checkpoint(self.request, 'left')
-                self.thread.last_post.save(force_update=True)
+                self.thread.set_checkpoint(self.request, 'left')
                 self.request.messages.set_flash(Message(_('You have left the "%(thread)s" thread.') % {'thread': self.thread.name}), 'info', 'threads')
                 self.request.messages.set_flash(Message(_('You have left the "%(thread)s" thread.') % {'thread': self.thread.name}), 'info', 'threads')
                 return self.threads_list_redirect()
                 return self.threads_list_redirect()
             # Nope, somebody else removed user
             # Nope, somebody else removed user
             user.sync_pds = True
             user.sync_pds = True
             user.save(force_update=True)
             user.save(force_update=True)
-            self.thread.last_post.set_checkpoint(self.request, 'removed', user)
-            self.thread.last_post.save(force_update=True)
+            self.thread.set_checkpoint(self.request, 'removed', user)
             self.request.messages.set_flash(Message(_('Selected participant was removed from thread.')), 'info', 'threads')
             self.request.messages.set_flash(Message(_('Selected participant was removed from thread.')), 'info', 'threads')
             return self.retreat_redirect()
             return self.retreat_redirect()
         except User.DoesNotExist:
         except User.DoesNotExist:

+ 11 - 2
misago/apps/privatethreads/list.py

@@ -1,4 +1,5 @@
 from itertools import chain
 from itertools import chain
+from django.http import Http404
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
 from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
 from misago.models import Forum, Thread
 from misago.models import Forum, Thread
@@ -11,7 +12,12 @@ class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
         self.forum = Forum.objects.get(special='private_threads')
         self.forum = Forum.objects.get(special='private_threads')
 
 
     def threads_queryset(self):
     def threads_queryset(self):
-        return self.forum.thread_set.filter(participants__id=self.request.user.pk).order_by('-last')
+        qs_threads = self.forum.thread_set.filter(participants__id=self.request.user.pk).order_by('-last')
+        if self.request.acl.private_threads.is_mod():
+            qs_reported = self.forum.thread_set.filter(replies_reported__gt=0)
+            qs_threads = qs_threads | qs_reported
+            qs_threads = qs_threads.distinct()
+        return qs_threads
 
 
     def fetch_threads(self):
     def fetch_threads(self):
         qs_threads = self.threads_queryset()
         qs_threads = self.threads_queryset()
@@ -21,7 +27,10 @@ class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
             qs_threads = qs_threads.prefetch_related('start_poster', 'last_poster')
             qs_threads = qs_threads.prefetch_related('start_poster', 'last_poster')
 
 
         self.count = qs_threads.count()
         self.count = qs_threads.count()
-        self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, self.request.settings.threads_per_page)
+        try:
+            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, self.request.settings.threads_per_page)
+        except Http404:
+            return self.threads_list_redirect()
 
 
         tracker_forum = ThreadsTracker(self.request, self.forum)
         tracker_forum = ThreadsTracker(self.request, self.forum)
         for thread in qs_threads[self.pagination['start']:self.pagination['stop']]:
         for thread in qs_threads[self.pagination['start']:self.pagination['stop']]:

+ 12 - 10
misago/apps/privatethreads/mixins.py

@@ -12,31 +12,33 @@ class TypeMixin(object):
     def check_permissions(self):
     def check_permissions(self):
         try:
         try:
             if self.thread.pk:
             if self.thread.pk:
-                if not self.request.user in self.thread.participants.all():
+                if not ((self.thread.replies_reported > 0 and self.request.acl.private_threads.is_mod())
+                        or (self.request.user in self.thread.participants.all())):
                     raise ACLError404()
                     raise ACLError404()
         except AttributeError:
         except AttributeError:
             pass
             pass
 
 
     def invite_users(self, users):
     def invite_users(self, users):
-        sync_last_post = False
         for user in users:
         for user in users:
             if not user in self.thread.participants.all():
             if not user in self.thread.participants.all():
                 self.thread.participants.add(user)
                 self.thread.participants.add(user)
                 user.email_user(self.request, 'private_thread_invite', _("You've been invited to private thread \"%(thread)s\" by %(user)s") % {'thread': self.thread.name, 'user': self.request.user.username}, {'author': self.request.user, 'thread': self.thread})
                 user.email_user(self.request, 'private_thread_invite', _("You've been invited to private thread \"%(thread)s\" by %(user)s") % {'thread': self.thread.name, 'user': self.request.user.username}, {'author': self.request.user, 'thread': self.thread})
                 if self.action == 'new_reply':
                 if self.action == 'new_reply':
-                    self.thread.last_post.set_checkpoint(self.request, 'invited', user)
-        if sync_last_post:
-            self.thread.last_post.save(force_update=True)
+                    self.thread.set_checkpoint(self.request, 'invited', user)
 
 
     def force_stats_sync(self):
     def force_stats_sync(self):
         self.thread.participants.exclude(id=self.request.user.id).update(sync_pds=True)
         self.thread.participants.exclude(id=self.request.user.id).update(sync_pds=True)
                 
                 
     def whitelist_mentions(self):
     def whitelist_mentions(self):
-        participants = self.thread.participants.all()
-        mentioned = self.post.mentions.all()
-        for user in self.md.mentions:
-            if user not in participants and user not in mentioned:
-                self.post.mentioned.add(user)
+        try:
+            if self.md.mentions:
+                participants = self.thread.participants.all()
+                mentioned = self.post.mentions.all()
+                for user in self.md.mentions:
+                    if user not in participants and user not in mentioned:
+                        self.post.mentioned.add(user)
+        except AttributeError:
+            pass
 
 
     def threads_list_redirect(self):
     def threads_list_redirect(self):
         return redirect(reverse('private_threads'))
         return redirect(reverse('private_threads'))

+ 5 - 0
misago/apps/privatethreads/posting.py

@@ -58,6 +58,11 @@ class EditThreadView(EditThreadBaseView, TypeMixin):
 class NewReplyView(NewReplyBaseView, TypeMixin):
 class NewReplyView(NewReplyBaseView, TypeMixin):
     form_type = NewReplyForm
     form_type = NewReplyForm
 
 
+    def set_context(self):
+        super(NewReplyView, self).set_context()
+        if not (self.request.acl.private_threads.is_mod() or len(self.thread.participants) < 2):
+            raise ACLError403(_("This thread needs to have more than one participant to allow new replies."))
+
     def after_form(self, form):
     def after_form(self, form):
         try:
         try:
             self.invite_users(form.invite_users)
             self.invite_users(form.invite_users)

+ 27 - 0
misago/apps/privatethreads/search.py

@@ -0,0 +1,27 @@
+from misago.decorators import block_crawlers
+from misago.models import Post
+from misago.apps.errors import error404
+from misago.apps.search.views import do_search, results
+
+def allow_search(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        if not (request.acl.private_threads.can_participate()
+                and request.settings['enable_private_threads']):
+            return error404()
+        return f(*args, **kwargs)
+    return decorator
+
+
+@block_crawlers
+@allow_search
+def search_private_threads(request):
+    threads = [t.pk for t in request.user.private_thread_set.all()]
+    queryset = Post.objects.filter(thread_id__in=threads)
+    return do_search(request, queryset, 'private_threads')
+
+
+@block_crawlers
+@allow_search
+def show_private_threads_results(request, page=0):
+    return results(request, page, 'private_threads')

+ 6 - 6
misago/apps/privatethreads/thread.py

@@ -17,10 +17,10 @@ class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
                 actions.append(('unprotect', _('Remove posts protection')))
                 actions.append(('unprotect', _('Remove posts protection')))
             if acl['can_delete_posts']:
             if acl['can_delete_posts']:
                 if self.thread.replies_deleted > 0:
                 if self.thread.replies_deleted > 0:
-                    actions.append(('undelete', _('Undelete posts')))
-                actions.append(('soft', _('Soft delete posts')))
+                    actions.append(('undelete', _('Restore posts')))
+                actions.append(('soft', _('Hide posts')))
             if acl['can_delete_posts'] == 2:
             if acl['can_delete_posts'] == 2:
-                actions.append(('hard', _('Hard delete posts')))
+                actions.append(('hard', _('Delete posts')))
         except KeyError:
         except KeyError:
             pass
             pass
         return actions
         return actions
@@ -36,11 +36,11 @@ class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
                     actions.append(('close', _('Close this thread')))
                     actions.append(('close', _('Close this thread')))
             if acl['can_delete_threads']:
             if acl['can_delete_threads']:
                 if self.thread.deleted:
                 if self.thread.deleted:
-                    actions.append(('undelete', _('Undelete this thread')))
+                    actions.append(('undelete', _('Restore this thread')))
                 else:
                 else:
-                    actions.append(('soft', _('Soft delete this thread')))
+                    actions.append(('soft', _('Hide this thread')))
             if acl['can_delete_threads'] == 2:
             if acl['can_delete_threads'] == 2:
-                actions.append(('hard', _('Hard delete this thread')))
+                actions.append(('hard', _('Delete this thread')))
         except KeyError:
         except KeyError:
             pass
             pass
         return actions
         return actions

+ 18 - 6
misago/apps/privatethreads/urls.py

@@ -2,7 +2,7 @@ from django.conf.urls import patterns, url
 
 
 urlpatterns = patterns('misago.apps.privatethreads',
 urlpatterns = patterns('misago.apps.privatethreads',
     url(r'^$', 'list.ThreadsListView', name="private_threads"),
     url(r'^$', 'list.ThreadsListView', name="private_threads"),
-    url(r'^(?P<page>\d+)/$', 'list.ThreadsListView', name="private_threads"),
+    url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'list.ThreadsListView', name="private_threads"),
     url(r'^start/$', 'posting.NewThreadView', name="private_thread_start"),
     url(r'^start/$', 'posting.NewThreadView', name="private_thread_start"),
     url(r'^start/(?P<username>\w+)-(?P<user>\d+)/$', 'posting.NewThreadView', name="private_thread_start_with"),
     url(r'^start/(?P<username>\w+)-(?P<user>\d+)/$', 'posting.NewThreadView', name="private_thread_start_with"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="private_thread_edit"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="private_thread_edit"),
@@ -10,23 +10,35 @@ urlpatterns = patterns('misago.apps.privatethreads',
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="private_thread_reply"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="private_thread_reply"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', 'posting.EditReplyView', name="private_post_edit"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', 'posting.EditReplyView', name="private_post_edit"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'thread.ThreadView', name="private_thread"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'thread.ThreadView', name="private_thread"),
-    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>\d+)/$', 'thread.ThreadView', name="private_thread"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>[1-9]([0-9]+)?)/$', 'thread.ThreadView', name="private_thread"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'jumps.LastReplyView', name="private_thread_last"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'jumps.LastReplyView', name="private_thread_last"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', 'jumps.FindReplyView', name="private_thread_find"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', 'jumps.FindReplyView', name="private_thread_find"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'jumps.NewReplyView', name="private_thread_new"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'jumps.NewReplyView', name="private_thread_new"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/reported/$', 'jumps.FirstReportedView', name="private_thread_reported"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/show-hidden/$', 'jumps.ShowHiddenRepliesView', name="private_thread_show_hidden"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/show-hidden/$', 'jumps.ShowHiddenRepliesView', name="private_thread_show_hidden"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/report/$', 'jumps.ReportPostView', name="private_post_report"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show-report/$', 'jumps.ShowPostReportView', name="private_post_report_show"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/$', 'jumps.WatchThreadView', name="private_thread_watch"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/$', 'jumps.WatchThreadView', name="private_thread_watch"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$', 'jumps.WatchEmailThreadView', name="private_thread_watch_email"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$', 'jumps.WatchEmailThreadView', name="private_thread_watch_email"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$', 'jumps.UnwatchThreadView', name="private_thread_unwatch"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$', 'jumps.UnwatchThreadView', name="private_thread_unwatch"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="private_thread_unwatch_email"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="private_thread_unwatch_email"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/invite/$', 'jumps.InviteUserView', name="private_thread_invite_user"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/invite/$', 'jumps.InviteUserView', name="private_thread_invite_user"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/remove/$', 'jumps.RemoveUserView', name="private_thread_remove_user"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/remove/$', 'jumps.RemoveUserView', name="private_thread_remove_user"),
-    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="private_thread_delete", kwargs={'mode': 'delete_thread'}),
-    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="private_thread_hide", kwargs={'mode': 'hide_thread'}),
-    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="private_post_delete", kwargs={'mode': 'delete_post'}),
-    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="private_post_hide", kwargs={'mode': 'hide_post'}),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="private_thread_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="private_thread_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/show/$', 'delete.ShowThreadView', name="private_thread_show"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="private_post_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="private_post_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show/$', 'delete.ShowReplyView', name="private_post_show"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/delete/$', 'delete.DeleteCheckpointView', name="private_post_checkpoint_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/hide/$', 'delete.HideCheckpointView', name="private_post_checkpoint_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/show/$', 'delete.ShowCheckpointView', name="private_post_checkpoint_show"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="private_post_info"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="private_post_info"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="private_thread_changelog"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="private_thread_changelog"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="private_thread_changelog_diff"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="private_thread_changelog_diff"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="private_thread_changelog_revert"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="private_thread_changelog_revert"),
+    # Extra search routes
+    url(r'^search/$', 'search.search_private_threads', name="private_threads_search"),
+    url(r'^search/results/$', 'search.show_private_threads_results', name="private_threads_results"),
+    url(r'^search/results/(?P<page>[1-9]([0-9]+)?)/$', 'search.show_private_threads_results', name="private_threads_results"),
 )
 )

+ 5 - 1
misago/apps/profiles/decorators.py

@@ -1,4 +1,5 @@
 from functools import wraps
 from functools import wraps
+from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from misago.apps.errors import error404
 from misago.apps.errors import error404
@@ -12,7 +13,10 @@ def profile_view(fallback='user'):
             user_pk = int(user)
             user_pk = int(user)
             user_slug = username
             user_slug = username
             try:
             try:
-                user = User.objects.get(pk=user_pk)
+                user = User.objects
+                if settings.PROFILE_EXTENSIONS_PRELOAD:
+                    user = user.select_related(*settings.PROFILE_EXTENSIONS_PRELOAD)
+                user = user.get(pk=user_pk)
                 if user.username_slug != user_slug:
                 if user.username_slug != user_slug:
                     # Force crawlers to take notice of updated username
                     # Force crawlers to take notice of updated username
                     return redirect(reverse(fallback, args=(user.username_slug, user.pk)), permanent=True)
                     return redirect(reverse(fallback, args=(user.username_slug, user.pk)), permanent=True)

+ 2 - 2
misago/apps/profiles/followers/urls.py

@@ -6,11 +6,11 @@ def register_profile_urls(first=False):
         urlpatterns += patterns('misago.apps.profiles.followers.views',
         urlpatterns += patterns('misago.apps.profiles.followers.views',
             url(r'^$', 'followers', name="user"),
             url(r'^$', 'followers', name="user"),
             url(r'^$', 'followers', name="user_followers"),
             url(r'^$', 'followers', name="user_followers"),
-            url(r'^(?P<page>\d+)/$', 'followers', name="user_followers"),
+            url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'followers', name="user_followers"),
         )
         )
     else:
     else:
         urlpatterns += patterns('misago.apps.profiles.followers.views',
         urlpatterns += patterns('misago.apps.profiles.followers.views',
             url(r'^followers/$', 'followers', name="user_followers"),
             url(r'^followers/$', 'followers', name="user_followers"),
-            url(r'^followers/(?P<page>\d+)/$', 'followers', name="user_followers"),
+            url(r'^followers/(?P<page>[1-9]([0-9]+)?)/$', 'followers', name="user_followers"),
         )
         )
     return urlpatterns
     return urlpatterns

+ 7 - 1
misago/apps/profiles/followers/views.py

@@ -1,3 +1,6 @@
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
 from misago.apps.profiles.decorators import profile_view
 from misago.apps.profiles.decorators import profile_view
 from misago.apps.profiles.template import RequestContext
 from misago.apps.profiles.template import RequestContext
 from misago.utils.pagination import make_pagination
 from misago.utils.pagination import make_pagination
@@ -6,7 +9,10 @@ from misago.utils.pagination import make_pagination
 def followers(request, user, page=0):
 def followers(request, user, page=0):
     queryset = user.follows_set.order_by('username_slug')
     queryset = user.follows_set.order_by('username_slug')
     count = queryset.count()
     count = queryset.count()
-    pagination = make_pagination(page, count, 24)
+    try:
+        pagination = make_pagination(page, count, 24)
+    except Http404:
+        return redirect(reverse('user_followers', kwargs={'user': user.id, 'username': user.username_slug}))
     
     
     return request.theme.render_to_response('profiles/followers.html',
     return request.theme.render_to_response('profiles/followers.html',
                                             context_instance=RequestContext(request, {
                                             context_instance=RequestContext(request, {

+ 2 - 2
misago/apps/profiles/follows/urls.py

@@ -6,11 +6,11 @@ def register_profile_urls(first=False):
         urlpatterns += patterns('misago.apps.profiles.follows.views',
         urlpatterns += patterns('misago.apps.profiles.follows.views',
             url(r'^$', 'follows', name="user"),
             url(r'^$', 'follows', name="user"),
             url(r'^$', 'follows', name="user_follows"),
             url(r'^$', 'follows', name="user_follows"),
-            url(r'^(?P<page>\d+)/$', 'follows', name="user_follows"),
+            url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'follows', name="user_follows"),
         )
         )
     else:
     else:
         urlpatterns += patterns('misago.apps.profiles.follows.views',
         urlpatterns += patterns('misago.apps.profiles.follows.views',
             url(r'^follows/$', 'follows', name="user_follows"),
             url(r'^follows/$', 'follows', name="user_follows"),
-            url(r'^follows/(?P<page>\d+)/$', 'follows', name="user_follows"),
+            url(r'^follows/(?P<page>[1-9]([0-9]+)?)/$', 'follows', name="user_follows"),
         )
         )
     return urlpatterns
     return urlpatterns

+ 8 - 2
misago/apps/profiles/follows/views.py

@@ -1,13 +1,19 @@
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
 from misago.apps.profiles.decorators import profile_view
 from misago.apps.profiles.decorators import profile_view
 from misago.apps.profiles.template import RequestContext
 from misago.apps.profiles.template import RequestContext
 from misago.utils.pagination import make_pagination
 from misago.utils.pagination import make_pagination
 
 
 @profile_view('user_follows')
 @profile_view('user_follows')
 def follows(request, user, page=0):
 def follows(request, user, page=0):
-    
     queryset = user.follows.order_by('username_slug')
     queryset = user.follows.order_by('username_slug')
     count = queryset.count()
     count = queryset.count()
-    pagination = make_pagination(page, count, 24)
+    try:
+        pagination = make_pagination(page, count, 24)
+    except Http404:
+        return redirect(reverse('user_follows', kwargs={'user': user.id, 'username': user.username_slug}))
+
     return request.theme.render_to_response('profiles/follows.html',
     return request.theme.render_to_response('profiles/follows.html',
                                             context_instance=RequestContext(request, {
                                             context_instance=RequestContext(request, {
                                              'profile': user,
                                              'profile': user,

+ 2 - 2
misago/apps/profiles/posts/urls.py

@@ -6,11 +6,11 @@ def register_profile_urls(first=False):
         urlpatterns += patterns('misago.apps.profiles.posts.views',
         urlpatterns += patterns('misago.apps.profiles.posts.views',
             url(r'^$', 'posts', name="user"),
             url(r'^$', 'posts', name="user"),
             url(r'^$', 'posts', name="user_posts"),
             url(r'^$', 'posts', name="user_posts"),
-            url(r'^(?P<page>\d+)/$', 'posts', name="user_posts"),
+            url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'posts', name="user_posts"),
         )
         )
     else:
     else:
         urlpatterns += patterns('misago.apps.profiles.posts.views',
         urlpatterns += patterns('misago.apps.profiles.posts.views',
             url(r'^posts/$', 'posts', name="user_posts"),
             url(r'^posts/$', 'posts', name="user_posts"),
-            url(r'^posts/(?P<page>\d+)/$', 'posts', name="user_posts"),
+            url(r'^posts/(?P<page>[1-9]([0-9]+)?)/$', 'posts', name="user_posts"),
         )
         )
     return urlpatterns
     return urlpatterns

+ 8 - 1
misago/apps/profiles/posts/views.py

@@ -1,3 +1,6 @@
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
 from misago.apps.profiles.decorators import profile_view
 from misago.apps.profiles.decorators import profile_view
 from misago.apps.profiles.template import RequestContext
 from misago.apps.profiles.template import RequestContext
 from misago.models import Forum
 from misago.models import Forum
@@ -7,7 +10,11 @@ from misago.utils.pagination import make_pagination
 def posts(request, user, page=0):
 def posts(request, user, page=0):
     queryset = user.post_set.filter(forum_id__in=Forum.objects.readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).select_related('thread', 'forum').order_by('-id')
     queryset = user.post_set.filter(forum_id__in=Forum.objects.readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).select_related('thread', 'forum').order_by('-id')
     count = queryset.count()
     count = queryset.count()
-    pagination = make_pagination(page, count, 12)
+    try:
+        pagination = make_pagination(page, count, 12)
+    except Http404:
+        return redirect(reverse('user_posts', kwargs={'user': user.id, 'username': user.username_slug}))
+    
     return request.theme.render_to_response('profiles/posts.html',
     return request.theme.render_to_response('profiles/posts.html',
                                             context_instance=RequestContext(request, {
                                             context_instance=RequestContext(request, {
                                              'profile': user,
                                              'profile': user,

+ 0 - 1
misago/apps/profiles/template.py

@@ -38,7 +38,6 @@ def RequestContext(request, context=None):
 
 
     # Sync member
     # Sync member
     if context['profile'].sync_profile():
     if context['profile'].sync_profile():
-        print 'SYNCED!'
         context['profile'].save(force_update=True)
         context['profile'].save(force_update=True)
 
 
     context['tabs'] = []
     context['tabs'] = []

+ 2 - 2
misago/apps/profiles/threads/urls.py

@@ -6,11 +6,11 @@ def register_profile_urls(first=False):
         urlpatterns += patterns('misago.apps.profiles.threads.views',
         urlpatterns += patterns('misago.apps.profiles.threads.views',
             url(r'^$', 'threads', name="user"),
             url(r'^$', 'threads', name="user"),
             url(r'^$', 'threads', name="user_threads"),
             url(r'^$', 'threads', name="user_threads"),
-            url(r'^(?P<page>\d+)/$', 'threads', name="user_threads"),
+            url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'threads', name="user_threads"),
         )
         )
     else:
     else:
         urlpatterns += patterns('misago.apps.profiles.threads.views',
         urlpatterns += patterns('misago.apps.profiles.threads.views',
             url(r'^threads/$', 'threads', name="user_threads"),
             url(r'^threads/$', 'threads', name="user_threads"),
-            url(r'^threads/(?P<page>\d+)/$', 'threads', name="user_threads"),
+            url(r'^threads/(?P<page>[1-9]([0-9]+)?)/$', 'threads', name="user_threads"),
         )
         )
     return urlpatterns
     return urlpatterns

+ 7 - 1
misago/apps/profiles/threads/views.py

@@ -1,3 +1,6 @@
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
 from misago.apps.profiles.decorators import profile_view
 from misago.apps.profiles.decorators import profile_view
 from misago.apps.profiles.template import RequestContext
 from misago.apps.profiles.template import RequestContext
 from misago.models import Forum
 from misago.models import Forum
@@ -7,7 +10,10 @@ from misago.utils.pagination import make_pagination
 def threads(request, user, page=0):
 def threads(request, user, page=0):
     queryset = user.thread_set.filter(forum_id__in=Forum.objects.readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).select_related('start_post', 'forum').order_by('-id')
     queryset = user.thread_set.filter(forum_id__in=Forum.objects.readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).select_related('start_post', 'forum').order_by('-id')
     count = queryset.count()
     count = queryset.count()
-    pagination = make_pagination(page, count, 12)
+    try:
+        pagination = make_pagination(page, count, 12)
+    except Http404:
+        return redirect(reverse('user_threads', kwargs={'user': user.id, 'username': user.username_slug}))
     
     
     return request.theme.render_to_response('profiles/threads.html',
     return request.theme.render_to_response('profiles/threads.html',
                                             context_instance=RequestContext(request, {
                                             context_instance=RequestContext(request, {

+ 2 - 2
misago/apps/profiles/urls.py

@@ -4,7 +4,7 @@ from django.utils.importlib import import_module
 
 
 urlpatterns = patterns('misago.apps.profiles.views',
 urlpatterns = patterns('misago.apps.profiles.views',
     url(r'^$', 'list', name="users"),
     url(r'^$', 'list', name="users"),
-    url(r'^(?P<page>[0-9]+)/$', 'list', name="users"),
+    url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'list', name="users"),
 )
 )
 
 
 # Build extensions URLs
 # Build extensions URLs
@@ -21,5 +21,5 @@ for extension in settings.PROFILE_EXTENSIONS:
 
 
 urlpatterns += patterns('misago.apps.profiles.views',
 urlpatterns += patterns('misago.apps.profiles.views',
     url(r'^(?P<slug>(\w|-)+)/$', 'list', name="users"),
     url(r'^(?P<slug>(\w|-)+)/$', 'list', name="users"),
-    url(r'^(?P<slug>(\w|-)+)/(?P<page>[0-9]+)/$', 'list', name="users"),
+    url(r'^(?P<slug>(\w|-)+)/(?P<page>[1-9]([0-9]+)?)/$', 'list', name="users"),
 )
 )

+ 19 - 4
misago/apps/profiles/views.py

@@ -1,4 +1,6 @@
+from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
+from django.http import Http404
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from misago.apps.errors import error403, error404
 from misago.apps.errors import error403, error404
@@ -9,7 +11,7 @@ from misago.utils.strings import slugify
 from misago.utils.pagination import make_pagination
 from misago.utils.pagination import make_pagination
 from misago.apps.profiles.forms import QuickFindUserForm
 from misago.apps.profiles.forms import QuickFindUserForm
 
 
-def list(request, slug=None, page=1):
+def list(request, slug=None, page=0):
     ranks = Rank.objects.filter(as_tab=1).order_by('order')
     ranks = Rank.objects.filter(as_tab=1).order_by('order')
 
 
     # Find active rank
     # Find active rank
@@ -45,7 +47,10 @@ def list(request, slug=None, page=1):
             # Direct hit?
             # Direct hit?
             username = search_form.cleaned_data['username']
             username = search_form.cleaned_data['username']
             try:
             try:
-                user = User.objects.get(username__iexact=username)
+                user = User.objects
+                if settings.PROFILE_EXTENSIONS_PRELOAD:
+                    user = user.select_related(*settings.PROFILE_EXTENSIONS_PRELOAD)
+                user = user.get(username__iexact=username)
                 return redirect(reverse('user', args=(user.username_slug, user.pk)))
                 return redirect(reverse('user', args=(user.username_slug, user.pk)))
             except User.DoesNotExist:
             except User.DoesNotExist:
                 pass
                 pass
@@ -61,7 +66,10 @@ def list(request, slug=None, page=1):
 
 
             # Go for rought match
             # Go for rought match
             if len(username) > 0:
             if len(username) > 0:
-                users = User.objects.filter(username_slug__startswith=username).order_by('username_slug')[:10]
+                users = User.objects
+                if settings.PROFILE_EXTENSIONS_PRELOAD:
+                    users = users.select_related(*settings.PROFILE_EXTENSIONS_PRELOAD)
+                users = users.filter(username_slug__startswith=username).order_by('username_slug')[:10]
         elif search_form.non_field_errors()[0] == 'form_contains_errors':
         elif search_form.non_field_errors()[0] == 'form_contains_errors':
             message = Message(_("To search users you have to enter username in search field."), 'error')
             message = Message(_("To search users you have to enter username in search field."), 'error')
         else:
         else:
@@ -71,7 +79,14 @@ def list(request, slug=None, page=1):
         if active_rank:
         if active_rank:
             users = User.objects.filter(rank=active_rank)
             users = User.objects.filter(rank=active_rank)
             items_total = users.count()
             items_total = users.count()
-            pagination = make_pagination(page, items_total, request.settings['profiles_per_list'])
+            try:
+                pagination = make_pagination(page, items_total, request.settings['profiles_per_list'])
+            except Http404:
+                if not default_rank and active_rank:
+                    return redirect(reverse('users', kwargs={'slug': active_rank.slug}))
+                return redirect(reverse('users'))
+            if settings.PROFILE_EXTENSIONS_PRELOAD:
+                users = users.select_related(*settings.PROFILE_EXTENSIONS_PRELOAD)
             users = users.order_by('username_slug')[pagination['start']:pagination['stop']]
             users = users.order_by('username_slug')[pagination['start']:pagination['stop']]
 
 
     return request.theme.render_to_response('profiles/list.html',
     return request.theme.render_to_response('profiles/list.html',

+ 15 - 0
misago/apps/reports/changelog.py

@@ -0,0 +1,15 @@
+from misago.apps.threadtype.changelog import (ChangelogChangesBaseView,
+                                              ChangelogDiffBaseView,
+                                              ChangelogRevertBaseView)
+from misago.apps.reports.mixins import TypeMixin
+
+class ChangelogView(ChangelogChangesBaseView, TypeMixin):
+    pass
+
+
+class ChangelogDiffView(ChangelogDiffBaseView, TypeMixin):
+    pass
+
+
+class ChangelogRevertView(ChangelogRevertBaseView, TypeMixin):
+    pass

+ 37 - 0
misago/apps/reports/delete.py

@@ -0,0 +1,37 @@
+from misago.apps.threadtype.delete import *
+from misago.apps.reports.mixins import TypeMixin
+
+class DeleteThreadView(DeleteThreadBaseView, TypeMixin):
+    pass
+
+
+class HideThreadView(HideThreadBaseView, TypeMixin):
+    pass
+
+
+class ShowThreadView(ShowThreadBaseView, TypeMixin):
+    pass
+
+
+class DeleteReplyView(DeleteReplyBaseView, TypeMixin):
+    pass
+
+
+class HideReplyView(HideReplyBaseView, TypeMixin):
+    pass
+
+
+class ShowReplyView(ShowReplyBaseView, TypeMixin):
+    pass
+
+
+class DeleteCheckpointView(DeleteCheckpointBaseView, TypeMixin):
+    pass
+
+
+class HideCheckpointView(HideCheckpointBaseView, TypeMixin):
+    pass
+
+
+class ShowCheckpointView(ShowCheckpointBaseView, TypeMixin):
+    pass

+ 5 - 0
misago/apps/reports/details.py

@@ -0,0 +1,5 @@
+from misago.apps.threadtype.details import DetailsBaseView, KarmaVotesBaseView
+from misago.apps.reports.mixins import TypeMixin
+
+class DetailsView(DetailsBaseView, TypeMixin):
+    pass

+ 30 - 0
misago/apps/reports/forms.py

@@ -0,0 +1,30 @@
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from misago.apps.threadtype.posting.forms import (EditThreadForm as EditThreadBaseForm,
+                                                  NewReplyForm as NewReplyBaseForm,
+                                                  EditReplyForm as EditReplyBaseForm)
+from misago.forms import Form
+
+class ReportFormMixin(object):
+    def type_fields(self):
+        self.thread.original_weight = self.thread.weight
+
+        thread_weight = []
+        if self.thread.weight == 2:
+            thread_weight.append((2, _("Unresolved")))
+        thread_weight.append((1, _("Resolved")))
+        thread_weight.append((0, _("Bogus")))
+
+        self.fields['thread_weight'].choices = thread_weight
+
+
+class EditThreadForm(ReportFormMixin, EditThreadBaseForm):
+    pass
+
+
+class NewReplyForm(ReportFormMixin, NewReplyBaseForm):
+    pass
+
+
+class EditReplyForm(ReportFormMixin, EditReplyBaseForm):
+    pass

+ 29 - 0
misago/apps/reports/jumps.py

@@ -0,0 +1,29 @@
+from misago.apps.threadtype.jumps import *
+from misago.apps.reports.mixins import TypeMixin
+
+class LastReplyView(LastReplyBaseView, TypeMixin):
+    pass
+
+
+class FindReplyView(FindReplyBaseView, TypeMixin):
+    pass
+
+
+class NewReplyView(NewReplyBaseView, TypeMixin):
+    pass
+
+
+class WatchThreadView(WatchThreadBaseView, TypeMixin):
+    pass
+
+
+class WatchEmailThreadView(WatchEmailThreadBaseView, TypeMixin):
+    pass
+
+
+class UnwatchThreadView(UnwatchThreadBaseView, TypeMixin):
+    pass
+
+
+class UnwatchEmailThreadView(UnwatchEmailThreadBaseView, TypeMixin):
+    pass

+ 116 - 0
misago/apps/reports/list.py

@@ -0,0 +1,116 @@
+from itertools import chain
+from django.core.urlresolvers import reverse
+from django.db.models import F
+from django.http import Http404
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
+from misago.messages import Message
+from misago.models import Forum, Thread, Post
+from misago.readstrackers import ThreadsTracker
+from misago.utils.pagination import make_pagination
+from misago.apps.reports.mixins import TypeMixin
+
+class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
+    def fetch_forum(self):
+        self.forum = Forum.objects.get(special='reports')
+
+    def threads_queryset(self):
+        announcements = self.forum.thread_set.filter(weight=2).prefetch_related('report_for').order_by('-pk')
+        threads = self.forum.thread_set.filter(weight__lt=2).prefetch_related('report_for').order_by('-weight', '-last')
+
+        # Add in first and last poster
+        if self.request.settings.avatars_on_threads_list:
+            announcements = announcements.prefetch_related('start_poster', 'last_poster')
+            threads = threads.prefetch_related('start_poster', 'last_poster')
+
+        return announcements, threads
+
+    def fetch_threads(self):
+        qs_announcements, qs_threads = self.threads_queryset()
+        self.count = qs_threads.count()
+
+        try:
+            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, self.request.settings.threads_per_page)
+        except Http404:
+            return self.threads_list_redirect()
+
+        tracker_forum = ThreadsTracker(self.request, self.forum)
+        unresolved_count = 0
+        for thread in list(chain(qs_announcements, qs_threads[self.pagination['start']:self.pagination['stop']])):
+            thread.original_weight = thread.weight
+            if thread.weight == 2:
+                unresolved_count += 1
+            thread.is_read = tracker_forum.is_read(thread)
+            thread.report_forum = None
+            if thread.report_for_id:
+                thread.report_forum = Forum.objects.forums_tree.get(thread.report_for.forum_id)
+            self.threads.append(thread)
+
+        if int(self.request.monitor['reported_posts']) != unresolved_count:
+            self.request.monitor['reported_posts'] = unresolved_count
+
+    def threads_actions(self):
+        acl = self.request.acl.threads.get_role(self.forum)
+        actions = []
+        try:
+            actions.append(('sticky', _('Change to resolved')))
+            actions.append(('normal', _('Change to bogus')))
+            if acl['can_delete_threads']:
+                actions.append(('undelete', _('Restore reports')))
+                actions.append(('soft', _('Hide reports')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Delete reports')))
+        except KeyError:
+            pass
+        return actions
+
+    def mass_resolve(self, ids):
+        reported_posts = []
+        reported_threads = []
+        for thread in self.threads:
+            if thread.pk in ids:
+                if thread.original_weight != thread.weight:
+                    if thread.weight == 1:
+                        thread.set_checkpoint(self.request, 'resolved')
+                    if thread.weight == 0:
+                        thread.set_checkpoint(self.request, 'bogus')
+                if thread.original_weight == 2 and thread.report_for_id:
+                    reported_posts.append(thread.report_for.pk)
+                    reported_threads.append(thread.report_for.thread_id)
+        if reported_threads:
+            Thread.objects.filter(id__in=reported_threads).update(replies_reported=F('replies_reported') - 1)
+            Post.objects.filter(id__in=reported_posts).update(reported=False)
+
+    def action_sticky(self, ids):
+        if self._action_sticky(ids):
+            self.mass_resolve(ids)
+            self.request.messages.set_flash(Message(_('Selected reports were set as resolved.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were set as resolved.')), 'info', 'threads')
+
+    def action_normal(self, ids):
+        if self._action_normal(ids):
+            self.mass_resolve(ids)
+            self.request.messages.set_flash(Message(_('Selected reports were set as bogus.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were set as bogus.')), 'info', 'threads')
+
+    def action_undelete(self, ids):
+        if self._action_undelete(ids):
+            self.request.messages.set_flash(Message(_('Selected reports have been restored.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were restored.')), 'info', 'threads')
+
+    def action_soft(self, ids):
+        if self._action_soft(ids):
+            self.mass_resolve(ids)
+            self.request.messages.set_flash(Message(_('Selected reports have been hidden.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were hidden.')), 'info', 'threads')
+
+    def action_hard(self, ids):
+        if self._action_hard(ids):
+            self.request.messages.set_flash(Message(_('Selected reports have been deleted.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No reports were deleted.')), 'info', 'threads')

+ 7 - 1
misago/apps/reports/mixins.py

@@ -1,2 +1,8 @@
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+
 class TypeMixin(object):
 class TypeMixin(object):
-    templates_prefix = 'reports'
+    type_prefix = 'report'
+
+    def threads_list_redirect(self):
+        return redirect(reverse('reports'))

+ 44 - 0
misago/apps/reports/posting.py

@@ -0,0 +1,44 @@
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago.apps.threadtype.posting import EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
+from misago.messages import Message
+from misago.models import Forum, Thread, Post
+from misago.apps.reports.mixins import TypeMixin
+from misago.apps.reports.forms import EditThreadForm, NewReplyForm, EditReplyForm
+
+class SetStateCheckpointMixin(object):
+    def post_form(self, form):
+        self.thread.original_weight = self.thread_weight
+        super(SetStateCheckpointMixin, self).post_form(form)
+        if self.thread.original_weight != self.thread_weight:
+            if self.thread.original_weight == 2:
+                self.request.monitor.decrease('reported_posts')
+            if self.thread.weight == 1:
+                self.thread.set_checkpoint(self.request, 'resolved')
+            if self.thread.weight == 0:
+                self.thread.set_checkpoint(self.request, 'bogus')
+
+
+class EditThreadView(SetStateCheckpointMixin, EditThreadBaseView, TypeMixin):
+    form_type = EditThreadForm
+
+    def response(self):
+        self.request.messages.set_flash(Message(_("Report has been edited.")), 'success', 'threads_%s' % self.post.pk)
+        return redirect(reverse('report', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+
+
+class NewReplyView(SetStateCheckpointMixin, NewReplyBaseView, TypeMixin):
+    form_type = NewReplyForm
+
+    def response(self):
+        self.request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)
+
+
+class EditReplyView(SetStateCheckpointMixin, EditReplyBaseView, TypeMixin):
+    form_type = EditReplyForm
+    
+    def response(self):
+        self.request.messages.set_flash(Message(_("Your reply has been changed.")), 'success', 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)

+ 25 - 0
misago/apps/reports/search.py

@@ -0,0 +1,25 @@
+from misago.decorators import block_crawlers
+from misago.models import Forum, Post
+from misago.apps.errors import error404
+from misago.apps.search.views import do_search, results
+
+def allow_search(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        if not request.acl.reports.can_handle():
+            return error404()
+        return f(*args, **kwargs)
+    return decorator
+
+
+@block_crawlers
+@allow_search
+def search_reports(request):
+    queryset = Post.objects.filter(forum=Forum.objects.special_pk('reports'))
+    return do_search(request, queryset, 'reports')
+
+
+@block_crawlers
+@allow_search
+def show_reports_results(request, page=0):
+    return results(request, page, 'reports')

+ 56 - 0
misago/apps/reports/thread.py

@@ -0,0 +1,56 @@
+from django.utils.translation import ugettext as _
+from misago.apps.threadtype.thread import ThreadBaseView, ThreadModeration, PostsModeration
+from misago.messages import Message
+from misago.models import Forum, Thread
+from misago.apps.reports.mixins import TypeMixin
+
+class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
+    def posts_actions(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        actions = []
+        try:
+            if acl['can_delete_posts']:
+                if self.thread.replies_deleted > 0:
+                    actions.append(('undelete', _('Restore posts')))
+                actions.append(('soft', _('Hide posts')))
+            if acl['can_delete_posts'] == 2:
+                actions.append(('hard', _('Delete posts')))
+        except KeyError:
+            pass
+        return actions
+
+    def thread_actions(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        actions = []
+        try:
+            if self.thread.weight != 1:
+                actions.append(('sticky', _('Change to resolved')))
+            if self.thread.weight != 0:
+                actions.append(('normal', _('Change to bogus')))
+            if acl['can_delete_threads']:
+                if self.thread.deleted:
+                    actions.append(('undelete', _('Restore this report')))
+                else:
+                    actions.append(('soft', _('Hide this report')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Delete this report')))
+        except KeyError:
+            pass
+        return actions
+
+    def after_thread_action_sticky(self):
+        self.thread.set_checkpoint(self.request, 'resolved')
+        self.request.messages.set_flash(Message(_('Report has been set as resolved.')), 'success', 'threads')
+
+    def after_thread_action_normal(self):
+        self.thread.set_checkpoint(self.request, 'bogus')
+        self.request.messages.set_flash(Message(_('Report has been set as bogus.')), 'success', 'threads')
+
+    def after_thread_action_undelete(self):
+        self.request.messages.set_flash(Message(_('Report has been restored.')), 'success', 'threads')
+
+    def after_thread_action_soft(self):
+        self.request.messages.set_flash(Message(_('Report has been hidden.')), 'success', 'threads')
+
+    def after_thread_action_hard(self):
+        self.request.messages.set_flash(Message(_('Report "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')

+ 33 - 3
misago/apps/reports/urls.py

@@ -1,6 +1,36 @@
 from django.conf.urls import patterns, url
 from django.conf.urls import patterns, url
 
 
-urlpatterns = patterns('misago.apps.reports.views',
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'ThreadsListView', name="reports"),
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>\d+)/$', 'ThreadsView', name="reports"),
+urlpatterns = patterns('misago.apps.reports',
+    url(r'^$', 'list.ThreadsListView', name="reports"),
+    url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'list.ThreadsListView', name="reports"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="report_edit"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'posting.NewReplyView', name="report_reply"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="report_reply"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', 'posting.EditReplyView', name="report_post_edit"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'thread.ThreadView', name="report"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>[1-9]([0-9]+)?)/$', 'thread.ThreadView', name="report"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'jumps.LastReplyView', name="report_last"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', 'jumps.FindReplyView', name="report_find"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'jumps.NewReplyView', name="report_new"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/$', 'jumps.WatchThreadView', name="report_watch"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$', 'jumps.WatchEmailThreadView', name="report_watch_email"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$', 'jumps.UnwatchThreadView', name="report_unwatch"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="report_unwatch_email"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="report_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="report_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/show/$', 'delete.ShowThreadView', name="report_show"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="report_post_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="report_post_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show/$', 'delete.ShowReplyView', name="report_post_show"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/delete/$', 'delete.DeleteCheckpointView', name="report_post_checkpoint_delete"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/hide/$', 'delete.HideCheckpointView', name="report_post_checkpoint_hide"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/show/$', 'delete.ShowCheckpointView', name="report_post_checkpoint_show"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="report_post_info"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="report_changelog"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="report_changelog_diff"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="report_changelog_revert"),
+    # Extra search routes
+    url(r'^search/$', 'search.search_reports', name="reports_search"),
+    url(r'^search/results/$', 'search.show_reports_results', name="reports_results"),
+    url(r'^search/results/(?P<page>[1-9]([0-9]+)?)/$', 'search.show_reports_results', name="reports_results"),
 )
 )

+ 0 - 0
misago/apps/admin/clients/__init__.py → misago/apps/search/__init__.py


+ 75 - 0
misago/apps/search/forms.py

@@ -0,0 +1,75 @@
+from django import forms
+from django.utils import timezone
+from django.utils.translation import ungettext_lazy, ugettext_lazy as _
+from misago.forms import Form
+
+class QuickSearchForm(Form):
+    search_query = forms.CharField(max_length=255)
+
+    def clean_search_query(self):
+        data = self.cleaned_data['search_query']
+        if len(data) < 3:
+            raise forms.ValidationError(_("Search query should be at least 3 characters long."))
+
+        self.mode = None
+
+        if data[0:6].lower() == 'forum:':
+            forum_name = data[6:].strip()
+            if len(forum_name) < 2:
+                raise forms.ValidationError(_("In order to jump to forum, You have to enter full forum name or first few characters of it."))
+            self.mode = 'forum'
+            self.target = forum_name
+
+        if data[0:5].lower() == 'user:':
+            username = data[5:].strip()
+            if len(username) < 2:
+                raise forms.ValidationError(_("In order to jump to user profile, You have to enter full user name or first few characters of it."))
+            self.mode = 'user'
+            self.target = username
+
+        return data
+
+    def clean(self):
+        cleaned_data = super(QuickSearchForm, self).clean()
+        if self.request.user.is_authenticated():
+            self.check_flood_user()
+        if self.request.user.is_anonymous():
+            self.check_flood_guest()
+        return cleaned_data
+
+    def check_flood_user(self):
+        if self.request.user.last_search:
+            diff = timezone.now() - self.request.user.last_search
+            diff = diff.seconds + (diff.days * 86400)
+            wait_for = self.request.acl.search.search_cooldown() - diff
+            if wait_for > 0:
+                if wait_for < 5:
+                    raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
+                else:
+                    raise forms.ValidationError(ungettext(
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
+                        wait_for) % {
+                            'seconds': wait_for,
+                        })
+
+    def check_flood_guest(self):
+        if not self.request.session.matched:
+            raise forms.ValidationError(_("Search requires enabled cookies in order to work."))
+        
+        if self.request.session.get('last_search'):
+            diff = timezone.now() - self.request.session.get('last_search')
+            diff = diff.seconds + (diff.days * 86400)
+            wait_for = self.request.acl.search.search_cooldown() - diff
+            if wait_for > 0:
+                if wait_for < 5:
+                    raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
+                else:
+                    raise forms.ValidationError(ungettext(
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
+                        wait_for) % {
+                            'seconds': wait_for,
+                        })
+
+

+ 7 - 0
misago/apps/search/urls.py

@@ -0,0 +1,7 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns('misago.apps.search.views',
+    url(r'^$', 'search', name="search"),
+    url(r'^results/$', 'show_results', name="search_results"),
+    url(r'^results/(?P<page>[1-9]([0-9]+)?)/$', 'show_results', name="search_results"),
+)

+ 125 - 0
misago/apps/search/views.py

@@ -0,0 +1,125 @@
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from haystack.query import RelatedSearchQuerySet
+from misago.decorators import block_crawlers
+from misago.forms import FormFields
+from misago.models import Forum, Thread, Post, User
+from misago.search import SearchException
+from misago.utils.pagination import make_pagination
+from misago.apps.errors import error403, error404
+from misago.apps.profiles.views import list as users_list
+from misago.apps.search.forms import QuickSearchForm
+
+@block_crawlers
+def search(request):
+    queryset = Post.objects.filter(forum__in=Forum.objects.readable_forums(request.acl))
+    return do_search(request, queryset)
+
+
+def do_search(request, queryset, search_route=None):
+    if not request.acl.search.can_search():
+        return error403(request, _("You don't have permission to search community."))
+
+    search_route = search_route or 'search'
+
+    if request.method != "POST":
+        form = QuickSearchForm(request=request)
+        return request.theme.render_to_response('search/home.html',
+                                                {
+                                                 'form': FormFields(form),
+                                                 'search_route': search_route,
+                                                 'search_result': request.session.get('%s_result' % search_route),
+                                                 'disable_search': True,
+                                                },
+                                                context_instance=RequestContext(request))
+        
+    try:
+        form = QuickSearchForm(request.POST, request=request)
+        if form.is_valid():
+            if form.mode == 'forum':
+                jump_to = Forum.objects.forum_by_name(form.target, request.acl)
+                if jump_to:
+                    if jump_to.level == 1:
+                        return redirect(reverse('index') + ('#%s' % jump_to.slug))
+                    return redirect(jump_to.url)
+                else:
+                    raise SearchException(_('Forum "%(forum)s" could not be found.') % {'forum': form.target})
+            if form.mode == 'user':
+                request.POST = request.POST.copy()
+                request.POST['username'] = form.target
+                return users_list(request)
+            sqs = RelatedSearchQuerySet().auto_query(form.cleaned_data['search_query']).order_by('-id').load_all()
+            sqs = sqs.load_all_queryset(Post, queryset.filter(deleted=False).filter(moderated=False).select_related('thread', 'forum'))[:120]
+
+            if request.user.is_authenticated():
+                request.user.last_search = timezone.now()
+                request.user.save(force_update=True)
+            if request.user.is_anonymous():
+                request.session['last_search'] = timezone.now()
+
+            if not sqs:
+                raise SearchException(_("Search returned no results. Change search query and try again."))
+            request.session['%s_result' % search_route] = {
+                                                           'search_query': form.cleaned_data['search_query'],
+                                                           'search_results': [p.object.pk for p in sqs],
+                                                           }
+            return redirect(reverse('%s_results' % search_route))
+        else:
+            raise SearchException(form.errors['search_query'][0])
+    except SearchException as e:
+        return request.theme.render_to_response('search/error.html',
+                                                {
+                                                 'form': FormFields(form),
+                                                 'search_route': search_route,
+                                                 'message': unicode(e),
+                                                 'disable_search': True,
+                                                },
+                                                context_instance=RequestContext(request))
+
+
+@block_crawlers
+def show_results(request, page=0):
+    return results(request, page)
+
+
+def results(request, page=0, search_route=None):
+    if not request.acl.search.can_search():
+        return error403(request, _("You don't have permission to search community."))
+
+    search_route = search_route or 'search'
+    result = request.session.get('%s_result' % search_route)
+    if not result:
+        form = QuickSearchForm(request=request)
+        return request.theme.render_to_response('search/error.html',
+                                                {
+                                                 'form': FormFields(form),
+                                                 'search_route': search_route,
+                                                 'message': _("No search results were found."),
+                                                 'disable_search': True,
+                                                },
+                                                context_instance=RequestContext(request))
+
+    queryset = Post.objects.filter(id__in=result['search_results'])
+    items_total = queryset.count();
+    try:
+        pagination = make_pagination(page, items_total, 12)
+    except Http404:
+        return redirect(reverse('%s_results' % search_route))
+
+    form = QuickSearchForm(request=request, initial={'search_query': result['search_query']})
+    return request.theme.render_to_response('search/results.html',
+                                            {
+                                             'form': FormFields(form),
+                                             'search_route': search_route,
+                                             'search_query': result['search_query'],
+                                             'results': queryset.order_by('-pk').select_related('thread', 'forum', 'user')[pagination['start']:pagination['stop']],
+                                             'disable_search': True,
+                                             'items_total': items_total,
+                                             'pagination': pagination,
+                                            },
+                                            context_instance=RequestContext(request))
+

+ 1 - 1
misago/apps/signin/forms.py

@@ -12,7 +12,7 @@ class SignInForm(Form):
                None,
                None,
                (
                (
                 ('user_email', {'attrs': {'placeholder': _("Enter your e-mail")}}),
                 ('user_email', {'attrs': {'placeholder': _("Enter your e-mail")}}),
-                ('user_password', {'has_value': False, 'placeholder': _("Enter your password")}),
+                ('user_password', {'has_value': False, 'attrs': {'placeholder': _("Enter your password")}}),
                 )
                 )
                ),
                ),
               (
               (

+ 8 - 0
misago/apps/signin/views.py

@@ -1,3 +1,4 @@
+from django.core.cache import cache
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
@@ -106,4 +107,11 @@ def signout(request):
     request.messages.set_flash(Message(_("You have been signed out.")), 'info', 'security')
     request.messages.set_flash(Message(_("You have been signed out.")), 'info', 'security')
     if request.firewall.admin:
     if request.firewall.admin:
         return redirect(reverse(site.get_admin_index()))
         return redirect(reverse(site.get_admin_index()))
+    else:
+        ranks_online = cache.get('ranks_online', 'nada')
+        if ranks_online != 'nada':
+            for rank in ranks_online:
+                if rank['id'] == user.rank_id:
+                    cache.delete('ranks_online')
+                    break
     return redirect(reverse('index'))
     return redirect(reverse('index'))

+ 20 - 0
misago/apps/threads/delete.py

@@ -9,9 +9,29 @@ class HideThreadView(HideThreadBaseView, TypeMixin):
     pass
     pass
 
 
 
 
+class ShowThreadView(ShowThreadBaseView, TypeMixin):
+    pass
+
+
 class DeleteReplyView(DeleteReplyBaseView, TypeMixin):
 class DeleteReplyView(DeleteReplyBaseView, TypeMixin):
     pass
     pass
 
 
 
 
 class HideReplyView(HideReplyBaseView, TypeMixin):
 class HideReplyView(HideReplyBaseView, TypeMixin):
+    pass
+
+
+class ShowReplyView(ShowReplyBaseView, TypeMixin):
+    pass
+
+
+class DeleteCheckpointView(DeleteCheckpointBaseView, TypeMixin):
+    pass
+
+
+class HideCheckpointView(HideCheckpointBaseView, TypeMixin):
+    pass
+
+
+class ShowCheckpointView(ShowCheckpointBaseView, TypeMixin):
     pass
     pass

+ 8 - 0
misago/apps/threads/jumps.py

@@ -47,3 +47,11 @@ class UpvotePostView(UpvotePostBaseView, TypeMixin):
 
 
 class DownvotePostView(DownvotePostBaseView, TypeMixin):
 class DownvotePostView(DownvotePostBaseView, TypeMixin):
     pass
     pass
+
+
+class ReportPostView(ReportPostBaseView, TypeMixin):
+    pass
+
+
+class ShowPostReportView(ShowPostReportBaseView, TypeMixin):
+    pass

+ 12 - 5
misago/apps/threads/list.py

@@ -1,4 +1,7 @@
 from itertools import chain
 from itertools import chain
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
 from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
 from misago.models import Forum, Thread
 from misago.models import Forum, Thread
@@ -12,7 +15,7 @@ class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
 
 
     def threads_queryset(self):
     def threads_queryset(self):
         announcements = self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set).filter(weight=2).order_by('-pk')
         announcements = self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set).filter(weight=2).order_by('-pk')
-        threads = self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set).filter(weight__lt=2).order_by('-last')
+        threads = self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set).filter(weight__lt=2).order_by('-weight', '-last')
 
 
         # Dont display threads by ignored users (unless they are important)
         # Dont display threads by ignored users (unless they are important)
         if self.request.user.is_authenticated():
         if self.request.user.is_authenticated():
@@ -30,7 +33,11 @@ class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
     def fetch_threads(self):
     def fetch_threads(self):
         qs_announcements, qs_threads = self.threads_queryset()
         qs_announcements, qs_threads = self.threads_queryset()
         self.count = qs_threads.count()
         self.count = qs_threads.count()
-        self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, self.request.settings.threads_per_page)
+
+        try:
+            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, self.request.settings.threads_per_page)
+        except Http404:
+            return self.threads_list_redirect()
 
 
         tracker_forum = ThreadsTracker(self.request, self.forum)
         tracker_forum = ThreadsTracker(self.request, self.forum)
         for thread in list(chain(qs_announcements, qs_threads[self.pagination['start']:self.pagination['stop']])):
         for thread in list(chain(qs_announcements, qs_threads[self.pagination['start']:self.pagination['stop']])):
@@ -56,10 +63,10 @@ class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
                 actions.append(('open', _('Open threads')))
                 actions.append(('open', _('Open threads')))
                 actions.append(('close', _('Close threads')))
                 actions.append(('close', _('Close threads')))
             if acl['can_delete_threads']:
             if acl['can_delete_threads']:
-                actions.append(('undelete', _('Undelete threads')))
-                actions.append(('soft', _('Soft delete threads')))
+                actions.append(('undelete', _('Restore threads')))
+                actions.append(('soft', _('Hide threads')))
             if acl['can_delete_threads'] == 2:
             if acl['can_delete_threads'] == 2:
-                actions.append(('hard', _('Hard delete threads')))
+                actions.append(('hard', _('Delete threads')))
         except KeyError:
         except KeyError:
             pass
             pass
         return actions
         return actions

+ 6 - 6
misago/apps/threads/thread.py

@@ -19,10 +19,10 @@ class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
                 actions.append(('unprotect', _('Remove posts protection')))
                 actions.append(('unprotect', _('Remove posts protection')))
             if acl['can_delete_posts']:
             if acl['can_delete_posts']:
                 if self.thread.replies_deleted > 0:
                 if self.thread.replies_deleted > 0:
-                    actions.append(('undelete', _('Undelete posts')))
-                actions.append(('soft', _('Soft delete posts')))
+                    actions.append(('undelete', _('Restore posts')))
+                actions.append(('soft', _('Hide posts')))
             if acl['can_delete_posts'] == 2:
             if acl['can_delete_posts'] == 2:
-                actions.append(('hard', _('Hard delete posts')))
+                actions.append(('hard', _('Delete posts')))
         except KeyError:
         except KeyError:
             pass
             pass
         return actions
         return actions
@@ -51,11 +51,11 @@ class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
                     actions.append(('close', _('Close this thread')))
                     actions.append(('close', _('Close this thread')))
             if acl['can_delete_threads']:
             if acl['can_delete_threads']:
                 if self.thread.deleted:
                 if self.thread.deleted:
-                    actions.append(('undelete', _('Undelete this thread')))
+                    actions.append(('undelete', _('Restore this thread')))
                 else:
                 else:
-                    actions.append(('soft', _('Soft delete this thread')))
+                    actions.append(('soft', _('Hide this thread')))
             if acl['can_delete_threads'] == 2:
             if acl['can_delete_threads'] == 2:
-                actions.append(('hard', _('Hard delete this thread')))
+                actions.append(('hard', _('Delete this thread')))
         except KeyError:
         except KeyError:
             pass
             pass
         return actions
         return actions

+ 13 - 6
misago/apps/threads/urls.py

@@ -2,14 +2,14 @@ from django.conf.urls import patterns, url
 
 
 urlpatterns = patterns('misago.apps.threads',
 urlpatterns = patterns('misago.apps.threads',
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'list.ThreadsListView', name="forum"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'list.ThreadsListView', name="forum"),
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>\d+)/$', 'list.ThreadsListView', name="forum"),
+    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>[1-9]([0-9]+)?)/$', 'list.ThreadsListView', name="forum"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/start/$', 'posting.NewThreadView', name="thread_start"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/start/$', 'posting.NewThreadView', name="thread_start"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="thread_edit"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="thread_edit"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', 'posting.EditReplyView', name="post_edit"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', 'posting.EditReplyView', name="post_edit"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'thread.ThreadView', name="thread"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'thread.ThreadView', name="thread"),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>\d+)/$', 'thread.ThreadView', name="thread"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>[1-9]([0-9]+)?)/$', 'thread.ThreadView', name="thread"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'jumps.LastReplyView', name="thread_last"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'jumps.LastReplyView', name="thread_last"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', 'jumps.FindReplyView', name="thread_find"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', 'jumps.FindReplyView', name="thread_find"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'jumps.NewReplyView', name="thread_new"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'jumps.NewReplyView', name="thread_new"),
@@ -22,10 +22,17 @@ urlpatterns = patterns('misago.apps.threads',
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="thread_unwatch_email"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'jumps.UnwatchEmailThreadView', name="thread_unwatch_email"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', 'jumps.UpvotePostView', name="post_upvote"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/upvote/$', 'jumps.UpvotePostView', name="post_upvote"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', 'jumps.DownvotePostView', name="post_downvote"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/downvote/$', 'jumps.DownvotePostView', name="post_downvote"),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="thread_delete", kwargs={'mode': 'delete_thread'}),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="thread_hide", kwargs={'mode': 'hide_thread'}),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="post_delete", kwargs={'mode': 'delete_post'}),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="post_hide", kwargs={'mode': 'hide_post'}),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/report/$', 'jumps.ReportPostView', name="post_report"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show-report/$', 'jumps.ShowPostReportView', name="post_report_show"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/delete/$', 'delete.DeleteThreadView', name="thread_delete"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'delete.HideThreadView', name="thread_hide"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/show/$', 'delete.ShowThreadView', name="thread_show"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'delete.DeleteReplyView', name="post_delete"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'delete.HideReplyView', name="post_hide"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/show/$', 'delete.ShowReplyView', name="post_show"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/delete/$', 'delete.DeleteCheckpointView', name="post_checkpoint_delete"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/hide/$', 'delete.HideCheckpointView', name="post_checkpoint_hide"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/checkpoint/(?P<checkpoint>\d+)/show/$', 'delete.ShowCheckpointView', name="post_checkpoint_show"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="post_info"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'details.DetailsView', name="post_info"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/votes/$', 'details.KarmaVotesView', name="post_votes"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/votes/$', 'details.KarmaVotesView', name="post_votes"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="thread_changelog"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="thread_changelog"),

+ 8 - 6
misago/apps/threadtype/base.py

@@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse
 from django.http import Http404
 from django.http import Http404
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from misago.models import Forum, Thread, Post
 from misago.models import Forum, Thread, Post
-from misago.utils.pagination import make_pagination
+from misago.utils.pagination import page_number
 
 
 class ViewBase(object):
 class ViewBase(object):
     def __new__(cls, request, **kwargs):
     def __new__(cls, request, **kwargs):
@@ -48,11 +48,13 @@ class ViewBase(object):
         except AttributeError:
         except AttributeError:
             pass
             pass
 
 
-    def redirect_to_post(self, post):
-        pagination = make_pagination(0, self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set).filter(id__lte=post.pk).count(), self.request.settings.posts_per_page)
-        if pagination['total'] > 1:
-            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug, 'page': pagination['total']}) + ('#post-%s' % post.pk))
-        return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % post.pk))
+    def redirect_to_post(self, post, type_prefix=None):
+        type_prefix = type_prefix or self.type_prefix
+        queryset = self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set)
+        page = page_number(queryset.filter(id__lte=post.pk).count(), queryset.count(), self.request.settings.posts_per_page)
+        if page > 1:
+            return redirect(reverse(type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug, 'page': page}) + ('#post-%s' % post.pk))
+        return redirect(reverse(type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % post.pk))
 
 
     def template_vars(self, context):
     def template_vars(self, context):
         return context
         return context

+ 1 - 2
misago/apps/threadtype/changelog.py

@@ -10,7 +10,6 @@ from misago.messages import Message
 from misago.models import Forum, Thread, Post, Change
 from misago.models import Forum, Thread, Post, Change
 from misago.utils.datesformats import reldate
 from misago.utils.datesformats import reldate
 from misago.utils.strings import slugify
 from misago.utils.strings import slugify
-from misago.utils.pagination import make_pagination
 from misago.apps.threadtype.base import ViewBase
 from misago.apps.threadtype.base import ViewBase
 
 
 class ChangelogBaseView(ViewBase):
 class ChangelogBaseView(ViewBase):
@@ -120,7 +119,7 @@ class ChangelogRevertBaseView(ChangelogDiffBaseView):
 
 
         if self.change.post_content != self.post.post:
         if self.change.post_content != self.post.post:
             self.post.post = self.change.post_content
             self.post.post = self.change.post_content
-            md, self.post.post_preparsed = post_markdown(request, self.change.post_content)
+            md, self.post.post_preparsed = post_markdown(self.change.post_content)
             self.post.save(force_update=True)
             self.post.save(force_update=True)
 
 
         request.messages.set_flash(Message(_("Post has been reverted to state from %(date)s.") % {'date': reldate(self.change.date).lower()}), 'success', 'threads_%s' % self.post.pk)
         request.messages.set_flash(Message(_("Post has been reverted to state from %(date)s.") % {'date': reldate(self.change.date).lower()}), 'success', 'threads_%s' % self.post.pk)

+ 106 - 18
misago/apps/threadtype/delete.py

@@ -5,10 +5,13 @@ from django.utils.translation import ugettext as _
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.apps.errors import error403, error404
 from misago.apps.errors import error403, error404
 from misago.messages import Message
 from misago.messages import Message
-from misago.models import Forum, Thread, Post
+from misago.models import Forum, Thread, Post, Checkpoint
 from misago.apps.threadtype.base import ViewBase
 from misago.apps.threadtype.base import ViewBase
 
 
 class DeleteHideBaseView(ViewBase):
 class DeleteHideBaseView(ViewBase):
+    def set_context(self):
+        pass
+
     def _set_context(self):
     def _set_context(self):
         self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
         self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
         self.forum = self.thread.forum
         self.forum = self.thread.forum
@@ -23,6 +26,10 @@ class DeleteHideBaseView(ViewBase):
             self.post = self.thread.post_set.get(id=self.kwargs.get('post'))
             self.post = self.thread.post_set.get(id=self.kwargs.get('post'))
             self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
             self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
 
 
+        if self.kwargs.get('checkpoint'):
+            self.checkpoint = self.thread.checkpoint_set.get(id=self.kwargs.get('checkpoint'))
+            self.request.acl.threads.allow_checkpoint_view(self.forum, self.checkpoint)
+
         self.set_context()
         self.set_context()
 
 
     def __call__(self, request, **kwargs):
     def __call__(self, request, **kwargs):
@@ -36,7 +43,7 @@ class DeleteHideBaseView(ViewBase):
             self.delete()
             self.delete()
             self.message()
             self.message()
             return self.response()
             return self.response()
-        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
+        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist, Checkpoint.DoesNotExist):
             return error404(request)
             return error404(request)
         except ACLError403 as e:
         except ACLError403 as e:
             return error403(request, unicode(e))
             return error403(request, unicode(e))
@@ -48,11 +55,6 @@ class DeleteThreadBaseView(DeleteHideBaseView):
     def set_context(self):
     def set_context(self):
         self.request.acl.threads.allow_delete_thread(self.request.user, self.proxy,
         self.request.acl.threads.allow_delete_thread(self.request.user, self.proxy,
                                                      self.thread, self.thread.start_post, True)
                                                      self.thread, self.thread.start_post, True)
-        # Assert we are not user trying to delete thread with replies
-        acl = self.request.acl.threads.get_role(self.thread.forum_id)
-        if not acl['can_delete_threads']:
-            if self.thread.post_set.exclude(user_id=self.request.user.id).count() > 0:
-                raise ACLError403(_("Somebody has already replied to this thread. You cannot delete it."))
 
 
     def delete(self):
     def delete(self):
         self.thread.delete()
         self.thread.delete()
@@ -79,8 +81,7 @@ class HideThreadBaseView(DeleteHideBaseView):
     def delete(self):
     def delete(self):
         self.thread.start_post.deleted = True
         self.thread.start_post.deleted = True
         self.thread.start_post.save(force_update=True)
         self.thread.start_post.save(force_update=True)
-        self.thread.last_post.set_checkpoint(self.request, 'deleted')
-        self.thread.last_post.save(force_update=True)
+        self.thread.set_checkpoint(self.request, 'deleted')
         self.thread.sync()
         self.thread.sync()
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
         self.forum.sync()
         self.forum.sync()
@@ -95,13 +96,36 @@ class HideThreadBaseView(DeleteHideBaseView):
         return self.threads_list_redirect()
         return self.threads_list_redirect()
 
 
 
 
+class ShowThreadBaseView(DeleteHideBaseView):
+    def set_context(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        if not acl['can_delete_threads']:
+            raise ACLError403(_("You cannot undelete this thread."))
+        if not self.thread.start_post.deleted:
+            raise ACLError403(_('This thread is already visible!'))
+
+    def delete(self):
+        self.thread.start_post.deleted = False
+        self.thread.start_post.save(force_update=True)
+        self.thread.set_checkpoint(self.request, 'undeleted')
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        self.forum.sync()
+        self.forum.save(force_update=True)
+
+    def message(self):
+        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been restored.') % {'thread': self.thread.name}), 'success', 'threads')
+
+    def response(self):
+        if self.request.acl.threads.can_see_deleted_threads(self.thread.forum):
+            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+        return self.threads_list_redirect()
+
+
 class DeleteReplyBaseView(DeleteHideBaseView):
 class DeleteReplyBaseView(DeleteHideBaseView):
     def set_context(self):
     def set_context(self):
         self.request.acl.threads.allow_delete_post(self.request.user, self.forum,
         self.request.acl.threads.allow_delete_post(self.request.user, self.forum,
                                                    self.thread, self.post, True)
                                                    self.thread, self.post, True)
-        acl = self.request.acl.threads.get_role(self.thread.forum_id)
-        if not acl['can_delete_posts'] and self.thread.post_set.filter(id__gt=self.post.pk).count() > 0:
-            raise ACLError403(_("Somebody has already replied to this post, you cannot delete it."))
 
 
     def delete(self):
     def delete(self):
         self.post.delete()
         self.post.delete()
@@ -111,7 +135,7 @@ class DeleteReplyBaseView(DeleteHideBaseView):
         self.forum.save(force_update=True)
         self.forum.save(force_update=True)
 
 
     def message(self):
     def message(self):
-        self.request.messages.set_flash(Message(_("Selected Reply has been deleted.")), 'success', 'threads')
+        self.request.messages.set_flash(Message(_("Selected reply has been deleted.")), 'success', 'threads')
 
 
     def response(self):
     def response(self):
         return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
         return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
@@ -126,11 +150,8 @@ class HideReplyBaseView(DeleteHideBaseView):
             raise ACLError403(_("Somebody has already replied to this post, you cannot delete it."))
             raise ACLError403(_("Somebody has already replied to this post, you cannot delete it."))
 
 
     def delete(self):
     def delete(self):
+        self.post.current_date = timezone.now()
         self.post.deleted = True
         self.post.deleted = True
-        self.post.edit_date = timezone.now()
-        self.post.edit_user = self.request.user
-        self.post.edit_user_name = self.request.user.username
-        self.post.edit_user_slug = self.request.user.username_slug
         self.post.save(force_update=True)
         self.post.save(force_update=True)
         self.thread.sync()
         self.thread.sync()
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
@@ -138,7 +159,74 @@ class HideReplyBaseView(DeleteHideBaseView):
         self.forum.save(force_update=True)
         self.forum.save(force_update=True)
 
 
     def message(self):
     def message(self):
-        self.request.messages.set_flash(Message(_("Selected Reply has been deleted.")), 'success', 'threads_%s' % self.post.pk)
+        self.request.messages.set_flash(Message(_("Selected reply has been deleted.")), 'success', 'threads_%s' % self.post.pk)
+
+    def response(self):
+        return self.redirect_to_post(self.post)
+
+
+class ShowReplyBaseView(DeleteHideBaseView):
+    def set_context(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        if not acl['can_delete_posts']:
+            raise ACLError403(_("You cannot undelete this reply."))
+        if not self.post.deleted:
+            raise ACLError403(_('This reply is already visible!'))
+
+    def delete(self):
+        self.post.deleted = False
+        self.post.save(force_update=True)
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        self.forum.sync()
+        self.forum.save(force_update=True)
+
+    def message(self):
+        self.request.messages.set_flash(Message(_("Selected reply has been restored.")), 'success', 'threads_%s' % self.post.pk)
 
 
     def response(self):
     def response(self):
         return self.redirect_to_post(self.post)
         return self.redirect_to_post(self.post)
+
+
+class DeleteCheckpointBaseView(DeleteHideBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_checkpoint_delete(self.forum)
+
+    def delete(self):
+        self.checkpoint.delete()
+
+    def message(self):
+        self.request.messages.set_flash(Message(_("Selected checkpoint has been deleted.")), 'success', 'threads')
+
+    def response(self):
+        if 'retreat' in self.request.POST:
+            return redirect(self.request.POST.get('retreat'))
+        return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+
+
+class HideCheckpointBaseView(DeleteCheckpointBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_checkpoint_hide(self.forum)
+        if self.checkpoint.deleted:
+            raise ACLError403(_('This checkpoint is already hidden!'))
+
+    def delete(self):
+        self.checkpoint.deleted = True
+        self.checkpoint.save(force_update=True)
+
+    def message(self):
+        self.request.messages.set_flash(Message(_("Selected checkpoint has been hidden.")), 'success', 'threads')
+
+
+class ShowCheckpointBaseView(DeleteCheckpointBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_checkpoint_show(self.forum)
+        if not self.checkpoint.deleted:
+            raise ACLError403(_('This checkpoint is already visible!'))
+
+    def delete(self):
+        self.checkpoint.deleted = False
+        self.checkpoint.save(force_update=True)
+
+    def message(self):
+        self.request.messages.set_flash(Message(_("Selected checkpoint has been restored.")), 'success', 'threads')

+ 120 - 2
misago/apps/threadtype/jumps.py

@@ -1,3 +1,4 @@
+from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.utils import timezone
 from django.utils import timezone
@@ -5,10 +6,11 @@ from django.utils.translation import ugettext as _
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.apps.errors import error403, error404
 from misago.apps.errors import error403, error404
 from misago.decorators import block_guest, check_csrf
 from misago.decorators import block_guest, check_csrf
+from misago.markdown import post_markdown
 from misago.messages import Message
 from misago.messages import Message
-from misago.models import Forum, Thread, Post, Karma, WatchedThread
+from misago.models import Forum, Checkpoint, Thread, Post, Karma, WatchedThread
 from misago.readstrackers import ThreadsTracker
 from misago.readstrackers import ThreadsTracker
-from misago.utils.pagination import make_pagination
+from misago.utils.strings import short_string, slugify
 from misago.utils.views import json_response
 from misago.utils.views import json_response
 from misago.apps.threadtype.base import ViewBase
 from misago.apps.threadtype.base import ViewBase
 
 
@@ -244,3 +246,119 @@ class DownvotePostBaseView(UpvotePostBaseView):
     
     
     def make_vote(self, request, vote):
     def make_vote(self, request, vote):
         vote.score = -1
         vote.score = -1
+
+
+class ReportPostBaseView(JumpView):
+    def make_jump(self):
+        self.request.acl.reports.allow_report()
+
+        @block_guest
+        @check_csrf
+        def view(request):
+            report = None
+            made_report = False
+            if self.post.reported:
+                report = self.post.live_report()
+
+                if report and report.start_poster_id != request.user.pk:
+                    # Append our q.q to existing report?
+                    try:
+                        report.checkpoint_set.get(user=request.user, action="reported")
+                    except Checkpoint.DoesNotExist:
+                        report.set_checkpoint(self.request, 'reported', user)
+                    made_report = True
+
+            if not report:
+                # File up new report
+                now = timezone.now()
+
+                reason_post = _('''
+Member @%(reporter)s has reported following post by @%(reported)s:
+
+%(quote)s
+**Post link:** <%(post)s>
+''')
+
+                reason_post = reason_post.strip() % {
+                                             'reporter': request.user.username,
+                                             'reported': self.post.user_name,
+                                             'post': settings.BOARD_ADDRESS + self.redirect_to_post(self.post)['Location'],
+                                             'quote': self.post.quote(),
+                                            }
+
+                md, reason_post_preparsed = post_markdown(reason_post)
+
+                reports = Forum.objects.special_model('reports')
+                report = Thread.objects.create(
+                                               forum=reports,
+                                               weight=2,
+                                               name=self.thread.name,
+                                               slug=slugify(self.thread.slug),
+                                               start=now,
+                                               start_poster=request.user,
+                                               start_poster_name=request.user.username,
+                                               start_poster_slug=request.user.username_slug,
+                                               start_poster_style=request.user.rank.style,
+                                               last=now,
+                                               last_poster=request.user,
+                                               last_poster_name=request.user.username,
+                                               last_poster_slug=request.user.username_slug,
+                                               last_poster_style=request.user.rank.style,
+                                               report_for=self.post,
+                                               )
+
+                reason = Post.objects.create(
+                                             forum=reports,
+                                             thread=report,
+                                             user=request.user,
+                                             user_name=request.user.username,
+                                             ip=request.session.get_ip(self.request),
+                                             agent=request.META.get('HTTP_USER_AGENT'),
+                                             post=reason_post,
+                                             post_preparsed=reason_post_preparsed,
+                                             date=now,
+                                             current_date=now,
+                                             )
+
+                report.start_post = reason
+                report.last_post = reason
+                report.save(force_update=True)
+
+                for m in self.post.mentions.all():
+                    reason.mentions.add(m)
+
+                self.post.reported = True
+                self.post.save(force_update=True)
+                self.thread.replies_reported += 1
+                self.thread.save(force_update=True)
+                request.monitor.increase('reported_posts')
+                made_report = True
+
+            if made_report:
+                if request.is_ajax():
+                    return json_response(request, message=_("Selected post has been reported and will receive moderator attention. Thank you."))
+                self.request.messages.set_flash(Message(_("Selected post has been reported and will receive moderator attention. Thank you.")), 'info', 'threads_%s' % self.post.pk)
+            else:
+                if request.is_ajax():
+                    return json_response(request, message=_("You have already reported this post. One of moderators will handle it as soon as it is possible. Thank you for your patience."))
+                self.request.messages.set_flash(Message(_("You have already reported this post. One of moderators will handle it as soon as it is possible. Thank you for your patience.")), 'info', 'threads_%s' % self.post.pk)
+
+            return self.redirect_to_post(self.post)
+        return view(self.request)
+
+
+class ShowPostReportBaseView(JumpView):
+    def make_jump(self):
+        self.request.acl.reports.allow_report()
+
+        @block_guest
+        def view(request):
+            if not self.post.reported:
+                return error404()
+            reports = Forum.objects.special_model('reports')
+            self.request.acl.forums.allow_forum_view(reports)
+            report = self.post.live_report()
+            if not report:
+                return error404()
+            return redirect(reverse('report', kwargs={'thread': report.pk, 'slug': report.slug}))
+        return view(self.request)

+ 2 - 26
misago/apps/threadtype/list/forms.py

@@ -42,11 +42,12 @@ class MergeThreadsForm(Form, ValidateThreadNameMixin):
         self.fields['new_forum'] = ForumChoiceField(queryset=Forum.objects.get(special='root').get_descendants().filter(pk__in=self.request.acl.forums.acl['can_browse']), initial=self.threads[0].forum)
         self.fields['new_forum'] = ForumChoiceField(queryset=Forum.objects.get(special='root').get_descendants().filter(pk__in=self.request.acl.forums.acl['can_browse']), initial=self.threads[0].forum)
         self.fields['thread_name'] = forms.CharField(
         self.fields['thread_name'] = forms.CharField(
                                                      max_length=self.request.settings['thread_name_max'],
                                                      max_length=self.request.settings['thread_name_max'],
-                                                     initial=self.threads[0].name,
+                                                     initial=self.threads[-1].name,
                                                      validators=[validate_sluggable(
                                                      validators=[validate_sluggable(
                                                                                     _("Thread name must contain at least one alpha-numeric character."),
                                                                                     _("Thread name must contain at least one alpha-numeric character."),
                                                                                     _("Thread name is too long. Try shorter name.")
                                                                                     _("Thread name is too long. Try shorter name.")
                                                                                     )])
                                                                                     )])
+
         self.layout = [
         self.layout = [
                        [
                        [
                         None,
                         None,
@@ -55,35 +56,10 @@ class MergeThreadsForm(Form, ValidateThreadNameMixin):
                          ('new_forum', {'label': _("Thread Forum"), 'help_text': _("Select forum you want to put new thread in.")}),
                          ('new_forum', {'label': _("Thread Forum"), 'help_text': _("Select forum you want to put new thread in.")}),
                          ],
                          ],
                         ],
                         ],
-                       [
-                        _("Merge Order"),
-                        [
-                         ],
-                        ],
                        ]
                        ]
 
 
-        choices = []
-        for i, thread in enumerate(self.threads):
-            choices.append((str(i), i + 1))
-        for i, thread in enumerate(self.threads):
-            self.fields['thread_%s' % thread.pk] = forms.ChoiceField(choices=choices, initial=str(i))
-            self.layout[1][1].append(('thread_%s' % thread.pk, {'label': thread.name}))
-
     def clean_new_forum(self):
     def clean_new_forum(self):
         new_forum = self.cleaned_data['new_forum']
         new_forum = self.cleaned_data['new_forum']
-        # Assert its forum
         if new_forum.type != 'forum':
         if new_forum.type != 'forum':
             raise forms.ValidationError(_("This is not forum."))
             raise forms.ValidationError(_("This is not forum."))
         return new_forum
         return new_forum
-
-    def clean(self):
-        cleaned_data = super(MergeThreadsForm, self).clean()
-        self.merge_order = {}
-        lookback = []
-        for thread in self.threads:
-            order = int(cleaned_data['thread_%s' % thread.pk])
-            if order in lookback:
-                raise forms.ValidationError(_("One or more threads have same position in merge order."))
-            lookback.append(order)
-            self.merge_order[order] = thread
-        return cleaned_data

+ 88 - 50
misago/apps/threadtype/list/moderation.py

@@ -1,15 +1,22 @@
 from django.forms import ValidationError
 from django.forms import ValidationError
 from django.template import RequestContext
 from django.template import RequestContext
+from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.forms import FormLayout
 from misago.forms import FormLayout
 from misago.messages import Message
 from misago.messages import Message
 from misago.models import Forum, Thread, Post
 from misago.models import Forum, Thread, Post
 from misago.apps.threadtype.list.forms import MoveThreadsForm, MergeThreadsForm
 from misago.apps.threadtype.list.forms import MoveThreadsForm, MergeThreadsForm
+from misago.utils.strings import slugify
 
 
 class ThreadsListModeration(object):
 class ThreadsListModeration(object):
     def action_accept(self, ids):
     def action_accept(self, ids):
+        if self._action_accept(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been marked as reviewed and made visible to other members.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were marked as reviewed.')), 'info', 'threads')
+
+    def _action_accept(self, ids):
         accepted = 0
         accepted = 0
-        last_posts = []
         users = []
         users = []
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and thread.moderated:
             if thread.pk in ids and thread.moderated:
@@ -20,24 +27,28 @@ class ThreadsListModeration(object):
                 thread.save(force_update=True)
                 thread.save(force_update=True)
                 thread.start_post.moderated = False
                 thread.start_post.moderated = False
                 thread.start_post.save(force_update=True)
                 thread.start_post.save(force_update=True)
-                thread.last_post.set_checkpoint(self.request, 'accepted')
-                last_posts.append(thread.last_post.pk)
+                thread.set_checkpoint(self.request, 'accepted')
                 # Sync user
                 # Sync user
                 if thread.last_post.user:
                 if thread.last_post.user:
                     thread.start_post.user.threads += 1
                     thread.start_post.user.threads += 1
                     thread.start_post.user.posts += 1
                     thread.start_post.user.posts += 1
                     users.append(thread.start_post.user)
                     users.append(thread.start_post.user)
         if accepted:
         if accepted:
-            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             self.request.monitor['threads'] = int(self.request.monitor['threads']) + accepted
             self.request.monitor['threads'] = int(self.request.monitor['threads']) + accepted
             self.request.monitor['posts'] = int(self.request.monitor['posts']) + accepted
             self.request.monitor['posts'] = int(self.request.monitor['posts']) + accepted
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
             for user in users:
             for user in users:
                 user.save(force_update=True)
                 user.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected threads have been marked as reviewed and made visible to other members.')), 'success', 'threads')
+        return accepted
 
 
     def action_annouce(self, ids):
     def action_annouce(self, ids):
+        if self._action_annouce(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been turned into announcements.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were turned into announcements.')), 'info', 'threads')
+
+    def _action_annouce(self, ids):
         acl = self.request.acl.threads.get_role(self.forum)
         acl = self.request.acl.threads.get_role(self.forum)
         annouced = []
         annouced = []
         for thread in self.threads:
         for thread in self.threads:
@@ -45,9 +56,15 @@ class ThreadsListModeration(object):
                 annouced.append(thread.pk)
                 annouced.append(thread.pk)
         if annouced:
         if annouced:
             Thread.objects.filter(id__in=annouced).update(weight=2)
             Thread.objects.filter(id__in=annouced).update(weight=2)
-            self.request.messages.set_flash(Message(_('Selected threads have been turned into announcements.')), 'success', 'threads')
+        return annouced
 
 
     def action_sticky(self, ids):
     def action_sticky(self, ids):
+        if self._action_sticky(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been sticked to the top of list.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were turned into stickies.')), 'info', 'threads')
+
+    def _action_sticky(self, ids):
         acl = self.request.acl.threads.get_role(self.forum)
         acl = self.request.acl.threads.get_role(self.forum)
         sticky = []
         sticky = []
         for thread in self.threads:
         for thread in self.threads:
@@ -55,16 +72,22 @@ class ThreadsListModeration(object):
                 sticky.append(thread.pk)
                 sticky.append(thread.pk)
         if sticky:
         if sticky:
             Thread.objects.filter(id__in=sticky).update(weight=1)
             Thread.objects.filter(id__in=sticky).update(weight=1)
-            self.request.messages.set_flash(Message(_('Selected threads have been sticked to the top of list.')), 'success', 'threads')
+        return sticky
 
 
     def action_normal(self, ids):
     def action_normal(self, ids):
+        if self._action_normal(ids):
+            self.request.messages.set_flash(Message(_('Selected threads weight has been removed.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads have had their weight removed.')), 'info', 'threads')
+
+    def _action_normal(self, ids):
         normalised = []
         normalised = []
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and thread.weight > 0:
             if thread.pk in ids and thread.weight > 0:
                 normalised.append(thread.pk)
                 normalised.append(thread.pk)
         if normalised:
         if normalised:
             Thread.objects.filter(id__in=normalised).update(weight=0)
             Thread.objects.filter(id__in=normalised).update(weight=0)
-            self.request.messages.set_flash(Message(_('Selected threads weight has been removed.')), 'success', 'threads')
+        return normalised
 
 
     def action_move(self, ids):
     def action_move(self, ids):
         threads = []
         threads = []
@@ -78,6 +101,7 @@ class ThreadsListModeration(object):
                 for thread in threads:
                 for thread in threads:
                     thread.move_to(new_forum)
                     thread.move_to(new_forum)
                     thread.save(force_update=True)
                     thread.save(force_update=True)
+                    thread.set_checkpoint(self.request, 'moved', forum=self.forum)
                 new_forum.sync()
                 new_forum.sync()
                 new_forum.save(force_update=True)
                 new_forum.save(force_update=True)
                 self.forum.sync()
                 self.forum.sync()
@@ -115,16 +139,10 @@ class ThreadsListModeration(object):
                                                    start=timezone.now(),
                                                    start=timezone.now(),
                                                    last=timezone.now()
                                                    last=timezone.now()
                                                    )
                                                    )
-                last_merge = 0
-                last_thread = None
                 merged = []
                 merged = []
-                for i in range(0, len(threads)):
-                    thread = form.merge_order[i]
+                for thread in reversed(threads):
                     merged.append(thread.pk)
                     merged.append(thread.pk)
-                    if last_thread and last_thread.last > thread.start:
-                        last_merge += thread.merges + 1
-                    thread.merge_with(new_thread, last_merge=last_merge)
-                    last_thread = thread
+                    thread.merge_with(new_thread)
                 Thread.objects.filter(id__in=merged).delete()
                 Thread.objects.filter(id__in=merged).delete()
                 new_thread.sync()
                 new_thread.sync()
                 new_thread.save(force_update=True)
                 new_thread.save(force_update=True)
@@ -138,10 +156,21 @@ class ThreadsListModeration(object):
             self.message = Message(form.non_field_errors()[0], 'error')
             self.message = Message(form.non_field_errors()[0], 'error')
         else:
         else:
             form = MergeThreadsForm(request=self.request, threads=threads)
             form = MergeThreadsForm(request=self.request, threads=threads)
+
+        warning = None
+        lookback = threads[0].last_post_id
+        for thread in threads[1:]:
+            if thread.start_post_id < lookback:
+                warning = Message(_("Warning: Posting times in one or more of threads that you are going to merge are overlapping. This may result in disturbed flow of merged thread."), 'warning')
+                break
+            else:
+                lookback = thread.last_post_id
+
         return self.request.theme.render_to_response(('%ss/merge.html' % self.type_prefix),
         return self.request.theme.render_to_response(('%ss/merge.html' % self.type_prefix),
                                                      {
                                                      {
                                                       'type_prefix': self.type_prefix,
                                                       'type_prefix': self.type_prefix,
                                                       'message': self.message,
                                                       'message': self.message,
+                                                      'warning': warning,
                                                       'forum': self.forum,
                                                       'forum': self.forum,
                                                       'parents': self.parents,
                                                       'parents': self.parents,
                                                       'threads': threads,
                                                       'threads': threads,
@@ -150,83 +179,92 @@ class ThreadsListModeration(object):
                                                      context_instance=RequestContext(self.request));
                                                      context_instance=RequestContext(self.request));
 
 
     def action_open(self, ids):
     def action_open(self, ids):
+        if self._action_open(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were opened.')), 'info', 'threads')
+
+    def _action_open(self, ids):        
         opened = []
         opened = []
-        last_posts = []
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and thread.closed:
             if thread.pk in ids and thread.closed:
                 opened.append(thread.pk)
                 opened.append(thread.pk)
-                thread.last_post.set_checkpoint(self.request, 'opened')
-                last_posts.append(thread.last_post.pk)
+                thread.set_checkpoint(self.request, 'opened')
         if opened:
         if opened:
-            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=opened).update(closed=False)
             Thread.objects.filter(id__in=opened).update(closed=False)
-            self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
+        return opened
 
 
     def action_close(self, ids):
     def action_close(self, ids):
+        if self._action_close(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been closed.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were closed.')), 'info', 'threads')
+
+    def _action_close(self, ids):
         closed = []
         closed = []
-        last_posts = []
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and not thread.closed:
             if thread.pk in ids and not thread.closed:
                 closed.append(thread.pk)
                 closed.append(thread.pk)
-                thread.last_post.set_checkpoint(self.request, 'closed')
-                last_posts.append(thread.last_post.pk)
+                thread.set_checkpoint(self.request, 'closed')
         if closed:
         if closed:
-            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
             Thread.objects.filter(id__in=closed).update(closed=True)
             Thread.objects.filter(id__in=closed).update(closed=True)
-            self.request.messages.set_flash(Message(_('Selected threads have been closed.')), 'success', 'threads')
+        return closed
 
 
     def action_undelete(self, ids):
     def action_undelete(self, ids):
+        if self._action_undelete(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been restored.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were restored.')), 'info', 'threads')
+
+    def _action_undelete(self, ids):
         undeleted = []
         undeleted = []
-        last_posts = []
-        posts = 0
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and thread.deleted:
             if thread.pk in ids and thread.deleted:
                 undeleted.append(thread.pk)
                 undeleted.append(thread.pk)
-                posts += thread.replies + 1
                 thread.start_post.deleted = False
                 thread.start_post.deleted = False
                 thread.start_post.save(force_update=True)
                 thread.start_post.save(force_update=True)
-                thread.last_post.set_checkpoint(self.request, 'undeleted')
+                thread.sync()
+                thread.save(force_update=True)
+                thread.set_checkpoint(self.request, 'undeleted')
         if undeleted:
         if undeleted:
-            self.request.monitor['threads'] = int(self.request.monitor['threads']) + len(undeleted)
-            self.request.monitor['posts'] = int(self.request.monitor['posts']) + posts
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
-            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
-            Thread.objects.filter(id__in=undeleted).update(deleted=False)
-            self.request.messages.set_flash(Message(_('Selected threads have been undeleted.')), 'success', 'threads')
+        return undeleted
 
 
     def action_soft(self, ids):
     def action_soft(self, ids):
+        if self._action_soft(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been hidden.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were hidden.')), 'info', 'threads')
+
+    def _action_soft(self, ids):
         deleted = []
         deleted = []
-        last_posts = []
-        posts = 0
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids and not thread.deleted:
             if thread.pk in ids and not thread.deleted:
                 deleted.append(thread.pk)
                 deleted.append(thread.pk)
-                posts += thread.replies + 1
                 thread.start_post.deleted = True
                 thread.start_post.deleted = True
                 thread.start_post.save(force_update=True)
                 thread.start_post.save(force_update=True)
-                thread.last_post.set_checkpoint(self.request, 'deleted')
-                last_posts.append(thread.last_post.pk)
+                thread.sync()
+                thread.save(force_update=True)
+                thread.set_checkpoint(self.request, 'deleted')
         if deleted:
         if deleted:
-            self.request.monitor['threads'] = int(self.request.monitor['threads']) - len(deleted)
-            self.request.monitor['posts'] = int(self.request.monitor['posts']) - posts
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
-            Post.objects.filter(id__in=last_posts).update(checkpoints=True)
-            Thread.objects.filter(id__in=deleted).update(deleted=True)
-            self.request.messages.set_flash(Message(_('Selected threads have been softly deleted.')), 'success', 'threads')
+        return deleted
 
 
     def action_hard(self, ids):
     def action_hard(self, ids):
+        if self._action_hard(ids):
+            self.request.messages.set_flash(Message(_('Selected threads have been deleted.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No threads were deleted.')), 'info', 'threads')
+    
+    def _action_hard(self, ids):        
         deleted = []
         deleted = []
-        posts = 0
         for thread in self.threads:
         for thread in self.threads:
             if thread.pk in ids:
             if thread.pk in ids:
                 deleted.append(thread.pk)
                 deleted.append(thread.pk)
-                posts += thread.replies + 1
                 thread.delete()
                 thread.delete()
         if deleted:
         if deleted:
-            self.request.monitor['threads'] = int(self.request.monitor['threads']) - len(deleted)
-            self.request.monitor['posts'] = int(self.request.monitor['posts']) - posts
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected threads have been deleted.')), 'success', 'threads')
+        return deleted

+ 4 - 1
misago/apps/threadtype/list/views.py

@@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.apps.errors import error403, error404
 from misago.apps.errors import error403, error404
 from misago.forms import Form, FormFields
 from misago.forms import Form, FormFields
+from misago.messages import Message
 from misago.models import Forum, Thread, Post
 from misago.models import Forum, Thread, Post
 from misago.readstrackers import ForumsTracker
 from misago.readstrackers import ForumsTracker
 from misago.apps.threadtype.base import ViewBase
 from misago.apps.threadtype.base import ViewBase
@@ -97,7 +98,9 @@ class ThreadsListBaseView(ViewBase):
             self._type_available()
             self._type_available()
             self._fetch_forum()
             self._fetch_forum()
             self._check_permissions()
             self._check_permissions()
-            self.fetch_threads()
+            response = self.fetch_threads()
+            if response:
+                return response
             self.form = None
             self.form = None
             self.make_form()
             self.make_form()
             if self.form:
             if self.form:

+ 26 - 4
misago/apps/threadtype/mixins.py

@@ -1,19 +1,41 @@
 from django import forms
 from django import forms
-from django.utils.translation import ungettext
+from django.utils import timezone
+from django.utils.translation import ungettext_lazy, ugettext_lazy as _
 from misago.utils.strings import slugify
 from misago.utils.strings import slugify
 
 
+class FloodProtectionMixin(object):
+    def clean(self):
+        cleaned_data = super(FloodProtectionMixin, self).clean()
+        if self.request.user.last_post:
+            diff = timezone.now() - self.request.user.last_post
+            diff = diff.seconds + (diff.days * 86400)
+            flood_limit = 35
+            wait_for = flood - diff
+            if wait_for > 0:
+                if wait_for < 5:
+                    raise forms.ValidationError(_("You can't post one message so quickly after another. Please wait a moment and try again."))
+                else:
+                    raise forms.ValidationError(ungettext(
+                            "You can't post one message so quickly after another. Please wait %(seconds)d second and try again.",
+                            "You can't post one message so quickly after another. Please wait %(seconds)d seconds and try again.",
+                        wait_for) % {
+                            'seconds': wait_for,
+                        })
+        return cleaned_data
+
+
 class ValidateThreadNameMixin(object):
 class ValidateThreadNameMixin(object):
     def clean_thread_name(self):
     def clean_thread_name(self):
         data = self.cleaned_data['thread_name']
         data = self.cleaned_data['thread_name']
         slug = slugify(data)
         slug = slugify(data)
         if len(slug) < self.request.settings['thread_name_min']:
         if len(slug) < self.request.settings['thread_name_min']:
-            raise forms.ValidationError(ungettext(
+            raise forms.ValidationError(ungettext_laxy(
                                                   "Thread name must contain at least one alpha-numeric character.",
                                                   "Thread name must contain at least one alpha-numeric character.",
                                                   "Thread name must contain at least %(count)d alpha-numeric characters.",
                                                   "Thread name must contain at least %(count)d alpha-numeric characters.",
                                                   self.request.settings['thread_name_min']
                                                   self.request.settings['thread_name_min']
                                                   ) % {'count': self.request.settings['thread_name_min']})
                                                   ) % {'count': self.request.settings['thread_name_min']})
         if len(data) > self.request.settings['thread_name_max']:
         if len(data) > self.request.settings['thread_name_max']:
-            raise forms.ValidationError(ungettext(
+            raise forms.ValidationError(ungettext_laxy(
                                                   "Thread name cannot be longer than %(count)d character.",
                                                   "Thread name cannot be longer than %(count)d character.",
                                                   "Thread name cannot be longer than %(count)d characters.",
                                                   "Thread name cannot be longer than %(count)d characters.",
                                                   self.request.settings['thread_name_max']
                                                   self.request.settings['thread_name_max']
@@ -25,7 +47,7 @@ class ValidatePostLengthMixin(object):
     def clean_post(self):
     def clean_post(self):
         data = self.cleaned_data['post']
         data = self.cleaned_data['post']
         if len(data) < self.request.settings['post_length_min']:
         if len(data) < self.request.settings['post_length_min']:
-            raise forms.ValidationError(ungettext(
+            raise forms.ValidationError(ungettext_laxy(
                                                   "Post content cannot be empty.",
                                                   "Post content cannot be empty.",
                                                   "Post content cannot be shorter than %(count)d characters.",
                                                   "Post content cannot be shorter than %(count)d characters.",
                                                   self.request.settings['post_length_min']
                                                   self.request.settings['post_length_min']

+ 26 - 4
misago/apps/threadtype/posting/base.py

@@ -3,7 +3,7 @@ from django.utils import timezone
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.apps.errors import error403, error404
 from misago.apps.errors import error403, error404
 from misago.forms import FormLayout
 from misago.forms import FormLayout
-from misago.markdown import post_markdown
+from misago.markdown import emojis, post_markdown
 from misago.messages import Message
 from misago.messages import Message
 from misago.models import Forum, Thread, Post, WatchedThread
 from misago.models import Forum, Thread, Post, WatchedThread
 from misago.utils.translation import ugettext_lazy
 from misago.utils.translation import ugettext_lazy
@@ -22,6 +22,12 @@ class PostingBaseView(ViewBase):
             self.parents = Forum.objects.forum_parents(self.forum.pk)
             self.parents = Forum.objects.forum_parents(self.forum.pk)
 
 
     def record_edit(self, form, old_name, old_post):
     def record_edit(self, form, old_name, old_post):
+        self.post.current_date = timezone.now()
+        self.post.edits += 1
+        self.post.edit_user = self.request.user
+        self.post.edit_user_name = self.request.user.username
+        self.post.edit_user_slug = self.request.user.username_slug
+        self.post.save(force_update=True)
         self.post.change_set.create(
         self.post.change_set.create(
                                     forum=self.forum,
                                     forum=self.forum,
                                     thread=self.thread,
                                     thread=self.thread,
@@ -29,7 +35,7 @@ class PostingBaseView(ViewBase):
                                     user=self.request.user,
                                     user=self.request.user,
                                     user_name=self.request.user.username,
                                     user_name=self.request.user.username,
                                     user_slug=self.request.user.username_slug,
                                     user_slug=self.request.user.username_slug,
-                                    date=self.post.edit_date,
+                                    date=self.post.current_date,
                                     ip=self.request.session.get_ip(self.request),
                                     ip=self.request.session.get_ip(self.request),
                                     agent=self.request.META.get('HTTP_USER_AGENT'),
                                     agent=self.request.META.get('HTTP_USER_AGENT'),
                                     reason=form.cleaned_data['edit_reason'],
                                     reason=form.cleaned_data['edit_reason'],
@@ -43,21 +49,36 @@ class PostingBaseView(ViewBase):
     def after_form(self, form):
     def after_form(self, form):
         pass
         pass
 
 
+    def email_watchers(self, notified_users):
+        pass
+
     def notify_users(self):
     def notify_users(self):
         try:
         try:
             post_content = self.md
             post_content = self.md
         except AttributeError:
         except AttributeError:
             post_content = False
             post_content = False
 
 
+        notified_users = []
+
         if post_content:
         if post_content:
             try:
             try:
-                if self.quote and self.quote.user_id:
+                if (self.quote and self.quote.user_id and
+                        self.quote.user.username_slug in post_content.mentions):
                     del post_content.mentions[self.quote.user.username_slug]
                     del post_content.mentions[self.quote.user.username_slug]
+                    if not self.quote.user in self.post.mentions.all():
+                        notified_users.append(self.quote.user)
+                        self.post.mentions.add(self.quote.user)
+                        alert = self.quote.user.alert(ugettext_lazy("%(username)s has replied to your post in thread %(thread)s").message)
+                        alert.profile('username', self.request.user)
+                        alert.post('thread', self.type_prefix, self.thread, self.post)
+                        alert.save_all()
             except KeyError:
             except KeyError:
                 pass
                 pass
             if post_content.mentions:
             if post_content.mentions:
+                notified_users += [x for x in post_content.mentions.itervalues()]
                 self.post.notify_mentioned(self.request, self.type_prefix, post_content.mentions)
                 self.post.notify_mentioned(self.request, self.type_prefix, post_content.mentions)
                 self.post.save(force_update=True)
                 self.post.save(force_update=True)
+        self.email_watchers(notified_users)
 
 
     def watch_thread(self):
     def watch_thread(self):
         if self.request.user.subscribe_start:
         if self.request.user.subscribe_start:
@@ -105,7 +126,7 @@ class PostingBaseView(ViewBase):
                 if 'preview' in request.POST:
                 if 'preview' in request.POST:
                     form.empty_errors()
                     form.empty_errors()
                     if form['post'].value():
                     if form['post'].value():
-                        md, post_preview = post_markdown(request, form['post'].value())
+                        md, post_preview = post_markdown(form['post'].value())
                     else:
                     else:
                         md, post_preview = None, None
                         md, post_preview = None, None
                 else:
                 else:
@@ -138,5 +159,6 @@ class PostingBaseView(ViewBase):
                                                  'parents': self.parents,
                                                  'parents': self.parents,
                                                  'preview': post_preview,
                                                  'preview': post_preview,
                                                  'form': FormLayout(form),
                                                  'form': FormLayout(form),
+                                                 'emojis': emojis(),
                                                  }),
                                                  }),
                                                 context_instance=RequestContext(request));
                                                 context_instance=RequestContext(request));

+ 3 - 11
misago/apps/threadtype/posting/editreply.py

@@ -20,7 +20,6 @@ class EditReplyBaseView(PostingBaseView):
                 }
                 }
 
 
     def post_form(self, form):
     def post_form(self, form):
-        now = timezone.now()
         old_post = self.post.post
         old_post = self.post.post
 
 
         changed_thread = False
         changed_thread = False
@@ -33,11 +32,9 @@ class EditReplyBaseView(PostingBaseView):
             self.thread.closed = not self.thread.closed
             self.thread.closed = not self.thread.closed
             changed_thread = True
             changed_thread = True
             if self.thread.closed:
             if self.thread.closed:
-                self.thread.last_post.set_checkpoint(self.request, 'closed')
+                self.thread.set_checkpoint(self.request, 'closed')
             else:
             else:
-                self.thread.last_post.set_checkpoint(self.request, 'opened')
-            if self.thread.last_post_id != self.post.pk or not changed_post:
-                self.thread.last_post.save(force_update=True)
+                self.thread.set_checkpoint(self.request, 'opened')
 
 
         if ('thread_weight' in form.cleaned_data and
         if ('thread_weight' in form.cleaned_data and
                 form.cleaned_data['thread_weight'] != self.thread.weight):
                 form.cleaned_data['thread_weight'] != self.thread.weight):
@@ -49,11 +46,6 @@ class EditReplyBaseView(PostingBaseView):
 
 
         if changed_post:
         if changed_post:
             self.post.post = form.cleaned_data['post']
             self.post.post = form.cleaned_data['post']
-            self.md, self.post.post_preparsed = post_markdown(self.request, form.cleaned_data['post'])
-            self.post.edits += 1
-            self.post.edit_date = now
-            self.post.edit_user = self.request.user
-            self.post.edit_user_name = self.request.user.username
-            self.post.edit_user_slug = self.request.user.username_slug
+            self.md, self.post.post_preparsed = post_markdown(form.cleaned_data['post'])
             self.post.save(force_update=True)
             self.post.save(force_update=True)
             self.record_edit(form, self.thread.name, old_post)
             self.record_edit(form, self.thread.name, old_post)

+ 7 - 11
misago/apps/threadtype/posting/editthread.py

@@ -22,7 +22,6 @@ class EditThreadBaseView(PostingBaseView):
                 }
                 }
 
 
     def post_form(self, form):
     def post_form(self, form):
-        now = timezone.now()
         old_name = self.thread.name
         old_name = self.thread.name
         old_post = self.post.post
         old_post = self.post.post
 
 
@@ -36,11 +35,9 @@ class EditThreadBaseView(PostingBaseView):
             self.thread.closed = not self.thread.closed
             self.thread.closed = not self.thread.closed
             changed_thread = True
             changed_thread = True
             if self.thread.closed:
             if self.thread.closed:
-                self.thread.last_post.set_checkpoint(self.request, 'closed')
+                self.thread.set_checkpoint(self.request, 'closed')
             else:
             else:
-                self.thread.last_post.set_checkpoint(self.request, 'opened')
-            if self.thread.last_post_id != self.post.pk or not changed_post:
-                self.thread.last_post.save(force_update=True)
+                self.thread.set_checkpoint(self.request, 'opened')
 
 
         if ('thread_weight' in form.cleaned_data and
         if ('thread_weight' in form.cleaned_data and
                 form.cleaned_data['thread_weight'] != self.thread.weight):
                 form.cleaned_data['thread_weight'] != self.thread.weight):
@@ -51,15 +48,14 @@ class EditThreadBaseView(PostingBaseView):
             self.thread.name = form.cleaned_data['thread_name']
             self.thread.name = form.cleaned_data['thread_name']
             self.thread.slug = slugify(form.cleaned_data['thread_name'])
             self.thread.slug = slugify(form.cleaned_data['thread_name'])
             self.thread.save(force_update=True)
             self.thread.save(force_update=True)
+            if self.forum.last_thread_id == self.thread.pk:
+                self.forum.last_thread_name = self.thread.name
+                self.forum.last_thread_slug = self.thread.slug
+                self.forum.save(force_update=True)
 
 
         if changed_post:
         if changed_post:
             self.post.post = form.cleaned_data['post']
             self.post.post = form.cleaned_data['post']
-            self.md, self.post.post_preparsed = post_markdown(self.request, form.cleaned_data['post'])
-            self.post.edits += 1
-            self.post.edit_date = now
-            self.post.edit_user = self.request.user
-            self.post.edit_user_name = self.request.user.username
-            self.post.edit_user_slug = self.request.user.username_slug
+            self.md, self.post.post_preparsed = post_markdown(form.cleaned_data['post'])
             self.post.save(force_update=True)
             self.post.save(force_update=True)
 
 
         if changed_thread or changed_post:
         if changed_thread or changed_post:

+ 6 - 3
misago/apps/threadtype/posting/forms.py

@@ -1,11 +1,14 @@
 from django import forms
 from django import forms
 from django.conf import settings
 from django.conf import settings
+from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
-from misago.apps.threadtype.mixins import ValidateThreadNameMixin, ValidatePostLengthMixin
+from misago.apps.threadtype.mixins import (FloodProtectionMixin,
+                                           ValidateThreadNameMixin,
+                                           ValidatePostLengthMixin)
 from misago.forms import Form
 from misago.forms import Form
 from misago.validators import validate_sluggable
 from misago.validators import validate_sluggable
 
 
-class PostingForm(Form, ValidatePostLengthMixin):
+class PostingForm(FloodProtectionMixin, Form, ValidatePostLengthMixin):
     include_thread_weight = True
     include_thread_weight = True
     include_close_thread = True
     include_close_thread = True
     post = forms.CharField(widget=forms.Textarea)
     post = forms.CharField(widget=forms.Textarea)
@@ -20,7 +23,7 @@ class PostingForm(Form, ValidatePostLengthMixin):
                        [
                        [
                         None,
                         None,
                         [
                         [
-                         ('post', {'label': _("Post Content")}),
+                         ('post', {'label': _("Message Body")}),
                          ]
                          ]
                         ]
                         ]
                        ]
                        ]

+ 29 - 38
misago/apps/threadtype/posting/newreply.py

@@ -22,15 +22,7 @@ class NewReplyBaseView(PostingBaseView):
 
 
     def form_initial_data(self):
     def form_initial_data(self):
         if self.quote:
         if self.quote:
-            quote_post = []
-            if self.quote.user:
-                quote_post.append('@%s' % self.quote.user.username)
-            else:
-                quote_post.append('@%s' % self.quote.user_name)
-            for line in self.quote.post.splitlines():
-                quote_post.append('> %s' % line)
-            quote_post.append('\r\n')
-            return {'post': '\r\n'.join(quote_post)}
+            return {'post': self.quote.quote()}
         return {}
         return {}
 
 
     def post_form(self, form):
     def post_form(self, form):
@@ -39,7 +31,7 @@ class NewReplyBaseView(PostingBaseView):
                       and self.request.acl.threads.acl[self.forum.pk]['can_start_threads'] == 1)
                       and self.request.acl.threads.acl[self.forum.pk]['can_start_threads'] == 1)
 
 
         self.thread.previous_last = self.thread.last_post
         self.thread.previous_last = self.thread.last_post
-        self.md, post_preparsed = post_markdown(self.request, form.cleaned_data['post'])
+        self.md, post_preparsed = post_markdown(form.cleaned_data['post'])
 
 
         # Count merge diff and see if we are merging
         # Count merge diff and see if we are merging
         merge_diff = (now - self.thread.last)
         merge_diff = (now - self.thread.last)
@@ -51,8 +43,9 @@ class NewReplyBaseView(PostingBaseView):
             merged = True
             merged = True
             self.post = self.thread.last_post
             self.post = self.thread.last_post
             self.post.date = now
             self.post.date = now
-            self.post.post = '%s\n\n- - -\n**%s**\n%s' % (self.post.post, _("Added on %(date)s:") % {'date': date(now, 'SHORT_DATETIME_FORMAT')}, form.cleaned_data['post'])
-            self.md, self.post.post_preparsed = post_markdown(self.request, self.post.post)
+            self.post.current_date = now
+            self.post.post = '%s\n\n%s' % (self.post.post, form.cleaned_data['post'])
+            self.md, self.post.post_preparsed = post_markdown(self.post.post)
             self.post.save(force_update=True)
             self.post.save(force_update=True)
         else:
         else:
             # Create new post
             # Create new post
@@ -67,7 +60,7 @@ class NewReplyBaseView(PostingBaseView):
                                             post=form.cleaned_data['post'],
                                             post=form.cleaned_data['post'],
                                             post_preparsed=post_preparsed,
                                             post_preparsed=post_preparsed,
                                             date=now,
                                             date=now,
-                                            merge=self.thread.merges,
+                                            current_date=now,
                                             moderated=moderation,
                                             moderated=moderation,
                                         )
                                         )
 
 
@@ -85,12 +78,9 @@ class NewReplyBaseView(PostingBaseView):
             if self.thread.last_poster_id != self.request.user.pk:
             if self.thread.last_poster_id != self.request.user.pk:
                 self.thread.score += self.request.settings['thread_ranking_reply_score']
                 self.thread.score += self.request.settings['thread_ranking_reply_score']
 
 
-        # Save updated thread
-        self.thread.save(force_update=True)
-
         # Update forum and monitor
         # Update forum and monitor
         if not moderation and not merged:
         if not moderation and not merged:
-            self.request.monitor['posts'] = int(self.request.monitor['posts']) + 1
+            self.request.monitor.increase('posts')
             self.forum.posts += 1
             self.forum.posts += 1
             self.forum.new_last_thread(self.thread)
             self.forum.new_last_thread(self.thread)
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
@@ -102,7 +92,6 @@ class NewReplyBaseView(PostingBaseView):
 
 
         # Update user
         # Update user
         if not moderation and not merged:
         if not moderation and not merged:
-            self.request.user.threads += 1
             self.request.user.posts += 1
             self.request.user.posts += 1
         self.request.user.last_post = now
         self.request.user.last_post = now
         self.request.user.save(force_update=True)
         self.request.user.save(force_update=True)
@@ -116,30 +105,32 @@ class NewReplyBaseView(PostingBaseView):
                 and not merged and not moderation and not self.thread.closed
                 and not merged and not moderation and not self.thread.closed
                 and self.thread.replies >= self.request.settings.thread_length):
                 and self.thread.replies >= self.request.settings.thread_length):
             self.thread.closed = True
             self.thread.closed = True
-            self.post.set_checkpoint(self.request, 'limit')
-            self.post.save(force_update=True)
-            self.thread.save(force_update=True)
+            self.thread.set_checkpoint(self.request, 'limit')
         elif 'close_thread' in form.cleaned_data and form.cleaned_data['close_thread']:
         elif 'close_thread' in form.cleaned_data and form.cleaned_data['close_thread']:
             self.thread.closed = not self.thread.closed
             self.thread.closed = not self.thread.closed
-            if merged:
-                checkpoint_post = self.post
-            else:
-                checkpoint_post = self.thread.previous_last
             if self.thread.closed:
             if self.thread.closed:
-                checkpoint_post.set_checkpoint(self.request, 'closed')
+                self.thread.set_checkpoint(self.request, 'closed')
             else:
             else:
-                checkpoint_post.set_checkpoint(self.request, 'opened')
-            checkpoint_post.save(force_update=True)
-            self.thread.save(force_update=True)
+                self.thread.set_checkpoint(self.request, 'opened')
 
 
-        # Notify user we quoted?
-        if (self.quote and self.quote.user_id and not merged
+        # Save updated thread
+        self.thread.save(force_update=True)
+
+        # Mute quoted user?
+        if not (self.quote and self.quote.user_id and not merged
                 and self.quote.user_id != self.request.user.pk
                 and self.quote.user_id != self.request.user.pk
                 and not self.quote.user.is_ignoring(self.request.user)):
                 and not self.quote.user.is_ignoring(self.request.user)):
-            alert = self.quote.user.alert(ugettext_lazy("%(username)s has replied to your post in thread %(thread)s").message)
-            alert.profile('username', self.request.user)
-            alert.post('thread', self.type_prefix, self.thread, self.post)
-            alert.save_all()
-
-        # E-mail users about new response
-        self.thread.email_watchers(self.request, self.type_prefix, self.post)
+            self.quote = None
+
+    # E-mail users about new response
+    def email_watchers(self, notified_users):
+        emailed = self.thread.email_watchers(self.request, self.type_prefix, self.post)
+        for user in emailed:
+            if not user in notified_users:
+                if user.pk == self.thread.start_poster_id:
+                    alert = user.alert(ugettext_lazy("%(username)s has replied to your thread %(thread)s").message)
+                else:
+                    alert = user.alert(ugettext_lazy("%(username)s has replied to thread %(thread)s that you are watching").message)
+                alert.profile('username', self.request.user)
+                alert.post('thread', self.type_prefix, self.thread, self.post)
+                alert.save_all()

+ 4 - 3
misago/apps/threadtype/posting/newthread.py

@@ -33,7 +33,7 @@ class NewThreadBaseView(PostingBaseView):
                                             )
                                             )
 
 
         # Create our post
         # Create our post
-        self.md, post_preparsed = post_markdown(self.request, form.cleaned_data['post'])
+        self.md, post_preparsed = post_markdown(form.cleaned_data['post'])
         self.post = Post.objects.create(
         self.post = Post.objects.create(
                                         forum=self.forum,
                                         forum=self.forum,
                                         thread=self.thread,
                                         thread=self.thread,
@@ -44,6 +44,7 @@ class NewThreadBaseView(PostingBaseView):
                                         post=form.cleaned_data['post'],
                                         post=form.cleaned_data['post'],
                                         post_preparsed=post_preparsed,
                                         post_preparsed=post_preparsed,
                                         date=now,
                                         date=now,
+                                        current_date=now,
                                         moderated=moderation,
                                         moderated=moderation,
                                         )
                                         )
 
 
@@ -62,8 +63,8 @@ class NewThreadBaseView(PostingBaseView):
 
 
         # Update forum monitor
         # Update forum monitor
         if not moderation:
         if not moderation:
-            self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
-            self.request.monitor['posts'] = int(self.request.monitor['posts']) + 1
+            self.request.monitor.increase('threads')
+            self.request.monitor.increase('posts')
             self.forum.threads += 1
             self.forum.threads += 1
             self.forum.posts += 1
             self.forum.posts += 1
             self.forum.new_last_thread(self.thread)
             self.forum.new_last_thread(self.thread)

+ 3 - 2
misago/apps/threadtype/thread/forms.py

@@ -1,6 +1,7 @@
 from django import forms
 from django import forms
 from misago.forms import Form
 from misago.forms import Form
-from misago.apps.threadtype.mixins import ValidatePostLengthMixin
+from misago.apps.threadtype.mixins import (FloodProtectionMixin,
+                                           ValidatePostLengthMixin)
 
 
-class QuickReplyForm(Form, ValidatePostLengthMixin):
+class QuickReplyForm(FloodProtectionMixin, Form, ValidatePostLengthMixin):
     post = forms.CharField(widget=forms.Textarea)
     post = forms.CharField(widget=forms.Textarea)

+ 3 - 2
misago/apps/threadtype/thread/moderation/forms.py

@@ -2,9 +2,10 @@ from django import forms
 from django.http import Http404
 from django.http import Http404
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.acl.exceptions import ACLError403, ACLError404
-from misago.apps.threadtype.mixins import ValidateThreadNameMixin
-from misago.forms import Form
+from misago.forms import Form, ForumChoiceField
+from misago.models import Thread
 from misago.validators import validate_sluggable
 from misago.validators import validate_sluggable
+from misago.apps.threadtype.mixins import ValidateThreadNameMixin
 
 
 class SplitThreadForm(Form, ValidateThreadNameMixin):
 class SplitThreadForm(Form, ValidateThreadNameMixin):
     def finalize_form(self):
     def finalize_form(self):

+ 16 - 15
misago/apps/threadtype/thread/moderation/posts.py

@@ -21,6 +21,8 @@ class PostsModeration(object):
             self.thread.sync()
             self.thread.sync()
             self.thread.save(force_update=True)
             self.thread.save(force_update=True)
             self.request.messages.set_flash(Message(_('Selected posts have been accepted and made visible to other members.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Selected posts have been accepted and made visible to other members.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were accepted.')), 'info', 'threads')
 
 
     def post_action_merge(self, ids):
     def post_action_merge(self, ids):
         users = []
         users = []
@@ -38,7 +40,8 @@ class PostsModeration(object):
         for post in posts[1:]:
         for post in posts[1:]:
             post.merge_with(new_post)
             post.merge_with(new_post)
             post.delete()
             post.delete()
-        md, new_post.post_preparsed = post_markdown(self.request, new_post.post)
+        md, new_post.post_preparsed = post_markdown(new_post.post)
+        new_post.current_date = timezone.now()
         new_post.save(force_update=True)
         new_post.save(force_update=True)
         self.thread.sync()
         self.thread.sync()
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
@@ -65,14 +68,8 @@ class PostsModeration(object):
                 new_thread.last_poster_name = 'n'
                 new_thread.last_poster_name = 'n'
                 new_thread.last_poster_slug = 'n'
                 new_thread.last_poster_slug = 'n'
                 new_thread.save(force_insert=True)
                 new_thread.save(force_insert=True)
-                prev_merge = -1
-                merge = -1
                 for post in self.posts:
                 for post in self.posts:
                     if post.pk in ids:
                     if post.pk in ids:
-                        if prev_merge != post.merge:
-                            prev_merge = post.merge
-                            merge += 1
-                        post.merge = merge
                         post.move_to(new_thread)
                         post.move_to(new_thread)
                         post.save(force_update=True)
                         post.save(force_update=True)
                 new_thread.sync()
                 new_thread.sync()
@@ -110,14 +107,8 @@ class PostsModeration(object):
             form = MovePostsForm(self.request.POST, request=self.request, thread=self.thread)
             form = MovePostsForm(self.request.POST, request=self.request, thread=self.thread)
             if form.is_valid():
             if form.is_valid():
                 thread = form.cleaned_data['thread_url']
                 thread = form.cleaned_data['thread_url']
-                prev_merge = -1
-                merge = -1
                 for post in self.posts:
                 for post in self.posts:
                     if post.pk in ids:
                     if post.pk in ids:
-                        if prev_merge != post.merge:
-                            prev_merge = post.merge
-                            merge += 1
-                        post.merge = merge + thread.merges
                         post.move_to(thread)
                         post.move_to(thread)
                         post.save(force_update=True)
                         post.save(force_update=True)
                 if self.thread.post_set.count() == 0:
                 if self.thread.post_set.count() == 0:
@@ -161,6 +152,8 @@ class PostsModeration(object):
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
             self.request.messages.set_flash(Message(_('Selected posts have been restored.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Selected posts have been restored.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were restored.')), 'info', 'threads')
 
 
     def post_action_protect(self, ids):
     def post_action_protect(self, ids):
         protected = 0
         protected = 0
@@ -170,6 +163,8 @@ class PostsModeration(object):
         if protected:
         if protected:
             self.thread.post_set.filter(id__in=ids).update(protected=True)
             self.thread.post_set.filter(id__in=ids).update(protected=True)
             self.request.messages.set_flash(Message(_('Selected posts have been protected from edition.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Selected posts have been protected from edition.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were protected.')), 'info', 'threads')
 
 
     def post_action_unprotect(self, ids):
     def post_action_unprotect(self, ids):
         unprotected = 0
         unprotected = 0
@@ -179,6 +174,8 @@ class PostsModeration(object):
         if unprotected:
         if unprotected:
             self.thread.post_set.filter(id__in=ids).update(protected=False)
             self.thread.post_set.filter(id__in=ids).update(protected=False)
             self.request.messages.set_flash(Message(_('Protection from editions has been removed from selected posts.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Protection from editions has been removed from selected posts.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were unprotected.')), 'info', 'threads')
 
 
     def post_action_soft(self, ids):
     def post_action_soft(self, ids):
         deleted = []
         deleted = []
@@ -188,12 +185,14 @@ class PostsModeration(object):
                     raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
                     raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
                 deleted.append(post.pk)
                 deleted.append(post.pk)
         if deleted:
         if deleted:
-            self.thread.post_set.filter(id__in=deleted).update(deleted=True)
+            self.thread.post_set.filter(id__in=deleted).update(deleted=True, current_date=timezone.now())
             self.thread.sync()
             self.thread.sync()
             self.thread.save(force_update=True)
             self.thread.save(force_update=True)
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected posts have been deleted.')), 'success', 'threads')
+            self.request.messages.set_flash(Message(_('Selected posts have been hidden.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were hidden.')), 'info', 'threads')
 
 
     def post_action_hard(self, ids):
     def post_action_hard(self, ids):
         deleted = []
         deleted = []
@@ -211,3 +210,5 @@ class PostsModeration(object):
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
             self.request.messages.set_flash(Message(_('Selected posts have been deleted.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Selected posts have been deleted.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were deleted.')), 'info', 'threads')

+ 51 - 24
misago/apps/threadtype/thread/moderation/thread.py

@@ -12,7 +12,7 @@ class ThreadModeration(object):
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
         self.thread.start_post.moderated = False
         self.thread.start_post.moderated = False
         self.thread.start_post.save(force_update=True)
         self.thread.start_post.save(force_update=True)
-        self.thread.last_post.set_checkpoint(self.request, 'accepted')
+        self.thread.set_checkpoint(self.request, 'accepted')
         # Sync user
         # Sync user
         if self.thread.last_post.user:
         if self.thread.last_post.user:
             self.thread.start_post.user.threads += 1
             self.thread.start_post.user.threads += 1
@@ -22,23 +22,36 @@ class ThreadModeration(object):
         self.forum.sync()
         self.forum.sync()
         self.forum.save(force_update=True)
         self.forum.save(force_update=True)
         # Update monitor
         # Update monitor
-        self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
-        self.request.monitor['posts'] = int(self.request.monitor['posts']) + self.thread.replies + 1
+        self.request.monitor.increase('threads')
+        self.request.monitor.increase('posts', self.thread.replies + 1)
+        # After
+        self.after_thread_action_accept()
+
+    def after_thread_action_accept(self):
         self.request.messages.set_flash(Message(_('Thread has been marked as reviewed and made visible to other members.')), 'success', 'threads')
         self.request.messages.set_flash(Message(_('Thread has been marked as reviewed and made visible to other members.')), 'success', 'threads')
 
 
     def thread_action_annouce(self):
     def thread_action_annouce(self):
         self.thread.weight = 2
         self.thread.weight = 2
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
+        self.after_thread_action_annouce()
+
+    def after_thread_action_annouce(self):
         self.request.messages.set_flash(Message(_('Thread has been turned into announcement.')), 'success', 'threads')
         self.request.messages.set_flash(Message(_('Thread has been turned into announcement.')), 'success', 'threads')
 
 
     def thread_action_sticky(self):
     def thread_action_sticky(self):
         self.thread.weight = 1
         self.thread.weight = 1
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
+        self.after_thread_action_sticky()
+    
+    def after_thread_action_sticky(self):
         self.request.messages.set_flash(Message(_('Thread has been turned into sticky.')), 'success', 'threads')
         self.request.messages.set_flash(Message(_('Thread has been turned into sticky.')), 'success', 'threads')
 
 
     def thread_action_normal(self):
     def thread_action_normal(self):
         self.thread.weight = 0
         self.thread.weight = 0
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
+        self.after_thread_action_normal()
+
+    def after_thread_action_normal(self):
         self.request.messages.set_flash(Message(_('Thread weight has been changed to normal.')), 'success', 'threads')
         self.request.messages.set_flash(Message(_('Thread weight has been changed to normal.')), 'success', 'threads')
 
 
     def thread_action_move(self):
     def thread_action_move(self):
@@ -49,6 +62,7 @@ class ThreadModeration(object):
                 new_forum = form.cleaned_data['new_forum']
                 new_forum = form.cleaned_data['new_forum']
                 self.thread.move_to(new_forum)
                 self.thread.move_to(new_forum)
                 self.thread.save(force_update=True)
                 self.thread.save(force_update=True)
+                self.thread.set_checkpoint(self.request, 'moved', forum=self.forum)
                 self.forum.sync()
                 self.forum.sync()
                 self.forum.save(force_update=True)
                 self.forum.save(force_update=True)
                 new_forum.sync()
                 new_forum.sync()
@@ -72,50 +86,60 @@ class ThreadModeration(object):
     def thread_action_open(self):
     def thread_action_open(self):
         self.thread.closed = False
         self.thread.closed = False
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
-        self.thread.last_post.set_checkpoint(self.request, 'opened')
+        self.thread.set_checkpoint(self.request, 'opened')
+        self.after_thread_action_open()
+
+    def after_thread_action_open(self):
         self.request.messages.set_flash(Message(_('Thread has been opened.')), 'success', 'threads')
         self.request.messages.set_flash(Message(_('Thread has been opened.')), 'success', 'threads')
 
 
     def thread_action_close(self):
     def thread_action_close(self):
         self.thread.closed = True
         self.thread.closed = True
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
-        self.thread.last_post.set_checkpoint(self.request, 'closed')
+        self.thread.set_checkpoint(self.request, 'closed')
+        self.after_thread_action_close()
+
+    def after_thread_action_close(self):
         self.request.messages.set_flash(Message(_('Thread has been closed.')), 'success', 'threads')
         self.request.messages.set_flash(Message(_('Thread has been closed.')), 'success', 'threads')
 
 
     def thread_action_undelete(self):
     def thread_action_undelete(self):
-        # Update thread
-        self.thread.deleted = False
-        self.thread.replies_deleted -= 1
-        self.thread.save(force_update=True)
         # Update first post in thread
         # Update first post in thread
         self.thread.start_post.deleted = False
         self.thread.start_post.deleted = False
         self.thread.start_post.save(force_update=True)
         self.thread.start_post.save(force_update=True)
+        # Update thread
+        self.thread.sync()
+        self.thread.save(force_update=True)
         # Set checkpoint
         # Set checkpoint
-        self.thread.last_post.set_checkpoint(self.request, 'undeleted')
+        self.thread.set_checkpoint(self.request, 'undeleted')
         # Update forum
         # Update forum
         self.forum.sync()
         self.forum.sync()
         self.forum.save(force_update=True)
         self.forum.save(force_update=True)
         # Update monitor
         # Update monitor
-        self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
-        self.request.monitor['posts'] = int(self.request.monitor['posts']) + self.thread.replies + 1
-        self.request.messages.set_flash(Message(_('Thread has been undeleted.')), 'success', 'threads')
+        self.request.monitor.increase('threads')
+        self.request.monitor.increase('posts', self.thread.replies + 1)
+        self.after_thread_action_undelete()
+
+    def after_thread_action_undelete(self):
+        self.request.messages.set_flash(Message(_('Thread has been restored.')), 'success', 'threads')
 
 
     def thread_action_soft(self):
     def thread_action_soft(self):
-        # Update thread
-        self.thread.deleted = True
-        self.thread.replies_deleted += 1
-        self.thread.save(force_update=True)
         # Update first post in thread
         # Update first post in thread
         self.thread.start_post.deleted = True
         self.thread.start_post.deleted = True
         self.thread.start_post.save(force_update=True)
         self.thread.start_post.save(force_update=True)
+        # Update thread
+        self.thread.sync()
+        self.thread.save(force_update=True)
         # Set checkpoint
         # Set checkpoint
-        self.thread.last_post.set_checkpoint(self.request, 'deleted')
+        self.thread.set_checkpoint(self.request, 'deleted')
         # Update forum
         # Update forum
         self.forum.sync()
         self.forum.sync()
         self.forum.save(force_update=True)
         self.forum.save(force_update=True)
         # Update monitor
         # Update monitor
-        self.request.monitor['threads'] = int(self.request.monitor['threads']) - 1
-        self.request.monitor['posts'] = int(self.request.monitor['posts']) - self.thread.replies - 1
-        self.request.messages.set_flash(Message(_('Thread has been deleted.')), 'success', 'threads')
+        self.request.monitor.decrease('threads')
+        self.request.monitor.decrease('posts', self.thread.replies + 1)
+        self.after_thread_action_soft()
+
+    def after_thread_action_soft(self):
+        self.request.messages.set_flash(Message(_('Thread has been hidden.')), 'success', 'threads')
 
 
     def thread_action_hard(self):
     def thread_action_hard(self):
         # Delete thread
         # Delete thread
@@ -124,7 +148,10 @@ class ThreadModeration(object):
         self.forum.sync()
         self.forum.sync()
         self.forum.save(force_update=True)
         self.forum.save(force_update=True)
         # Update monitor
         # Update monitor
-        self.request.monitor['threads'] = int(self.request.monitor['threads']) - 1
-        self.request.monitor['posts'] = int(self.request.monitor['posts']) - self.thread.replies - 1
-        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
+        self.request.monitor.decrease('threads')
+        self.request.monitor.decrease('posts', self.thread.replies + 1)
+        self.after_thread_action_hard()
         return self.threads_list_redirect()
         return self.threads_list_redirect()
+
+    def after_thread_action_hard(self):
+        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')

+ 21 - 8
misago/apps/threadtype/thread/views.py

@@ -1,6 +1,7 @@
 from django import forms
 from django import forms
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.forms import ValidationError
 from django.forms import ValidationError
+from django.http import Http404
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils import timezone
 from django.utils import timezone
@@ -8,6 +9,7 @@ from django.utils.translation import ugettext as _
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.apps.errors import error403, error404
 from misago.apps.errors import error403, error404
 from misago.forms import Form, FormLayout, FormFields
 from misago.forms import Form, FormLayout, FormFields
+from misago.markdown import emojis
 from misago.messages import Message
 from misago.messages import Message
 from misago.models import Forum, Thread, Post, Karma, WatchedThread
 from misago.models import Forum, Thread, Post, Karma, WatchedThread
 from misago.readstrackers import ThreadsTracker
 from misago.readstrackers import ThreadsTracker
@@ -35,16 +37,21 @@ class ThreadBaseView(ViewBase):
 
 
     def fetch_posts(self):
     def fetch_posts(self):
         self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
         self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
-        self.posts = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).prefetch_related('checkpoint_set', 'user', 'user__rank')
+        self.posts = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).prefetch_related('user', 'user__rank')
         
         
-        if self.thread.merges > 0:
-            self.posts = self.posts.order_by('merge', 'pk')
-        else:
-            self.posts = self.posts.order_by('pk')
+        self.posts = self.posts.order_by('id')
+
+        try:
+            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, self.request.settings.posts_per_page)
+        except Http404:
+            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
 
 
-        self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, self.request.settings.posts_per_page)
+        checkpoints_range = None
         if self.request.settings.posts_per_page < self.count:
         if self.request.settings.posts_per_page < self.count:
-            self.posts = self.posts[self.pagination['start']:self.pagination['stop']]
+            self.posts = self.posts[self.pagination['start']:self.pagination['stop'] + 1]
+            posts_len = len(self.posts)
+            checkpoints_range = self.posts[posts_len - 1].date
+            self.posts = self.posts[0:(posts_len - 2)]
 
 
         self.read_date = self.tracker.read_date(self.thread)
         self.read_date = self.tracker.read_date(self.thread)
 
 
@@ -62,6 +69,9 @@ class ThreadBaseView(ViewBase):
             if post.ignored:
             if post.ignored:
                 self.ignored = True
                 self.ignored = True
 
 
+        self.thread.set_checkpoints(self.request.acl.threads.can_see_all_checkpoints(self.forum),
+                                    self.posts, checkpoints_range)
+
         last_post = self.posts[len(self.posts) - 1]
         last_post = self.posts[len(self.posts) - 1]
 
 
         if not self.tracker.is_read(self.thread):
         if not self.tracker.is_read(self.thread):
@@ -173,7 +183,9 @@ class ThreadBaseView(ViewBase):
             self.fetch_thread()
             self.fetch_thread()
             self.check_forum_type()
             self.check_forum_type()
             self._check_permissions()
             self._check_permissions()
-            self.fetch_posts()
+            response = self.fetch_posts()
+            if response:
+                return response
             self.make_thread_form()
             self.make_thread_form()
             if self.thread_form:
             if self.thread_form:
                 response = self.handle_thread_form()
                 response = self.handle_thread_form()
@@ -207,6 +219,7 @@ class ThreadBaseView(ViewBase):
                                                  'ignored_posts': self.ignored,
                                                  'ignored_posts': self.ignored,
                                                  'watcher': self.watcher,
                                                  'watcher': self.watcher,
                                                  'pagination': self.pagination,
                                                  'pagination': self.pagination,
+                                                 'emojis': emojis(),
                                                  'quick_reply': FormFields(QuickReplyForm(request=request)).fields,
                                                  'quick_reply': FormFields(QuickReplyForm(request=request)).fields,
                                                  'thread_form': FormFields(self.thread_form).fields if self.thread_form else None,
                                                  'thread_form': FormFields(self.thread_form).fields if self.thread_form else None,
                                                  'posts_form': FormFields(self.posts_form).fields if self.posts_form else None,
                                                  'posts_form': FormFields(self.posts_form).fields if self.posts_form else None,

+ 4 - 0
misago/apps/usercp/avatar/views.py

@@ -1,5 +1,6 @@
 from path import path
 from path import path
 from PIL import Image
 from PIL import Image
+from zipfile import is_zipfile
 from django.conf import settings
 from django.conf import settings
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
@@ -122,6 +123,9 @@ def upload(request):
                     destination.write(chunk)
                     destination.write(chunk)
             request.user.save()
             request.user.save()
             try:
             try:
+                if is_zipfile(image_path):
+                    # Composite file upload
+                    raise ValidationError()                 
                 image = Image.open(image_path)
                 image = Image.open(image_path)
                 if not image.format in ['GIF', 'PNG', 'JPEG']:
                 if not image.format in ['GIF', 'PNG', 'JPEG']:
                     raise ValidationError()
                     raise ValidationError()

+ 2 - 2
misago/apps/usercp/credentials/forms.py

@@ -17,7 +17,7 @@ class CredentialsChangeForm(Form):
                [
                [
                 ('new_email', {'label': _('New E-mail'), 'help_text': _("Enter new e-mail address or leave this field empty if you want only to change your password.")}),
                 ('new_email', {'label': _('New E-mail'), 'help_text': _("Enter new e-mail address or leave this field empty if you want only to change your password.")}),
                 ('new_password', {'label': _('New Password'), 'help_text': _("Enter new password or leave this empty if you only want to change your e-mail address.")}),
                 ('new_password', {'label': _('New Password'), 'help_text': _("Enter new password or leave this empty if you only want to change your e-mail address.")}),
-                ('current_password', {'label': _('Current Password'), 'help_text': _("Confirm changes by entering new password.")})
+                ('current_password', {'label': _('Current Password'), 'help_text': _("Confirm changes by entering your current password.")})
                 ]
                 ]
                ),
                ),
               ]
               ]
@@ -37,7 +37,7 @@ class CredentialsChangeForm(Form):
 
 
     def clean_new_password(self):
     def clean_new_password(self):
         if self.cleaned_data['new_password']:
         if self.cleaned_data['new_password']:
-            validate_password(self.cleaned_data['new_password'])
+            validate_password(self.cleaned_data['new_password'],  self.request.settings)
         return self.cleaned_data['new_password']
         return self.cleaned_data['new_password']
 
 
     def clean_current_password(self):
     def clean_current_password(self):

+ 2 - 2
misago/apps/watchedthreads/urls.py

@@ -2,7 +2,7 @@ from django.conf.urls import patterns, url
 
 
 urlpatterns = patterns('misago.apps.watchedthreads.views',
 urlpatterns = patterns('misago.apps.watchedthreads.views',
     url(r'^$', 'watched_threads', name="watched_threads"),
     url(r'^$', 'watched_threads', name="watched_threads"),
-    url(r'^(?P<page>\d+)/$', 'watched_threads', name="watched_threads"),
+    url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'watched_threads', name="watched_threads"),
     url(r'^new/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),
     url(r'^new/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),
-    url(r'^new/(?P<page>\d+)/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),
+    url(r'^new/(?P<page>[1-9]([0-9]+)?)/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),
 )
 )

+ 9 - 1
misago/apps/watchedthreads/views.py

@@ -1,6 +1,7 @@
 from django import forms
 from django import forms
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db.models import F
 from django.db.models import F
+from django.http import Http404
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
@@ -17,10 +18,17 @@ def watched_threads(request, page=0, new=False):
     if not request.settings['enable_private_threads']:
     if not request.settings['enable_private_threads']:
         readable_forums.remove(Forum.objects.special_pk('private_threads'))
         readable_forums.remove(Forum.objects.special_pk('private_threads'))
     queryset = WatchedThread.objects.filter(user=request.user).filter(forum_id__in=readable_forums).select_related('thread').filter(thread__moderated=False).filter(thread__deleted=False)
     queryset = WatchedThread.objects.filter(user=request.user).filter(forum_id__in=readable_forums).select_related('thread').filter(thread__moderated=False).filter(thread__deleted=False)
+    if request.settings['avatars_on_threads_list']:
+        queryset = queryset.prefetch_related('thread__last_poster')
     if new:
     if new:
         queryset = queryset.filter(last_read__lt=F('thread__last'))
         queryset = queryset.filter(last_read__lt=F('thread__last'))
     count = queryset.count()
     count = queryset.count()
-    pagination = make_pagination(page, count, request.settings.threads_per_page)
+    try:
+        pagination = make_pagination(page, count, request.settings.threads_per_page)
+    except Http404:
+        if new:
+            return redirect(reverse('watched_threads_new'))
+        return redirect(reverse('watched_threads'))
     queryset = queryset.order_by('-thread__last')
     queryset = queryset.order_by('-thread__last')
     if request.settings.threads_per_page < count:
     if request.settings.threads_per_page < count:
         queryset = queryset[pagination['start']:pagination['stop']]
         queryset = queryset[pagination['start']:pagination['stop']]

+ 2 - 1
misago/auth.py

@@ -120,4 +120,5 @@ def sign_user_in(request, user):
                         )
                         )
     user.save(force_update=True)
     user.save(force_update=True)
     request.session.set_user(user)
     request.session.set_user(user)
-    request.session.set_hidden(user.hide_activity > 0)
+    if not request.firewall.admin:
+        request.onlines.sign_in()

+ 21 - 3
misago/context_processors.py

@@ -4,8 +4,25 @@ from misago.admin import site
 from misago.models import Forum
 from misago.models import Forum
 
 
 def common(request):
 def common(request):
+    context = {
+        'hook_append_extra': u'',
+        'hook_primary_menu_prepend': u'',
+        'hook_primary_menu_append': u'',
+        'hook_foot_menu_prepend': u'',
+        'hook_foot_menu_append': u'',
+        'hook_guest_menu_prepend': u'',
+        'hook_guest_menu_append': u'',
+        'hook_user_menu_prepend': u'',
+        'hook_user_menu_append': u'',
+        'hook_user_menu_important_prepend': u'',
+        'hook_user_menu_important_append': u'',
+        'hook_user_menu_dropdown_prepend': u'',
+        'hook_user_menu_dropdown_append': u'',
+        'hook_html_credits_side': u'',
+    }
+
     try:
     try:
-        context = {
+        context.update({
             'acl': request.acl,
             'acl': request.acl,
             'board_address': settings.BOARD_ADDRESS,
             'board_address': settings.BOARD_ADDRESS,
             'messages' : request.messages.messages,
             'messages' : request.messages.messages,
@@ -15,7 +32,8 @@ def common(request):
             'stopwatch': request.stopwatch.time(),
             'stopwatch': request.stopwatch.time(),
             'user': request.user,
             'user': request.user,
             'version': __version__,
             'version': __version__,
-        }
+            'disable_search': False,
+        })
         context.update({
         context.update({
             'csrf_id': request.csrf.csrf_id,
             'csrf_id': request.csrf.csrf_id,
             'csrf_token': request.csrf.csrf_token,
             'csrf_token': request.csrf.csrf_token,
@@ -25,7 +43,7 @@ def common(request):
             'reports': Forum.objects.special_model('reports'),
             'reports': Forum.objects.special_model('reports'),
         })
         })
     except AttributeError as e:
     except AttributeError as e:
-        pass
+        pass 
     return context
     return context
 
 
 
 

+ 1 - 3
misago/dbsettings.py

@@ -19,7 +19,6 @@ class DBSettings(object):
                 for i in Setting.objects.all():
                 for i in Setting.objects.all():
                     self._models[i.pk] = i
                     self._models[i.pk] = i
                     self._settings[i.pk] = i.get_value()
                     self._settings[i.pk] = i.get_value()
-                cache.set('settings', self._models)
             except DatabaseError:
             except DatabaseError:
                 pass
                 pass
         else:
         else:
@@ -36,11 +35,10 @@ class DBSettings(object):
         return self._settings[key]
         return self._settings[key]
 
 
     def __setitem__(self, key, value):
     def __setitem__(self, key, value):
-        if key in self._settings.keys():
+        if key in self._settings:
             self._models[key].set_value(value)
             self._models[key].set_value(value)
             self._models[key].save(force_update=True)
             self._models[key].save(force_update=True)
             self._settings[key] = value
             self._settings[key] = value
-            cache.set('settings', self._models)
         return value
         return value
 
 
     def __delitem__(self, key):
     def __delitem__(self, key):

+ 2 - 1
misago/fixtures/accountssetings.py

@@ -8,9 +8,10 @@ settings_fixture = (
         'description': _("Those settings allow you to increase security of your members accounts."),
         'description': _("Those settings allow you to increase security of your members accounts."),
         'settings': (
         'settings': (
             ('account_activation', {
             ('account_activation', {
+                'value':        "none",
                 'type':         "string",
                 'type':         "string",
                 'input':        "choice",
                 'input':        "choice",
-                'extra':        {'choices': [('', _("No validation required")), ('user', _("Activation Token sent to User")), ('admin', _("Activation by Administrator")), ('block', _("Dont allow new registrations"))]},
+                'extra':        {'choices': [('none', _("No validation required")), ('user', _("Activation Token sent to User")), ('admin', _("Activation by Administrator")), ('block', _("Dont allow new registrations"))]},
                 'separator':    _("Users Registrations"),
                 'separator':    _("Users Registrations"),
                 'name':         _("New accounts validation"),
                 'name':         _("New accounts validation"),
             }),
             }),

+ 10 - 2
misago/fixtures/aclmonitor.py

@@ -1,4 +1,12 @@
-from misago.utils.fixtures import load_monitor_fixture
+from misago.utils.fixtures import load_monitor_fixture, update_monitor_fixture
+
+monitor_fixture = {
+                   'acl_version': (0, 'int'),
+                  }
 
 
 def load():
 def load():
-    load_monitor_fixture({'acl_version': 0})
+    load_monitor_fixture(monitor_fixture)
+
+
+def update():
+    update_monitor_fixture(monitor_fixture)

+ 11 - 2
misago/fixtures/bansmonitor.py

@@ -1,4 +1,13 @@
-from misago.utils.fixtures import load_monitor_fixture
+from misago.utils.fixtures import load_monitor_fixture, update_monitor_fixture
+
+monitor_fixture = {
+                   'bans_version': (0, 'int'),
+                  }
+
 
 
 def load():
 def load():
-    load_monitor_fixture({'bans_version': 0})
+    load_monitor_fixture(monitor_fixture)
+
+
+def update():
+    update_monitor_fixture(monitor_fixture)

+ 12 - 2
misago/fixtures/forums.py

@@ -1,8 +1,13 @@
 from django.utils import timezone
 from django.utils import timezone
 from misago.models import Forum, Thread, Post 
 from misago.models import Forum, Thread, Post 
-from misago.utils.fixtures import load_monitor_fixture
+from misago.utils.fixtures import load_monitor_fixture, update_monitor_fixture
 from misago.utils.strings import slugify
 from misago.utils.strings import slugify
 
 
+monitor_fixture = {
+                   'threads': (1, 'int'),
+                   'posts': (1, 'int'),
+                  }
+
 def load():
 def load():
     Forum(special='private_threads', name='private', slug='private', type='forum').insert_at(None, save=True)
     Forum(special='private_threads', name='private', slug='private', type='forum').insert_at(None, save=True)
     Forum(special='reports', name='reports', slug='reports', type='forum').insert_at(None, save=True)
     Forum(special='reports', name='reports', slug='reports', type='forum').insert_at(None, save=True)
@@ -33,6 +38,7 @@ def load():
                                post='Welcome to Misago!',
                                post='Welcome to Misago!',
                                post_preparsed='Welcome to Misago!',
                                post_preparsed='Welcome to Misago!',
                                date=now,
                                date=now,
+                               current_date=now,
                                )
                                )
     thread.start_post = post
     thread.start_post = post
     thread.start_poster_name = 'MisagoProject'
     thread.start_poster_name = 'MisagoProject'
@@ -50,4 +56,8 @@ def load():
     forum.last_poster_slug = thread.last_poster_slug
     forum.last_poster_slug = thread.last_poster_slug
     forum.save(force_update=True)
     forum.save(force_update=True)
 
 
-    load_monitor_fixture({'threads': 1, 'posts': 1})
+    load_monitor_fixture(monitor_fixture)
+
+
+def update():
+    update_monitor_fixture(monitor_fixture)

+ 2 - 0
misago/fixtures/forumsroles.py

@@ -38,6 +38,8 @@ def load():
                         'can_delete_posts': 2,
                         'can_delete_posts': 2,
                         'can_delete_polls': 2,
                         'can_delete_polls': 2,
                         'can_delete_attachments': True,
                         'can_delete_attachments': True,
+                        'can_see_deleted_checkpoints': True,
+                        'can_delete_checkpoints': 2,
                        }
                        }
     role.save(force_insert=True)
     role.save(force_insert=True)
 
 

+ 14 - 0
misago/fixtures/onlinemonitor.py

@@ -0,0 +1,14 @@
+from misago.utils.fixtures import load_monitor_fixture, update_monitor_fixture
+
+monitor_fixture = {
+                   'online_members': (0, 'int'),
+                   'online_all': (0, 'int'),
+                  }
+
+
+def load():
+    load_monitor_fixture(monitor_fixture)
+
+
+def update():
+    update_monitor_fixture(monitor_fixture)

+ 13 - 0
misago/fixtures/reportsmonitor.py

@@ -0,0 +1,13 @@
+from misago.utils.fixtures import load_monitor_fixture, update_monitor_fixture
+
+monitor_fixture = {
+                   'reported_posts': (0, 'int'),
+                  }
+
+
+def load():
+    load_monitor_fixture(monitor_fixture)
+
+
+def update():
+    update_monitor_fixture(monitor_fixture)

+ 17 - 0
misago/fixtures/signingsettings.py

@@ -37,6 +37,23 @@ settings_fixture = (
                 'name':         _('Allow "Remember Me" tokens refreshing'),
                 'name':         _('Allow "Remember Me" tokens refreshing'),
                 'description':  _('Set this setting to off if you want to force your users to periodically update their "Remember Me" tokens by signing in. If this option is on, Tokens are updated when they are used to open new session.'),
                 'description':  _('Set this setting to off if you want to force your users to periodically update their "Remember Me" tokens by signing in. If this option is on, Tokens are updated when they are used to open new session.'),
             }),
             }),
+            ('online_counting', {
+                'value':        "real",
+                'type':         "string",
+                'input':        "choice",
+                'extra':        {'choices': [('no', _("Don't count users online")), ('snap', _("Periodically count and cache onlines")), ('real', _("Real time"))]},
+                'separator':    _("Online Counting"),
+                'name':         _("Count and display number of users online on board index."),
+                'description':  _("Online counter helps members tell how active other members are at the moment. Large forums should use periodical counting that saves resources but is not accurate while small ones can use real time counting that offers complete accuracy without putting much stress on sessions table."),
+            }),
+            ('online_counting_frequency', {
+                'value':        300,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 1},
+                'name':         _("Cache expiration"),
+                'description':  _('If you are using cache to count number of users online, here you can enter number of seconds after which cache is marked as expired and refreshed with new data.'),
+            }),
         ),
         ),
     }),
     }),
 )
 )

+ 2 - 2
misago/fixtures/threadssettings.py

@@ -17,7 +17,7 @@ settings_fixture = (
                 'description':  _('Minimal allowed thread name length.'),
                 'description':  _('Minimal allowed thread name length.'),
             }),
             }),
             ('thread_name_max', {
             ('thread_name_max', {
-                'value':        50,
+                'value':        60,
                 'type':         "integer",
                 'type':         "integer",
                 'input':        "text",
                 'input':        "text",
                 'extra':        {'min': 5, 'max': 100},
                 'extra':        {'min': 5, 'max': 100},
@@ -78,7 +78,7 @@ settings_fixture = (
                 'input':        "text",
                 'input':        "text",
                 'extra':        {'min': 0, 'max': 99},
                 'extra':        {'min': 0, 'max': 99},
                 'name':         _('Score inflation'),
                 'name':         _('Score inflation'),
-                'description':  _("Thread popularity system requires inflation to be defined in order to be effective. updatethreadranking task will lower thread scores by percent defined here on every launch. For example, yf you enter 5, thread scores will be lowered by 5% on every update. Enter zero to disable inflation."),
+                'description':  _("Thread popularity system requires inflation to be defined in order to be effective. updatethreadranking task will lower thread scores by percent defined here on every launch. For example, if you enter 5, thread scores will be lowered by 5%% on every update. Enter zero to disable inflation."),
             }),
             }),
             ('post_length_min', {
             ('post_length_min', {
                 'value':        5,
                 'value':        5,

+ 17 - 0
misago/fixtures/userroles.py

@@ -6,6 +6,8 @@ def load():
     role.permissions = {
     role.permissions = {
                         'name_changes_allowed': 5,
                         'name_changes_allowed': 5,
                         'changes_expire': 7,
                         'changes_expire': 7,
+                        'can_search_forums': True,
+                        'search_cooldown': 0,
                         'can_use_acp': True,
                         'can_use_acp': True,
                         'can_use_mcp': True,
                         'can_use_mcp': True,
                         'can_use_signature': True,
                         'can_use_signature': True,
@@ -22,6 +24,11 @@ def load():
                         'private_thread_attachments_limit': 0,
                         'private_thread_attachments_limit': 0,
                         'can_invite_ignoring': True,
                         'can_invite_ignoring': True,
                         'private_threads_mod': True,
                         'private_threads_mod': True,
+                        'can_delete_checkpoints': 2,
+                        'can_report_content': True,
+                        'can_handle_reports': True,
+                        'can_mod_reports_discussions': True,
+                        'can_delete_reports': True,
                         'forums': {3: 1, 5: 1, 6: 1},
                         'forums': {3: 1, 5: 1, 6: 1},
                        }
                        }
     role.save(force_insert=True)
     role.save(force_insert=True)
@@ -30,6 +37,8 @@ def load():
     role.permissions = {
     role.permissions = {
                         'name_changes_allowed': 3,
                         'name_changes_allowed': 3,
                         'changes_expire': 14,
                         'changes_expire': 14,
+                        'can_search_forums': True,
+                        'search_cooldown': 0,
                         'can_use_mcp': True,
                         'can_use_mcp': True,
                         'can_use_signature': True,
                         'can_use_signature': True,
                         'allow_signature_links': True,
                         'allow_signature_links': True,
@@ -44,6 +53,9 @@ def load():
                         'private_thread_attachments_limit': 0,
                         'private_thread_attachments_limit': 0,
                         'can_invite_ignoring': True,
                         'can_invite_ignoring': True,
                         'private_threads_mod': True,
                         'private_threads_mod': True,
+                        'can_delete_checkpoints': 1,
+                        'can_report_content': True,
+                        'can_handle_reports': True,
                         'forums': {3: 1, 5: 1, 6: 1},
                         'forums': {3: 1, 5: 1, 6: 1},
                        }
                        }
     role.save(force_insert=True)
     role.save(force_insert=True)
@@ -51,6 +63,8 @@ def load():
     role = Role(name=_("Registered").message, _special='registered')
     role = Role(name=_("Registered").message, _special='registered')
     role.permissions = {
     role.permissions = {
                         'name_changes_allowed': 2,
                         'name_changes_allowed': 2,
+                        'can_search_forums': True,
+                        'search_cooldown': 20,
                         'can_use_signature': False,
                         'can_use_signature': False,
                         'can_search_users': True,
                         'can_search_users': True,
                         'can_use_private_threads': True,
                         'can_use_private_threads': True,
@@ -60,12 +74,15 @@ def load():
                         'private_thread_attachments_limit': 30,
                         'private_thread_attachments_limit': 30,
                         'can_invite_ignoring': False,
                         'can_invite_ignoring': False,
                         'private_threads_mod': False,
                         'private_threads_mod': False,
+                        'can_report_content': True,
                         'forums': {4: 3, 5: 3, 6: 3},
                         'forums': {4: 3, 5: 3, 6: 3},
                        }
                        }
     role.save(force_insert=True)
     role.save(force_insert=True)
     
     
     role = Role(name=_("Guest").message, _special='guest')
     role = Role(name=_("Guest").message, _special='guest')
     role.permissions = {
     role.permissions = {
+                        'can_search_forums': True,
+                        'search_cooldown': 45,
                         'can_search_users': True,
                         'can_search_users': True,
                         'forums': {4: 6, 5: 6, 6: 6},
                         'forums': {4: 6, 5: 6, 6: 6},
                        }
                        }

+ 11 - 8
misago/fixtures/usersmonitor.py

@@ -1,14 +1,17 @@
-from misago.utils.fixtures import load_monitor_fixture
+from misago.utils.fixtures import load_monitor_fixture, update_monitor_fixture
 
 
 monitor_fixture = {
 monitor_fixture = {
-                   'users': 0,
-                   'users_inactive': 0,
-                   'users_reported': 0,
-                   'last_user': None,
-                   'last_user_name': None,
-                   'last_user_slug': None,
+                   'users': (0, 'int'),
+                   'users_inactive': (0, 'int'),
+                   'last_user': ('', 'string'),
+                   'last_user_name': ('', 'string'),
+                   'last_user_slug': ('', 'string'),
                   }
                   }
 
 
 
 
 def load():
 def load():
-    load_monitor_fixture(monitor_fixture)
+    load_monitor_fixture(monitor_fixture)
+
+
+def update():
+    update_monitor_fixture(monitor_fixture)

BIN
misago/locale/pl/LC_MESSAGES/django.mo


+ 1901 - 1315
misago/locale/pl/LC_MESSAGES/django.po

@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 msgstr ""
 "Project-Id-Version: Misago PL\n"
 "Project-Id-Version: Misago PL\n"
 "Report-Msgid-Bugs-To: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-04-03 18:32+0000\n"
-"PO-Revision-Date: 2013-04-06 00:44+0100\n"
+"POT-Creation-Date: 2013-04-16 20:50+0000\n"
+"PO-Revision-Date: 2013-04-16 23:43+0100\n"
 "Last-Translator: L0ud PL <loudml@gmail.com>\n"
 "Last-Translator: L0ud PL <loudml@gmail.com>\n"
 "Language-Team: FxDev <loudpl@gmail.com>\n"
 "Language-Team: FxDev <loudpl@gmail.com>\n"
 "MIME-Version: 1.0\n"
 "MIME-Version: 1.0\n"
@@ -118,7 +118,6 @@ msgstr[1] "Hasło musi być długie na co najmniej %(count)d znaki."
 msgstr[2] "Hasło musi być długie na co najmniej %(count)d znaków."
 msgstr[2] "Hasło musi być długie na co najmniej %(count)d znaków."
 
 
 #: validators.py:68
 #: validators.py:68
-#, fuzzy
 msgid "Password must contain alphabetical characters."
 msgid "Password must contain alphabetical characters."
 msgstr "Hasło musi zawierać jakiekolwiek znaki alfabetu."
 msgstr "Hasło musi zawierać jakiekolwiek znaki alfabetu."
 
 
@@ -127,9 +126,8 @@ msgid "Password must contain characters that have different case."
 msgstr "Hasło musi zawierać znaki o różnej wielkości."
 msgstr "Hasło musi zawierać znaki o różnej wielkości."
 
 
 #: validators.py:74
 #: validators.py:74
-#, fuzzy
 msgid "Password must contain digits in addition to characters."
 msgid "Password must contain digits in addition to characters."
-msgstr "Hasło musi zawierać też jakieś cyfry."
+msgstr "Hasło musi zawierać też cyfry."
 
 
 #: validators.py:77
 #: validators.py:77
 msgid "Password must contain special (non alphanumerical) characters."
 msgid "Password must contain special (non alphanumerical) characters."
@@ -141,11 +139,11 @@ msgstr "To forum zabrania rejestracji z podanego adresu e-mail."
 
 
 #: acl/panels.py:10
 #: acl/panels.py:10
 msgid "Misago ACL"
 msgid "Misago ACL"
-msgstr ""
+msgstr "Misago ACL"
 
 
 #: acl/panels.py:13
 #: acl/panels.py:13
 msgid "Misago User ACL"
 msgid "Misago User ACL"
-msgstr ""
+msgstr "ACL Użytkownika"
 
 
 #: acl/permissions/forums.py:11 apps/admin/roles/views.py:38
 #: acl/permissions/forums.py:11 apps/admin/roles/views.py:38
 msgid "Forums Permissions"
 msgid "Forums Permissions"
@@ -163,8 +161,28 @@ msgstr "Ma dostęp do zawartości forum"
 msgid "You don't have permission to browse this forum."
 msgid "You don't have permission to browse this forum."
 msgstr "Nie masz wystarczających uprawnień, aby przeglądać to forum."
 msgstr "Nie masz wystarczających uprawnień, aby przeglądać to forum."
 
 
-#: acl/permissions/privatethreads.py:18 fixtures/privatethreadssettings.py:14
-#: models/forummodel.py:173 templates/cranefly/watched.html:105
+#: acl/permissions/privatethreads.py:18 acl/permissions/threads.py:11
+#: acl/permissions/threads.py:16 acl/permissions/threads.py:23
+#: acl/permissions/threads.py:32 acl/permissions/threads.py:49
+#: acl/permissions/threads.py:58 acl/permissions/threads.py:63
+#: acl/permissions/threads.py:68 acl/permissions/threads.py:74
+msgid "No"
+msgstr "Nie"
+
+#: acl/permissions/privatethreads.py:19 acl/permissions/threads.py:59
+#: acl/permissions/threads.py:64 acl/permissions/threads.py:69
+#: acl/permissions/threads.py:75
+msgid "Yes, soft-delete"
+msgstr "Tak, z możliwością przywrócenia"
+
+#: acl/permissions/privatethreads.py:20 acl/permissions/threads.py:60
+#: acl/permissions/threads.py:65 acl/permissions/threads.py:70
+#: acl/permissions/threads.py:76
+msgid "Yes, hard-delete"
+msgstr "Tak, z możliwością kompletnego usunięcia"
+
+#: acl/permissions/privatethreads.py:24 fixtures/privatethreadssettings.py:14
+#: models/forummodel.py:174 templates/cranefly/watched.html:105
 #: templates/cranefly/private_threads/changelog.html:7
 #: templates/cranefly/private_threads/changelog.html:7
 #: templates/cranefly/private_threads/changelog_diff.html:7
 #: templates/cranefly/private_threads/changelog_diff.html:7
 #: templates/cranefly/private_threads/details.html:7
 #: templates/cranefly/private_threads/details.html:7
@@ -174,47 +192,49 @@ msgstr "Nie masz wystarczających uprawnień, aby przeglądać to forum."
 #: templates/cranefly/private_threads/posting.html:25
 #: templates/cranefly/private_threads/posting.html:25
 #: templates/cranefly/private_threads/thread.html:9
 #: templates/cranefly/private_threads/thread.html:9
 msgid "Private Threads"
 msgid "Private Threads"
-msgstr "Prywatne tematy"
+msgstr "Twoje prywatne dyskusje"
 
 
-#: acl/permissions/privatethreads.py:20
+#: acl/permissions/privatethreads.py:26
 msgid "Can participate in private threads"
 msgid "Can participate in private threads"
 msgstr "Może uczestniczyć w prywatnych tematach"
 msgstr "Może uczestniczyć w prywatnych tematach"
 
 
-#: acl/permissions/privatethreads.py:21
+#: acl/permissions/privatethreads.py:27
 msgid "Can start private threads"
 msgid "Can start private threads"
-msgstr "Może rozpoczynać prywatne tematy"
+msgstr "Może rozpoczynać prywatne dyskusje"
 
 
-#: acl/permissions/privatethreads.py:22
+#: acl/permissions/privatethreads.py:28
 msgid "Can upload files in attachments"
 msgid "Can upload files in attachments"
 msgstr "Może umieszczać pliki w załącznikach"
 msgstr "Może umieszczać pliki w załącznikach"
 
 
-#: acl/permissions/privatethreads.py:23
+#: acl/permissions/privatethreads.py:29
 msgid "Max. size of single attachment (in KB)"
 msgid "Max. size of single attachment (in KB)"
 msgstr "Maksymalny rozmiar pojedynczego załacznika (w KB)"
 msgstr "Maksymalny rozmiar pojedynczego załacznika (w KB)"
 
 
-#: acl/permissions/privatethreads.py:24
+#: acl/permissions/privatethreads.py:30
 msgid "Max. number of attachments per post"
 msgid "Max. number of attachments per post"
 msgstr "Maksymalna liczba załączników w poście"
 msgstr "Maksymalna liczba załączników w poście"
 
 
-#: acl/permissions/privatethreads.py:25
-#, fuzzy
+#: acl/permissions/privatethreads.py:31
 msgid "Can invite users that ignore him"
 msgid "Can invite users that ignore him"
 msgstr "Może zapraszać do prywatnych dyskusji użytkowników, którzy go ignorują"
 msgstr "Może zapraszać do prywatnych dyskusji użytkowników, którzy go ignorują"
 
 
-#: acl/permissions/privatethreads.py:26
+#: acl/permissions/privatethreads.py:32
 msgid "Can moderate threads"
 msgid "Can moderate threads"
 msgstr "Może moderować tematy"
 msgstr "Może moderować tematy"
 
 
-#: acl/permissions/privatethreads.py:26
+#: acl/permissions/privatethreads.py:32
 msgid ""
 msgid ""
 "Makes user with this role Private Threads moderator capable of closing, "
 "Makes user with this role Private Threads moderator capable of closing, "
 "deleting and editing all private threads he participates in at will."
 "deleting and editing all private threads he participates in at will."
 msgstr ""
 msgstr ""
-"Czyni użytkownika moderatorem prywatnych tematów. Moderator może zamykać, "
+"Czyni użytkownika moderatorem prywatnych dyskusji. Moderator może zamykać, "
 "modyfikować i edytować wszystkie prywatne tematy, w których uczestniczy."
 "modyfikować i edytować wszystkie prywatne tematy, w których uczestniczy."
 
 
+#: acl/permissions/privatethreads.py:33 acl/permissions/threads.py:139
+msgid "Can delete checkpoints"
+msgstr "Może usuwać etykiety zdarzeń"
+
 #: acl/permissions/special.py:10
 #: acl/permissions/special.py:10
-#, fuzzy
 msgid "Special Access"
 msgid "Special Access"
 msgstr "Specjalny dostęp"
 msgstr "Specjalny dostęp"
 
 
@@ -241,13 +261,6 @@ msgstr ""
 "Ustawienie tej opcji na Tak, mianuje użytkowników z daną rolą "
 "Ustawienie tej opcji na Tak, mianuje użytkowników z daną rolą "
 "administratorami tego forum."
 "administratorami tego forum."
 
 
-#: acl/permissions/threads.py:11 acl/permissions/threads.py:16
-#: acl/permissions/threads.py:23 acl/permissions/threads.py:32
-#: acl/permissions/threads.py:49 acl/permissions/threads.py:58
-#: acl/permissions/threads.py:63 acl/permissions/threads.py:68
-msgid "No"
-msgstr "Nie"
-
 #: acl/permissions/threads.py:12
 #: acl/permissions/threads.py:12
 msgid "Yes, owned"
 msgid "Yes, owned"
 msgstr "Tak, własne"
 msgstr "Tak, własne"
@@ -257,9 +270,8 @@ msgid "Yes, all"
 msgstr "Tak, wszystkie"
 msgstr "Tak, wszystkie"
 
 
 #: acl/permissions/threads.py:17 acl/permissions/threads.py:24
 #: acl/permissions/threads.py:17 acl/permissions/threads.py:24
-#, fuzzy
 msgid "Yes, with moderation"
 msgid "Yes, with moderation"
-msgstr "Akceptacja przez moderatora"
+msgstr "Tak, z wymogiem akceptacji przez moderatora"
 
 
 #: acl/permissions/threads.py:18 acl/permissions/threads.py:25
 #: acl/permissions/threads.py:18 acl/permissions/threads.py:25
 msgid "Yes"
 msgid "Yes"
@@ -281,332 +293,328 @@ msgstr "Tak, dla przyklejonych tematów"
 msgid "Yes, to announcements"
 msgid "Yes, to announcements"
 msgstr "Tak, dla ogłoszeń"
 msgstr "Tak, dla ogłoszeń"
 
 
-#: acl/permissions/threads.py:59 acl/permissions/threads.py:64
-#: acl/permissions/threads.py:69
-msgid "Yes, soft-delete"
-msgstr "Tak, z możliwością przywrócenia"
-
-#: acl/permissions/threads.py:60 acl/permissions/threads.py:65
-#: acl/permissions/threads.py:70
-msgid "Yes, hard-delete"
-msgstr "Tak, z możliwością kompletnego usunięcia"
-
-#: acl/permissions/threads.py:75 apps/profiles/threads/profile.py:4
+#: acl/permissions/threads.py:81 apps/profiles/threads/profile.py:4
 #: fixtures/threadssettings.py:15 templates/admin/index.html:52
 #: fixtures/threadssettings.py:15 templates/admin/index.html:52
-#: templates/cranefly/category.html:81 templates/cranefly/index.html:67
-#: templates/cranefly/index.html.py:124
-#: templates/cranefly/threads/list.html:85
+#: templates/cranefly/category.html:83 templates/cranefly/index.html:71
+#: templates/cranefly/index.html.py:134
+#: templates/cranefly/threads/list.html:87
 msgid "Threads"
 msgid "Threads"
 msgstr "Tematy"
 msgstr "Tematy"
 
 
-#: acl/permissions/threads.py:77
+#: acl/permissions/threads.py:83
 msgid "Can read threads"
 msgid "Can read threads"
 msgstr "Może czytać tematy"
 msgstr "Może czytać tematy"
 
 
-#: acl/permissions/threads.py:78
+#: acl/permissions/threads.py:84
 msgid "Can start new threads"
 msgid "Can start new threads"
 msgstr "Może otwierać nowe tematy"
 msgstr "Może otwierać nowe tematy"
 
 
-#: acl/permissions/threads.py:79
+#: acl/permissions/threads.py:85
 msgid "Can edit own threads"
 msgid "Can edit own threads"
 msgstr "Może modyfikować swoje tematy"
 msgstr "Może modyfikować swoje tematy"
 
 
-#: acl/permissions/threads.py:80
+#: acl/permissions/threads.py:86
 msgid "Can soft-delete own threads"
 msgid "Can soft-delete own threads"
 msgstr "Może usuwać swoje tematy (nie na trwałe)"
 msgstr "Może usuwać swoje tematy (nie na trwałe)"
 
 
-#: acl/permissions/threads.py:84 apps/profiles/posts/profile.py:4
+#: acl/permissions/threads.py:90 apps/profiles/posts/profile.py:4
 #: fixtures/threadssettings.py:88 templates/admin/index.html:48
 #: fixtures/threadssettings.py:88 templates/admin/index.html:48
-#: templates/cranefly/category.html:80 templates/cranefly/index.html:66
-#: templates/cranefly/index.html.py:121
-#: templates/cranefly/threads/list.html:84
+#: templates/cranefly/category.html:82 templates/cranefly/index.html:70
+#: templates/cranefly/index.html.py:131
+#: templates/cranefly/threads/list.html:86
 msgid "Posts"
 msgid "Posts"
 msgstr "Posty"
 msgstr "Posty"
 
 
-#: acl/permissions/threads.py:86
+#: acl/permissions/threads.py:92
 msgid "Can write posts"
 msgid "Can write posts"
 msgstr "Może publikować posty"
 msgstr "Może publikować posty"
 
 
-#: acl/permissions/threads.py:87
+#: acl/permissions/threads.py:93
 msgid "Can edit own posts"
 msgid "Can edit own posts"
 msgstr "Może edytować swoje posty"
 msgstr "Może edytować swoje posty"
 
 
-#: acl/permissions/threads.py:88
+#: acl/permissions/threads.py:94
 msgid "Can soft-delete own posts"
 msgid "Can soft-delete own posts"
 msgstr "Może usuwać swoje posty (nie na trwałe)"
 msgstr "Może usuwać swoje posty (nie na trwałe)"
 
 
-#: acl/permissions/threads.py:92
+#: acl/permissions/threads.py:98
 msgid "Karma"
 msgid "Karma"
-msgstr "Karma"
+msgstr "Ocenianie postów"
 
 
-#: acl/permissions/threads.py:94
-#, fuzzy
+#: acl/permissions/threads.py:100
 msgid "Can upvote posts"
 msgid "Can upvote posts"
 msgstr "Może głosować za"
 msgstr "Może głosować za"
 
 
-#: acl/permissions/threads.py:95
-#, fuzzy
+#: acl/permissions/threads.py:101
 msgid "Can downvote posts"
 msgid "Can downvote posts"
 msgstr "Może głosować przeciw"
 msgstr "Może głosować przeciw"
 
 
-#: acl/permissions/threads.py:96
-#, fuzzy
+#: acl/permissions/threads.py:102
 msgid "Can see post score"
 msgid "Can see post score"
 msgstr "Może zobaczyć ocenę postu"
 msgstr "Może zobaczyć ocenę postu"
 
 
-#: acl/permissions/threads.py:97
+#: acl/permissions/threads.py:103
 msgid "Can see who voted on post"
 msgid "Can see who voted on post"
 msgstr "Może zobaczyć, kto głosował na dany post"
 msgstr "Może zobaczyć, kto głosował na dany post"
 
 
-#: acl/permissions/threads.py:101
+#: acl/permissions/threads.py:107
 msgid "Polls"
 msgid "Polls"
 msgstr "Ankiety"
 msgstr "Ankiety"
 
 
-#: acl/permissions/threads.py:103
+#: acl/permissions/threads.py:109
 msgid "Can make polls"
 msgid "Can make polls"
 msgstr "Może tworzyć ankiety"
 msgstr "Może tworzyć ankiety"
 
 
-#: acl/permissions/threads.py:104
+#: acl/permissions/threads.py:110
 msgid "Can vote in polls"
 msgid "Can vote in polls"
 msgstr "Może głosować w ankietach"
 msgstr "Może głosować w ankietach"
 
 
-#: acl/permissions/threads.py:105
+#: acl/permissions/threads.py:111
 msgid "Can see who voted in poll"
 msgid "Can see who voted in poll"
 msgstr "Może zobaczyć kto głosował w ankiecie"
 msgstr "Może zobaczyć kto głosował w ankiecie"
 
 
-#: acl/permissions/threads.py:109 apps/admin/sections/forums.py:88
+#: acl/permissions/threads.py:115 apps/admin/sections/forums.py:76
 msgid "Attachments"
 msgid "Attachments"
 msgstr "Załączniki"
 msgstr "Załączniki"
 
 
-#: acl/permissions/threads.py:111
+#: acl/permissions/threads.py:117
 msgid "Can see attachments"
 msgid "Can see attachments"
 msgstr "Może zobaczyć załączniki"
 msgstr "Może zobaczyć załączniki"
 
 
-#: acl/permissions/threads.py:112
+#: acl/permissions/threads.py:118
 msgid "Can upload attachments"
 msgid "Can upload attachments"
 msgstr "Może umieszczać załaczniki"
 msgstr "Może umieszczać załaczniki"
 
 
-#: acl/permissions/threads.py:113
+#: acl/permissions/threads.py:119
 msgid "Can download attachments"
 msgid "Can download attachments"
 msgstr "Może pobierać załączniki"
 msgstr "Może pobierać załączniki"
 
 
-#: acl/permissions/threads.py:114
+#: acl/permissions/threads.py:120
 msgid "Max size of single attachment (in Kb)"
 msgid "Max size of single attachment (in Kb)"
 msgstr "Maksymalny rozmiar pojedynczego załacznika (w KB)"
 msgstr "Maksymalny rozmiar pojedynczego załacznika (w KB)"
 
 
-#: acl/permissions/threads.py:114 acl/permissions/threads.py:115
+#: acl/permissions/threads.py:120 acl/permissions/threads.py:121
 msgid "Enter zero for no limit."
 msgid "Enter zero for no limit."
 msgstr "Wpisz 0, aby wyłączyć limit."
 msgstr "Wpisz 0, aby wyłączyć limit."
 
 
-#: acl/permissions/threads.py:115
+#: acl/permissions/threads.py:121
 msgid "Max number of attachments per post"
 msgid "Max number of attachments per post"
 msgstr "Maksymalna liczba załączników na post."
 msgstr "Maksymalna liczba załączników na post."
 
 
-#: acl/permissions/threads.py:119
+#: acl/permissions/threads.py:125
 msgid "Moderation"
 msgid "Moderation"
 msgstr "Moderacja"
 msgstr "Moderacja"
 
 
-#: acl/permissions/threads.py:121
+#: acl/permissions/threads.py:127
 msgid "Can accept threads and posts"
 msgid "Can accept threads and posts"
 msgstr "Może zatwierdzać tematy i posty"
 msgstr "Może zatwierdzać tematy i posty"
 
 
-#: acl/permissions/threads.py:122
+#: acl/permissions/threads.py:128
 msgid "Can edit thread labels"
 msgid "Can edit thread labels"
 msgstr "Może modyfikować etykiety tematów"
 msgstr "Może modyfikować etykiety tematów"
 
 
-#: acl/permissions/threads.py:123
+#: acl/permissions/threads.py:129
 msgid "Can see edits history"
 msgid "Can see edits history"
 msgstr "Może zobaczyć historię edycji"
 msgstr "Może zobaczyć historię edycji"
 
 
-#: acl/permissions/threads.py:124
-#, fuzzy
+#: acl/permissions/threads.py:130
 msgid "Can change threads weight"
 msgid "Can change threads weight"
-msgstr "Może zmienić istotność tematu"
+msgstr "Może przekształcać tematy (w ogłoszenie, w przypięty i zwykły temat)"
 
 
-#: acl/permissions/threads.py:125
+#: acl/permissions/threads.py:131
 msgid "Can edit threads and posts"
 msgid "Can edit threads and posts"
 msgstr "Może modyfikować tematy i posty"
 msgstr "Może modyfikować tematy i posty"
 
 
-#: acl/permissions/threads.py:126
+#: acl/permissions/threads.py:132
 msgid "Can move, merge and split threads and posts"
 msgid "Can move, merge and split threads and posts"
 msgstr "Może przenosić, łączyć i rozdzielać tematy i posty"
 msgstr "Może przenosić, łączyć i rozdzielać tematy i posty"
 
 
-#: acl/permissions/threads.py:127
+#: acl/permissions/threads.py:133
 msgid "Can close threads"
 msgid "Can close threads"
 msgstr "Może zamykać tematy"
 msgstr "Może zamykać tematy"
 
 
-#: acl/permissions/threads.py:128
+#: acl/permissions/threads.py:134
 msgid "Can protect posts"
 msgid "Can protect posts"
 msgstr "Może zabezpieczać posty"
 msgstr "Może zabezpieczać posty"
 
 
-#: acl/permissions/threads.py:128
+#: acl/permissions/threads.py:134
 msgid "Protected posts cannot be changed by their owners."
 msgid "Protected posts cannot be changed by their owners."
 msgstr "Zabezpieczone posty nie mogą być zmodyfikowane przez ich właścicieli."
 msgstr "Zabezpieczone posty nie mogą być zmodyfikowane przez ich właścicieli."
 
 
-#: acl/permissions/threads.py:129
+#: acl/permissions/threads.py:135
 msgid "Can delete threads"
 msgid "Can delete threads"
 msgstr "Może usuwać tematy"
 msgstr "Może usuwać tematy"
 
 
-#: acl/permissions/threads.py:130
+#: acl/permissions/threads.py:136
 msgid "Can delete posts"
 msgid "Can delete posts"
 msgstr "Może usuwać posty"
 msgstr "Może usuwać posty"
 
 
-#: acl/permissions/threads.py:131
+#: acl/permissions/threads.py:137
 msgid "Can delete polls"
 msgid "Can delete polls"
 msgstr "Może usuwać ankiety"
 msgstr "Może usuwać ankiety"
 
 
-#: acl/permissions/threads.py:132
+#: acl/permissions/threads.py:138
 msgid "Can delete attachments"
 msgid "Can delete attachments"
 msgstr "Może usuwać załączniki"
 msgstr "Może usuwać załączniki"
 
 
-#: acl/permissions/threads.py:151 acl/permissions/threads.py:159
+#: acl/permissions/threads.py:140
+msgid "Can see deleted checkpoints"
+msgstr "Może zobaczyć usunięte etykiety zdarzeń"
+
+#: acl/permissions/threads.py:159 acl/permissions/threads.py:167
 msgid "You don't have permission to read threads in this forum."
 msgid "You don't have permission to read threads in this forum."
 msgstr "Nie posiadasz uprawnień do czytania tematów na tym forum."
 msgstr "Nie posiadasz uprawnień do czytania tematów na tym forum."
 
 
-#: acl/permissions/threads.py:218 acl/permissions/threads.py:222
+#: acl/permissions/threads.py:226 acl/permissions/threads.py:230
 msgid "You don't have permission to start new threads in this forum."
 msgid "You don't have permission to start new threads in this forum."
 msgstr "Nie posiadasz uprawnień do otwierania nowych tematów na tym forum."
 msgstr "Nie posiadasz uprawnień do otwierania nowych tematów na tym forum."
 
 
-#: acl/permissions/threads.py:220
+#: acl/permissions/threads.py:228
 msgid "This forum is closed, you can't start new threads in it."
 msgid "This forum is closed, you can't start new threads in it."
 msgstr "To forum jest zamknięte, nie możesz otwierać w nim nowych tematów."
 msgstr "To forum jest zamknięte, nie możesz otwierać w nim nowych tematów."
 
 
-#: acl/permissions/threads.py:244
+#: acl/permissions/threads.py:252
 msgid "You can't edit threads in closed forums."
 msgid "You can't edit threads in closed forums."
 msgstr "Nie możesz modyfikować tematów w zamkniętych forach."
 msgstr "Nie możesz modyfikować tematów w zamkniętych forach."
 
 
-#: acl/permissions/threads.py:246
+#: acl/permissions/threads.py:254
 msgid "You can't edit closed threads."
 msgid "You can't edit closed threads."
 msgstr "Nie możesz modyfikować zamkniętych tematów."
 msgstr "Nie możesz modyfikować zamkniętych tematów."
 
 
-#: acl/permissions/threads.py:249
+#: acl/permissions/threads.py:257
 msgid "You can't edit other members threads."
 msgid "You can't edit other members threads."
 msgstr "Nie możesz modyfikować tematów innych użytkowników."
 msgstr "Nie możesz modyfikować tematów innych użytkowników."
 
 
-#: acl/permissions/threads.py:251
+#: acl/permissions/threads.py:259
 msgid "You can't edit your threads."
 msgid "You can't edit your threads."
 msgstr "Nie możesz modyfikować swoich tematów."
 msgstr "Nie możesz modyfikować swoich tematów."
 
 
-#: acl/permissions/threads.py:253
+#: acl/permissions/threads.py:261
 msgid "This thread is protected, you cannot edit it."
 msgid "This thread is protected, you cannot edit it."
 msgstr "Ten temat jest chroniony, nie możesz go modyfikować."
 msgstr "Ten temat jest chroniony, nie możesz go modyfikować."
 
 
-#: acl/permissions/threads.py:255
+#: acl/permissions/threads.py:263
 msgid "You don't have permission to edit threads in this forum."
 msgid "You don't have permission to edit threads in this forum."
 msgstr "Nie posiadasz uprawnień do modyfikowania tematów na tym forum."
 msgstr "Nie posiadasz uprawnień do modyfikowania tematów na tym forum."
 
 
-#: acl/permissions/threads.py:272 acl/permissions/threads.py:279
+#: acl/permissions/threads.py:280 acl/permissions/threads.py:287
 msgid "You don't have permission to write replies in this forum."
 msgid "You don't have permission to write replies in this forum."
 msgstr "Nie posiadasz uprawnień do postowania odpowiedzi na tym forum."
 msgstr "Nie posiadasz uprawnień do postowania odpowiedzi na tym forum."
 
 
-#: acl/permissions/threads.py:275
+#: acl/permissions/threads.py:283
 msgid "You can't write replies in closed forums."
 msgid "You can't write replies in closed forums."
 msgstr "Nie możesz pisać odpowiedzi w zamkniętych forach."
 msgstr "Nie możesz pisać odpowiedzi w zamkniętych forach."
 
 
-#: acl/permissions/threads.py:277
+#: acl/permissions/threads.py:285
 msgid "You can't write replies in closed threads."
 msgid "You can't write replies in closed threads."
 msgstr "Nie możesz pisać odpowiedzi w zamkniętych tematach."
 msgstr "Nie możesz pisać odpowiedzi w zamkniętych tematach."
 
 
-#: acl/permissions/threads.py:301
+#: acl/permissions/threads.py:309
 msgid "You can't edit replies in closed forums."
 msgid "You can't edit replies in closed forums."
 msgstr "Nie mozesz edytować odpowiedzi w zamkniętych forach."
 msgstr "Nie mozesz edytować odpowiedzi w zamkniętych forach."
 
 
-#: acl/permissions/threads.py:303
+#: acl/permissions/threads.py:311
 msgid "You can't edit replies in closed threads."
 msgid "You can't edit replies in closed threads."
 msgstr "Nie możesz edytować odpowiedzi w zamkniętych tematach."
 msgstr "Nie możesz edytować odpowiedzi w zamkniętych tematach."
 
 
-#: acl/permissions/threads.py:306
+#: acl/permissions/threads.py:314
 msgid "You can't edit other members replies."
 msgid "You can't edit other members replies."
 msgstr "Nie możesz edytować odpowiedzi innych użytkowników."
 msgstr "Nie możesz edytować odpowiedzi innych użytkowników."
 
 
-#: acl/permissions/threads.py:308
+#: acl/permissions/threads.py:316
 msgid "You can't edit your replies."
 msgid "You can't edit your replies."
 msgstr "Nie możesz edytować swoich odpowiedzi."
 msgstr "Nie możesz edytować swoich odpowiedzi."
 
 
-#: acl/permissions/threads.py:310
+#: acl/permissions/threads.py:318
 msgid "This reply is protected, you cannot edit it."
 msgid "This reply is protected, you cannot edit it."
 msgstr "Ta odpowiedź jest chroniona, nie możesz jej modyfikować."
 msgstr "Ta odpowiedź jest chroniona, nie możesz jej modyfikować."
 
 
-#: acl/permissions/threads.py:312
+#: acl/permissions/threads.py:320
 msgid "You don't have permission to edit replies in this forum."
 msgid "You don't have permission to edit replies in this forum."
 msgstr "Nie posiadasz uprawnień do modyfikacji odpowiedzi na tym forum."
 msgstr "Nie posiadasz uprawnień do modyfikacji odpowiedzi na tym forum."
 
 
-#: acl/permissions/threads.py:327 acl/permissions/threads.py:329
+#: acl/permissions/threads.py:335 acl/permissions/threads.py:337
 msgid "You don't have permission to see history of changes made to this post."
 msgid "You don't have permission to see history of changes made to this post."
 msgstr ""
 msgstr ""
 "Nie posiadasz uprawnień, potrzebnych do zobaczenia historii edycji tego "
 "Nie posiadasz uprawnień, potrzebnych do zobaczenia historii edycji tego "
 "postu."
 "postu."
 
 
-#: acl/permissions/threads.py:345
+#: acl/permissions/threads.py:353
 msgid "You can't make reverts in closed forums."
 msgid "You can't make reverts in closed forums."
 msgstr "Nie możesz dokonywać przywróceń w zamkniętych forach."
 msgstr "Nie możesz dokonywać przywróceń w zamkniętych forach."
 
 
-#: acl/permissions/threads.py:347
+#: acl/permissions/threads.py:355
 msgid "You can't make reverts in closed threads."
 msgid "You can't make reverts in closed threads."
 msgstr "Nie możesz dokonywać przywróceń w zamkniętych tematach."
 msgstr "Nie możesz dokonywać przywróceń w zamkniętych tematach."
 
 
-#: acl/permissions/threads.py:349 acl/permissions/threads.py:351
+#: acl/permissions/threads.py:357 acl/permissions/threads.py:359
 msgid "You don't have permission to make reverts in this forum."
 msgid "You don't have permission to make reverts in this forum."
 msgstr "Nie posiadasz uprawnień do dokonywania przywróceń na tym forum."
 msgstr "Nie posiadasz uprawnień do dokonywania przywróceń na tym forum."
 
 
-#: acl/permissions/threads.py:427
+#: acl/permissions/threads.py:437
 msgid "You don't have permission to delete threads in closed forum."
 msgid "You don't have permission to delete threads in closed forum."
 msgstr "Nie posiadasz uprawnień do usuwania tematów na tym forum."
 msgstr "Nie posiadasz uprawnień do usuwania tematów na tym forum."
 
 
-#: acl/permissions/threads.py:429
+#: acl/permissions/threads.py:439
 msgid "This thread is closed, you cannot delete it."
 msgid "This thread is closed, you cannot delete it."
 msgstr "Ten temat jest zamknięty, nie możesz go usunąć."
 msgstr "Ten temat jest zamknięty, nie możesz go usunąć."
 
 
-#: acl/permissions/threads.py:431 acl/permissions/threads.py:465
+#: acl/permissions/threads.py:441 acl/permissions/threads.py:477
 msgid "This post is protected, you cannot delete it."
 msgid "This post is protected, you cannot delete it."
 msgstr "Ten post jest chroniony, nie możesz go usunąć."
 msgstr "Ten post jest chroniony, nie możesz go usunąć."
 
 
-#: acl/permissions/threads.py:433
-msgid "You cannot hard delete this thread."
-msgstr "Nie możesz trwale usunąć tego tematu."
-
-#: acl/permissions/threads.py:435 acl/permissions/threads.py:439
+#: acl/permissions/threads.py:445 acl/permissions/threads.py:449
 msgid "You don't have permission to delete this thread."
 msgid "You don't have permission to delete this thread."
 msgstr "Nie posiadasz uprawnień do usunięcia tego tematu."
 msgstr "Nie posiadasz uprawnień do usunięcia tego tematu."
 
 
-#: acl/permissions/threads.py:437
+#: acl/permissions/threads.py:447
 msgid "This thread is already deleted."
 msgid "This thread is already deleted."
 msgstr "Ten temat został już usunięty."
 msgstr "Ten temat został już usunięty."
 
 
-#: acl/permissions/threads.py:461
+#: acl/permissions/threads.py:473
 msgid "You don't have permission to delete posts in closed forum."
 msgid "You don't have permission to delete posts in closed forum."
 msgstr "Nie posiadasz uprawnień do usuwania postów w zamkniętym forum."
 msgstr "Nie posiadasz uprawnień do usuwania postów w zamkniętym forum."
 
 
-#: acl/permissions/threads.py:463
+#: acl/permissions/threads.py:475
 msgid "This thread is closed, you cannot delete its posts."
 msgid "This thread is closed, you cannot delete its posts."
 msgstr "Ten temat jest zamknięty, nie możesz usunąć jego postów."
 msgstr "Ten temat jest zamknięty, nie możesz usunąć jego postów."
 
 
-#: acl/permissions/threads.py:467
-msgid "You cannot hard delete this post."
-msgstr "Nie mozesz usunąć tego postu na trwałe."
-
-#: acl/permissions/threads.py:469 acl/permissions/threads.py:473
+#: acl/permissions/threads.py:481 acl/permissions/threads.py:485
 msgid "You don't have permission to delete this post."
 msgid "You don't have permission to delete this post."
 msgstr "Nie masz uprawnień do usunięcia tego postu."
 msgstr "Nie masz uprawnień do usunięcia tego postu."
 
 
-#: acl/permissions/threads.py:471
+#: acl/permissions/threads.py:483
 msgid "This post is already deleted."
 msgid "This post is already deleted."
 msgstr "Ten post został już usunięty."
 msgstr "Ten post został już usunięty."
 
 
-#: acl/permissions/threads.py:529 acl/permissions/threads.py:531
-#, fuzzy
+#: acl/permissions/threads.py:541 acl/permissions/threads.py:543
 msgid "You cannot upvote posts in this forum."
 msgid "You cannot upvote posts in this forum."
 msgstr "Nie możesz głosować za postami na tym forum."
 msgstr "Nie możesz głosować za postami na tym forum."
 
 
-#: acl/permissions/threads.py:537 acl/permissions/threads.py:539
-#, fuzzy
+#: acl/permissions/threads.py:549 acl/permissions/threads.py:551
 msgid "You cannot downvote posts in this forum."
 msgid "You cannot downvote posts in this forum."
 msgstr "Nie możesz głosować przeciw postom na tym forum."
 msgstr "Nie możesz głosować przeciw postom na tym forum."
 
 
-#: acl/permissions/threads.py:545 acl/permissions/threads.py:547
+#: acl/permissions/threads.py:557 acl/permissions/threads.py:559
 msgid "You don't have permission to see who voted on this post."
 msgid "You don't have permission to see who voted on this post."
 msgstr "Nie posiadasz uprawnień, aby zobaczyć kto głosował na ten post."
 msgstr "Nie posiadasz uprawnień, aby zobaczyć kto głosował na ten post."
 
 
+#: acl/permissions/threads.py:580 acl/permissions/threads.py:582
+msgid "Selected checkpoint could not be found."
+msgstr "Żądana etykieta zdarzenia nie została odnaleziona."
+
+#: acl/permissions/threads.py:588 acl/permissions/threads.py:590
+msgid "You cannot hide checkpoints!"
+msgstr "Nie mozesz ukrywać etykiet zdarzeń!"
+
+#: acl/permissions/threads.py:596 acl/permissions/threads.py:598
+msgid "You cannot delete checkpoints!"
+msgstr "Nie mozesz usuwać etykiet zdarzeń!"
+
+#: acl/permissions/threads.py:604 acl/permissions/threads.py:606
+msgid "You cannot show checkpoints!"
+msgstr "Nie możesz odkrywać etykiet zdarzeń!"
+
 #: acl/permissions/usercp.py:16
 #: acl/permissions/usercp.py:16
 msgid "Profile Settings"
 msgid "Profile Settings"
 msgstr "Ustawienia profilu"
 msgstr "Ustawienia profilu"
@@ -682,7 +690,6 @@ msgid "You don't have permission to see requested page."
 msgstr "Nie posiadasz uprawnień do zobaczenia tej strony."
 msgstr "Nie posiadasz uprawnień do zobaczenia tej strony."
 
 
 #: apps/errors.py:43
 #: apps/errors.py:43
-#, fuzzy
 msgid "You are banned."
 msgid "You are banned."
 msgstr "Twoje konto zostało zablokowane."
 msgstr "Twoje konto zostało zablokowane."
 
 
@@ -691,7 +698,6 @@ msgid "All forums have been marked as read."
 msgstr "Wszystkie fora zostały oznaczone jako przeczytane."
 msgstr "Wszystkie fora zostały oznaczone jako przeczytane."
 
 
 #: apps/redirect.py:12
 #: apps/redirect.py:12
-#, fuzzy
 msgid "You don't have permission to follow this redirect."
 msgid "You don't have permission to follow this redirect."
 msgstr "Nie posiadasz uprawnień do podążania za tym przekierowaniem."
 msgstr "Nie posiadasz uprawnień do podążania za tym przekierowaniem."
 
 
@@ -748,16 +754,16 @@ msgstr ""
 "zażądaj nowego e-maila z linkiem aktywacyjnym."
 "zażądaj nowego e-maila z linkiem aktywacyjnym."
 
 
 #: apps/activation/views.py:83
 #: apps/activation/views.py:83
-#, fuzzy, python-format
+#, python-format
 msgid ""
 msgid ""
 "%(username)s, your account has been successfully reactivated after change of "
 "%(username)s, your account has been successfully reactivated after change of "
 "sign-in credentials."
 "sign-in credentials."
 msgstr ""
 msgstr ""
-"%(username)s, Twoje konto zostało pomyślnie reaktywowane po zamianie danych "
+"%(username)s, Twoje konto zostało pomyślnie reaktywowane po zmianie danych "
 "logowania."
 "logowania."
 
 
 #: apps/activation/views.py:85
 #: apps/activation/views.py:85
-#, fuzzy, python-format
+#, python-format
 msgid ""
 msgid ""
 "%(username)s, your account has been successfully activated. Welcome aboard!"
 "%(username)s, your account has been successfully activated. Welcome aboard!"
 msgstr ""
 msgstr ""
@@ -767,54 +773,53 @@ msgstr ""
 msgid "Team Member"
 msgid "Team Member"
 msgstr "Członek zespołu forum"
 msgstr "Członek zespołu forum"
 
 
-#: apps/admin/widgets.py:95 templates/admin/admin/list.html:70
+#: apps/admin/widgets.py:97 templates/admin/admin/list.html:70
 #: templates/cranefly/private_threads/list.html:120
 #: templates/cranefly/private_threads/list.html:120
-#: templates/cranefly/private_threads/thread.html:347
-#: templates/cranefly/private_threads/thread.html:355
-#: templates/cranefly/threads/list.html:189
-#: templates/cranefly/threads/thread.html:375
-#: templates/cranefly/threads/thread.html:383
+#: templates/cranefly/private_threads/thread.html:358
+#: templates/cranefly/private_threads/thread.html:366
+#: templates/cranefly/threads/list.html:191
+#: templates/cranefly/threads/thread.html:378
+#: templates/cranefly/threads/thread.html:386
 msgid "Go"
 msgid "Go"
 msgstr "Idź"
 msgstr "Idź"
 
 
-#: apps/admin/widgets.py:96
+#: apps/admin/widgets.py:98
 msgid "There are no items to display"
 msgid "There are no items to display"
 msgstr "Nie ma żadnych elementów do wyświetlenia"
 msgstr "Nie ma żadnych elementów do wyświetlenia"
 
 
-#: apps/admin/widgets.py:97
+#: apps/admin/widgets.py:99
 msgid "Search has returned no items"
 msgid "Search has returned no items"
 msgstr "Wyszukiwanie nie zwróciło żadnych wyników"
 msgstr "Wyszukiwanie nie zwróciło żadnych wyników"
 
 
-#: apps/admin/widgets.py:98
+#: apps/admin/widgets.py:100
 msgid "You have to select at least one item."
 msgid "You have to select at least one item."
 msgstr "Musisz wybrać przynajmniej jeden element."
 msgstr "Musisz wybrać przynajmniej jeden element."
 
 
-#: apps/admin/widgets.py:317
+#: apps/admin/widgets.py:286
 msgid "No search criteria have been defined."
 msgid "No search criteria have been defined."
 msgstr "Nie zdefiniowano żadnych kryteriów wyszukiwania."
 msgstr "Nie zdefiniowano żadnych kryteriów wyszukiwania."
 
 
-#: apps/admin/widgets.py:322
+#: apps/admin/widgets.py:291
 msgid "Search form contains errors."
 msgid "Search form contains errors."
 msgstr "Formularz wyszukiwania zawiera błędy."
 msgstr "Formularz wyszukiwania zawiera błędy."
 
 
-#: apps/admin/widgets.py:330
+#: apps/admin/widgets.py:299
 msgid "Search criteria have been cleared."
 msgid "Search criteria have been cleared."
 msgstr "Kryteria wyszukiwania zostały wyczyszczone."
 msgstr "Kryteria wyszukiwania zostały wyczyszczone."
 
 
-#: apps/admin/widgets.py:368 apps/admin/widgets.py:373
-#: apps/threadtype/list/views.py:83 apps/threadtype/thread/views.py:111
-#: apps/threadtype/thread/views.py:158
-#, fuzzy
+#: apps/admin/widgets.py:337 apps/admin/widgets.py:342
+#: apps/threadtype/list/views.py:84 apps/threadtype/thread/views.py:121
+#: apps/threadtype/thread/views.py:168
 msgid "Action requested is incorrect."
 msgid "Action requested is incorrect."
 msgstr "Żądana akcja jest niepoprawna."
 msgstr "Żądana akcja jest niepoprawna."
 
 
-#: apps/admin/widgets.py:412
+#: apps/admin/widgets.py:381
 #: templates/cranefly/private_threads/posting.html:172
 #: templates/cranefly/private_threads/posting.html:172
-#: templates/cranefly/threads/posting.html:181
+#: templates/cranefly/threads/posting.html:179
 msgid "Save Changes"
 msgid "Save Changes"
 msgstr "Zapisz zmiany"
 msgstr "Zapisz zmiany"
 
 
-#: apps/admin/widgets.py:534
+#: apps/admin/widgets.py:503
 msgid "Action authorization is invalid."
 msgid "Action authorization is invalid."
 msgstr "Autoryzacja dla żądanej akcji jest niepoprawna."
 msgstr "Autoryzacja dla żądanej akcji jest niepoprawna."
 
 
@@ -896,7 +901,7 @@ msgstr "Opcjonalny. Powód blokady, który wyświetli się członkom zespołu fo
 msgid "Username and e-mail"
 msgid "Username and e-mail"
 msgstr "Nazwa użytkownika i e-mail"
 msgstr "Nazwa użytkownika i e-mail"
 
 
-#: apps/admin/bans/forms.py:43 apps/admin/online/forms.py:22
+#: apps/admin/bans/forms.py:43 apps/admin/online/forms.py:21
 #: apps/admin/users/forms.py:36 apps/admin/users/forms.py:133
 #: apps/admin/users/forms.py:36 apps/admin/users/forms.py:133
 #: apps/admin/users/forms.py:200 apps/register/forms.py:31
 #: apps/admin/users/forms.py:200 apps/register/forms.py:31
 #: templates/_email/users/activation/none.html:9
 #: templates/_email/users/activation/none.html:9
@@ -909,12 +914,12 @@ msgstr "Nazwa użytkownika"
 msgid "E-mail address"
 msgid "E-mail address"
 msgstr "Adres e-mail"
 msgstr "Adres e-mail"
 
 
-#: apps/admin/bans/forms.py:45 apps/admin/online/forms.py:21
+#: apps/admin/bans/forms.py:45 apps/admin/online/forms.py:20
 #: templates/admin/banning/list.html:11
 #: templates/admin/banning/list.html:11
 #: templates/cranefly/private_threads/details.html:30
 #: templates/cranefly/private_threads/details.html:30
 #: templates/cranefly/profiles/details.html:189
 #: templates/cranefly/profiles/details.html:189
 #: templates/cranefly/profiles/details.html:220
 #: templates/cranefly/profiles/details.html:220
-#: templates/cranefly/threads/details.html:32
+#: templates/cranefly/threads/details.html:30
 msgid "IP Address"
 msgid "IP Address"
 msgstr "Adres IP"
 msgstr "Adres IP"
 
 
@@ -1100,7 +1105,7 @@ msgid "Adjustment using theme \"%(name)s\" has been deleted."
 msgstr "Przypisanie dla szablonu \"%(name)s\" zostało usunięte."
 msgstr "Przypisanie dla szablonu \"%(name)s\" zostało usunięte."
 
 
 #: apps/admin/forumroles/forms.py:8 apps/admin/roles/forms.py:8
 #: apps/admin/forumroles/forms.py:8 apps/admin/roles/forms.py:8
-msgid "Role name must be sluggable."
+msgid "Role name must contain alphanumeric characters."
 msgstr "Nazwa roli nie może zawierać wyłącznie znaków specjalnych."
 msgstr "Nazwa roli nie może zawierać wyłącznie znaków specjalnych."
 
 
 #: apps/admin/forumroles/forms.py:9 apps/admin/roles/forms.py:9
 #: apps/admin/forumroles/forms.py:9 apps/admin/roles/forms.py:9
@@ -1118,7 +1123,7 @@ msgstr "Nazwa roli"
 #: apps/admin/forumroles/forms.py:17 apps/admin/roles/forms.py:18
 #: apps/admin/forumroles/forms.py:17 apps/admin/roles/forms.py:18
 msgid "Role Name is used to identify this role in Admin Control Panel."
 msgid "Role Name is used to identify this role in Admin Control Panel."
 msgstr ""
 msgstr ""
-"Nazwa roli służy do zidentyfikowania tej roli w panelu administratora (ACP)."
+"Nazwa roli służy do zidentyfikowania jej w panelu administratora (ACP)."
 
 
 #: apps/admin/forumroles/views.py:25 apps/admin/roles/views.py:26
 #: apps/admin/forumroles/views.py:25 apps/admin/roles/views.py:26
 #: templates/admin/roles/forums.html:12
 #: templates/admin/roles/forums.html:12
@@ -1182,7 +1187,7 @@ msgstr "Zmiany w roli \"%(name)s\" zostały zachowane."
 
 
 #: apps/admin/forumroles/views.py:99
 #: apps/admin/forumroles/views.py:99
 msgid "Change Forum Role Permissions"
 msgid "Change Forum Role Permissions"
-msgstr ""
+msgstr "Zmień uprawnienia roli użytkownika"
 
 
 #: apps/admin/forumroles/views.py:132
 #: apps/admin/forumroles/views.py:132
 #, python-format
 #, python-format
@@ -1194,51 +1199,153 @@ msgstr "Uprawnienia roli \"%(name)s\" zostały zmienione."
 msgid "Forum Role \"%(name)s\" has been deleted."
 msgid "Forum Role \"%(name)s\" has been deleted."
 msgstr "Rola \"%(name)s\" została usunięta."
 msgstr "Rola \"%(name)s\" została usunięta."
 
 
-#: apps/admin/forums/forms.py:12
-msgid "Category name must be sluggable."
+#: apps/admin/forums/forms.py:23
+msgid "Category"
+msgstr "Kategoria"
+
+#: apps/admin/forums/forms.py:24 apps/admin/forums/views.py:28
+#: templates/admin/roles/forums.html:11
+msgid "Forum"
+msgstr "Forum"
+
+#: apps/admin/forums/forms.py:25
+msgid "Redirection"
+msgstr "Przekierowanie"
+
+#: apps/admin/forums/forms.py:28 apps/admin/forums/forms.py:82
+msgid "Category name must contain alphanumeric characters."
 msgstr "Nazwa kategorii nie może zawierać samych znaków specjalnych."
 msgstr "Nazwa kategorii nie może zawierać samych znaków specjalnych."
 
 
-#: apps/admin/forums/forms.py:13
+#: apps/admin/forums/forms.py:29 apps/admin/forums/forms.py:83
 msgid "Category name is too long."
 msgid "Category name is too long."
 msgstr "Nazwa kategorii jest zbyt długa."
 msgstr "Nazwa kategorii jest zbyt długa."
 
 
-#: apps/admin/forums/forms.py:23 apps/admin/forums/forms.py:73
-#: apps/admin/forums/forms.py:126
-#, fuzzy
+#: apps/admin/forums/forms.py:40 apps/admin/forums/forms.py:93
+#: apps/admin/forums/forms.py:134 apps/admin/forums/forms.py:185
 msgid "Basic Options"
 msgid "Basic Options"
-msgstr "Dane podstawowe"
+msgstr "Opcje podstawowe"
 
 
-#: apps/admin/forums/forms.py:25
-msgid "Category Parent"
-msgstr "Rodzic kategorii"
+#: apps/admin/forums/forms.py:42
+msgid "Node Parent"
+msgstr "Rodzic węzła"
 
 
-#: apps/admin/forums/forms.py:26 apps/admin/forums/forms.py:76
-#: apps/admin/forums/forms.py:129
+#: apps/admin/forums/forms.py:43 apps/admin/forums/forms.py:96
+#: apps/admin/forums/forms.py:137 apps/admin/forums/forms.py:188
 msgid "Copy Permissions from"
 msgid "Copy Permissions from"
 msgstr "Skopiuj uprawnienia z"
 msgstr "Skopiuj uprawnienia z"
 
 
-#: apps/admin/forums/forms.py:27
+#: apps/admin/forums/forms.py:44
+msgid "Node Type"
+msgstr "Typ węzła"
+
+#: apps/admin/forums/forms.py:44
+msgid ""
+"Each Node has specific role in forums tree. This role cannot be changed "
+"after node is created."
+msgstr ""
+"Każdy węzeł ma swoją konkretną rolę w drzewie forów. Rola nie może zostać "
+"zmieniona po utworzeniu węzła."
+
+#: apps/admin/forums/forms.py:45
+msgid "Node Name"
+msgstr "Nazwa węzła"
+
+#: apps/admin/forums/forms.py:46
+msgid "Node Description"
+msgstr "Opis węzła"
+
+#: apps/admin/forums/forms.py:47 apps/admin/forums/forms.py:190
+msgid "Redirect URL"
+msgstr "Adres URL przekierowania"
+
+#: apps/admin/forums/forms.py:47
+msgid ""
+"Redirection nodes require you to specify URL they will redirect users to "
+"upon click."
+msgstr ""
+"Węzeł przekierowujący wymaga podania adres URL, do którego po kliknięciu "
+"będzie przekierowywał użytkowników."
+
+#: apps/admin/forums/forms.py:48
+msgid "Closed Node"
+msgstr "Zamknięty węzeł"
+
+#: apps/admin/forums/forms.py:52 apps/admin/forums/forms.py:103
+#: apps/admin/forums/forms.py:152 apps/admin/forums/forms.py:195
+msgid "Display Options"
+msgstr "Opcje wyświetlania"
+
+#: apps/admin/forums/forms.py:54
+msgid "Node Attributes"
+msgstr "Atrybuty węzła"
+
+#: apps/admin/forums/forms.py:54
+msgid ""
+"Custom templates can check nodes for predefined attributes that will change "
+"way they are rendered."
+msgstr ""
+"Specjalne szablony, mogą różnie wyświetlać węzły, w zależności od "
+"zdefiniowanych atrybutów."
+
+#: apps/admin/forums/forms.py:55 apps/admin/forums/forms.py:106
+#: apps/admin/forums/forms.py:155
+msgid "Show Subforums Details"
+msgstr "Pokaż szczegóły podforów"
+
+#: apps/admin/forums/forms.py:55
+msgid ""
+"Allows you to prevent this node subforums from displaying statistics, last "
+"post data, etc. ect. on forums lists."
+msgstr ""
+"Umożliwia wyłączenie wyświetlania statystyk (np. informacji o ostatnim "
+"poście) dla podforów danego węzła."
+
+#: apps/admin/forums/forms.py:56
+msgid "Node Style"
+msgstr "Styl węzła"
+
+#: apps/admin/forums/forms.py:56
+msgid ""
+"You can add custom CSS classess to this node, to change way it looks on "
+"board index."
+msgstr ""
+"Możesz zdefiniować specjalne klasy CSS, które zmienią wygląd tego węzła na "
+"stronie głównej forum."
+
+#: apps/admin/forums/forms.py:63 apps/admin/forums/forms.py:113
+#: apps/admin/forums/forms.py:162 apps/admin/forums/forms.py:204
+msgid "Don't copy permissions"
+msgstr "Nie kopiuj uprawnień"
+
+#: apps/admin/forums/forms.py:70
+msgid "Only categories can use Root Category as their parent."
+msgstr "Tylko kategorie mogą mieć główny korzeń jako rodzica."
+
+#: apps/admin/forums/forms.py:72
+msgid "You have to define redirection URL"
+msgstr "Musisz zdefiniować adres URL przekierowania"
+
+#: apps/admin/forums/forms.py:95
+msgid "Category Parent"
+msgstr "Rodzic kategorii"
+
+#: apps/admin/forums/forms.py:97
 msgid "Category Name"
 msgid "Category Name"
 msgstr "Nazwa kategorii"
 msgstr "Nazwa kategorii"
 
 
-#: apps/admin/forums/forms.py:28
+#: apps/admin/forums/forms.py:98
 msgid "Category Description"
 msgid "Category Description"
 msgstr "Opis kategorii"
 msgstr "Opis kategorii"
 
 
-#: apps/admin/forums/forms.py:29
+#: apps/admin/forums/forms.py:99
 msgid "Closed Category"
 msgid "Closed Category"
-msgstr "Zamknięta kategoria"
-
-#: apps/admin/forums/forms.py:33 apps/admin/forums/forms.py:90
-#: apps/admin/forums/forms.py:136
-msgid "Display Options"
-msgstr "Wyświetl opcje"
+msgstr "Kategoria zamknięta"
 
 
-#: apps/admin/forums/forms.py:35
+#: apps/admin/forums/forms.py:105
 msgid "Category Attributes"
 msgid "Category Attributes"
 msgstr "Atrybuty kategorii"
 msgstr "Atrybuty kategorii"
 
 
-#: apps/admin/forums/forms.py:35
+#: apps/admin/forums/forms.py:105
 msgid ""
 msgid ""
 "Custom templates can check categories for predefined attributes that will "
 "Custom templates can check categories for predefined attributes that will "
 "change way they are rendered."
 "change way they are rendered."
@@ -1246,11 +1353,7 @@ msgstr ""
 "Specjalne szablony, mogą różnie wyświetlać kategorie, w zależności od "
 "Specjalne szablony, mogą różnie wyświetlać kategorie, w zależności od "
 "zdefiniowanych atrybutów."
 "zdefiniowanych atrybutów."
 
 
-#: apps/admin/forums/forms.py:36 apps/admin/forums/forms.py:93
-msgid "Show Subforums Details"
-msgstr "Pokaż szczczegóły podforów"
-
-#: apps/admin/forums/forms.py:36
+#: apps/admin/forums/forms.py:106
 msgid ""
 msgid ""
 "Allows you to prevent this category subforums from displaying statistics, "
 "Allows you to prevent this category subforums from displaying statistics, "
 "last post data, etc. ect. on forums lists."
 "last post data, etc. ect. on forums lists."
@@ -1258,11 +1361,11 @@ msgstr ""
 "Umożliwia wyłączenie wyświetlania statystyk (np. informacji o ostatnim "
 "Umożliwia wyłączenie wyświetlania statystyk (np. informacji o ostatnim "
 "poście) dla podforów danej kategorii."
 "poście) dla podforów danej kategorii."
 
 
-#: apps/admin/forums/forms.py:37
+#: apps/admin/forums/forms.py:107
 msgid "Category Style"
 msgid "Category Style"
 msgstr "Styl kategorii"
 msgstr "Styl kategorii"
 
 
-#: apps/admin/forums/forms.py:37
+#: apps/admin/forums/forms.py:107
 msgid ""
 msgid ""
 "You can add custom CSS classess to this category, to change way it looks on "
 "You can add custom CSS classess to this category, to change way it looks on "
 "board index."
 "board index."
@@ -1270,44 +1373,39 @@ msgstr ""
 "Możesz zdefiniować specjalne klasy CSS, które zmienią wygląd kategorii w na "
 "Możesz zdefiniować specjalne klasy CSS, które zmienią wygląd kategorii w na "
 "stronie głównej forum."
 "stronie głównej forum."
 
 
-#: apps/admin/forums/forms.py:44 apps/admin/forums/forms.py:101
-#: apps/admin/forums/forms.py:145
-msgid "Don't copy permissions"
-msgstr "Nie kopiuj uprawnień"
-
-#: apps/admin/forums/forms.py:60
-msgid "Forum name must be sluggable."
+#: apps/admin/forums/forms.py:121
+msgid "Forum name must contain alphanumeric characters."
 msgstr "Nazwa forum nie może składać się wyłącznie ze znaków specjlanych."
 msgstr "Nazwa forum nie może składać się wyłącznie ze znaków specjlanych."
 
 
-#: apps/admin/forums/forms.py:61
+#: apps/admin/forums/forms.py:122
 msgid "Forum name is too long."
 msgid "Forum name is too long."
 msgstr "Nazwa forum jest zbyt długa."
 msgstr "Nazwa forum jest zbyt długa."
 
 
-#: apps/admin/forums/forms.py:75
+#: apps/admin/forums/forms.py:136
 msgid "Forum Parent"
 msgid "Forum Parent"
 msgstr "Rodzic forum"
 msgstr "Rodzic forum"
 
 
-#: apps/admin/forums/forms.py:77
+#: apps/admin/forums/forms.py:138
 msgid "Forum Name"
 msgid "Forum Name"
 msgstr "Nazwa forum"
 msgstr "Nazwa forum"
 
 
-#: apps/admin/forums/forms.py:78
+#: apps/admin/forums/forms.py:139
 msgid "Forum Description"
 msgid "Forum Description"
 msgstr "Opis forum"
 msgstr "Opis forum"
 
 
-#: apps/admin/forums/forms.py:79
+#: apps/admin/forums/forms.py:140
 msgid "Closed Forum"
 msgid "Closed Forum"
 msgstr "Zamknięte forum"
 msgstr "Zamknięte forum"
 
 
-#: apps/admin/forums/forms.py:83
+#: apps/admin/forums/forms.py:144
 msgid "Prune Forum"
 msgid "Prune Forum"
 msgstr "Automatyczne czyszczenie forum"
 msgstr "Automatyczne czyszczenie forum"
 
 
-#: apps/admin/forums/forms.py:85
+#: apps/admin/forums/forms.py:146
 msgid "Delete threads with first post older than"
 msgid "Delete threads with first post older than"
 msgstr "Usuń tematy, których pierwszy post jest starszy niż"
 msgstr "Usuń tematy, których pierwszy post jest starszy niż"
 
 
-#: apps/admin/forums/forms.py:85
+#: apps/admin/forums/forms.py:146
 msgid ""
 msgid ""
 "Enter number of days since thread start after which thread will be deleted "
 "Enter number of days since thread start after which thread will be deleted "
 "or zero to don't delete threads."
 "or zero to don't delete threads."
@@ -1315,11 +1413,11 @@ msgstr ""
 "Podaj ilość dni od rozpoczęcia tematu, po których zostanie on usunięty. "
 "Podaj ilość dni od rozpoczęcia tematu, po których zostanie on usunięty. "
 "Wpisz 0, aby wyłączyć usuwanie na podstawie tego kryterium."
 "Wpisz 0, aby wyłączyć usuwanie na podstawie tego kryterium."
 
 
-#: apps/admin/forums/forms.py:86
+#: apps/admin/forums/forms.py:147
 msgid "Delete threads with last post older than"
 msgid "Delete threads with last post older than"
 msgstr "Usuń tematy, których ostatni post jest starszy niż"
 msgstr "Usuń tematy, których ostatni post jest starszy niż"
 
 
-#: apps/admin/forums/forms.py:86
+#: apps/admin/forums/forms.py:147
 msgid ""
 msgid ""
 "Enter number of days since since last reply in thread after which thread "
 "Enter number of days since since last reply in thread after which thread "
 "will be deleted or zero to don't delete threads."
 "will be deleted or zero to don't delete threads."
@@ -1327,61 +1425,82 @@ msgstr ""
 "Podaj ilość dni od ostatniej odpowiedzi w temacie, po których zostanie on "
 "Podaj ilość dni od ostatniej odpowiedzi w temacie, po których zostanie on "
 "usunięty. Wpisz 0, aby wyłączyć usuwanie na podstawie tego kryterium."
 "usunięty. Wpisz 0, aby wyłączyć usuwanie na podstawie tego kryterium."
 
 
-#: apps/admin/forums/forms.py:92
-msgid "Subforums List Attributes"
+#: apps/admin/forums/forms.py:148
+msgid "Archive pruned threads?"
+msgstr "Czy archiwizować automatycznie usuwane tematy?"
+
+#: apps/admin/forums/forms.py:148
+msgid ""
+"If you want, you can archive pruned threads in other forum instead of "
+"deleting them."
 msgstr ""
 msgstr ""
+"Jeżeli chcesz, możesz zamiast usuwać, archiwizować stare tematy, podczas "
+"automatycznego czyszczenia forum."
+
+#: apps/admin/forums/forms.py:154 apps/admin/forums/forms.py:197
+msgid "Forum Attributes"
+msgstr "Atrybuty forum"
 
 
-#: apps/admin/forums/forms.py:92
+#: apps/admin/forums/forms.py:154 apps/admin/forums/forms.py:197
 msgid ""
 msgid ""
 "Custom templates can check forums for predefined attributes that will change "
 "Custom templates can check forums for predefined attributes that will change "
 "way subforums lists are rendered."
 "way subforums lists are rendered."
 msgstr ""
 msgstr ""
+"Niestandardowe szablony moga szukać w forach predefiniowanych atrybutów i na "
+"ich podstawie zmieniać wygląd listy subforów."
 
 
-#: apps/admin/forums/forms.py:93
+#: apps/admin/forums/forms.py:155
 msgid ""
 msgid ""
 "Allows you to prevent this forum's subforums from displaying statistics, "
 "Allows you to prevent this forum's subforums from displaying statistics, "
 "last post data, etc. ect. on subforums list."
 "last post data, etc. ect. on subforums list."
 msgstr ""
 msgstr ""
+"Umożliwia wyłączenie wyświetlania statystyk i danych (typu ostatni post) na "
+"liście subforów tego forum."
 
 
-#: apps/admin/forums/forms.py:94
+#: apps/admin/forums/forms.py:156
 msgid "Forum Style"
 msgid "Forum Style"
 msgstr "Styl forum"
 msgstr "Styl forum"
 
 
-#: apps/admin/forums/forms.py:94
+#: apps/admin/forums/forms.py:156
 msgid ""
 msgid ""
 "You can add custom CSS classess to this forum to change way it looks on "
 "You can add custom CSS classess to this forum to change way it looks on "
 "forums lists."
 "forums lists."
 msgstr ""
 msgstr ""
+"Możesz dodać własne klasy CSS, aby zmienić wygląd tego forum na liście forów."
+
+#: apps/admin/forums/forms.py:163
+msgid "Don't archive pruned threads"
+msgstr "Nie archiwizuj automatycznie usuwanych tematów."
+
+#: apps/admin/forums/forms.py:168
+msgid "Forum cannot be its own archive."
+msgstr "Forum nie może stanowić swojego własnego archiwum."
 
 
-#: apps/admin/forums/forms.py:117
-msgid "Redirect name must be sluggable."
+#: apps/admin/forums/forms.py:176
+msgid "Redirect name must contain alphanumeric characters."
 msgstr "Nazwa przekierowania nie może się składać tylko ze znaków specjalnych."
 msgstr "Nazwa przekierowania nie może się składać tylko ze znaków specjalnych."
 
 
-#: apps/admin/forums/forms.py:118
+#: apps/admin/forums/forms.py:177
 msgid "Redirect name is too long."
 msgid "Redirect name is too long."
 msgstr "Nazwa przekierowania jest zbyt długa."
 msgstr "Nazwa przekierowania jest zbyt długa."
 
 
-#: apps/admin/forums/forms.py:128
+#: apps/admin/forums/forms.py:187
 msgid "Redirect Parent"
 msgid "Redirect Parent"
 msgstr "Rodzic przekierowania"
 msgstr "Rodzic przekierowania"
 
 
-#: apps/admin/forums/forms.py:130
+#: apps/admin/forums/forms.py:189
 msgid "Redirect Name"
 msgid "Redirect Name"
 msgstr "Nazwa przekierowania"
 msgstr "Nazwa przekierowania"
 
 
-#: apps/admin/forums/forms.py:131
-msgid "Redirect URL"
-msgstr "Adres URL przekierowania"
-
-#: apps/admin/forums/forms.py:132
+#: apps/admin/forums/forms.py:191
 msgid "Redirect Description"
 msgid "Redirect Description"
 msgstr "Opis przekierowania"
 msgstr "Opis przekierowania"
 
 
-#: apps/admin/forums/forms.py:138
+#: apps/admin/forums/forms.py:198
 msgid "Redirect Style"
 msgid "Redirect Style"
 msgstr "Styl przekierowania"
 msgstr "Styl przekierowania"
 
 
-#: apps/admin/forums/forms.py:138
+#: apps/admin/forums/forms.py:198
 msgid ""
 msgid ""
 "You can add custom CSS classess to this redirect to change way it looks on "
 "You can add custom CSS classess to this redirect to change way it looks on "
 "forums lists."
 "forums lists."
@@ -1389,192 +1508,188 @@ msgstr ""
 "Do przekierowania możesz przypisać specjalną klasę CSS, aby zmienić jego "
 "Do przekierowania możesz przypisać specjalną klasę CSS, aby zmienić jego "
 "wygląd na liście forów."
 "wygląd na liście forów."
 
 
-#: apps/admin/forums/forms.py:151
+#: apps/admin/forums/forms.py:210
 msgid "Delete Options"
 msgid "Delete Options"
 msgstr "Opcje usuwania"
 msgstr "Opcje usuwania"
 
 
-#: apps/admin/forums/forms.py:153
+#: apps/admin/forums/forms.py:212
 msgid "Move threads to"
 msgid "Move threads to"
 msgstr "Przenieś tematy do"
 msgstr "Przenieś tematy do"
 
 
-#: apps/admin/forums/forms.py:154
+#: apps/admin/forums/forms.py:213
 msgid "Move subforums to"
 msgid "Move subforums to"
 msgstr "Przenieś subfora do"
 msgstr "Przenieś subfora do"
 
 
-#: apps/admin/forums/forms.py:164 apps/admin/forums/forms.py:165
-#: apps/admin/forums/views.py:326
+#: apps/admin/forums/forms.py:223 apps/admin/forums/forms.py:224
+#: apps/admin/forums/views.py:352
 msgid "Remove with forum"
 msgid "Remove with forum"
-msgstr ""
+msgstr "Usuń razem z forum"
 
 
-#: apps/admin/forums/forms.py:171
+#: apps/admin/forums/forms.py:230
 msgid "Categories cannot contain threads."
 msgid "Categories cannot contain threads."
 msgstr "Kategorie nie mogą zawierać tematów."
 msgstr "Kategorie nie mogą zawierać tematów."
 
 
-#: apps/admin/forums/forms.py:173
+#: apps/admin/forums/forms.py:232
 msgid "Redirects cannot contain threads."
 msgid "Redirects cannot contain threads."
 msgstr "Przekierowania nie mogą zawierać tematów."
 msgstr "Przekierowania nie mogą zawierać tematów."
 
 
-#: apps/admin/forums/forms.py:179
+#: apps/admin/forums/forms.py:238
 msgid ""
 msgid ""
 "Destination you want to move this forum's threads to will be deleted with "
 "Destination you want to move this forum's threads to will be deleted with "
 "this forum."
 "this forum."
 msgstr ""
 msgstr ""
+"Nowe miejsce do, którego miałyby zostać przeniesione tematy, zostałoby "
+"usunięte wraz z usuwanym forum."
 
 
-#: apps/admin/forums/views.py:25 templates/admin/roles/forums.html:11
-msgid "Forum"
-msgstr "Forum"
-
-#: apps/admin/forums/views.py:27
+#: apps/admin/forums/views.py:30
 msgid "You have to select at least one forum."
 msgid "You have to select at least one forum."
 msgstr "Musisz zaznaczyć przynajmniej jedno forum."
 msgstr "Musisz zaznaczyć przynajmniej jedno forum."
 
 
-#: apps/admin/forums/views.py:29
-msgid "Resynchronise forums"
-msgstr ""
-
-#: apps/admin/forums/views.py:30
-msgid "Prune forums"
-msgstr ""
+#: apps/admin/forums/views.py:32
+msgid "Resynchronize forums (fast)"
+msgstr "Zsynchronizuj fora (szybko)"
 
 
-#: apps/admin/forums/views.py:30
-msgid "Are you sure you want to delete all content from selected forums?"
-msgstr "Czy ta pewno chcesz usunąć całą zawartość zaznaczonych forów?"
+#: apps/admin/forums/views.py:33
+msgid "Resynchronize forums"
+msgstr "Zsynchronizuj fora"
 
 
-#: apps/admin/forums/views.py:32 apps/admin/roles/views.py:123
+#: apps/admin/forums/views.py:35 apps/admin/roles/views.py:123
 msgid "No forums are currently defined."
 msgid "No forums are currently defined."
-msgstr ""
+msgstr "Nie zdefiniowano jeszcze żadnych forów."
 
 
-#: apps/admin/forums/views.py:43
+#: apps/admin/forums/views.py:46
 msgid "Move Category Up"
 msgid "Move Category Up"
 msgstr "Przesuń kategorię w górę"
 msgstr "Przesuń kategorię w górę"
 
 
-#: apps/admin/forums/views.py:44
+#: apps/admin/forums/views.py:47
 msgid "Move Category Down"
 msgid "Move Category Down"
 msgstr "Przesuń kategorię w dół"
 msgstr "Przesuń kategorię w dół"
 
 
-#: apps/admin/forums/views.py:45 apps/admin/forums/views.py:231
+#: apps/admin/forums/views.py:48 apps/admin/forums/views.py:251
 msgid "Edit Category"
 msgid "Edit Category"
 msgstr "Zmodyfikuj kategorię"
 msgstr "Zmodyfikuj kategorię"
 
 
-#: apps/admin/forums/views.py:46 apps/admin/forums/views.py:313
+#: apps/admin/forums/views.py:49 apps/admin/forums/views.py:339
 msgid "Delete Category"
 msgid "Delete Category"
 msgstr "Usuń kategorię"
 msgstr "Usuń kategorię"
 
 
-#: apps/admin/forums/views.py:51
+#: apps/admin/forums/views.py:54
 msgid "Move Forum Up"
 msgid "Move Forum Up"
 msgstr "Przesuń forum w górę"
 msgstr "Przesuń forum w górę"
 
 
-#: apps/admin/forums/views.py:52
+#: apps/admin/forums/views.py:55
 msgid "Move Forum Down"
 msgid "Move Forum Down"
 msgstr "Przesuń forum w dół"
 msgstr "Przesuń forum w dół"
 
 
-#: apps/admin/forums/views.py:53 apps/admin/forums/views.py:216
+#: apps/admin/forums/views.py:56 apps/admin/forums/views.py:236
 msgid "Edit Forum"
 msgid "Edit Forum"
 msgstr "Zmodyfikuj forum"
 msgstr "Zmodyfikuj forum"
 
 
-#: apps/admin/forums/views.py:54 apps/admin/forums/views.py:300
+#: apps/admin/forums/views.py:57 apps/admin/forums/views.py:326
 #: templates/admin/forums/delete.html:5
 #: templates/admin/forums/delete.html:5
 msgid "Delete Forum"
 msgid "Delete Forum"
 msgstr "Usuń forum"
 msgstr "Usuń forum"
 
 
-#: apps/admin/forums/views.py:58
+#: apps/admin/forums/views.py:61
 msgid "Move Redirect Up"
 msgid "Move Redirect Up"
 msgstr "Przesuń przekierowanie w górę"
 msgstr "Przesuń przekierowanie w górę"
 
 
-#: apps/admin/forums/views.py:59
+#: apps/admin/forums/views.py:62
 msgid "Move Redirect Down"
 msgid "Move Redirect Down"
 msgstr "Przesuń przekierowanie w doł"
 msgstr "Przesuń przekierowanie w doł"
 
 
-#: apps/admin/forums/views.py:60 apps/admin/forums/views.py:234
+#: apps/admin/forums/views.py:63 apps/admin/forums/views.py:254
 msgid "Edit Redirect"
 msgid "Edit Redirect"
 msgstr "Zmodyfikuj przekierowanie"
 msgstr "Zmodyfikuj przekierowanie"
 
 
-#: apps/admin/forums/views.py:61 apps/admin/forums/views.py:315
+#: apps/admin/forums/views.py:64 apps/admin/forums/views.py:341
 msgid "Delete Redirect"
 msgid "Delete Redirect"
 msgstr "Usuń przekierowanie"
 msgstr "Usuń przekierowanie"
 
 
-#: apps/admin/forums/views.py:65
-msgid "Selected forums have been resynchronised successfully."
+#: apps/admin/forums/views.py:72
+msgid "Selected forums have been resynchronized successfully."
 msgstr "Zaznaczone fora zostały pomyślnie zsynchronizowane."
 msgstr "Zaznaczone fora zostały pomyślnie zsynchronizowane."
 
 
-#: apps/admin/forums/views.py:68
-msgid "Selected forums have been pruned successfully."
-msgstr ""
+#: apps/admin/forums/views.py:80
+msgid "Only forums can be resynchronized."
+msgstr "Zsynchronizowane mogą zostać jedynie fora."
+
+#: apps/admin/forums/views.py:89
+msgid "No forums to resynchronize."
+msgstr "Nie zdefiniowano forów do synchronizacji."
+
+#: apps/admin/forums/views.py:97
+msgid "Forum for resynchronization does not exist."
+msgstr "Wskazane forum do zsynchronizowania nie istnieje."
+
+#: apps/admin/forums/views.py:112
+msgid "Resynchronizing Forums"
+msgstr "Synchronizowanie forów"
 
 
-#: apps/admin/forums/views.py:76
-msgid "Save Category"
-msgstr "Zapisz kategorię"
+#: apps/admin/forums/views.py:114
+#, python-format
+msgid "Resynchronized %(progress)s from %(total)s threads"
+msgstr "Zsynchronizowano %(progress)s z %(total)s tematów"
+
+#: apps/admin/forums/views.py:134
+msgid "Save Node"
+msgstr "Zapisz węzeł"
 
 
-#: apps/admin/forums/views.py:102
+#: apps/admin/forums/views.py:198
 msgid "New Category has been created."
 msgid "New Category has been created."
 msgstr "Pomyślnie utworzono nową kategorię."
 msgstr "Pomyślnie utworzono nową kategorię."
 
 
-#: apps/admin/forums/views.py:110 apps/admin/forums/views.py:152
-msgid "Save Forum"
-msgstr "Zapisz forum"
-
-#: apps/admin/forums/views.py:138
+#: apps/admin/forums/views.py:200
 msgid "New Forum has been created."
 msgid "New Forum has been created."
 msgstr "Pomyślnie utworzono nowe forum."
 msgstr "Pomyślnie utworzono nowe forum."
 
 
-#: apps/admin/forums/views.py:142
-msgid ""
-"You have to create at least one category before you will be able to create "
-"forums."
-msgstr ""
-"Aby utworzyć forum, musisz najpierw posiadać przynajmniej jedną kategorię."
-
-#: apps/admin/forums/views.py:176
+#: apps/admin/forums/views.py:202
 msgid "New Redirect has been created."
 msgid "New Redirect has been created."
 msgstr "Pomyślnie utworzono nowe przekierowanie."
 msgstr "Pomyślnie utworzono nowe przekierowanie."
 
 
-#: apps/admin/forums/views.py:180
-msgid ""
-"You have to create at least one category before you will be able to create "
-"redirects."
-msgstr ""
-"Aby utworzyć przekierowanie, musisz najpierw posiadać przynajmniej jedną "
-"kategorię."
-
-#: apps/admin/forums/views.py:189 apps/admin/forums/views.py:203
-#: apps/admin/forums/views.py:220 apps/admin/forums/views.py:305
+#: apps/admin/forums/views.py:209 apps/admin/forums/views.py:223
+#: apps/admin/forums/views.py:240 apps/admin/forums/views.py:331
 msgid "Requested Forum could not be found."
 msgid "Requested Forum could not be found."
 msgstr "Żądane forum nie zostało odnalezione."
 msgstr "Żądane forum nie zostało odnalezione."
 
 
-#: apps/admin/forums/views.py:195
+#: apps/admin/forums/views.py:215
 #, python-format
 #, python-format
 msgid "Forum \"%(name)s\" has been moved up."
 msgid "Forum \"%(name)s\" has been moved up."
 msgstr "Forum \"%(name)s\" zostało przesunięte w górę."
 msgstr "Forum \"%(name)s\" zostało przesunięte w górę."
 
 
-#: apps/admin/forums/views.py:196
+#: apps/admin/forums/views.py:216
 #, python-format
 #, python-format
 msgid ""
 msgid ""
 "Forum \"%(name)s\" is first child of its parent node and cannot be moved up."
 "Forum \"%(name)s\" is first child of its parent node and cannot be moved up."
 msgstr ""
 msgstr ""
+"Forum \"%(name)s\" jest pierwsze na tym poziomie i odgałęzieniu. Nie może "
+"być przesunięte wyżej."
 
 
-#: apps/admin/forums/views.py:209
+#: apps/admin/forums/views.py:229
 #, python-format
 #, python-format
 msgid "Forum \"%(name)s\" has been moved down."
 msgid "Forum \"%(name)s\" has been moved down."
 msgstr "Forum \"%(name)s\" zostało przesunięte w dół."
 msgstr "Forum \"%(name)s\" zostało przesunięte w dół."
 
 
-#: apps/admin/forums/views.py:210
+#: apps/admin/forums/views.py:230
 #, python-format
 #, python-format
 msgid ""
 msgid ""
 "Forum \"%(name)s\" is last child of its parent node and cannot be moved down."
 "Forum \"%(name)s\" is last child of its parent node and cannot be moved down."
 msgstr ""
 msgstr ""
+"Forum \"%(name)s\" jest ostatnie na tym poziomie i odgałęzieniu. Nie może "
+"być przesunięte niżej."
 
 
-#: apps/admin/forums/views.py:294
+#: apps/admin/forums/views.py:320
 #, python-format
 #, python-format
 msgid "Changes in forum \"%(name)s\" have been saved."
 msgid "Changes in forum \"%(name)s\" have been saved."
 msgstr "Pomyślnie zachowano zmiany w forum \"%(name)s\"."
 msgstr "Pomyślnie zachowano zmiany w forum \"%(name)s\"."
 
 
-#: apps/admin/forums/views.py:348
+#: apps/admin/forums/views.py:374
 #, python-format
 #, python-format
 msgid "Forum \"%(name)s\" has been deleted."
 msgid "Forum \"%(name)s\" has been deleted."
 msgstr "Forum \"%(name)s\" zostało usunięte."
 msgstr "Forum \"%(name)s\" zostało usunięte."
 
 
 #: apps/admin/newsletters/forms.py:10
 #: apps/admin/newsletters/forms.py:10
-msgid "Newsletter name must be sluggable."
+msgid "Newsletter name must contain alphanumeric characters."
 msgstr "Nazwa newslettera nie może składać się tylko ze znaków specjalnych."
 msgstr "Nazwa newslettera nie może składać się tylko ze znaków specjalnych."
 
 
 #: apps/admin/newsletters/forms.py:11
 #: apps/admin/newsletters/forms.py:11
@@ -1598,68 +1713,81 @@ msgstr ""
 
 
 #: apps/admin/newsletters/forms.py:24
 #: apps/admin/newsletters/forms.py:24
 msgid "Step Size"
 msgid "Step Size"
-msgstr ""
+msgstr "Użytkowników na jeden krok wysyłania"
 
 
 #: apps/admin/newsletters/forms.py:24
 #: apps/admin/newsletters/forms.py:24
 msgid ""
 msgid ""
 "Number of users that message will be sent to before forum refreshes page "
 "Number of users that message will be sent to before forum refreshes page "
 "displaying sending progress."
 "displaying sending progress."
 msgstr ""
 msgstr ""
+"Liczba użytkowników do których zostanie wysłany newsletter, zanim zostanie "
+"odświeżona strona z postępem wysyłania. Zbyt mała liczba znacząco spowolni "
+"wysyłanie. Zbyt duża liczba uniemożliwi Ci kontrolowanie procesu i może "
+"zbytnio obciążyć serwer."
 
 
 #: apps/admin/newsletters/forms.py:25
 #: apps/admin/newsletters/forms.py:25
 msgid "Limit to roles"
 msgid "Limit to roles"
-msgstr ""
+msgstr "Roześlij tylko do posiadaczy rangi"
 
 
 #: apps/admin/newsletters/forms.py:25
 #: apps/admin/newsletters/forms.py:25
 msgid ""
 msgid ""
 "You can limit this newsletter only to members who have specific ranks. If "
 "You can limit this newsletter only to members who have specific ranks. If "
 "you dont set any ranks, this newsletter will be sent to every user."
 "you dont set any ranks, this newsletter will be sent to every user."
 msgstr ""
 msgstr ""
+"Możesz przeznaczyć ten newsletter tylko dla posiadaczy wybranych rang. "
+"Jeżeli nie zaznaczysz żadnych rangi, newsletter zostanie wysłany do "
+"wszystkich użytkowników."
 
 
 #: apps/admin/newsletters/forms.py:26
 #: apps/admin/newsletters/forms.py:26
 msgid "Ignore members preferences"
 msgid "Ignore members preferences"
-msgstr ""
+msgstr "Ignoruj preferencje użytkowników"
 
 
 #: apps/admin/newsletters/forms.py:26
 #: apps/admin/newsletters/forms.py:26
 msgid ""
 msgid ""
 "Change this option to yes if you want to send this newsletter to members "
 "Change this option to yes if you want to send this newsletter to members "
 "that don't want to receive newsletters. This is good for emergencies."
 "that don't want to receive newsletters. This is good for emergencies."
 msgstr ""
 msgstr ""
+"Ustaw tę opcję na \"Tak\", w celu rozesłania newslettera nawet do członków, "
+"którzy nie wyrazili chęci otrzymywania newsletterów. Opcja przydatna w "
+"sytuacjach krytycznych."
 
 
 #: apps/admin/newsletters/forms.py:30
 #: apps/admin/newsletters/forms.py:30
-#: templates/cranefly/profiles/profile.html:66
 msgid "Message"
 msgid "Message"
-msgstr ""
+msgstr "Wiadomość"
 
 
 #: apps/admin/newsletters/forms.py:32
 #: apps/admin/newsletters/forms.py:32
 msgid "HTML Message"
 msgid "HTML Message"
-msgstr ""
+msgstr "Wiadomość w formacie HTML"
 
 
 #: apps/admin/newsletters/forms.py:32
 #: apps/admin/newsletters/forms.py:32
 msgid "HTML message visible to members who can read HTML e-mails."
 msgid "HTML message visible to members who can read HTML e-mails."
 msgstr ""
 msgstr ""
+"Wiadomość w formacie HTML dla członków z klientami pocztowymi "
+"wyświetlającymi HTML."
 
 
 #: apps/admin/newsletters/forms.py:33
 #: apps/admin/newsletters/forms.py:33
 msgid "Plain Text Message"
 msgid "Plain Text Message"
-msgstr ""
+msgstr "Wiadomość w czystym tekście"
 
 
 #: apps/admin/newsletters/forms.py:33
 #: apps/admin/newsletters/forms.py:33
 msgid ""
 msgid ""
 "Alternative plain text message that will be visible to members that can't or "
 "Alternative plain text message that will be visible to members that can't or "
 "dont want to read HTML e-mails."
 "dont want to read HTML e-mails."
 msgstr ""
 msgstr ""
+"Alternatywna forma wiadomości - czysty tekst, który będzie wyświetlany w "
+"przypadku programu pocztowego z brakiem obsługi HTML."
 
 
 #: apps/admin/newsletters/forms.py:42
 #: apps/admin/newsletters/forms.py:42
 msgid "Only to subscribers"
 msgid "Only to subscribers"
-msgstr ""
+msgstr "Tylko dla zapisanych do newslettera"
 
 
 #: apps/admin/newsletters/forms.py:42
 #: apps/admin/newsletters/forms.py:42
 msgid "To every member"
 msgid "To every member"
-msgstr ""
+msgstr "Dla każdego użytkownika"
 
 
 #: apps/admin/newsletters/forms.py:47
 #: apps/admin/newsletters/forms.py:47
 msgid "Search Newsletters"
 msgid "Search Newsletters"
-msgstr ""
+msgstr "Wyszukiwanie newsletterów"
 
 
 #: apps/admin/newsletters/forms.py:49
 #: apps/admin/newsletters/forms.py:49
 msgid "Name contains..."
 msgid "Name contains..."
@@ -1667,7 +1795,7 @@ msgstr "Nazwa zawiera..."
 
 
 #: apps/admin/newsletters/forms.py:50
 #: apps/admin/newsletters/forms.py:50
 msgid "Message Contents"
 msgid "Message Contents"
-msgstr "Wiadomość zawiera..."
+msgstr "Wiadomość zawiera"
 
 
 #: apps/admin/newsletters/forms.py:50
 #: apps/admin/newsletters/forms.py:50
 msgid "Message contains..."
 msgid "Message contains..."
@@ -1679,7 +1807,7 @@ msgstr "Typ newslettera"
 
 
 #: apps/admin/newsletters/forms.py:52
 #: apps/admin/newsletters/forms.py:52
 msgid "Recipient Rank"
 msgid "Recipient Rank"
-msgstr ""
+msgstr "Przeznaczony dla rangi"
 
 
 #: apps/admin/newsletters/views.py:27
 #: apps/admin/newsletters/views.py:27
 msgid "Newsletter"
 msgid "Newsletter"
@@ -1771,38 +1899,34 @@ msgid "Registered Members Sessions"
 msgstr "Sesje zarejestrowanych użytkowników"
 msgstr "Sesje zarejestrowanych użytkowników"
 
 
 #: apps/admin/online/forms.py:12
 #: apps/admin/online/forms.py:12
-msgid "Hidden Sessions"
-msgstr "Ukryte sesje"
-
-#: apps/admin/online/forms.py:13
 msgid "Guests Sessions"
 msgid "Guests Sessions"
 msgstr "Sesje gości"
 msgstr "Sesje gości"
 
 
-#: apps/admin/online/forms.py:14
+#: apps/admin/online/forms.py:13
 msgid "Crawler Sessions"
 msgid "Crawler Sessions"
 msgstr "Sesje botów wyszukiwarek"
 msgstr "Sesje botów wyszukiwarek"
 
 
-#: apps/admin/online/forms.py:19
+#: apps/admin/online/forms.py:18
 msgid "Search Sessions"
 msgid "Search Sessions"
 msgstr "Wyszukiwanie sesji"
 msgstr "Wyszukiwanie sesji"
 
 
-#: apps/admin/online/forms.py:21
+#: apps/admin/online/forms.py:20
 msgid "IP begins with..."
 msgid "IP begins with..."
 msgstr "Adres IP zaczyna się na..."
 msgstr "Adres IP zaczyna się na..."
 
 
-#: apps/admin/online/forms.py:22
+#: apps/admin/online/forms.py:21
 msgid "Username begings with..."
 msgid "Username begings with..."
 msgstr "Nazwa użytkownika zaczyna się na..."
 msgstr "Nazwa użytkownika zaczyna się na..."
 
 
-#: apps/admin/online/forms.py:23
+#: apps/admin/online/forms.py:22
 msgid "User Agent"
 msgid "User Agent"
 msgstr "User Agent"
 msgstr "User Agent"
 
 
-#: apps/admin/online/forms.py:23
+#: apps/admin/online/forms.py:22
 msgid "User Agent contains..."
 msgid "User Agent contains..."
 msgstr "User Agent zaczyna się na..."
 msgstr "User Agent zaczyna się na..."
 
 
-#: apps/admin/online/forms.py:24
+#: apps/admin/online/forms.py:23
 msgid "Session Type"
 msgid "Session Type"
 msgstr "Typ sesji"
 msgstr "Typ sesji"
 
 
@@ -1823,42 +1947,48 @@ msgid "Looks like nobody is currently online on forums."
 msgstr "Wygląda na to, że w danej chwili nikt nie jest zalogowany."
 msgstr "Wygląda na to, że w danej chwili nikt nie jest zalogowany."
 
 
 #: apps/admin/pruneusers/forms.py:8
 #: apps/admin/pruneusers/forms.py:8
-msgid "Policy name must be sluggable."
+msgid "Policy name must contain alphanumeric characters."
+<<<<<<< HEAD
 msgstr ""
 msgstr ""
+=======
+msgstr "Nazwa reguły musi zawierać alfanumeryczne znaki."
+>>>>>>> f3e4c488d945673e27b191c64910210333eeb611
 
 
 #: apps/admin/pruneusers/forms.py:9
 #: apps/admin/pruneusers/forms.py:9
 msgid "Policy name is too long."
 msgid "Policy name is too long."
-msgstr ""
+msgstr "Nazwa reguły jest zbyt długa."
 
 
 #: apps/admin/pruneusers/forms.py:18
 #: apps/admin/pruneusers/forms.py:18
 msgid "Basic Policy Options"
 msgid "Basic Policy Options"
-msgstr ""
+msgstr "Podstawowe opcje reguły"
 
 
 #: apps/admin/pruneusers/forms.py:20
 #: apps/admin/pruneusers/forms.py:20
 msgid "Policy Name"
 msgid "Policy Name"
-msgstr ""
+msgstr "Nazwa reguły"
 
 
 #: apps/admin/pruneusers/forms.py:20
 #: apps/admin/pruneusers/forms.py:20
 msgid "Short, descriptive name of this pruning policy."
 msgid "Short, descriptive name of this pruning policy."
-msgstr ""
+msgstr "Krótka nazwa opisująca tą regułę usuwania użytkowników."
 
 
 #: apps/admin/pruneusers/forms.py:24
 #: apps/admin/pruneusers/forms.py:24
 msgid "Pruning Policy Criteria"
 msgid "Pruning Policy Criteria"
-msgstr ""
+msgstr "Kryteria tej reguły usuwania"
 
 
 #: apps/admin/pruneusers/forms.py:26
 #: apps/admin/pruneusers/forms.py:26
 msgid "Member E-mail Address ends with"
 msgid "Member E-mail Address ends with"
-msgstr ""
+msgstr "Adres e-mail użytkownika kończy się na"
 
 
 #: apps/admin/pruneusers/forms.py:26
 #: apps/admin/pruneusers/forms.py:26
 msgid ""
 msgid ""
 "If you want to, you can enter more than one e-mail suffix by separating them "
 "If you want to, you can enter more than one e-mail suffix by separating them "
 "with comma."
 "with comma."
 msgstr ""
 msgstr ""
+"Jeżeli chcesz, możesz zdefiniować więcej końcówek adresów e-mail, "
+"oddzielając je przecinkiem."
 
 
 #: apps/admin/pruneusers/forms.py:27
 #: apps/admin/pruneusers/forms.py:27
 msgid "Member has no more posts than"
 msgid "Member has no more posts than"
-msgstr ""
+msgstr "Użytkownik ma nie więcej postów niż"
 
 
 #: apps/admin/pruneusers/forms.py:27
 #: apps/admin/pruneusers/forms.py:27
 msgid ""
 msgid ""
@@ -1867,10 +1997,14 @@ msgid ""
 "that has less than 10 posts will be deleted. Enter zero to dont use this "
 "that has less than 10 posts will be deleted. Enter zero to dont use this "
 "criteria"
 "criteria"
 msgstr ""
 msgstr ""
+"Maksymalna liczba postów, którą może mieć użytkownik, aby podpaść pod to "
+"kryterium. Dla przykładu, jeżeli wpiszesz tutaj 10 i będzie to jedyne "
+"kryterium usuwania, profile użytkowników posiadających mniej niż 10 postów "
+"zostaną usunięte.  Wpisz zero, aby nie używać tego kryterium."
 
 
 #: apps/admin/pruneusers/forms.py:28
 #: apps/admin/pruneusers/forms.py:28
 msgid "User is member for no more than"
 msgid "User is member for no more than"
-msgstr ""
+msgstr "Użytkownik jest członkiem forum nie dłużej niż"
 
 
 #: apps/admin/pruneusers/forms.py:28
 #: apps/admin/pruneusers/forms.py:28
 msgid ""
 msgid ""
@@ -1878,10 +2012,14 @@ msgid ""
 "days and make this only criteria, every user who is member for less than 15 "
 "days and make this only criteria, every user who is member for less than 15 "
 "days will be deleted. Enter zero to dont use this criteria."
 "days will be deleted. Enter zero to dont use this criteria."
 msgstr ""
 msgstr ""
+"Maksymalna liczba dni od rejestracji. Dla przykładu, jeżeli wpiszesz tutaj "
+"15 i będzie to jedyne kryterium usuwania użytkowników, profil każdego "
+"użytkownika, który zarejestrował się wcześniej niż 15 dni temu zostanie "
+"usunięty. Wpisz zero, aby nie używać tego kryterium."
 
 
 #: apps/admin/pruneusers/forms.py:29
 #: apps/admin/pruneusers/forms.py:29
 msgid "User last visit was before"
 msgid "User last visit was before"
-msgstr ""
+msgstr "Ostatnia wizyta użytkownika była przed"
 
 
 #: apps/admin/pruneusers/forms.py:29
 #: apps/admin/pruneusers/forms.py:29
 msgid ""
 msgid ""
@@ -1890,95 +2028,106 @@ msgid ""
 "not signed into forums in last 300 days will be deleted. Enter zero to dont "
 "not signed into forums in last 300 days will be deleted. Enter zero to dont "
 "use this criteria."
 "use this criteria."
 msgstr ""
 msgstr ""
+"Maksymalny dozwolony okres bezczynności w dniach. Dla przykładu, jeżeli "
+"wpiszesz tutaj 300 i będzie to jedyne kryterium usuwania użytkowników, "
+"profil każdego użytkownika, który nie logował się przez ostatnie 300 dni "
+"zostanie usunięty. Wpisz zero, aby nie używać tego kryterium."
 
 
 #: apps/admin/pruneusers/views.py:23
 #: apps/admin/pruneusers/views.py:23
 msgid "Pruning Policy"
 msgid "Pruning Policy"
-msgstr ""
+msgstr "Reguła usuwania"
 
 
 #: apps/admin/pruneusers/views.py:25
 #: apps/admin/pruneusers/views.py:25
 msgid "You have to check at least one policy."
 msgid "You have to check at least one policy."
-msgstr ""
+msgstr "Musisz zaznaczyć przynajmniej jedną regułę usuwania użytkowników."
 
 
 #: apps/admin/pruneusers/views.py:27
 #: apps/admin/pruneusers/views.py:27
 msgid "Delete selected policies"
 msgid "Delete selected policies"
-msgstr ""
+msgstr "Usuń zaznaczone reguły"
 
 
 #: apps/admin/pruneusers/views.py:27
 #: apps/admin/pruneusers/views.py:27
 msgid "Are you sure you want to delete selected policies?"
 msgid "Are you sure you want to delete selected policies?"
 msgstr ""
 msgstr ""
+"Czy aby na pewno chcesz usunąć zaznaczone reguły usuwania użytkowników?"
 
 
 #: apps/admin/pruneusers/views.py:35 templates/admin/prune/apply.html:13
 #: apps/admin/pruneusers/views.py:35 templates/admin/prune/apply.html:13
 msgid "Apply Policy"
 msgid "Apply Policy"
-msgstr ""
+msgstr "Zastosuj regułę"
 
 
 #: apps/admin/pruneusers/views.py:36
 #: apps/admin/pruneusers/views.py:36
 msgid "Edit Policy"
 msgid "Edit Policy"
-msgstr ""
+msgstr "Edytuj regułę"
 
 
 #: apps/admin/pruneusers/views.py:37
 #: apps/admin/pruneusers/views.py:37
 msgid "Delete Policy"
 msgid "Delete Policy"
-msgstr ""
+msgstr "Usuń regułę"
 
 
 #: apps/admin/pruneusers/views.py:37
 #: apps/admin/pruneusers/views.py:37
 msgid "Are you sure you want to delete this policy?"
 msgid "Are you sure you want to delete this policy?"
-msgstr ""
+msgstr "Czy aby na pewno chcesz usunąć tę regułę?"
 
 
 #: apps/admin/pruneusers/views.py:42 apps/admin/pruneusers/views.py:134
 #: apps/admin/pruneusers/views.py:42 apps/admin/pruneusers/views.py:134
 msgid "Only system administrators can delete pruning policies."
 msgid "Only system administrators can delete pruning policies."
 msgstr ""
 msgstr ""
+"Tylko administratorzy systemowi mogą usuwać reguły wycinania profili "
+"użytkowników."
 
 
 #: apps/admin/pruneusers/views.py:45
 #: apps/admin/pruneusers/views.py:45
 msgid "Selected pruning policies have been deleted successfully."
 msgid "Selected pruning policies have been deleted successfully."
-msgstr ""
+msgstr "Zaznaczone reguły zostały pomyślnie usunięte."
 
 
 #: apps/admin/pruneusers/views.py:53
 #: apps/admin/pruneusers/views.py:53
 msgid "Save Policy"
 msgid "Save Policy"
-msgstr ""
+msgstr "Zapisz regułę"
 
 
 #: apps/admin/pruneusers/views.py:72
 #: apps/admin/pruneusers/views.py:72
 msgid "New Pruning Policy has been created."
 msgid "New Pruning Policy has been created."
-msgstr ""
+msgstr "Nowa reguła usuwania użytkowników została utworzona."
 
 
 #: apps/admin/pruneusers/views.py:76
 #: apps/admin/pruneusers/views.py:76
 msgid "Only system administrators can set new pruning policies."
 msgid "Only system administrators can set new pruning policies."
 msgstr ""
 msgstr ""
+"Tylko administratorzy systemowi mogą dodawać nowe reguły usuwania "
+"użytkowników."
 
 
 #: apps/admin/pruneusers/views.py:85
 #: apps/admin/pruneusers/views.py:85
 msgid "Edit Pruning Policy"
 msgid "Edit Pruning Policy"
-msgstr ""
+msgstr "Edycja reguły usuwania użytkowników"
 
 
 #: apps/admin/pruneusers/views.py:89 apps/admin/pruneusers/views.py:130
 #: apps/admin/pruneusers/views.py:89 apps/admin/pruneusers/views.py:130
 #: apps/admin/pruneusers/views.py:147
 #: apps/admin/pruneusers/views.py:147
 msgid "Requested pruning policy could not be found."
 msgid "Requested pruning policy could not be found."
-msgstr ""
+msgstr "Żądana reguła nie została odnaleziona."
 
 
 #: apps/admin/pruneusers/views.py:116
 #: apps/admin/pruneusers/views.py:116
 #, python-format
 #, python-format
 msgid "Changes in policy \"%(name)s\" have been saved."
 msgid "Changes in policy \"%(name)s\" have been saved."
-msgstr ""
+msgstr "Zmiany w regule \"%(name)s\" zostały zachowane."
 
 
 #: apps/admin/pruneusers/views.py:120
 #: apps/admin/pruneusers/views.py:120
 msgid "Only system administrators can edit pruning policies."
 msgid "Only system administrators can edit pruning policies."
 msgstr ""
 msgstr ""
+"Tylko administratorzy systemowi mogą modyfikować reguły usuwania "
+"użytkowników."
 
 
 #: apps/admin/pruneusers/views.py:137
 #: apps/admin/pruneusers/views.py:137
 #, python-format
 #, python-format
 msgid "Pruning policy \"%(name)s\" has been deleted."
 msgid "Pruning policy \"%(name)s\" has been deleted."
-msgstr ""
+msgstr "Reguła \"%(name)s\" została usunięta."
 
 
 #: apps/admin/pruneusers/views.py:143
 #: apps/admin/pruneusers/views.py:143
 msgid "Apply Pruning Policy"
 msgid "Apply Pruning Policy"
-msgstr ""
+msgstr "Zastosuj regułę usuwania użytkowników"
 
 
 #: apps/admin/pruneusers/views.py:172
 #: apps/admin/pruneusers/views.py:172
 #, python-format
 #, python-format
 msgid "Policy \"%(name)s\" does not apply to any users."
 msgid "Policy \"%(name)s\" does not apply to any users."
-msgstr ""
+msgstr "Reguła \"%(name)s\" nie odnosi się do żadnych aktualnych użytkowników."
 
 
 #: apps/admin/pruneusers/views.py:181
 #: apps/admin/pruneusers/views.py:181
 #, python-format
 #, python-format
 msgid "User \"%(name)s\" is protected and was not deleted."
 msgid "User \"%(name)s\" is protected and was not deleted."
-msgstr ""
+msgstr "Użytkownik \"%(name)s\" jest chroniony i nie został usunięty."
 
 
 #: apps/admin/pruneusers/views.py:187
 #: apps/admin/pruneusers/views.py:187
 #, python-format
 #, python-format
@@ -1998,7 +2147,7 @@ msgstr ""
 "Autoryzacja żądania jest niepoprawna. Spróbuj ponownie wysłać formularz."
 "Autoryzacja żądania jest niepoprawna. Spróbuj ponownie wysłać formularz."
 
 
 #: apps/admin/ranks/forms.py:9
 #: apps/admin/ranks/forms.py:9
-msgid "Rank name must be sluggable."
+msgid "Rank name must contain alphanumeric characters."
 msgstr "Nazwa rangi nie może składać się tylko ze znaków specjalnych."
 msgstr "Nazwa rangi nie może składać się tylko ze znaków specjalnych."
 
 
 #: apps/admin/ranks/forms.py:10
 #: apps/admin/ranks/forms.py:10
@@ -2007,35 +2156,39 @@ msgstr "Nazwa rangi jest zbyt długa."
 
 
 #: apps/admin/ranks/forms.py:18
 #: apps/admin/ranks/forms.py:18
 msgid "This is incorrect rank match rule."
 msgid "This is incorrect rank match rule."
-msgstr ""
+msgstr "Niepoprawne kryteria przyznania rangi."
 
 
 #: apps/admin/ranks/forms.py:22
 #: apps/admin/ranks/forms.py:22
 msgid "Basic Rank Options"
 msgid "Basic Rank Options"
-msgstr ""
+msgstr "Podstawowe ustawienia rangi"
 
 
 #: apps/admin/ranks/forms.py:24
 #: apps/admin/ranks/forms.py:24
 msgid "Rank Name"
 msgid "Rank Name"
-msgstr ""
+msgstr "Nazwa rangi"
 
 
 #: apps/admin/ranks/forms.py:24
 #: apps/admin/ranks/forms.py:24
 msgid ""
 msgid ""
 "Rank Name is used to identify rank in Admin Control Panel and is used as "
 "Rank Name is used to identify rank in Admin Control Panel and is used as "
 "page and tab title if you decide to make this rank act as tab on users list."
 "page and tab title if you decide to make this rank act as tab on users list."
 msgstr ""
 msgstr ""
+"Nazwa rangi jest używana do jej identyfikacji w panelu administracyjnym ACP. "
+"Stanowi też treść karty przy wyświetlaniu rangi w module użytkowników."
 
 
 #: apps/admin/ranks/forms.py:25
 #: apps/admin/ranks/forms.py:25
 msgid "Rank Description"
 msgid "Rank Description"
-msgstr ""
+msgstr "Opis rangi"
 
 
 #: apps/admin/ranks/forms.py:25
 #: apps/admin/ranks/forms.py:25
 msgid ""
 msgid ""
 "If this rank acts as tab on users list, here you can enter optional "
 "If this rank acts as tab on users list, here you can enter optional "
 "description that will be displayed above list of users with this rank."
 "description that will be displayed above list of users with this rank."
 msgstr ""
 msgstr ""
+"Jeżeli ranga ma włączoną kartę w module użytkowników, możesz tutaj wpisać "
+"opis, który będzie wyświetlany nad listą posiadających ją członków forum."
 
 
 #: apps/admin/ranks/forms.py:26
 #: apps/admin/ranks/forms.py:26
 msgid "As Tab on Users List"
 msgid "As Tab on Users List"
-msgstr ""
+msgstr "Jako karta w module użytkowników"
 
 
 #: apps/admin/ranks/forms.py:26
 #: apps/admin/ranks/forms.py:26
 msgid ""
 msgid ""
@@ -2044,30 +2197,34 @@ msgid ""
 "used by forum team members or members that should be visible and easily "
 "used by forum team members or members that should be visible and easily "
 "reachable."
 "reachable."
 msgstr ""
 msgstr ""
+"Ustala, czy ranga posiada swoją kartę w module użytkowników, na której "
+"znajduje się lista jej posiadaczy. Dobrym pomysłem jest włączenie tej opcji "
+"tylko dla najbardziej wartościowych rang, jak np. ekipa forum, moderatorzy, "
+"przyjaciele forum itp."
 
 
 #: apps/admin/ranks/forms.py:27
 #: apps/admin/ranks/forms.py:27
 msgid "Display members online"
 msgid "Display members online"
-msgstr ""
+msgstr "Wyświetlaj użytkowników online"
 
 
 #: apps/admin/ranks/forms.py:27
 #: apps/admin/ranks/forms.py:27
 msgid "Should users online with this rank be displayed on board index?"
 msgid "Should users online with this rank be displayed on board index?"
-msgstr ""
+msgstr "Czy wyrózniać zalogowanych użytkowników z tą rangą na stronie głównej?"
 
 
 #: apps/admin/ranks/forms.py:31
 #: apps/admin/ranks/forms.py:31
 msgid "Rank Looks"
 msgid "Rank Looks"
-msgstr ""
+msgstr "Wygląd rangi"
 
 
 #: apps/admin/ranks/forms.py:33
 #: apps/admin/ranks/forms.py:33
 msgid "Rank Title"
 msgid "Rank Title"
-msgstr ""
+msgstr "Tytuł rangi"
 
 
 #: apps/admin/ranks/forms.py:33
 #: apps/admin/ranks/forms.py:33
 msgid "Short description of rank's bearer role in your community."
 msgid "Short description of rank's bearer role in your community."
-msgstr ""
+msgstr "Najkrótszy mozliwy opis dowolnego posiadacza tej rangi na Twoim forum."
 
 
 #: apps/admin/ranks/forms.py:34
 #: apps/admin/ranks/forms.py:34
 msgid "Rank CSS Class"
 msgid "Rank CSS Class"
-msgstr ""
+msgstr "Klasa CSS rangi"
 
 
 #: apps/admin/ranks/forms.py:34
 #: apps/admin/ranks/forms.py:34
 msgid ""
 msgid ""
@@ -2075,24 +2232,29 @@ msgid ""
 "rank's owner or his content, allowing you to make them stand out from other "
 "rank's owner or his content, allowing you to make them stand out from other "
 "members."
 "members."
 msgstr ""
 msgstr ""
+"Do rangi można przypisać opcjonalną klasę CSS, która zostanie dodana do "
+"każdego elementu związanego z jej członkami (profile, posty, tematy), "
+"umożliwiając ich wyróżnienie."
 
 
 #: apps/admin/ranks/forms.py:38
 #: apps/admin/ranks/forms.py:38
 msgid "Rank Attainability"
 msgid "Rank Attainability"
-msgstr ""
+msgstr "Dosięgalność rangi"
 
 
 #: apps/admin/ranks/forms.py:40
 #: apps/admin/ranks/forms.py:40
 msgid "Special Rank"
 msgid "Special Rank"
-msgstr ""
+msgstr "Ranga specjalna"
 
 
 #: apps/admin/ranks/forms.py:40
 #: apps/admin/ranks/forms.py:40
 msgid ""
 msgid ""
 "Special ranks are ignored during updates of user ranking, making them "
 "Special ranks are ignored during updates of user ranking, making them "
 "unattainable without admin ingerention."
 "unattainable without admin ingerention."
 msgstr ""
 msgstr ""
+"Specjalne rangi są ignorowane przy aktualizacjach rankingu użytkowników, co "
+"czyni je niezmiennymi bez ingerencji administratora."
 
 
 #: apps/admin/ranks/forms.py:41
 #: apps/admin/ranks/forms.py:41
 msgid "Rank Criteria"
 msgid "Rank Criteria"
-msgstr ""
+msgstr "Kryteria przyznania rangi"
 
 
 #: apps/admin/ranks/forms.py:41
 #: apps/admin/ranks/forms.py:41
 msgid ""
 msgid ""
@@ -2102,68 +2264,74 @@ msgid ""
 "10 most active members, enter \"10\". This setting is ignored for special "
 "10 most active members, enter \"10\". This setting is ignored for special "
 "ranks as they don't participate in user's ranking updates."
 "ranks as they don't participate in user's ranking updates."
 msgstr ""
 msgstr ""
+"To ustawienie umożliwia Ci określenie liczby użytkowników, którzy utrzymają "
+"daną rangę. Wpisanie 0 przypisuje tą rangę dla każdego użytkownika (dobre "
+"dla rangi określającej zarejestrowanego członka forum). Aby nadać rangę dla "
+"10% najbardziej aktywnych użytkowników, wpisz \"10%\". Aby nadać tą rangę 10 "
+"najbardziej aktywnym użytkownikom, wprowadź \"10\". To ustawienie jest "
+"ignorowane dla rank specjalnych (nie są one aktualizowane automatycznie)."
 
 
 #: apps/admin/ranks/views.py:24 templates/cranefly/profiles/details.html:104
 #: apps/admin/ranks/views.py:24 templates/cranefly/profiles/details.html:104
 msgid "Rank"
 msgid "Rank"
-msgstr ""
+msgstr "Ranga"
 
 
 #: apps/admin/ranks/views.py:26
 #: apps/admin/ranks/views.py:26
 msgid "Reorder Ranks"
 msgid "Reorder Ranks"
-msgstr ""
+msgstr "Zmień istotność rang"
 
 
 #: apps/admin/ranks/views.py:27
 #: apps/admin/ranks/views.py:27
 msgid "You have to check at least one rank."
 msgid "You have to check at least one rank."
-msgstr ""
+msgstr "Musisz zaznaczyć przynajmniej jedną rangę."
 
 
 #: apps/admin/ranks/views.py:29
 #: apps/admin/ranks/views.py:29
 msgid "Delete selected ranks"
 msgid "Delete selected ranks"
-msgstr ""
+msgstr "Usuń zaznaczone rangi"
 
 
 #: apps/admin/ranks/views.py:29
 #: apps/admin/ranks/views.py:29
 msgid "Are you sure you want to delete selected ranks?"
 msgid "Are you sure you want to delete selected ranks?"
-msgstr ""
+msgstr "Czy aby na pewno chcesz usunąć zaznaczone rangi?"
 
 
 #: apps/admin/ranks/views.py:53
 #: apps/admin/ranks/views.py:53
 msgid "Ranks order has been changed"
 msgid "Ranks order has been changed"
-msgstr ""
+msgstr "Istotność rang została zmieniona."
 
 
 #: apps/admin/ranks/views.py:60 apps/admin/ranks/views.py:104
 #: apps/admin/ranks/views.py:60 apps/admin/ranks/views.py:104
 msgid "Edit Rank"
 msgid "Edit Rank"
-msgstr ""
+msgstr "Zmodyfikuj rangę"
 
 
 #: apps/admin/ranks/views.py:61
 #: apps/admin/ranks/views.py:61
 msgid "Delete Rank"
 msgid "Delete Rank"
-msgstr ""
+msgstr "Usuń rangę"
 
 
 #: apps/admin/ranks/views.py:61
 #: apps/admin/ranks/views.py:61
 msgid "Are you sure you want to delete this rank?"
 msgid "Are you sure you want to delete this rank?"
-msgstr ""
+msgstr "Czy aby na pewno chcesz usunąć tę rangę?"
 
 
 #: apps/admin/ranks/views.py:66
 #: apps/admin/ranks/views.py:66
 msgid "Selected ranks have been deleted successfully."
 msgid "Selected ranks have been deleted successfully."
-msgstr ""
+msgstr "Zaznaczone rangi zostały pomyślnie usunięte."
 
 
 #: apps/admin/ranks/views.py:74
 #: apps/admin/ranks/views.py:74
 msgid "Save Rank"
 msgid "Save Rank"
-msgstr ""
+msgstr "Zapisz rangę"
 
 
 #: apps/admin/ranks/views.py:98
 #: apps/admin/ranks/views.py:98
 msgid "New Rank has been created."
 msgid "New Rank has been created."
-msgstr ""
+msgstr "Nowa ranga została utworzona."
 
 
 #: apps/admin/ranks/views.py:108 apps/admin/ranks/views.py:147
 #: apps/admin/ranks/views.py:108 apps/admin/ranks/views.py:147
 msgid "Requested Rank could not be found."
 msgid "Requested Rank could not be found."
-msgstr ""
+msgstr "Żądana ranga nie mogła zostać odnaleziona."
 
 
 #: apps/admin/ranks/views.py:140
 #: apps/admin/ranks/views.py:140
 #, python-format
 #, python-format
 msgid "Changes in rank \"%(name)s\" have been saved."
 msgid "Changes in rank \"%(name)s\" have been saved."
-msgstr ""
+msgstr "Zmiany w randze \"%(name)s\" zostały zachowane."
 
 
 #: apps/admin/ranks/views.py:151
 #: apps/admin/ranks/views.py:151
 #, python-format
 #, python-format
 msgid "Rank \"%(name)s\" has been deleted."
 msgid "Rank \"%(name)s\" has been deleted."
-msgstr ""
+msgstr "Ranga \"%(name)s\" została usunięta."
 
 
 #: apps/admin/roles/forms.py:19
 #: apps/admin/roles/forms.py:19
 msgid "Protect this Role"
 msgid "Protect this Role"
@@ -2233,7 +2401,7 @@ msgstr "Uprawnienia roli do forów zostały zapisane."
 
 
 #: apps/admin/roles/views.py:182
 #: apps/admin/roles/views.py:182
 msgid "No forum roles are currently set."
 msgid "No forum roles are currently set."
-msgstr ""
+msgstr "Nie zdefiniowano żadnych roli na forum."
 
 
 #: apps/admin/roles/views.py:190
 #: apps/admin/roles/views.py:190
 msgid "Change Role Permissions"
 msgid "Change Role Permissions"
@@ -2290,54 +2458,38 @@ msgid "All existing forums"
 msgstr "Wszystkie istniejące fora"
 msgstr "Wszystkie istniejące fora"
 
 
 #: apps/admin/sections/forums.py:23
 #: apps/admin/sections/forums.py:23
-msgid "New Category"
-msgstr "Nowa kategoria"
+msgid "New Node"
+msgstr "Nowy węzeł"
 
 
 #: apps/admin/sections/forums.py:24
 #: apps/admin/sections/forums.py:24
-msgid "Create new category"
-msgstr "Utwórz nową kategorię"
-
-#: apps/admin/sections/forums.py:29
-msgid "New Forum"
-msgstr "Nowe forum"
-
-#: apps/admin/sections/forums.py:30
-msgid "Create new forum"
-msgstr "Utwórz nowe forum"
+msgid "Create new forums tree node"
+msgstr "Utwórz nowy węzeł w drzewie forów"
 
 
-#: apps/admin/sections/forums.py:35
-msgid "New Redirect"
-msgstr "Nowe przekierowanie"
-
-#: apps/admin/sections/forums.py:36
-msgid "Create new redirect"
-msgstr "Utwórz nowe przekierowanie"
-
-#: apps/admin/sections/forums.py:55
+#: apps/admin/sections/forums.py:43
 msgid "Thread Labels"
 msgid "Thread Labels"
 msgstr "Etykiety tematów"
 msgstr "Etykiety tematów"
 
 
-#: apps/admin/sections/forums.py:56
+#: apps/admin/sections/forums.py:44
 msgid "Thread Labels allow you to group threads together within forums."
 msgid "Thread Labels allow you to group threads together within forums."
 msgstr "Etykiety tematów umożliwają grupowanie ich razem na forach."
 msgstr "Etykiety tematów umożliwają grupowanie ich razem na forach."
 
 
-#: apps/admin/sections/forums.py:66
+#: apps/admin/sections/forums.py:54
 msgid "Words Filter"
 msgid "Words Filter"
 msgstr "Filtr słów"
 msgstr "Filtr słów"
 
 
-#: apps/admin/sections/forums.py:67
+#: apps/admin/sections/forums.py:55
 msgid "Forbid usage of words in messages"
 msgid "Forbid usage of words in messages"
-msgstr ""
+msgstr "Zabroń używania niektórych słów w wiadomościach"
 
 
-#: apps/admin/sections/forums.py:77
+#: apps/admin/sections/forums.py:65
 msgid "Tests"
 msgid "Tests"
 msgstr "Testy wiadomości"
 msgstr "Testy wiadomości"
 
 
-#: apps/admin/sections/forums.py:78
+#: apps/admin/sections/forums.py:66
 msgid "Tests that new messages have to pass"
 msgid "Tests that new messages have to pass"
 msgstr "Testy, którymi będa poddawane nowe wiadomości"
 msgstr "Testy, którymi będa poddawane nowe wiadomości"
 
 
-#: apps/admin/sections/forums.py:89
+#: apps/admin/sections/forums.py:77
 msgid "Manage allowed attachment types."
 msgid "Manage allowed attachment types."
 msgstr "Zarządzanie dozwolonymi typami załączników."
 msgstr "Zarządzanie dozwolonymi typami załączników."
 
 
@@ -2355,25 +2507,25 @@ msgstr "Statystyki"
 
 
 #: apps/admin/sections/overview.py:22 templates/admin/stats/layout.html:9
 #: apps/admin/sections/overview.py:22 templates/admin/stats/layout.html:9
 msgid "Create Statistics Reports"
 msgid "Create Statistics Reports"
-msgstr ""
+msgstr "Generowanie raportów statystycznych"
 
 
-#: apps/admin/sections/overview.py:33 templates/cranefly/index.html:130
+#: apps/admin/sections/overview.py:33 templates/cranefly/index.html:140
 #: templates/cranefly/profiles/profile.html:24
 #: templates/cranefly/profiles/profile.html:24
 msgid "Online"
 msgid "Online"
-msgstr ""
+msgstr "Online"
 
 
 #: apps/admin/sections/overview.py:34
 #: apps/admin/sections/overview.py:34
 msgid "See who is currently online on forums."
 msgid "See who is currently online on forums."
-msgstr ""
+msgstr "Zobacz, kto jest aktualnie zalogowany na forum."
 
 
 #: apps/admin/sections/overview.py:40 apps/admin/sections/users.py:17
 #: apps/admin/sections/overview.py:40 apps/admin/sections/users.py:17
 #: templates/cranefly/layout.html:46
 #: templates/cranefly/layout.html:46
 msgid "Browse Users"
 msgid "Browse Users"
-msgstr ""
+msgstr "Użytkownicy"
 
 
 #: apps/admin/sections/overview.py:41 apps/admin/sections/users.py:18
 #: apps/admin/sections/overview.py:41 apps/admin/sections/users.py:18
 msgid "Browse all registered user accounts"
 msgid "Browse all registered user accounts"
-msgstr ""
+msgstr "Przeglądaj konta zarejestrowanych użytkowników"
 
 
 #: apps/admin/sections/overview.py:54 fixtures/ranks.py:6 fixtures/ranks.py:8
 #: apps/admin/sections/overview.py:54 fixtures/ranks.py:6 fixtures/ranks.py:8
 msgid "Forum Team"
 msgid "Forum Team"
@@ -2438,293 +2590,301 @@ msgstr "Dopasuj warstwę prezentacji dla przeglądarek"
 
 
 #: apps/admin/sections/system.py:30
 #: apps/admin/sections/system.py:30
 msgid "Browse Clients"
 msgid "Browse Clients"
-msgstr ""
+msgstr "Lista dostosowań"
 
 
 #: apps/admin/sections/system.py:31
 #: apps/admin/sections/system.py:31
 msgid "Browse all existing clients"
 msgid "Browse all existing clients"
-msgstr ""
+msgstr "Przeglądaj wszystkie istniejące przypisania stylów do przeglądarek"
 
 
 #: apps/admin/sections/system.py:36
 #: apps/admin/sections/system.py:36
 msgid "Add New Adjustment"
 msgid "Add New Adjustment"
-msgstr ""
+msgstr "Dodaj nowe dostosowanie"
 
 
 #: apps/admin/sections/system.py:37
 #: apps/admin/sections/system.py:37
 msgid "Create new client adjustment"
 msgid "Create new client adjustment"
-msgstr ""
+msgstr "Utwórz nowe dopasowanie stylu do przeglądarki"
 
 
-#: apps/admin/sections/users.py:10 fixtures/accountssetings.py:96
+#: apps/admin/sections/users.py:10 fixtures/accountssetings.py:97
 #: templates/cranefly/profiles/list.html:17
 #: templates/cranefly/profiles/list.html:17
 msgid "Users List"
 msgid "Users List"
-msgstr ""
+msgstr "Lista użytkowników"
 
 
 #: apps/admin/sections/users.py:11
 #: apps/admin/sections/users.py:11
 msgid "Search and browse users"
 msgid "Search and browse users"
-msgstr ""
+msgstr "Wyszukaj i przeglądaj użytkowników"
 
 
 #: apps/admin/sections/users.py:23
 #: apps/admin/sections/users.py:23
 msgid "Add User"
 msgid "Add User"
-msgstr ""
+msgstr "Dodaj użytkownika"
 
 
 #: apps/admin/sections/users.py:24
 #: apps/admin/sections/users.py:24
 msgid "Create new user account"
 msgid "Create new user account"
-msgstr ""
+msgstr "Utwórz nowe konto użytkownika"
 
 
 #: apps/admin/sections/users.py:41
 #: apps/admin/sections/users.py:41
 msgid "Ranks"
 msgid "Ranks"
-msgstr ""
+msgstr "Rangi"
 
 
 #: apps/admin/sections/users.py:42
 #: apps/admin/sections/users.py:42
 msgid "Administrate User Ranks"
 msgid "Administrate User Ranks"
-msgstr ""
+msgstr "Zarządzaj rangami użytkowników"
 
 
 #: apps/admin/sections/users.py:48
 #: apps/admin/sections/users.py:48
 msgid "Browse Ranks"
 msgid "Browse Ranks"
-msgstr ""
+msgstr "Przeglądadaj rangi"
 
 
 #: apps/admin/sections/users.py:49
 #: apps/admin/sections/users.py:49
 msgid "Browse all existing ranks"
 msgid "Browse all existing ranks"
-msgstr ""
+msgstr "Przeglądaj wszystkie istniejące rangi na forum."
 
 
 #: apps/admin/sections/users.py:54
 #: apps/admin/sections/users.py:54
 msgid "Add Rank"
 msgid "Add Rank"
-msgstr ""
+msgstr "Dodaj rangę"
 
 
 #: apps/admin/sections/users.py:55
 #: apps/admin/sections/users.py:55
 msgid "Create new rank"
 msgid "Create new rank"
-msgstr ""
+msgstr "Utwórz nową rangę"
 
 
 #: apps/admin/sections/users.py:70
 #: apps/admin/sections/users.py:70
 msgid "Bans"
 msgid "Bans"
-msgstr ""
+msgstr "Bany"
 
 
 #: apps/admin/sections/users.py:71
 #: apps/admin/sections/users.py:71
 msgid "Ban or unban users from forums."
 msgid "Ban or unban users from forums."
-msgstr ""
+msgstr "Zablokuj lub odblokuj profile użytkowników na forum."
 
 
 #: apps/admin/sections/users.py:77
 #: apps/admin/sections/users.py:77
 msgid "Browse Bans"
 msgid "Browse Bans"
-msgstr ""
+msgstr "Przeglądaj bany"
 
 
 #: apps/admin/sections/users.py:78
 #: apps/admin/sections/users.py:78
 msgid "Browse all existing bans"
 msgid "Browse all existing bans"
-msgstr ""
+msgstr "Przeglądaj blokady nałożone na użytkowników"
 
 
 #: apps/admin/sections/users.py:84
 #: apps/admin/sections/users.py:84
 msgid "Set new Ban"
 msgid "Set new Ban"
-msgstr ""
+msgstr "Nałóż nowego bana"
 
 
 #: apps/admin/sections/users.py:100
 #: apps/admin/sections/users.py:100
 msgid "Prune Users"
 msgid "Prune Users"
-msgstr ""
+msgstr "Masowe usuwanie"
 
 
 #: apps/admin/sections/users.py:101
 #: apps/admin/sections/users.py:101
 msgid "Delete multiple Users"
 msgid "Delete multiple Users"
-msgstr ""
+msgstr "Usuwanie wielu użytkowników na raz"
 
 
 #: apps/admin/sections/users.py:107
 #: apps/admin/sections/users.py:107
 msgid "Pruning Policies"
 msgid "Pruning Policies"
-msgstr ""
+msgstr "Reguły usuwania"
 
 
 #: apps/admin/sections/users.py:108
 #: apps/admin/sections/users.py:108
 msgid "Browse all existing pruning policies"
 msgid "Browse all existing pruning policies"
-msgstr ""
+msgstr "Przeglądaj wszystkie reguły usuwania użytkowników"
 
 
 #: apps/admin/sections/users.py:113
 #: apps/admin/sections/users.py:113
 msgid "Set New Policy"
 msgid "Set New Policy"
-msgstr ""
+msgstr "Ustal nową regułę"
 
 
 #: apps/admin/sections/users.py:114
 #: apps/admin/sections/users.py:114
 msgid "Set new pruning policy"
 msgid "Set new pruning policy"
-msgstr ""
+msgstr "Utwórz nową regułę usuwania użytkowników"
 
 
 #: apps/admin/sections/users.py:130 apps/usercp/options/forms.py:43
 #: apps/admin/sections/users.py:130 apps/usercp/options/forms.py:43
 msgid "Newsletters"
 msgid "Newsletters"
-msgstr ""
+msgstr "Newslettery"
 
 
 #: apps/admin/sections/users.py:131
 #: apps/admin/sections/users.py:131
 msgid "Manage and send Newsletters"
 msgid "Manage and send Newsletters"
-msgstr ""
+msgstr "Zarządzaj i wysyłaj newslettery"
 
 
 #: apps/admin/sections/users.py:137
 #: apps/admin/sections/users.py:137
 msgid "Browse Newsletters"
 msgid "Browse Newsletters"
-msgstr ""
+msgstr "Przeglądaj newslettery"
 
 
 #: apps/admin/sections/users.py:138
 #: apps/admin/sections/users.py:138
 msgid "Browse all existing Newsletters"
 msgid "Browse all existing Newsletters"
-msgstr ""
+msgstr "Przeglądaj wszystkie istniejące newslettery"
 
 
 #: apps/admin/sections/users.py:143
 #: apps/admin/sections/users.py:143
 msgid "New Newsletter"
 msgid "New Newsletter"
-msgstr ""
+msgstr "Nowy newsletter"
 
 
 #: apps/admin/sections/users.py:144
 #: apps/admin/sections/users.py:144
 msgid "Create new Newsletter"
 msgid "Create new Newsletter"
-msgstr ""
+msgstr "Utwórz nowy newsletter"
 
 
 #: apps/admin/settings/views.py:25
 #: apps/admin/settings/views.py:25
 msgid "Requested settings group could not be found."
 msgid "Requested settings group could not be found."
-msgstr ""
+msgstr "Ządana grupa ustawień nie została odnaleziona."
 
 
 #: apps/admin/settings/views.py:49
 #: apps/admin/settings/views.py:49
 msgid "Configuration has been changed."
 msgid "Configuration has been changed."
-msgstr ""
+msgstr "Konfiguracja została zmodyfikowana."
 
 
 #: apps/admin/settings/views.py:93
 #: apps/admin/settings/views.py:93
 #, python-format
 #, python-format
 msgid "One setting that matches search criteria has been found."
 msgid "One setting that matches search criteria has been found."
 msgid_plural "%(count)d settings that match search criteria have been found."
 msgid_plural "%(count)d settings that match search criteria have been found."
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "Odnaleziono jedno ustawienie spełniające podane kryteria."
+msgstr[1] "Odnaleziono %(count)d ustawienia spełniające podane kryteria."
+msgstr[2] "Odnaleziono %(count)d ustawień spełniających podane kryteria."
 
 
 #: apps/admin/settings/views.py:99
 #: apps/admin/settings/views.py:99
 msgid "No settings that match search criteria have been found."
 msgid "No settings that match search criteria have been found."
-msgstr ""
+msgstr "Nie odnaleziono żadnej opcji spełniającej podanych kryteriów."
 
 
 #: apps/admin/settings/views.py:101
 #: apps/admin/settings/views.py:101
 msgid "Search query is empty."
 msgid "Search query is empty."
-msgstr ""
+msgstr "Wyszukiwane hasło jest puste."
 
 
 #: apps/admin/settings/views.py:103 search/__init__.py:49
 #: apps/admin/settings/views.py:103 search/__init__.py:49
 msgid "Search query is invalid."
 msgid "Search query is invalid."
-msgstr ""
+msgstr "Wyszukiwane hasło jest nieprawidłowe."
 
 
 #: apps/admin/stats/forms.py:11
 #: apps/admin/stats/forms.py:11
 msgid "For each day"
 msgid "For each day"
-msgstr ""
+msgstr "Dla każdego dnia"
 
 
 #: apps/admin/stats/forms.py:11
 #: apps/admin/stats/forms.py:11
 msgid "For each week"
 msgid "For each week"
-msgstr ""
+msgstr "Dla każdego tygodnia"
 
 
 #: apps/admin/stats/forms.py:11
 #: apps/admin/stats/forms.py:11
 msgid "For each month"
 msgid "For each month"
-msgstr ""
+msgstr "Dla każdego miesiąca"
 
 
 #: apps/admin/stats/forms.py:11
 #: apps/admin/stats/forms.py:11
 msgid "For each year"
 msgid "For each year"
-msgstr ""
+msgstr "Dla każdego roku"
 
 
 #: apps/admin/stats/forms.py:15
 #: apps/admin/stats/forms.py:15
 msgid "Report Type"
 msgid "Report Type"
-msgstr ""
+msgstr "Typ raportu"
 
 
 #: apps/admin/stats/forms.py:15
 #: apps/admin/stats/forms.py:15
 msgid "Select statistics provider."
 msgid "Select statistics provider."
-msgstr ""
+msgstr "Wybierz źródło statystyk."
 
 
 #: apps/admin/stats/forms.py:17
 #: apps/admin/stats/forms.py:17
 msgid "Time"
 msgid "Time"
-msgstr ""
+msgstr "Czas"
 
 
 #: apps/admin/stats/forms.py:17
 #: apps/admin/stats/forms.py:17
 msgid ""
 msgid ""
 "Enter start and end date for time period you want to take data from to use "
 "Enter start and end date for time period you want to take data from to use "
 "in graph."
 "in graph."
 msgstr ""
 msgstr ""
+"Wprowadź przedział czasu (datę startową i końcową), dla którego zostanie "
+"wygenerowany wykres."
 
 
 #: apps/admin/stats/forms.py:17
 #: apps/admin/stats/forms.py:17
 msgid "Start Date: YYYY-MM-DD"
 msgid "Start Date: YYYY-MM-DD"
-msgstr ""
+msgstr "Data startowa: YYYY-MM-DD (rok, miesiąc, dzień)"
 
 
 #: apps/admin/stats/forms.py:18
 #: apps/admin/stats/forms.py:18
 msgid "End Date: YYYY-MM-DD"
 msgid "End Date: YYYY-MM-DD"
-msgstr ""
+msgstr "Data końcowa: YYYY-MM-DD (rok, miesiąc, dzień)"
 
 
 #: apps/admin/stats/forms.py:20
 #: apps/admin/stats/forms.py:20
 msgid "Graph Precision"
 msgid "Graph Precision"
-msgstr ""
+msgstr "Precyzja wykresu"
 
 
 #: apps/admin/stats/views.py:49
 #: apps/admin/stats/views.py:49
 msgid "Start and end date are same"
 msgid "Start and end date are same"
-msgstr ""
+msgstr "Data startowa i początkowa nie może być taka sama."
 
 
 #: apps/admin/stats/views.py:53
 #: apps/admin/stats/views.py:53
 msgid "Statistical report has been created."
 msgid "Statistical report has been created."
-msgstr ""
+msgstr "Raport statystyczny został wygenerowany."
 
 
 #: apps/admin/stats/views.py:123
 #: apps/admin/stats/views.py:123
 msgid "Too many many items to display on graph."
 msgid "Too many many items to display on graph."
-msgstr ""
+msgstr "Na wykresie jest zbyt wiele elementów do wyświetlenia."
 
 
 #: apps/admin/stats/views.py:128
 #: apps/admin/stats/views.py:128
 msgid "Too few items to display on graph"
 msgid "Too few items to display on graph"
-msgstr ""
+msgstr "Na wykresie jest zbyt mało elementów do wyświetlenia."
 
 
 #: apps/admin/users/forms.py:13 apps/admin/users/forms.py:124
 #: apps/admin/users/forms.py:13 apps/admin/users/forms.py:124
 msgid "No rank assigned"
 msgid "No rank assigned"
-msgstr ""
+msgstr "Brak przypisanej rangi"
 
 
 #: apps/admin/users/forms.py:34 apps/admin/users/forms.py:131
 #: apps/admin/users/forms.py:34 apps/admin/users/forms.py:131
 msgid "Basic Account Settings"
 msgid "Basic Account Settings"
-msgstr ""
+msgstr "Podstawowe ustawienia konta"
 
 
 #: apps/admin/users/forms.py:36 apps/admin/users/forms.py:133
 #: apps/admin/users/forms.py:36 apps/admin/users/forms.py:133
 msgid ""
 msgid ""
 "Username is name under which user is known to other users. Between 3 and 15 "
 "Username is name under which user is known to other users. Between 3 and 15 "
 "characters, only letters and digits are allowed."
 "characters, only letters and digits are allowed."
 msgstr ""
 msgstr ""
+"Nazwa użytkownika, pod którą jest on znany dla innych członków. Długość w "
+"przedziale 3 - 15 znaków, dozwolone tylko litery i cyfry."
 
 
 #: apps/admin/users/forms.py:37 apps/admin/users/forms.py:134
 #: apps/admin/users/forms.py:37 apps/admin/users/forms.py:134
 msgid "User Title"
 msgid "User Title"
-msgstr ""
+msgstr "Tytuł użytkownika"
 
 
 #: apps/admin/users/forms.py:37 apps/admin/users/forms.py:134
 #: apps/admin/users/forms.py:37 apps/admin/users/forms.py:134
 msgid "To override user title with custom one, enter it here."
 msgid "To override user title with custom one, enter it here."
-msgstr ""
+msgstr "Możesz zastąpić rangę użytkownika niestandardowym tytułem."
 
 
 #: apps/admin/users/forms.py:38 apps/admin/users/forms.py:135
 #: apps/admin/users/forms.py:38 apps/admin/users/forms.py:135
 msgid "User Rank"
 msgid "User Rank"
-msgstr ""
+msgstr "Ranga użytkownika"
 
 
 #: apps/admin/users/forms.py:38 apps/admin/users/forms.py:135
 #: apps/admin/users/forms.py:38 apps/admin/users/forms.py:135
 msgid "This user rank."
 msgid "This user rank."
-msgstr ""
+msgstr "Ranga tego użytkownika."
 
 
 #: apps/admin/users/forms.py:39 apps/admin/users/forms.py:136
 #: apps/admin/users/forms.py:39 apps/admin/users/forms.py:136
 msgid "This user roles. Roles are sets of user permissions"
 msgid "This user roles. Roles are sets of user permissions"
-msgstr ""
+msgstr "Role tego użytkownika. Role to zbiory uprawnień."
 
 
 #: apps/admin/users/forms.py:43 apps/admin/users/forms.py:140
 #: apps/admin/users/forms.py:43 apps/admin/users/forms.py:140
 msgid "Sign-in Credentials"
 msgid "Sign-in Credentials"
-msgstr ""
+msgstr "Dane do logowania"
 
 
 #: apps/admin/users/forms.py:45 apps/admin/users/forms.py:142
 #: apps/admin/users/forms.py:45 apps/admin/users/forms.py:142
 #: apps/admin/users/forms.py:201 templates/cranefly/profiles/details.html:24
 #: apps/admin/users/forms.py:201 templates/cranefly/profiles/details.html:24
 msgid "E-mail Address"
 msgid "E-mail Address"
-msgstr ""
+msgstr "Adres e-mail"
 
 
 #: apps/admin/users/forms.py:45 apps/admin/users/forms.py:142
 #: apps/admin/users/forms.py:45 apps/admin/users/forms.py:142
 msgid "Member e-mail address."
 msgid "Member e-mail address."
-msgstr ""
+msgstr "Adres e-mail użytkownika."
 
 
 #: apps/admin/users/forms.py:46
 #: apps/admin/users/forms.py:46
 msgid "Change User Password"
 msgid "Change User Password"
-msgstr ""
+msgstr "Zmień hasło użytkownika"
 
 
 #: apps/admin/users/forms.py:46
 #: apps/admin/users/forms.py:46
 msgid ""
 msgid ""
 "If you wish to change user password, enter here new password. Otherwhise "
 "If you wish to change user password, enter here new password. Otherwhise "
 "leave this field blank."
 "leave this field blank."
 msgstr ""
 msgstr ""
+"Jeżeli chcesz zmienić hasło użytkownika, wpisz tutaj nowe. W przeciwnym "
+"wypadku, pozostaw to pole puste."
 
 
 #: apps/admin/users/forms.py:50
 #: apps/admin/users/forms.py:50
 msgid "User Avatar"
 msgid "User Avatar"
-msgstr ""
+msgstr "Awatar użytkownika"
 
 
 #: apps/admin/users/forms.py:52
 #: apps/admin/users/forms.py:52
 msgid "Set Non-Standard Avatar"
 msgid "Set Non-Standard Avatar"
-msgstr ""
+msgstr "Ustaw niestandardowy awatar"
 
 
 #: apps/admin/users/forms.py:52
 #: apps/admin/users/forms.py:52
 msgid ""
 msgid ""
 "You can make this member use special avatar by entering name of image file "
 "You can make this member use special avatar by entering name of image file "
 "located in avatars directory here."
 "located in avatars directory here."
 msgstr ""
 msgstr ""
+"Możesz ustawić temu użytkownikowi specjalny awatar, wprowadzając tu nazwę "
+"pliku obrazka umieszczonego w katalogu awatarów."
 
 
 #: apps/admin/users/forms.py:53
 #: apps/admin/users/forms.py:53
 msgid "Lock Member's Avatar"
 msgid "Lock Member's Avatar"
-msgstr ""
+msgstr "Zablokuj awatar użytkownika"
 
 
 #: apps/admin/users/forms.py:53
 #: apps/admin/users/forms.py:53
 msgid ""
 msgid ""
@@ -2732,10 +2892,13 @@ msgid ""
 "replaced with random one selected from _removed gallery and member will not "
 "replaced with random one selected from _removed gallery and member will not "
 "be able to change his avatar."
 "be able to change his avatar."
 msgstr ""
 msgstr ""
+"Jeżeli ustawisz tę opcję na \"Tak\", awatar użytkownika zostanie usunięty i "
+"zastąpiony losowym z galerii \"_removed\", a sam użytkownik pozbawiony "
+"możliwości dalszych zmian awatara."
 
 
 #: apps/admin/users/forms.py:54 apps/admin/users/forms.py:63
 #: apps/admin/users/forms.py:54 apps/admin/users/forms.py:63
 msgid "User-visible reason for lock"
 msgid "User-visible reason for lock"
-msgstr ""
+msgstr "Uzasadnienie blokady awatara dla użytkownika"
 
 
 #: apps/admin/users/forms.py:54
 #: apps/admin/users/forms.py:54
 msgid ""
 msgid ""
@@ -2743,38 +2906,42 @@ msgid ""
 "his avatar anymore. This message will be displayed to member in his control "
 "his avatar anymore. This message will be displayed to member in his control "
 "panel."
 "panel."
 msgstr ""
 msgstr ""
+"Możesz tutaj pozostawić wiadomość, którą zobaczy użytkownik jako "
+"uzasadnienie braku możliwości dalszych zmian awatara."
 
 
 #: apps/admin/users/forms.py:55 apps/admin/users/forms.py:64
 #: apps/admin/users/forms.py:55 apps/admin/users/forms.py:64
 msgid "Forum Team-visible reason for lock"
 msgid "Forum Team-visible reason for lock"
-msgstr ""
+msgstr "Uzasadnienie dla ekipy forum"
 
 
 #: apps/admin/users/forms.py:55
 #: apps/admin/users/forms.py:55
 msgid ""
 msgid ""
 "You can leave message to other forum team members exmplaining why this "
 "You can leave message to other forum team members exmplaining why this "
 "member's avatar has been locked."
 "member's avatar has been locked."
 msgstr ""
 msgstr ""
+"Możesz tu wpisać uzasadnienie blokady awatara użytkownika dla innych "
+"członków ekipy forum."
 
 
 #: apps/admin/users/forms.py:59
 #: apps/admin/users/forms.py:59
 msgid "User Signature"
 msgid "User Signature"
-msgstr ""
+msgstr "Sygnaturka użytkownika"
 
 
 #: apps/admin/users/forms.py:61
 #: apps/admin/users/forms.py:61
 msgid "Signature"
 msgid "Signature"
-msgstr ""
+msgstr "Sygnaturka"
 
 
 #: apps/admin/users/forms.py:61
 #: apps/admin/users/forms.py:61
 msgid "Signature is short message attached at end of member's messages."
 msgid "Signature is short message attached at end of member's messages."
-msgstr ""
+msgstr "Sygnaturka to krótka wiadomość dołączana do postów użytkownika."
 
 
 #: apps/admin/users/forms.py:62
 #: apps/admin/users/forms.py:62
 msgid "Lock Member's Signature"
 msgid "Lock Member's Signature"
-msgstr ""
+msgstr "Zablokuj sygnaturkę użytkownika"
 
 
 #: apps/admin/users/forms.py:62
 #: apps/admin/users/forms.py:62
 msgid ""
 msgid ""
 "If you set this field to yes, this member will not be able to change his "
 "If you set this field to yes, this member will not be able to change his "
 "signature."
 "signature."
-msgstr ""
+msgstr "Ustawione na tak, uniemożliwia użytkownikowi zmiany swojej sygnaturki."
 
 
 #: apps/admin/users/forms.py:63
 #: apps/admin/users/forms.py:63
 msgid ""
 msgid ""
@@ -2782,444 +2949,473 @@ msgid ""
 "his signature anymore. This message will be displayed to member in his "
 "his signature anymore. This message will be displayed to member in his "
 "control panel."
 "control panel."
 msgstr ""
 msgstr ""
+"Możesz pozostawić użytkownikowi wiadomość z uzasadnieniem, dlaczego nie może "
+"ponownie zmieniać swojej sygnaturki. Będzie wyświetlona podczas próby jej "
+"zmiany."
 
 
 #: apps/admin/users/forms.py:64
 #: apps/admin/users/forms.py:64
 msgid ""
 msgid ""
 "You can leave message to other forum team members exmplaining why this "
 "You can leave message to other forum team members exmplaining why this "
 "member's signature has been locked."
 "member's signature has been locked."
 msgstr ""
 msgstr ""
+"Możesz pozostawić uzasadnienie zablokowania sygnaturki tego użytkownika dla "
+"innych członków ekipy forum."
 
 
 #: apps/admin/users/forms.py:71 apps/admin/users/forms.py:153
 #: apps/admin/users/forms.py:71 apps/admin/users/forms.py:153
 msgid "User must have at least one role assigned."
 msgid "User must have at least one role assigned."
-msgstr ""
+msgstr "Użytkownik musi mieć przypisaną przynamniej jedną rolę."
 
 
 #: apps/admin/users/forms.py:116
 #: apps/admin/users/forms.py:116
 msgid "Avatar does not exist or is not image file."
 msgid "Avatar does not exist or is not image file."
-msgstr ""
+msgstr "Awatar nie istnieje, lub nie jest obrazkiem."
 
 
 #: apps/admin/users/forms.py:143
 #: apps/admin/users/forms.py:143
 msgid "User Password"
 msgid "User Password"
-msgstr ""
+msgstr "Hasło użytkownika."
 
 
 #: apps/admin/users/forms.py:143
 #: apps/admin/users/forms.py:143
 msgid "Member password."
 msgid "Member password."
-msgstr ""
+msgstr "Hasło członka tego forum."
 
 
 #: apps/admin/users/forms.py:192
 #: apps/admin/users/forms.py:192
 msgid "Already Active"
 msgid "Already Active"
-msgstr ""
+msgstr "Już aktywowany"
 
 
 #: apps/admin/users/forms.py:192
 #: apps/admin/users/forms.py:192
 msgid "By User"
 msgid "By User"
-msgstr ""
+msgstr "Przez użytkownika"
 
 
 #: apps/admin/users/forms.py:192
 #: apps/admin/users/forms.py:192
 msgid "By Administrator"
 msgid "By Administrator"
-msgstr ""
+msgstr "Przez administratora"
 
 
 #: apps/admin/users/forms.py:198 templates/cranefly/profiles/list.html:38
 #: apps/admin/users/forms.py:198 templates/cranefly/profiles/list.html:38
 msgid "Search Users"
 msgid "Search Users"
-msgstr ""
+msgstr "Wyszukiwanie użytkowników"
 
 
 #: apps/admin/users/forms.py:200
 #: apps/admin/users/forms.py:200
 msgid "Username contains..."
 msgid "Username contains..."
-msgstr ""
+msgstr "Nazwa użytkownika zawiera..."
 
 
 #: apps/admin/users/forms.py:201
 #: apps/admin/users/forms.py:201
 msgid "E-mail address contains..."
 msgid "E-mail address contains..."
-msgstr ""
+msgstr "Adres e-mail zawiera..."
 
 
 #: apps/admin/users/forms.py:202
 #: apps/admin/users/forms.py:202
 msgid "Activation Requirement"
 msgid "Activation Requirement"
-msgstr ""
+msgstr "Oczekuje na aktywację"
 
 
 #: apps/admin/users/forms.py:203
 #: apps/admin/users/forms.py:203
 msgid "Rank is"
 msgid "Rank is"
-msgstr ""
+msgstr "Ranga to"
 
 
 #: apps/admin/users/forms.py:204
 #: apps/admin/users/forms.py:204
 msgid "Has Role"
 msgid "Has Role"
-msgstr ""
+msgstr "Posiada rolę"
 
 
 #: apps/admin/users/views.py:25
 #: apps/admin/users/views.py:25
 msgid "User Name"
 msgid "User Name"
-msgstr ""
+msgstr "Nazwa użytkownika"
 
 
 #: apps/admin/users/views.py:26
 #: apps/admin/users/views.py:26
 msgid "Join Date"
 msgid "Join Date"
-msgstr ""
+msgstr "Data dołączenia"
 
 
 #: apps/admin/users/views.py:35
 #: apps/admin/users/views.py:35
 msgid "You have to check at least one user."
 msgid "You have to check at least one user."
-msgstr ""
+msgstr "Musisz zaznaczyć przynajmniej jednego użytkownika."
 
 
 #: apps/admin/users/views.py:37
 #: apps/admin/users/views.py:37
 msgid "Activate users"
 msgid "Activate users"
-msgstr ""
+msgstr "Aktywacja użytkowników"
 
 
 #: apps/admin/users/views.py:37
 #: apps/admin/users/views.py:37
 msgid "Are you sure you want to activate selected members?"
 msgid "Are you sure you want to activate selected members?"
-msgstr ""
+msgstr "Czy aby na pewno chcesz aktywować zaznaczonych użytkowników?"
 
 
 #: apps/admin/users/views.py:38
 #: apps/admin/users/views.py:38
 msgid "Request e-mail validation"
 msgid "Request e-mail validation"
-msgstr ""
+msgstr "Zażądaj walidacji adresu e-mail"
 
 
 #: apps/admin/users/views.py:38
 #: apps/admin/users/views.py:38
 msgid ""
 msgid ""
 "Are you sure you want to deactivate selected members and request them to "
 "Are you sure you want to deactivate selected members and request them to "
 "revalidate their e-mail addresses?"
 "revalidate their e-mail addresses?"
 msgstr ""
 msgstr ""
+"Czy aby na pewno chcesz zdezaktywować zaznaczonych użytkowników i zażądać "
+"rewalidacji ich adresu e-mail?"
 
 
 #: apps/admin/users/views.py:39
 #: apps/admin/users/views.py:39
 msgid "Remove and lock avatars"
 msgid "Remove and lock avatars"
-msgstr ""
+msgstr "Usuń i zablokuj avataty"
 
 
 #: apps/admin/users/views.py:39
 #: apps/admin/users/views.py:39
 msgid ""
 msgid ""
 "Are you sure you want to remove selected members avatars and their ability "
 "Are you sure you want to remove selected members avatars and their ability "
 "to change them?"
 "to change them?"
 msgstr ""
 msgstr ""
+"Czy aby na pewno chcesz usunąć awatary zaznaczonych użytkowników i odebrać "
+"im możliwość ich ustawiania?"
 
 
 #: apps/admin/users/views.py:40
 #: apps/admin/users/views.py:40
 msgid "Remove and lock signatures"
 msgid "Remove and lock signatures"
-msgstr ""
+msgstr "Usuń i zablokuj sygnaturki"
 
 
 #: apps/admin/users/views.py:40
 #: apps/admin/users/views.py:40
 msgid ""
 msgid ""
 "Are you sure you want to remove selected members signatures and their "
 "Are you sure you want to remove selected members signatures and their "
 "ability to edit them?"
 "ability to edit them?"
 msgstr ""
 msgstr ""
+"Czy aby na pewno chcesz usunąć sygnaturki zaznaczonych użytkowników i "
+"odebrać im możliwość ich edycji?"
 
 
 #: apps/admin/users/views.py:41
 #: apps/admin/users/views.py:41
 msgid "Remove locks from avatars and signatures"
 msgid "Remove locks from avatars and signatures"
-msgstr ""
+msgstr "Usuń blokadę modyfikacji awatarów i sygnaturek"
 
 
 #: apps/admin/users/views.py:41
 #: apps/admin/users/views.py:41
 msgid ""
 msgid ""
 "Are you sure you want to remove locks from selected members avatars and "
 "Are you sure you want to remove locks from selected members avatars and "
 "signatures?"
 "signatures?"
 msgstr ""
 msgstr ""
+"Czy aby na pewno chcesz przywrócić możliwość modyfikacji awatarów i "
+"sygnaturek dla zaznaczonych użytkowników?"
 
 
 #: apps/admin/users/views.py:42
 #: apps/admin/users/views.py:42
 msgid "Reset passwords"
 msgid "Reset passwords"
-msgstr ""
+msgstr "Reset haseł"
 
 
 #: apps/admin/users/views.py:42
 #: apps/admin/users/views.py:42
 msgid "Are you sure you want to reset selected members passwords?"
 msgid "Are you sure you want to reset selected members passwords?"
-msgstr ""
+msgstr "Czy aby na pewno chcesz zresetować hasła zaznaczonych użytkowników?"
 
 
 #: apps/admin/users/views.py:43
 #: apps/admin/users/views.py:43
 msgid "Delete users with content"
 msgid "Delete users with content"
-msgstr ""
+msgstr "Usuń użytkowników wraz z postami"
 
 
 #: apps/admin/users/views.py:43
 #: apps/admin/users/views.py:43
 msgid "Are you sure you want to delete selected users and their content?"
 msgid "Are you sure you want to delete selected users and their content?"
 msgstr ""
 msgstr ""
+"Czy aby na pewno chcesz usunąć zaznaczonych użytkowników wraz z całą ich "
+"aktywnością?"
 
 
 #: apps/admin/users/views.py:44
 #: apps/admin/users/views.py:44
 msgid "Delete users"
 msgid "Delete users"
-msgstr ""
+msgstr "Usuń użytkowników"
 
 
 #: apps/admin/users/views.py:44
 #: apps/admin/users/views.py:44
 msgid "Are you sure you want to delete selected users?"
 msgid "Are you sure you want to delete selected users?"
-msgstr ""
+msgstr "Czy aby na pewno chcesz usunąć zaznaczonych użytkowników?"
 
 
 #: apps/admin/users/views.py:89
 #: apps/admin/users/views.py:89
 msgid "Edit User Details"
 msgid "Edit User Details"
-msgstr ""
+msgstr "Modyfikacja szczegółów użytkownika"
 
 
 #: apps/admin/users/views.py:90
 #: apps/admin/users/views.py:90
 msgid "Delete User"
 msgid "Delete User"
-msgstr ""
+msgstr "Usuń użytkownika"
 
 
 #: apps/admin/users/views.py:90
 #: apps/admin/users/views.py:90
 msgid "Are you sure you want to delete this user account?"
 msgid "Are you sure you want to delete this user account?"
-msgstr ""
+msgstr "Czy aby na pewno chcesz usunąć profil tego użytkownika?"
 
 
 #: apps/admin/users/views.py:102
 #: apps/admin/users/views.py:102
 msgid "Your Account has been activated"
 msgid "Your Account has been activated"
-msgstr ""
+msgstr "Twoje konto zostało aktywowane"
 
 
 #: apps/admin/users/views.py:105
 #: apps/admin/users/views.py:105
 msgid "Selected users accounts have been activated."
 msgid "Selected users accounts have been activated."
-msgstr ""
+msgstr "Konta zaznaczonych użytkowników zostały aktywowane."
 
 
 #: apps/admin/users/views.py:112
 #: apps/admin/users/views.py:112
 msgid "You cannot force validation of protected members e-mails."
 msgid "You cannot force validation of protected members e-mails."
-msgstr ""
+msgstr "Nie możesz wymusić walidacji e-maili chronionych użytkowników."
 
 
 #: apps/admin/users/views.py:126
 #: apps/admin/users/views.py:126
 msgid ""
 msgid ""
 "Selected users accounts have been deactivated and new activation links have "
 "Selected users accounts have been deactivated and new activation links have "
 "been sent to them."
 "been sent to them."
 msgstr ""
 msgstr ""
+"Konta zaznaczonych użytkowników zostały tymczasowo zdezaktywowane. Rozesłano "
+"do nich nowe e-maile aktywacyjne."
 
 
 #: apps/admin/users/views.py:133
 #: apps/admin/users/views.py:133
 msgid "You cannot remove and block protected members avatars."
 msgid "You cannot remove and block protected members avatars."
 msgstr ""
 msgstr ""
+"Nie możesz usunąć, ani zablokować możliwości zmiany awatara u chronionych "
+"użytkowników."
 
 
 #: apps/admin/users/views.py:141
 #: apps/admin/users/views.py:141
 msgid "Selected users avatars were deleted and locked."
 msgid "Selected users avatars were deleted and locked."
-msgstr ""
+msgstr "Awatary zaznaczonych użytkowników zostały usunięte i zablokowane."
 
 
 #: apps/admin/users/views.py:148
 #: apps/admin/users/views.py:148
 msgid "You cannot remove and block protected members signatures."
 msgid "You cannot remove and block protected members signatures."
 msgstr ""
 msgstr ""
+"Nie możesz usunąć, ani zablokować możliwości zmiany sygnaturki u chronionych "
+"użytkowników."
 
 
 #: apps/admin/users/views.py:158
 #: apps/admin/users/views.py:158
 msgid "Selected users signatures were deleted and locked."
 msgid "Selected users signatures were deleted and locked."
-msgstr ""
+msgstr "Sygnaturki zaznaczonych użytkowników zostały usunięte i zablokowane."
 
 
 #: apps/admin/users/views.py:168
 #: apps/admin/users/views.py:168
 msgid "Selected users can now edit their avatars and signatures."
 msgid "Selected users can now edit their avatars and signatures."
 msgstr ""
 msgstr ""
+"Zaznaczeniu użytkownicy mogą ponownie modyfikować swoje awatary i sygnaturki."
 
 
 #: apps/admin/users/views.py:175
 #: apps/admin/users/views.py:175
 msgid "You cannot reset protected members passwords."
 msgid "You cannot reset protected members passwords."
-msgstr ""
+msgstr "Nie możesz zrestartować hasła u chronionych użytkowników."
 
 
 #: apps/admin/users/views.py:186 apps/resetpswd/views.py:83
 #: apps/admin/users/views.py:186 apps/resetpswd/views.py:83
 msgid "Your New Password"
 msgid "Your New Password"
-msgstr ""
+msgstr "Twoje nowe hasło"
 
 
 #: apps/admin/users/views.py:192
 #: apps/admin/users/views.py:192
 msgid "Selected users passwords have been reset successfully."
 msgid "Selected users passwords have been reset successfully."
-msgstr ""
+msgstr "Hasła zaznaczonych użytkowników zostały pomyślnie zrestartowane."
 
 
 #: apps/admin/users/views.py:198 apps/admin/users/views.py:218
 #: apps/admin/users/views.py:198 apps/admin/users/views.py:218
 #: apps/admin/users/views.py:363
 #: apps/admin/users/views.py:363
 msgid "You cannot delete yourself."
 msgid "You cannot delete yourself."
-msgstr ""
+msgstr "Nie możesz usunąć swojego profilu."
 
 
 #: apps/admin/users/views.py:200 apps/admin/users/views.py:220
 #: apps/admin/users/views.py:200 apps/admin/users/views.py:220
 msgid "You cannot delete protected members."
 msgid "You cannot delete protected members."
-msgstr ""
+msgstr "Nie możesz usunąć chronionych użytkowników."
 
 
 #: apps/admin/users/views.py:212
 #: apps/admin/users/views.py:212
 msgid "Selected users and their content have been deleted successfully."
 msgid "Selected users and their content have been deleted successfully."
-msgstr ""
+msgstr "Zaznaczeni użytkownicy i ich posty zostały pomyślnie usunięte."
 
 
 #: apps/admin/users/views.py:227
 #: apps/admin/users/views.py:227
 msgid "Selected users have been deleted successfully."
 msgid "Selected users have been deleted successfully."
-msgstr ""
+msgstr "Pomyślnie usunięto zaznaczonych użytkowników."
 
 
 #: apps/admin/users/views.py:235
 #: apps/admin/users/views.py:235
 msgid "Save User"
 msgid "Save User"
-msgstr ""
+msgstr "Zapisz użytkownika"
 
 
 #: apps/admin/users/views.py:261
 #: apps/admin/users/views.py:261
 msgid "New User has been created."
 msgid "New User has been created."
-msgstr ""
+msgstr "Nowy użytkownik został dodany."
 
 
 #: apps/admin/users/views.py:267
 #: apps/admin/users/views.py:267
 msgid "Edit User"
 msgid "Edit User"
-msgstr ""
+msgstr "Zmodyfikuj użytkownika"
 
 
 #: apps/admin/users/views.py:272
 #: apps/admin/users/views.py:272
 msgid "Requested User could not be found."
 msgid "Requested User could not be found."
-msgstr ""
+msgstr "Nie odnaleziono żądanego użytkownika."
 
 
 #: apps/admin/users/views.py:352
 #: apps/admin/users/views.py:352
 #, python-format
 #, python-format
 msgid "Changes in user's \"%(name)s\" account have been saved."
 msgid "Changes in user's \"%(name)s\" account have been saved."
-msgstr ""
+msgstr "Zmiany w profilu użytkownika \"%(name)s\" zostały zachowane."
 
 
 #: apps/admin/users/views.py:359
 #: apps/admin/users/views.py:359
 msgid "Requested User account could not be found."
 msgid "Requested User account could not be found."
-msgstr ""
+msgstr "Żądane konto użytkownika nie zostało odnalezione."
 
 
 #: apps/admin/users/views.py:365
 #: apps/admin/users/views.py:365
 msgid "You cannot delete protected member."
 msgid "You cannot delete protected member."
-msgstr ""
+msgstr "Nie możesz usunąć chronionego użytkownika."
 
 
 #: apps/admin/users/views.py:368
 #: apps/admin/users/views.py:368
 #, python-format
 #, python-format
 msgid "User \"%(name)s\" has been deleted."
 msgid "User \"%(name)s\" has been deleted."
-msgstr ""
+msgstr "Użytkownik \"%(name)s\" został usunięty."
 
 
 #: apps/privatethreads/forms.py:13
 #: apps/privatethreads/forms.py:13
 msgid "Invite members to thread"
 msgid "Invite members to thread"
-msgstr ""
+msgstr "Zaproś użytkowników do dyskusji"
 
 
 #: apps/privatethreads/forms.py:13
 #: apps/privatethreads/forms.py:13
 msgid "user1, user2, user3..."
 msgid "user1, user2, user3..."
-msgstr ""
+msgstr "Użytkownik1, użytkownik2, użytkownik3..."
 
 
 #: apps/privatethreads/forms.py:29 apps/privatethreads/jumps.py:63
 #: apps/privatethreads/forms.py:29 apps/privatethreads/jumps.py:63
 #, python-format
 #, python-format
 msgid "%(user)s cannot participate in private threads."
 msgid "%(user)s cannot participate in private threads."
-msgstr ""
+msgstr "%(user)s nie może uczestniczyć w prywatnych dyskusjach."
 
 
 #: apps/privatethreads/forms.py:32 apps/privatethreads/jumps.py:66
 #: apps/privatethreads/forms.py:32 apps/privatethreads/jumps.py:66
 #: apps/privatethreads/posting.py:27
 #: apps/privatethreads/posting.py:27
 #, python-format
 #, python-format
 msgid "%(user)s restricts who can invite him to private threads."
 msgid "%(user)s restricts who can invite him to private threads."
-msgstr ""
+msgstr "%(user)s nie pozwala Ci zaprosić go do dyskusji."
 
 
 #: apps/privatethreads/forms.py:35
 #: apps/privatethreads/forms.py:35
 #, python-format
 #, python-format
 msgid "User \"%(username)s\" could not be found."
 msgid "User \"%(username)s\" could not be found."
-msgstr ""
+msgstr "Użytkownik \"%(username)s\" nie został odnaleziony."
 
 
 #: apps/privatethreads/forms.py:37
 #: apps/privatethreads/forms.py:37
 msgid ""
 msgid ""
 "You cannot invite more than 8 members at single time. Post thread and then "
 "You cannot invite more than 8 members at single time. Post thread and then "
 "invite additional members."
 "invite additional members."
 msgstr ""
 msgstr ""
+"Nie możesz zaprosić więcej niż 8 osób na raz. Utwórz temat i wtedy zaproś "
+"pozostałych członków."
 
 
 #: apps/privatethreads/jumps.py:52
 #: apps/privatethreads/jumps.py:52
 msgid "You have to enter name of user you want to invite to thread."
 msgid "You have to enter name of user you want to invite to thread."
 msgstr ""
 msgstr ""
+"Musisz wprowadzić nazwę użytkownika, którego chcesz zaprosić do dyskusji."
 
 
 #: apps/privatethreads/jumps.py:59
 #: apps/privatethreads/jumps.py:59
 msgid "You cannot add yourself to this thread."
 msgid "You cannot add yourself to this thread."
-msgstr ""
+msgstr "Nie możesz zaprosić tutaj sam siebie."
 
 
 #: apps/privatethreads/jumps.py:61
 #: apps/privatethreads/jumps.py:61
 #, python-format
 #, python-format
 msgid "%(user)s is already participating in this thread."
 msgid "%(user)s is already participating in this thread."
-msgstr ""
+msgstr "%(user)s już uczestniczy w tej dyskusji."
 
 
 #: apps/privatethreads/jumps.py:71 apps/privatethreads/mixins.py:25
 #: apps/privatethreads/jumps.py:71 apps/privatethreads/mixins.py:25
 #, python-format
 #, python-format
 msgid "You've been invited to private thread \"%(thread)s\" by %(user)s"
 msgid "You've been invited to private thread \"%(thread)s\" by %(user)s"
-msgstr ""
+msgstr "%(user)s zaprosił Cię do prywatnej dyskusji \"%(thread)s\""
 
 
 #: apps/privatethreads/jumps.py:74
 #: apps/privatethreads/jumps.py:74
 #, python-format
 #, python-format
 msgid "%(user)s has been added to this thread."
 msgid "%(user)s has been added to this thread."
-msgstr ""
+msgstr "%(user)s został dodany do dyskusji."
 
 
-#: apps/privatethreads/jumps.py:77
+#: apps/privatethreads/jumps.py:76
 msgid "User with requested username could not be found."
 msgid "User with requested username could not be found."
-msgstr ""
+msgstr "Użytkownik o podanej nazwie nie został odnaleziony."
 
 
-#: apps/privatethreads/jumps.py:87
+#: apps/privatethreads/jumps.py:86
 msgid "You don't have permission to remove discussion participants."
 msgid "You don't have permission to remove discussion participants."
-msgstr ""
+msgstr "Nie posiadasz uprawnień do usuwania członków dyskusji."
 
 
-#: apps/privatethreads/jumps.py:98
+#: apps/privatethreads/jumps.py:97
 msgid "Thread has been deleted because last participant left it."
 msgid "Thread has been deleted because last participant left it."
-msgstr ""
+msgstr "Temat został usunięty, ponieważ opuścił go ostatni uczestnik."
 
 
-#: apps/privatethreads/jumps.py:104
+#: apps/privatethreads/jumps.py:103
 #, python-format
 #, python-format
 msgid "You have left the \"%(thread)s\" thread."
 msgid "You have left the \"%(thread)s\" thread."
-msgstr ""
+msgstr "Opuściłeś temat \"%(thread)s\"."
 
 
-#: apps/privatethreads/jumps.py:111
+#: apps/privatethreads/jumps.py:110
 msgid "Selected participant was removed from thread."
 msgid "Selected participant was removed from thread."
-msgstr ""
+msgstr "Zaznaczony członek został wykluczony z dyskusji."
 
 
-#: apps/privatethreads/jumps.py:114
+#: apps/privatethreads/jumps.py:113
 msgid "Requested thread participant does not exist."
 msgid "Requested thread participant does not exist."
-msgstr ""
+msgstr "Nie odnaleziono żądanego członka dyskusji."
 
 
 #: apps/privatethreads/posting.py:24
 #: apps/privatethreads/posting.py:24
 msgid "This member can not participate in private threads."
 msgid "This member can not participate in private threads."
-msgstr ""
+msgstr "Ten użytkownik nie może uczestniczyć w prywatnych dyskusjach."
 
 
 #: apps/privatethreads/posting.py:41 apps/threads/posting.py:15
 #: apps/privatethreads/posting.py:41 apps/threads/posting.py:15
 msgid ""
 msgid ""
 "New thread has been posted. It will be hidden from other members until "
 "New thread has been posted. It will be hidden from other members until "
 "moderator reviews it."
 "moderator reviews it."
 msgstr ""
 msgstr ""
+"Nowy temat został utworzony. Do czasu przejrzenia przez moderatora, "
+"pozostanie ukryty dla innych użytkowników."
 
 
 #: apps/privatethreads/posting.py:43 apps/threads/posting.py:17
 #: apps/privatethreads/posting.py:43 apps/threads/posting.py:17
 msgid "New thread has been posted."
 msgid "New thread has been posted."
-msgstr ""
+msgstr "Nowy temat został utworzony."
 
 
 #: apps/privatethreads/posting.py:54 apps/threads/posting.py:23
 #: apps/privatethreads/posting.py:54 apps/threads/posting.py:23
 msgid "Your thread has been edited."
 msgid "Your thread has been edited."
-msgstr ""
+msgstr "Twój temat został zmodyfikowany."
 
 
 #: apps/privatethreads/posting.py:71 apps/threads/posting.py:30
 #: apps/privatethreads/posting.py:71 apps/threads/posting.py:30
 msgid ""
 msgid ""
 "Your reply has been posted. It will be hidden from other members until "
 "Your reply has been posted. It will be hidden from other members until "
 "moderator reviews it."
 "moderator reviews it."
 msgstr ""
 msgstr ""
+"Twoja odpowiedź została dodana. Do czasu przejrzenia przez moderatora, "
+"pozostanie ukryta dla innych użytkowników."
 
 
 #: apps/privatethreads/posting.py:73 apps/threads/posting.py:32
 #: apps/privatethreads/posting.py:73 apps/threads/posting.py:32
 msgid "Your reply has been posted."
 msgid "Your reply has been posted."
-msgstr ""
+msgstr "Twoja odpowiedź została zapisana."
 
 
 #: apps/privatethreads/posting.py:84 apps/threads/posting.py:38
 #: apps/privatethreads/posting.py:84 apps/threads/posting.py:38
 msgid "Your reply has been changed."
 msgid "Your reply has been changed."
-msgstr ""
+msgstr "Twoja odpowiedź została zmodyfikowana."
 
 
 #: apps/privatethreads/thread.py:14 apps/threads/thread.py:14
 #: apps/privatethreads/thread.py:14 apps/threads/thread.py:14
 msgid "Merge posts into one"
 msgid "Merge posts into one"
-msgstr ""
+msgstr "Połącz posty w jeden"
 
 
 #: apps/privatethreads/thread.py:16 apps/threads/thread.py:18
 #: apps/privatethreads/thread.py:16 apps/threads/thread.py:18
 msgid "Protect posts"
 msgid "Protect posts"
-msgstr ""
+msgstr "Zabezpiecz posty"
 
 
 #: apps/privatethreads/thread.py:17 apps/threads/thread.py:19
 #: apps/privatethreads/thread.py:17 apps/threads/thread.py:19
 msgid "Remove posts protection"
 msgid "Remove posts protection"
-msgstr ""
+msgstr "Zdejmij ochronę postów"
 
 
 #: apps/privatethreads/thread.py:20 apps/threads/thread.py:22
 #: apps/privatethreads/thread.py:20 apps/threads/thread.py:22
-msgid "Undelete posts"
-msgstr ""
+msgid "Restore posts"
+msgstr "Przywróć posty"
 
 
 #: apps/privatethreads/thread.py:21 apps/threads/thread.py:23
 #: apps/privatethreads/thread.py:21 apps/threads/thread.py:23
-msgid "Soft delete posts"
-msgstr ""
+msgid "Hide posts"
+msgstr "Ukryj posty"
 
 
 #: apps/privatethreads/thread.py:23 apps/threads/thread.py:25
 #: apps/privatethreads/thread.py:23 apps/threads/thread.py:25
-msgid "Hard delete posts"
-msgstr ""
+msgid "Delete posts"
+msgstr "Usuń posty"
 
 
 #: apps/privatethreads/thread.py:34 apps/threads/thread.py:49
 #: apps/privatethreads/thread.py:34 apps/threads/thread.py:49
 msgid "Open this thread"
 msgid "Open this thread"
-msgstr ""
+msgstr "Otwórz ten temat"
 
 
 #: apps/privatethreads/thread.py:36 apps/threads/thread.py:51
 #: apps/privatethreads/thread.py:36 apps/threads/thread.py:51
 msgid "Close this thread"
 msgid "Close this thread"
-msgstr ""
+msgstr "Zamknij ten temat"
 
 
 #: apps/privatethreads/thread.py:39 apps/threads/thread.py:54
 #: apps/privatethreads/thread.py:39 apps/threads/thread.py:54
-msgid "Undelete this thread"
-msgstr ""
+msgid "Restore this thread"
+msgstr "Przywróć ten temat"
 
 
 #: apps/privatethreads/thread.py:41 apps/threads/thread.py:56
 #: apps/privatethreads/thread.py:41 apps/threads/thread.py:56
-msgid "Soft delete this thread"
-msgstr ""
+msgid "Hide this thread"
+msgstr "Ukryj ten temat"
 
 
 #: apps/privatethreads/thread.py:43 apps/threads/thread.py:58
 #: apps/privatethreads/thread.py:43 apps/threads/thread.py:58
-msgid "Hard delete this thread"
-msgstr ""
+msgid "Delete this thread"
+msgstr "Usuń ten temat"
 
 
-#: apps/profiles/views.py:66
+#: apps/profiles/views.py:67
 msgid "To search users you have to enter username in search field."
 msgid "To search users you have to enter username in search field."
-msgstr ""
+msgstr "Aby szukać, wprowadź nazwę użytkownika w polu wyszukiwania."
 
 
 #: apps/profiles/details/profile.py:4
 #: apps/profiles/details/profile.py:4
 msgid "Profile Details"
 msgid "Profile Details"
-msgstr ""
+msgstr "Szczegóły"
 
 
 #: apps/profiles/followers/profile.py:4
 #: apps/profiles/followers/profile.py:4
 #: templates/cranefly/profiles/details.html:153
 #: templates/cranefly/profiles/details.html:153
 msgid "Followers"
 msgid "Followers"
-msgstr ""
+msgstr "Obserwatorzy"
 
 
 #: apps/profiles/follows/profile.py:4
 #: apps/profiles/follows/profile.py:4
 msgid "Follows"
 msgid "Follows"
-msgstr ""
+msgstr "Obserwuje"
 
 
 #: apps/register/forms.py:17
 #: apps/register/forms.py:17
 msgid "Acceptation of board ToS is mandatory for membership."
 msgid "Acceptation of board ToS is mandatory for membership."
-msgstr ""
+msgstr "Akceptacja regulaminu forum jest obowiązkowa do członkowstwa."
 
 
 #: apps/register/forms.py:21
 #: apps/register/forms.py:21
 msgid "Entered addresses do not match."
 msgid "Entered addresses do not match."
-msgstr ""
+msgstr "Wprowadzone adresy e-mail nie pasują do siebie."
 
 
 #: apps/register/forms.py:24
 #: apps/register/forms.py:24
 msgid "Entered passwords do not match."
 msgid "Entered passwords do not match."
-msgstr ""
+msgstr "Wprowadzone hasła nie pasują do siebie."
 
 
 #: apps/register/forms.py:31
 #: apps/register/forms.py:31
 #, python-format
 #, python-format
@@ -3227,56 +3423,62 @@ msgid ""
 "Your displayed username. Between %(min)s and %(max)s characters, only "
 "Your displayed username. Between %(min)s and %(max)s characters, only "
 "letters and digits are allowed."
 "letters and digits are allowed."
 msgstr ""
 msgstr ""
+"Twoja wyświetlana nazwa. Pomiędzy %(min)s a %(max)s znaków, dozwolone są "
+"tylko litery i cyfry."
 
 
 #: apps/register/forms.py:31
 #: apps/register/forms.py:31
 msgid "Enter your desired username"
 msgid "Enter your desired username"
-msgstr ""
+msgstr "Wprowadź pożądaną nazwę użytkownika"
 
 
 #: apps/register/forms.py:35
 #: apps/register/forms.py:35
 msgid ""
 msgid ""
 "Working e-mail inbox is required to maintain control over your forum account."
 "Working e-mail inbox is required to maintain control over your forum account."
 msgstr ""
 msgstr ""
+"Aby utrzymać kontrolę nad kontem użytkownika, potrzebny jest działający "
+"adres e-mail."
 
 
 #: apps/register/forms.py:35 apps/signin/forms.py:14
 #: apps/register/forms.py:35 apps/signin/forms.py:14
 msgid "Enter your e-mail"
 msgid "Enter your e-mail"
-msgstr ""
+msgstr "Wprowadź swój adres e-mail"
 
 
 #: apps/register/forms.py:35
 #: apps/register/forms.py:35
 msgid "Repeat your e-mail"
 msgid "Repeat your e-mail"
-msgstr ""
+msgstr "Powtórz swój adres e-mail"
 
 
 #: apps/register/forms.py:36 templates/_email/users/activation/none.html:11
 #: apps/register/forms.py:36 templates/_email/users/activation/none.html:11
 #: templates/_email/users/activation/none.txt:12
 #: templates/_email/users/activation/none.txt:12
 msgid "Password"
 msgid "Password"
-msgstr ""
+msgstr "Hasło"
 
 
 #: apps/register/forms.py:36
 #: apps/register/forms.py:36
 msgid ""
 msgid ""
 "Password you will be using to sign in to your account. Make sure it's strong."
 "Password you will be using to sign in to your account. Make sure it's strong."
 msgstr ""
 msgstr ""
+"Hasło będzie potrzebne do zalogowania się na Twoje konto. Upewnij się, że "
+"jest silne."
 
 
 #: apps/register/forms.py:36 apps/signin/forms.py:15
 #: apps/register/forms.py:36 apps/signin/forms.py:15
 msgid "Enter your password"
 msgid "Enter your password"
-msgstr ""
+msgstr "Wprowadź swoje hasło"
 
 
 #: apps/register/forms.py:36
 #: apps/register/forms.py:36
 msgid "Repeat your password"
 msgid "Repeat your password"
-msgstr ""
+msgstr "Powtórz swoje hasło"
 
 
 #: apps/register/forms.py:44 fixtures/tossettings.py:7
 #: apps/register/forms.py:44 fixtures/tossettings.py:7
 #: templates/cranefly/layout.html:47
 #: templates/cranefly/layout.html:47
 msgid "Forum Terms of Service"
 msgid "Forum Terms of Service"
-msgstr ""
+msgstr "Regulamin forum"
 
 
 #: apps/register/views.py:20
 #: apps/register/views.py:20
 msgid "We are sorry but we don't allow new members registrations at this time."
 msgid "We are sorry but we don't allow new members registrations at this time."
-msgstr ""
+msgstr "Przepraszamy, ale w danej chwili nie akceptujemy nowych rejestracji."
 
 
 #: apps/register/views.py:45
 #: apps/register/views.py:45
 #, python-format
 #, python-format
 msgid ""
 msgid ""
 "Welcome aboard, %(username)s! Your account has been registered successfully."
 "Welcome aboard, %(username)s! Your account has been registered successfully."
-msgstr ""
+msgstr "Twoje konto zostało pomyślnie zarejestrowane. Witaj, %(username)s!"
 
 
 #: apps/register/views.py:49
 #: apps/register/views.py:49
 #, python-format
 #, python-format
@@ -3285,11 +3487,14 @@ msgid ""
 "activate it before you will be able to sign-in. We have sent you an e-mail "
 "activate it before you will be able to sign-in. We have sent you an e-mail "
 "with activation link."
 "with activation link."
 msgstr ""
 msgstr ""
+"%(username)s, Twoje konto zostało zarejestrowane, jednak przed zalogowaniem "
+"się musisz je aktywować. Wysłaliśmy Ci wiadomość e-mail z linkiem "
+"aktywacyjnym."
 
 
 #: apps/register/views.py:53 apps/register/views.py:62
 #: apps/register/views.py:53 apps/register/views.py:62
 #, python-format
 #, python-format
 msgid "Welcome aboard, %(username)s!"
 msgid "Welcome aboard, %(username)s!"
-msgstr ""
+msgstr "Witaj na forum, %(username)s!"
 
 
 #: apps/register/views.py:58
 #: apps/register/views.py:58
 #, python-format
 #, python-format
@@ -3298,12 +3503,17 @@ msgid ""
 "sign in until board administrator accepts it. We'll notify when this "
 "sign in until board administrator accepts it. We'll notify when this "
 "happens. Thank you for your patience!"
 "happens. Thank you for your patience!"
 msgstr ""
 msgstr ""
+"%(username)s, Twoje konto zostało utworzone, ale nie możesz się zalogować, "
+"dopóki nie zostaniesz potwierdzony przez administratora. Poinformujemy Cię o "
+"tym. Dziękujemy za cierpliwość!"
 
 
 #: apps/resetpswd/forms.py:17
 #: apps/resetpswd/forms.py:17
 msgid ""
 msgid ""
 "Enter email address password reset confirmation e-mail will be sent to. It "
 "Enter email address password reset confirmation e-mail will be sent to. It "
 "must be valid e-mail you used to register on forums."
 "must be valid e-mail you used to register on forums."
 msgstr ""
 msgstr ""
+"Wprowadź adres e-mail, na który zostanie wysłane potwierdzenie zmiany hasła. "
+"Musi być to poprawny adres e-mail, podany podczas rejestracji na forum."
 
 
 #: apps/resetpswd/views.py:29 apps/resetpswd/views.py:65
 #: apps/resetpswd/views.py:29 apps/resetpswd/views.py:65
 #, python-format
 #, python-format
@@ -3311,16 +3521,19 @@ msgid ""
 "%(username)s, your account has to be activated in order for you to be able "
 "%(username)s, your account has to be activated in order for you to be able "
 "to request new password."
 "to request new password."
 msgstr ""
 msgstr ""
+"%(username)s, Twoje konto musi być aktywne, abyś mógł zażądać nowego hasła."
 
 
 #: apps/resetpswd/views.py:36
 #: apps/resetpswd/views.py:36
 msgid "Confirm New Password Request"
 msgid "Confirm New Password Request"
-msgstr ""
+msgstr "Potwierdź żądanie nowego hasła"
 
 
 #: apps/resetpswd/views.py:39
 #: apps/resetpswd/views.py:39
 #, python-format
 #, python-format
 msgid ""
 msgid ""
 "%(username)s, new password request confirmation has been sent to %(email)s."
 "%(username)s, new password request confirmation has been sent to %(email)s."
 msgstr ""
 msgstr ""
+"%(username)s, żądanie potwierdzenia zmiany hasła zostało wysłane na "
+"%(email)s."
 
 
 #: apps/resetpswd/views.py:68
 #: apps/resetpswd/views.py:68
 #, python-format
 #, python-format
@@ -3328,6 +3541,8 @@ msgid ""
 "%(username)s, request confirmation link is invalid. Please request new "
 "%(username)s, request confirmation link is invalid. Please request new "
 "confirmation link."
 "confirmation link."
 msgstr ""
 msgstr ""
+"%(username)s, link potwierdzający jest niepoprawny. Spróbuj powtórzyć całą "
+"procedurę."
 
 
 #: apps/resetpswd/views.py:87
 #: apps/resetpswd/views.py:87
 #, python-format
 #, python-format
@@ -3335,188 +3550,246 @@ msgid ""
 "%(username)s, your password has been changed with new one that was sent to "
 "%(username)s, your password has been changed with new one that was sent to "
 "%(email)s."
 "%(email)s."
 msgstr ""
 msgstr ""
+"%(username)s, Twoje hasło zostało pomyślnie zmienione na to, które zostało "
+"wysłane na %(email)s."
 
 
 #: apps/signin/forms.py:6
 #: apps/signin/forms.py:6
 msgid "Your email"
 msgid "Your email"
-msgstr ""
+msgstr "Twój adres e-mail"
 
 
 #: apps/signin/forms.py:7
 #: apps/signin/forms.py:7
 msgid "Your password"
 msgid "Your password"
-msgstr ""
+msgstr "Twoje hasło"
 
 
 #: apps/signin/forms.py:8
 #: apps/signin/forms.py:8
 msgid "Stay Signed In"
 msgid "Stay Signed In"
-msgstr ""
+msgstr "Zapamiętaj mnie"
 
 
 #: apps/signin/forms.py:8
 #: apps/signin/forms.py:8
 msgid "Sign me In automatically next time"
 msgid "Sign me In automatically next time"
-msgstr ""
+msgstr "Zaloguj mnie automatycznie przy kolejnych wizytach"
 
 
-#: apps/signin/views.py:65
+#: apps/signin/views.py:66
 #, python-format
 #, python-format
 msgid "Welcome back, %(username)s!"
 msgid "Welcome back, %(username)s!"
-msgstr ""
+msgstr "Witaj ponownie, %(username)s!"
 
 
-#: apps/signin/views.py:106
+#: apps/signin/views.py:107
 msgid "You have been signed out."
 msgid "You have been signed out."
-msgstr ""
+msgstr "Pomyślnie wylogowano z forum."
 
 
-#: apps/threads/list.py:45
+#: apps/threads/list.py:52
 msgid "Accept threads"
 msgid "Accept threads"
-msgstr ""
+msgstr "Zaakceptuj tematy"
 
 
-#: apps/threads/list.py:47
+#: apps/threads/list.py:54
 msgid "Change to announcements"
 msgid "Change to announcements"
-msgstr ""
+msgstr "Przekształć w ogłoszenia"
 
 
-#: apps/threads/list.py:49
+#: apps/threads/list.py:56
 msgid "Change to sticky threads"
 msgid "Change to sticky threads"
-msgstr ""
+msgstr "Zamień na przyklejone tematy"
 
 
-#: apps/threads/list.py:51
+#: apps/threads/list.py:58
 msgid "Change to standard thread"
 msgid "Change to standard thread"
-msgstr ""
+msgstr "Zamien na standardowe tematy"
 
 
-#: apps/threads/list.py:53
+#: apps/threads/list.py:60
 msgid "Move threads"
 msgid "Move threads"
-msgstr ""
+msgstr "Przenieś tematy"
 
 
-#: apps/threads/list.py:54
+#: apps/threads/list.py:61
 msgid "Merge threads"
 msgid "Merge threads"
-msgstr ""
+msgstr "Połącz tematy"
 
 
-#: apps/threads/list.py:56
+#: apps/threads/list.py:63
 msgid "Open threads"
 msgid "Open threads"
-msgstr ""
+msgstr "Otwórz tematy"
 
 
-#: apps/threads/list.py:57
+#: apps/threads/list.py:64
 msgid "Close threads"
 msgid "Close threads"
-msgstr ""
+msgstr "Zamknij tematy"
 
 
-#: apps/threads/list.py:59
-msgid "Undelete threads"
-msgstr ""
+#: apps/threads/list.py:66
+msgid "Restore threads"
+msgstr "Przywróć tematy"
 
 
-#: apps/threads/list.py:60
-msgid "Soft delete threads"
-msgstr ""
+#: apps/threads/list.py:67
+msgid "Hide threads"
+msgstr "Ukryj tematy"
 
 
-#: apps/threads/list.py:62
-msgid "Hard delete threads"
-msgstr ""
+#: apps/threads/list.py:69
+msgid "Delete threads"
+msgstr "Usuń tematy"
 
 
 #: apps/threads/thread.py:12
 #: apps/threads/thread.py:12
 msgid "Accept posts"
 msgid "Accept posts"
-msgstr ""
+msgstr "Zaakceptuj posty"
 
 
 #: apps/threads/thread.py:15
 #: apps/threads/thread.py:15
 msgid "Split posts to new thread"
 msgid "Split posts to new thread"
-msgstr ""
+msgstr "Wydziel posty do nowego tematu"
 
 
 #: apps/threads/thread.py:16
 #: apps/threads/thread.py:16
 msgid "Move posts to other thread"
 msgid "Move posts to other thread"
-msgstr ""
+msgstr "Przenieś posty do innego tematu"
 
 
 #: apps/threads/thread.py:35
 #: apps/threads/thread.py:35
 msgid "Accept this thread"
 msgid "Accept this thread"
-msgstr ""
+msgstr "Zaakceptuj ten temat"
 
 
 #: apps/threads/thread.py:37
 #: apps/threads/thread.py:37
 msgid "Change this thread to announcement"
 msgid "Change this thread to announcement"
-msgstr ""
+msgstr "Przekształć ten temat w ogłoszenie"
 
 
 #: apps/threads/thread.py:39
 #: apps/threads/thread.py:39
 msgid "Change this thread to sticky"
 msgid "Change this thread to sticky"
-msgstr ""
+msgstr "Przypnij ten temat"
 
 
 #: apps/threads/thread.py:42
 #: apps/threads/thread.py:42
 msgid "Change this thread to normal"
 msgid "Change this thread to normal"
-msgstr ""
+msgstr "Przekształć w zwykły temat"
 
 
 #: apps/threads/thread.py:44
 #: apps/threads/thread.py:44
 msgid "Unpin this thread"
 msgid "Unpin this thread"
-msgstr ""
+msgstr "Odepnij ten temat"
 
 
 #: apps/threads/thread.py:46
 #: apps/threads/thread.py:46
 msgid "Move this thread"
 msgid "Move this thread"
-msgstr ""
+msgstr "Przenieś ten temat"
 
 
-#: apps/threadtype/changelog.py:43
+#: apps/threadtype/changelog.py:42
 msgid "Guest, you have to sign-in in order to see posts changelogs."
 msgid "Guest, you have to sign-in in order to see posts changelogs."
 msgstr ""
 msgstr ""
+"Gościu, aby przegladać historię zmian postów, musisz się najpierw zalogować."
 
 
-#: apps/threadtype/changelog.py:108
+#: apps/threadtype/changelog.py:107
 msgid "No changes to revert."
 msgid "No changes to revert."
-msgstr ""
+msgstr "Brak zmian do cofnięcia."
 
 
-#: apps/threadtype/changelog.py:126
+#: apps/threadtype/changelog.py:125
 #, python-format
 #, python-format
 msgid "Post has been reverted to state from %(date)s."
 msgid "Post has been reverted to state from %(date)s."
-msgstr ""
+msgstr "Post został przwrócony do stanu z %(date)s."
+
+#: apps/threadtype/delete.py:65 apps/threadtype/delete.py:92
+#: apps/threadtype/thread/moderation/thread.py:131
+#, python-format
+msgid "Thread \"%(thread)s\" has been deleted."
+msgstr "Temat \"%(thread)s\" został usunięty."
 
 
-#: apps/threadtype/delete.py:55 apps/threadtype/delete.py:77
+#: apps/threadtype/delete.py:79
 msgid "Somebody has already replied to this thread. You cannot delete it."
 msgid "Somebody has already replied to this thread. You cannot delete it."
 msgstr ""
 msgstr ""
+"Ktoś już odpowiedział w tym temacie, w związku z czym nie możesz już go "
+"usunąć."
 
 
-#: apps/threadtype/delete.py:63 apps/threadtype/delete.py:90
-#: apps/threadtype/thread/moderation/thread.py:129
+#: apps/threadtype/delete.py:104
+msgid "You cannot undelete this thread."
+msgstr "Nie możesz przywrócić tego tematu."
+
+#: apps/threadtype/delete.py:106
+msgid "This thread is already visible!"
+msgstr "Ten temat jest już widoczny!"
+
+#: apps/threadtype/delete.py:119
 #, python-format
 #, python-format
-msgid "Thread \"%(thread)s\" has been deleted."
-msgstr ""
+msgid "Thread \"%(thread)s\" has been restored."
+msgstr "Temat \"%(thread)s\" został przywrócony."
+
+#: apps/threadtype/delete.py:140 apps/threadtype/delete.py:167
+msgid "Selected reply has been deleted."
+msgstr "Zaznaczona odpowiedź została usunięta."
 
 
-#: apps/threadtype/delete.py:104 apps/threadtype/delete.py:126
+#: apps/threadtype/delete.py:152
 msgid "Somebody has already replied to this post, you cannot delete it."
 msgid "Somebody has already replied to this post, you cannot delete it."
 msgstr ""
 msgstr ""
+"Ktoś już odpowiedział na tem post, w związku z czym nie możesz już go usunąć."
 
 
-#: apps/threadtype/delete.py:114 apps/threadtype/delete.py:141
-msgid "Selected Reply has been deleted."
-msgstr ""
+#: apps/threadtype/delete.py:177
+msgid "You cannot undelete this reply."
+msgstr "Nie możesz przywrócić tej odpowiedzi."
+
+#: apps/threadtype/delete.py:179
+msgid "This reply is already visible!"
+msgstr "Ta odpowiedź jest już widoczna!"
+
+#: apps/threadtype/delete.py:194
+msgid "Selected reply has been restored."
+msgstr "Zaznaczona odpowiedź została przywrócona."
+
+#: apps/threadtype/delete.py:210
+msgid "Selected checkpoint has been deleted."
+msgstr "Zaznaczone etykiety zdarzeń zostały usunięte."
+
+#: apps/threadtype/delete.py:220
+msgid "This checkpoint is already hidden!"
+msgstr "Ta etykieta zdarzenia jest już widoczna!"
+
+#: apps/threadtype/delete.py:227
+msgid "Selected checkpoint has been hidden."
+msgstr "Wybrana etykieta zdarzenia została ukryta."
 
 
-#: apps/threadtype/jumps.py:102
+#: apps/threadtype/delete.py:237
+msgid "This checkpoint is already visible!"
+msgstr "Ten punkt kontrolny jest już widoczny!"
+
+#: apps/threadtype/delete.py:244
+msgid "Selected checkpoint has been restored."
+msgstr "Wybrane etykiety zdarzeń zostały przywrócone."
+
+#: apps/threadtype/jumps.py:101
 msgid ""
 msgid ""
 "Replies made to this thread by members on your ignore list have been "
 "Replies made to this thread by members on your ignore list have been "
 "revealed."
 "revealed."
 msgstr ""
 msgstr ""
+"Odpowiedzi napisane w tym temacie przez ignorowanych przez Ciebie "
+"użytkowników, zostały odkryte."
 
 
-#: apps/threadtype/jumps.py:112
+#: apps/threadtype/jumps.py:111
 msgid "This thread has been added to your watched threads list."
 msgid "This thread has been added to your watched threads list."
-msgstr ""
+msgstr "Temat został dodany do Twojej listy obserwowanych tematów."
 
 
-#: apps/threadtype/jumps.py:139
+#: apps/threadtype/jumps.py:138
 msgid ""
 msgid ""
 "You will now receive e-mail with notification when somebody replies to this "
 "You will now receive e-mail with notification when somebody replies to this "
 "thread."
 "thread."
-msgstr ""
+msgstr "Poinformujemy Cię w wiadomości e-mail, gdy ktoś odpowie w tym temacie."
 
 
-#: apps/threadtype/jumps.py:141
+#: apps/threadtype/jumps.py:140
 msgid ""
 msgid ""
 "This thread has been added to your watched threads list. You will also "
 "This thread has been added to your watched threads list. You will also "
 "receive e-mail with notification when somebody replies to it."
 "receive e-mail with notification when somebody replies to it."
 msgstr ""
 msgstr ""
+"Temat został dodany do Twojej listy obserwowanych tematów. Poinformujemy Cię "
+"też w wiadomości e-mail, gdy ktoś odpowie w nim odpowie."
 
 
-#: apps/threadtype/jumps.py:149
+#: apps/threadtype/jumps.py:148
 msgid ""
 msgid ""
 "This thread has been removed from your watched threads list. You will no "
 "This thread has been removed from your watched threads list. You will no "
 "longer receive e-mails with notifications when somebody replies to it."
 "longer receive e-mails with notifications when somebody replies to it."
 msgstr ""
 msgstr ""
+"Temat został usunięty z Twojej listy obserwowanych tematów. Nie otrzymasz "
+"też więcej powiadomień na e-mail, gdy ktoś w nim odpowie."
 
 
-#: apps/threadtype/jumps.py:151
+#: apps/threadtype/jumps.py:150
 msgid "This thread has been removed from your watched threads list."
 msgid "This thread has been removed from your watched threads list."
-msgstr ""
+msgstr "Temat został usunięty z Twojej listy obserwowanych tematów."
 
 
-#: apps/threadtype/jumps.py:157
+#: apps/threadtype/jumps.py:156
 msgid ""
 msgid ""
 "You will no longer receive e-mails with notifications when somebody replies "
 "You will no longer receive e-mails with notifications when somebody replies "
 "to this thread."
 "to this thread."
 msgstr ""
 msgstr ""
+"Nie otrzymasz więcej powiadomień na e-mail, gdy ktoś odpowie w tym temacie."
 
 
-#: apps/threadtype/jumps.py:230
+#: apps/threadtype/jumps.py:229
 msgid "Your vote has been saved."
 msgid "Your vote has been saved."
-msgstr ""
+msgstr "Twój głos został zapisany."
 
 
 #: apps/threadtype/mixins.py:11 apps/threadtype/list/forms.py:47
 #: apps/threadtype/mixins.py:11 apps/threadtype/list/forms.py:47
 #: apps/threadtype/posting/forms.py:78
 #: apps/threadtype/posting/forms.py:78
-#: apps/threadtype/thread/moderation/forms.py:22
+#: apps/threadtype/thread/moderation/forms.py:23
 #, python-format
 #, python-format
 msgid "Thread name must contain at least one alpha-numeric character."
 msgid "Thread name must contain at least one alpha-numeric character."
 msgid_plural ""
 msgid_plural ""
@@ -3531,448 +3804,538 @@ msgstr[2] ""
 #, python-format
 #, python-format
 msgid "Thread name cannot be longer than %(count)d character."
 msgid "Thread name cannot be longer than %(count)d character."
 msgid_plural "Thread name cannot be longer than %(count)d characters."
 msgid_plural "Thread name cannot be longer than %(count)d characters."
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "Nazwa tematu nie może być dłuższa niż %(count)d znak."
+msgstr[1] "Nazwa tematu nie może być dłuższa niż %(count)d znaki."
+msgstr[2] "Nazwa tematu nie może być dłuższa niż %(count)d znaków."
 
 
 #: apps/threadtype/mixins.py:29
 #: apps/threadtype/mixins.py:29
 #, python-format
 #, python-format
 msgid "Post content cannot be empty."
 msgid "Post content cannot be empty."
 msgid_plural "Post content cannot be shorter than %(count)d characters."
 msgid_plural "Post content cannot be shorter than %(count)d characters."
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "Treść posta nie może być pusta."
+msgstr[1] "Treść posta nie może być krótsza niż %(count)d znaki."
+msgstr[2] "Treść posta nie może być krótsza niż %(count)d znaków."
 
 
 #: apps/threadtype/list/forms.py:21
 #: apps/threadtype/list/forms.py:21
 msgid "Move Threads to"
 msgid "Move Threads to"
-msgstr ""
+msgstr "Przenieś tematy do"
 
 
 #: apps/threadtype/list/forms.py:21
 #: apps/threadtype/list/forms.py:21
 msgid "Select forum you want to move threads to."
 msgid "Select forum you want to move threads to."
-msgstr ""
+msgstr "Wskaż forum, do którego chcesz przenieś zaznaczone tematy."
 
 
 #: apps/threadtype/list/forms.py:30 apps/threadtype/list/forms.py:76
 #: apps/threadtype/list/forms.py:30 apps/threadtype/list/forms.py:76
 msgid "This is not forum."
 msgid "This is not forum."
-msgstr ""
+msgstr "To nie jest forum."
 
 
 #: apps/threadtype/list/forms.py:32
 #: apps/threadtype/list/forms.py:32
 msgid "New forum is same as current one."
 msgid "New forum is same as current one."
-msgstr ""
+msgstr "Wskazane forum jest aktualnym forum tych tematów."
 
 
 #: apps/threadtype/list/forms.py:48 apps/threadtype/posting/forms.py:79
 #: apps/threadtype/list/forms.py:48 apps/threadtype/posting/forms.py:79
-#: apps/threadtype/thread/moderation/forms.py:23
+#: apps/threadtype/thread/moderation/forms.py:24
 msgid "Thread name is too long. Try shorter name."
 msgid "Thread name is too long. Try shorter name."
-msgstr ""
+msgstr "Nazwa tematu jest zbyt długa. Spróbuj ją skrócić."
 
 
 #: apps/threadtype/list/forms.py:54 apps/threadtype/posting/forms.py:76
 #: apps/threadtype/list/forms.py:54 apps/threadtype/posting/forms.py:76
 msgid "Thread Name"
 msgid "Thread Name"
-msgstr ""
+msgstr "Nazwa tematu"
 
 
 #: apps/threadtype/list/forms.py:54
 #: apps/threadtype/list/forms.py:54
 msgid "Name of new thread that will be created as result of merge."
 msgid "Name of new thread that will be created as result of merge."
-msgstr ""
+msgstr "Nazwa nowego tematu, który powstanie w wyniku połączenia zaznaczonych."
 
 
 #: apps/threadtype/list/forms.py:55
 #: apps/threadtype/list/forms.py:55
 msgid "Thread Forum"
 msgid "Thread Forum"
-msgstr ""
+msgstr "Forum dla tematu"
 
 
 #: apps/threadtype/list/forms.py:55
 #: apps/threadtype/list/forms.py:55
 msgid "Select forum you want to put new thread in."
 msgid "Select forum you want to put new thread in."
-msgstr ""
+msgstr "Wybierz forum, w którym ma zostać umieszczony wynikowy temat."
 
 
 #: apps/threadtype/list/forms.py:59
 #: apps/threadtype/list/forms.py:59
 msgid "Merge Order"
 msgid "Merge Order"
-msgstr ""
+msgstr "Kolejność łączenia"
 
 
 #: apps/threadtype/list/forms.py:86
 #: apps/threadtype/list/forms.py:86
 msgid "One or more threads have same position in merge order."
 msgid "One or more threads have same position in merge order."
-msgstr ""
+msgstr "Jeden lub więcej tematów ma taką samą pozycję w kolejności łączenia."
 
 
 #: apps/threadtype/list/moderation.py:38
 #: apps/threadtype/list/moderation.py:38
 msgid ""
 msgid ""
 "Selected threads have been marked as reviewed and made visible to other "
 "Selected threads have been marked as reviewed and made visible to other "
 "members."
 "members."
 msgstr ""
 msgstr ""
+"Zaznaczone tematy zostały oznaczone jako przejrzane i są już widoczne dla "
+"innych użytkowników."
+
+#: apps/threadtype/list/moderation.py:40
+msgid "No threads were marked as reviewed."
+msgstr "Nie oznaczono żadnych tematów jako przejrzane."
 
 
-#: apps/threadtype/list/moderation.py:48
+#: apps/threadtype/list/moderation.py:50
 msgid "Selected threads have been turned into announcements."
 msgid "Selected threads have been turned into announcements."
-msgstr ""
+msgstr "Zaznaczone tematy zostały przekształcone w ogłoszenia."
+
+#: apps/threadtype/list/moderation.py:52
+msgid "No threads were turned into announcements."
+msgstr "Nie przekształcono żadnych tematów w ogłoszenia."
 
 
-#: apps/threadtype/list/moderation.py:58
+#: apps/threadtype/list/moderation.py:62
 msgid "Selected threads have been sticked to the top of list."
 msgid "Selected threads have been sticked to the top of list."
-msgstr ""
+msgstr "Zaznaczone tematy zostały przypięte na górę listy."
 
 
-#: apps/threadtype/list/moderation.py:67
+#: apps/threadtype/list/moderation.py:64
+msgid "No threads were turned into stickies."
+msgstr "Nie przypięto żadnych tematów."
+
+#: apps/threadtype/list/moderation.py:73
 msgid "Selected threads weight has been removed."
 msgid "Selected threads weight has been removed."
-msgstr ""
+msgstr "Zaznaczone elementy przekształcono w zwykłe tematy."
+
+#: apps/threadtype/list/moderation.py:75
+msgid "No threads have had their weight removed."
+msgstr "Zaznaczono wyłącznie zwykłe tematy - nie dokonano żadnych zmian."
 
 
-#: apps/threadtype/list/moderation.py:85
+#: apps/threadtype/list/moderation.py:95
 #, python-format
 #, python-format
 msgid "Selected threads have been moved to \"%(forum)s\"."
 msgid "Selected threads have been moved to \"%(forum)s\"."
-msgstr ""
+msgstr "Zaznaczone tematy zostały przeniesione do \"%(forum)s\"."
 
 
-#: apps/threadtype/list/moderation.py:103
+#: apps/threadtype/list/moderation.py:113
 msgid "You have to pick two or more threads to merge."
 msgid "You have to pick two or more threads to merge."
-msgstr ""
+msgstr "Musisz zaznaczyć przynajmniej dwa tematy do połączenia."
 
 
-#: apps/threadtype/list/moderation.py:136
+#: apps/threadtype/list/moderation.py:146
 msgid "Selected threads have been merged into new one."
 msgid "Selected threads have been merged into new one."
-msgstr ""
+msgstr "Zaznaczone tematy zostały połaczone w jeden nowy."
 
 
-#: apps/threadtype/list/moderation.py:163
+#: apps/threadtype/list/moderation.py:173
 msgid "Selected threads have been opened."
 msgid "Selected threads have been opened."
-msgstr ""
+msgstr "Zaznaczone tematy zostały otwarte."
+
+#: apps/threadtype/list/moderation.py:175
+msgid "No threads were opened."
+msgstr "Nie otwarto żadnych tematów."
 
 
-#: apps/threadtype/list/moderation.py:176
+#: apps/threadtype/list/moderation.py:188
 msgid "Selected threads have been closed."
 msgid "Selected threads have been closed."
-msgstr ""
+msgstr "Zaznaczone tematy zostały zamknięte."
 
 
-#: apps/threadtype/list/moderation.py:196
-msgid "Selected threads have been undeleted."
-msgstr ""
+#: apps/threadtype/list/moderation.py:190
+msgid "No threads were closed."
+msgstr "Nie zamknięto żadnych tematów."
 
 
-#: apps/threadtype/list/moderation.py:217
-msgid "Selected threads have been softly deleted."
-msgstr ""
+#: apps/threadtype/list/moderation.py:210
+msgid "Selected threads have been restored."
+msgstr "Zaznaczone tematy zostały przywrócone."
+
+#: apps/threadtype/list/moderation.py:212
+msgid "No threads were restored."
+msgstr "Nie przywrócono żadnych tematów."
 
 
-#: apps/threadtype/list/moderation.py:232
+#: apps/threadtype/list/moderation.py:233
+msgid "Selected threads have been hidden."
+msgstr "Zaznaczone tematy zostały ukryte."
+
+#: apps/threadtype/list/moderation.py:235
+msgid "No threads were hidden."
+msgstr "Nie ukryto żadnych tematów."
+
+#: apps/threadtype/list/moderation.py:250
 msgid "Selected threads have been deleted."
 msgid "Selected threads have been deleted."
-msgstr ""
+msgstr "Zaznaczone tematy zostały usunięte."
 
 
-#: apps/threadtype/list/views.py:80
+#: apps/threadtype/list/moderation.py:252
+msgid "No threads were deleted."
+msgstr "Nie usunięto żadnych tematów."
+
+#: apps/threadtype/list/views.py:81
 #: templates/cranefly/private_threads/list.html:166
 #: templates/cranefly/private_threads/list.html:166
-#: templates/cranefly/threads/list.html:267
+#: templates/cranefly/threads/list.html:269
 msgid "You have to select at least one thread."
 msgid "You have to select at least one thread."
-msgstr ""
+msgstr "Musisz zaznaczyć przynajmniej jeden temat."
+
+#: apps/threadtype/posting/base.py:71
+#, python-format
+msgid "%(username)s has replied to your post in thread %(thread)s"
+msgstr "%(username)s odpowiedział na twój post w temacie %(thread)s"
 
 
 #: apps/threadtype/posting/forms.py:23
 #: apps/threadtype/posting/forms.py:23
-msgid "Post Content"
-msgstr ""
+#: templates/cranefly/private_threads/posting.html:67
+#: templates/cranefly/threads/posting.html:63
+msgid "Message Body"
+msgstr "Treść wiadomości"
 
 
 #: apps/threadtype/posting/forms.py:33
 #: apps/threadtype/posting/forms.py:33
 msgid "Announcement"
 msgid "Announcement"
-msgstr ""
+msgstr "Ogłoszenie"
 
 
 #: apps/threadtype/posting/forms.py:34
 #: apps/threadtype/posting/forms.py:34
 msgid "Sticky"
 msgid "Sticky"
-msgstr ""
+msgstr "Przypięty"
 
 
 #: apps/threadtype/posting/forms.py:35
 #: apps/threadtype/posting/forms.py:35
 msgid "Standard"
 msgid "Standard"
-msgstr ""
+msgstr "Standardowy"
 
 
 #: apps/threadtype/posting/forms.py:37
 #: apps/threadtype/posting/forms.py:37
 msgid "Thread Importance"
 msgid "Thread Importance"
-msgstr ""
+msgstr "Istotnośc tematu"
 
 
 #: apps/threadtype/posting/forms.py:52
 #: apps/threadtype/posting/forms.py:52
 msgid "Open Thread"
 msgid "Open Thread"
-msgstr ""
+msgstr "Otwórz temat"
 
 
 #: apps/threadtype/posting/forms.py:54
 #: apps/threadtype/posting/forms.py:54
 msgid "Close Thread"
 msgid "Close Thread"
-msgstr ""
+msgstr "Zamknij temat"
 
 
 #: apps/threadtype/posting/forms.py:85
 #: apps/threadtype/posting/forms.py:85
 msgid "Optional reason for editing this thread."
 msgid "Optional reason for editing this thread."
-msgstr ""
+msgstr "Opcjonalny powód edycji tego tematu."
 
 
 #: apps/threadtype/posting/forms.py:86 apps/threadtype/posting/forms.py:97
 #: apps/threadtype/posting/forms.py:86 apps/threadtype/posting/forms.py:97
 msgid "Edit Reason"
 msgid "Edit Reason"
-msgstr ""
+msgstr "Powód edycji"
 
 
 #: apps/threadtype/posting/forms.py:96
 #: apps/threadtype/posting/forms.py:96
 msgid "Optional reason for editing this reply."
 msgid "Optional reason for editing this reply."
-msgstr ""
+msgstr "Opcjonalny powód edycji tej odpowiedzi."
 
 
-#: apps/threadtype/posting/newreply.py:54
+#: apps/threadtype/posting/newreply.py:146
 #, python-format
 #, python-format
-msgid "Added on %(date)s:"
-msgstr ""
+msgid "%(username)s has replied to your thread %(thread)s"
+msgstr "%(username)s odpowiedział w Twoim temacie %(thread)s"
 
 
-#: apps/threadtype/posting/newreply.py:139
+#: apps/threadtype/posting/newreply.py:148
 #, python-format
 #, python-format
-msgid "%(username)s has replied to your post in thread %(thread)s"
+msgid "%(username)s has replied to thread %(thread)s that you are watching"
 msgstr ""
 msgstr ""
+"%(username)s odpowiedział w obserwowanym przez Ciebie temacie %(thread)s"
 
 
-#: apps/threadtype/thread/views.py:155
-#: templates/cranefly/private_threads/thread.html:453
-#: templates/cranefly/threads/thread.html:433
+#: apps/threadtype/thread/views.py:165
+#: templates/cranefly/private_threads/thread.html:464
+#: templates/cranefly/threads/thread.html:436
 msgid "You have to select at least one post."
 msgid "You have to select at least one post."
-msgstr ""
+msgstr "Musisz zaznaczyć przynajmniej jednego posta."
 
 
-#: apps/threadtype/thread/moderation/forms.py:15
+#: apps/threadtype/thread/moderation/forms.py:16
 msgid "New Thread Name"
 msgid "New Thread Name"
-msgstr ""
+msgstr "Nowa nazwa tematu"
 
 
-#: apps/threadtype/thread/moderation/forms.py:16
+#: apps/threadtype/thread/moderation/forms.py:17
 msgid "New Thread Forum"
 msgid "New Thread Forum"
-msgstr ""
+msgstr "Nowe forum tematu"
 
 
-#: apps/threadtype/thread/moderation/forms.py:31
+#: apps/threadtype/thread/moderation/forms.py:32
 msgid "This is not a forum."
 msgid "This is not a forum."
-msgstr ""
+msgstr "To nie jest forum."
 
 
-#: apps/threadtype/thread/moderation/forms.py:47
+#: apps/threadtype/thread/moderation/forms.py:48
 msgid "New Thread Link"
 msgid "New Thread Link"
-msgstr ""
+msgstr "Link do nowego tematu"
 
 
-#: apps/threadtype/thread/moderation/forms.py:47
+#: apps/threadtype/thread/moderation/forms.py:48
 msgid "To select new thread, simply copy and paste here its link."
 msgid "To select new thread, simply copy and paste here its link."
 msgstr ""
 msgstr ""
+"Aby wskazać nowy temat, po prostu skopiuj i wklej tutaj jego URL (adres)."
 
 
-#: apps/threadtype/thread/moderation/forms.py:64
+#: apps/threadtype/thread/moderation/forms.py:65
 msgid "New thread is same as current one."
 msgid "New thread is same as current one."
-msgstr ""
+msgstr "Temat do którego chcesz przenieść posty, jest taki sam jak obecny."
 
 
-#: apps/threadtype/thread/moderation/forms.py:67
+#: apps/threadtype/thread/moderation/forms.py:68
 msgid "This is not a correct thread URL."
 msgid "This is not a correct thread URL."
-msgstr ""
+msgstr "To nie jest poprawny URL do tematu."
 
 
-#: apps/threadtype/thread/moderation/forms.py:69
+#: apps/threadtype/thread/moderation/forms.py:70
 msgid "Thread could not be found."
 msgid "Thread could not be found."
-msgstr ""
+msgstr "Temat nie został odnaleziony."
 
 
 #: apps/threadtype/thread/moderation/posts.py:23
 #: apps/threadtype/thread/moderation/posts.py:23
 msgid "Selected posts have been accepted and made visible to other members."
 msgid "Selected posts have been accepted and made visible to other members."
 msgstr ""
 msgstr ""
+"Zaznaczone posty zostały zaakceptowane. Inni użytkownicy mogą je już "
+"zobaczyć."
 
 
-#: apps/threadtype/thread/moderation/posts.py:34
-msgid "You cannot merge replies made by different members!"
-msgstr ""
+#: apps/threadtype/thread/moderation/posts.py:25
+msgid "No posts were accepted."
+msgstr "Nie zaakceptowano żadnych postów."
 
 
 #: apps/threadtype/thread/moderation/posts.py:36
 #: apps/threadtype/thread/moderation/posts.py:36
+msgid "You cannot merge replies made by different members!"
+msgstr "Nie możesz połączyć postów napisanych przez różnych użytkowników!"
+
+#: apps/threadtype/thread/moderation/posts.py:38
 msgid "You have to select two or more posts you want to merge."
 msgid "You have to select two or more posts you want to merge."
-msgstr ""
+msgstr "Musisz wskazać przynajmniej dwa posty do połączenia."
 
 
-#: apps/threadtype/thread/moderation/posts.py:47
+#: apps/threadtype/thread/moderation/posts.py:49
 msgid "Selected posts have been merged into one message."
 msgid "Selected posts have been merged into one message."
-msgstr ""
+msgstr "Zaznaczone posty zostały połączone w jedną wiadomośc."
 
 
-#: apps/threadtype/thread/moderation/posts.py:52
+#: apps/threadtype/thread/moderation/posts.py:54
 msgid "You cannot split first post from thread."
 msgid "You cannot split first post from thread."
-msgstr ""
+msgstr "Nie możesz wydzelić pierwszego postu z tematu."
 
 
-#: apps/threadtype/thread/moderation/posts.py:87
+#: apps/threadtype/thread/moderation/posts.py:89
 msgid "Selected posts have been split to new thread."
 msgid "Selected posts have been split to new thread."
-msgstr ""
+msgstr "Zaznaczone posty zostały wydzielone w nowy temat."
 
 
-#: apps/threadtype/thread/moderation/posts.py:92
+#: apps/threadtype/thread/moderation/posts.py:94
 #, python-format
 #, python-format
 msgid "[Split] %s"
 msgid "[Split] %s"
-msgstr ""
+msgstr "[Wydzielony] %s"
 
 
-#: apps/threadtype/thread/moderation/posts.py:135
+#: apps/threadtype/thread/moderation/posts.py:137
 msgid "Selected posts have been moved to new thread."
 msgid "Selected posts have been moved to new thread."
-msgstr ""
+msgstr "Wybrane posty zostały przeniesione do nowego tematu."
 
 
-#: apps/threadtype/thread/moderation/posts.py:163
+#: apps/threadtype/thread/moderation/posts.py:165
 msgid "Selected posts have been restored."
 msgid "Selected posts have been restored."
-msgstr ""
+msgstr "Wybrane posty zostały przywrócone."
 
 
-#: apps/threadtype/thread/moderation/posts.py:172
+#: apps/threadtype/thread/moderation/posts.py:167
+msgid "No posts were restored."
+msgstr "Nie przywrócono żadnych postów."
+
+#: apps/threadtype/thread/moderation/posts.py:176
 msgid "Selected posts have been protected from edition."
 msgid "Selected posts have been protected from edition."
-msgstr ""
+msgstr "Wybrane posty zostały zabezpieczone przed edycją."
+
+#: apps/threadtype/thread/moderation/posts.py:178
+msgid "No posts were protected."
+msgstr "Nie objęto ochroną żadnych nowych postów."
 
 
-#: apps/threadtype/thread/moderation/posts.py:181
+#: apps/threadtype/thread/moderation/posts.py:187
 msgid "Protection from editions has been removed from selected posts."
 msgid "Protection from editions has been removed from selected posts."
-msgstr ""
+msgstr "Wybrane posty zostały pozbawione blokady edycji."
+
+#: apps/threadtype/thread/moderation/posts.py:189
+msgid "No posts were unprotected."
+msgstr "Nie zdjęto ochrony żadnych postów."
 
 
-#: apps/threadtype/thread/moderation/posts.py:188
-#: apps/threadtype/thread/moderation/posts.py:203
+#: apps/threadtype/thread/moderation/posts.py:196
+#: apps/threadtype/thread/moderation/posts.py:213
 msgid ""
 msgid ""
 "You cannot delete first post of thread using this action. If you want to "
 "You cannot delete first post of thread using this action. If you want to "
 "delete thread, use thread moderation instead."
 "delete thread, use thread moderation instead."
 msgstr ""
 msgstr ""
+"Nie możesz w ten sposób usunąć pierwszego postu w temacie. Jeżeli chcesz "
+"usunąć temat, użyj opcji dotyczących tematów, a nie postów."
 
 
-#: apps/threadtype/thread/moderation/posts.py:196
-#: apps/threadtype/thread/moderation/posts.py:213
+#: apps/threadtype/thread/moderation/posts.py:204
+msgid "Selected posts have been hidden."
+msgstr "Zaznaczone posty zostały ukryte."
+
+#: apps/threadtype/thread/moderation/posts.py:206
+msgid "No posts were hidden."
+msgstr "Nie ukryto żadnych postów."
+
+#: apps/threadtype/thread/moderation/posts.py:223
 msgid "Selected posts have been deleted."
 msgid "Selected posts have been deleted."
-msgstr ""
+msgstr "Zaznaczone posty zostały usunięte."
+
+#: apps/threadtype/thread/moderation/posts.py:225
+msgid "No posts were deleted."
+msgstr "Nie usunięto żadnych postów."
 
 
 #: apps/threadtype/thread/moderation/thread.py:27
 #: apps/threadtype/thread/moderation/thread.py:27
 msgid "Thread has been marked as reviewed and made visible to other members."
 msgid "Thread has been marked as reviewed and made visible to other members."
 msgstr ""
 msgstr ""
+"Temat został oznaczony jako przejrzany. Inni użytkownicy mogą już go "
+"zobaczyć."
 
 
 #: apps/threadtype/thread/moderation/thread.py:32
 #: apps/threadtype/thread/moderation/thread.py:32
 msgid "Thread has been turned into announcement."
 msgid "Thread has been turned into announcement."
-msgstr ""
+msgstr "Temat został przekształcony w ogłoszenie."
 
 
 #: apps/threadtype/thread/moderation/thread.py:37
 #: apps/threadtype/thread/moderation/thread.py:37
 msgid "Thread has been turned into sticky."
 msgid "Thread has been turned into sticky."
-msgstr ""
+msgstr "Temat został przypięty."
 
 
 #: apps/threadtype/thread/moderation/thread.py:42
 #: apps/threadtype/thread/moderation/thread.py:42
 msgid "Thread weight has been changed to normal."
 msgid "Thread weight has been changed to normal."
-msgstr ""
+msgstr "Pomyślnie przekształcono w zwykły temat."
 
 
-#: apps/threadtype/thread/moderation/thread.py:56
+#: apps/threadtype/thread/moderation/thread.py:58
 #, python-format
 #, python-format
 msgid "Thread has been moved to \"%(forum)s\"."
 msgid "Thread has been moved to \"%(forum)s\"."
-msgstr ""
+msgstr "Temat został przeniesiony do \"%(forum)s\"."
 
 
-#: apps/threadtype/thread/moderation/thread.py:76
+#: apps/threadtype/thread/moderation/thread.py:78
 msgid "Thread has been opened."
 msgid "Thread has been opened."
-msgstr ""
+msgstr "Temat został otwarty."
 
 
-#: apps/threadtype/thread/moderation/thread.py:82
+#: apps/threadtype/thread/moderation/thread.py:84
 msgid "Thread has been closed."
 msgid "Thread has been closed."
-msgstr ""
+msgstr "Temat został zamknięty."
 
 
-#: apps/threadtype/thread/moderation/thread.py:100
-msgid "Thread has been undeleted."
-msgstr ""
+#: apps/threadtype/thread/moderation/thread.py:102
+msgid "Thread has been restored."
+msgstr "Temat został przywrócony."
 
 
-#: apps/threadtype/thread/moderation/thread.py:118
-msgid "Thread has been deleted."
-msgstr ""
+#: apps/threadtype/thread/moderation/thread.py:120
+msgid "Thread has been hidden."
+msgstr "Temat został ukryty."
 
 
 #: apps/usercp/views.py:25
 #: apps/usercp/views.py:25
 #, python-format
 #, python-format
 msgid "You are now following %(username)s"
 msgid "You are now following %(username)s"
-msgstr ""
+msgstr "Zacząłeś obserwować %(username)s"
 
 
 #: apps/usercp/views.py:31
 #: apps/usercp/views.py:31
 #, python-format
 #, python-format
 msgid "%(username)s is now following you"
 msgid "%(username)s is now following you"
-msgstr ""
+msgstr "%(username)s zaczął Cię obserwować"
 
 
 #: apps/usercp/views.py:46
 #: apps/usercp/views.py:46
 #, python-format
 #, python-format
 msgid "You have stopped following %(username)s"
 msgid "You have stopped following %(username)s"
-msgstr ""
+msgstr "Przestałeś obserwować %(username)s"
 
 
 #: apps/usercp/views.py:62
 #: apps/usercp/views.py:62
 #, python-format
 #, python-format
 msgid "You are now ignoring %(username)s"
 msgid "You are now ignoring %(username)s"
-msgstr ""
+msgstr "%(username)s jest teraz przez Ciebie ignorowany"
 
 
 #: apps/usercp/views.py:74
 #: apps/usercp/views.py:74
 #, python-format
 #, python-format
 msgid "You have stopped ignoring %(username)s"
 msgid "You have stopped ignoring %(username)s"
-msgstr ""
+msgstr "Przestałeś ignorować użytkownika %(username)s"
 
 
 #: apps/usercp/avatar/forms.py:9
 #: apps/usercp/avatar/forms.py:9
 msgid "Uploaded file is not correct image."
 msgid "Uploaded file is not correct image."
-msgstr ""
+msgstr "Przesłany plik nie jest poprawnym obrazkiem."
 
 
 #: apps/usercp/avatar/forms.py:16
 #: apps/usercp/avatar/forms.py:16
 msgid "Upload Image File"
 msgid "Upload Image File"
-msgstr ""
+msgstr "Prześlij obrazek z komptera"
 
 
 #: apps/usercp/avatar/forms.py:16
 #: apps/usercp/avatar/forms.py:16
 msgid ""
 msgid ""
 "Select image file on your computer you wish to use as forum avatar. You will "
 "Select image file on your computer you wish to use as forum avatar. You will "
 "be able to crop image after upload. Animations will be stripped."
 "be able to crop image after upload. Animations will be stripped."
 msgstr ""
 msgstr ""
+"Wskaż plik obrazka, którego chciałbyś użyć jako awatara. Będziesz mógł go "
+"przyciąć po przesłaniu. Ewentualna animacja zostanie usunięta."
 
 
 #: apps/usercp/avatar/forms.py:29
 #: apps/usercp/avatar/forms.py:29
 #, python-format
 #, python-format
 msgid "Avatar image cannot be larger than %(limit)s."
 msgid "Avatar image cannot be larger than %(limit)s."
-msgstr ""
+msgstr "Obrazek awatara nie może być większy niż %(limit)s."
 
 
 #: apps/usercp/avatar/forms.py:31
 #: apps/usercp/avatar/forms.py:31
 msgid "Couldn't read uploaded image"
 msgid "Couldn't read uploaded image"
-msgstr ""
+msgstr "Nie udało się odczytać przesłanego obrazka"
 
 
 #: apps/usercp/avatar/usercp.py:4
 #: apps/usercp/avatar/usercp.py:4
 msgid "Change Avatar"
 msgid "Change Avatar"
-msgstr ""
+msgstr "Zmiana awatara"
 
 
 #: apps/usercp/avatar/views.py:51
 #: apps/usercp/avatar/views.py:51
 msgid "Your avatar has been changed to Gravatar."
 msgid "Your avatar has been changed to Gravatar."
-msgstr ""
+msgstr "Użyto obrazka z Gravatara."
 
 
 #: apps/usercp/avatar/views.py:53 apps/usercp/avatar/views.py:94
 #: apps/usercp/avatar/views.py:53 apps/usercp/avatar/views.py:94
 #: apps/usercp/avatar/views.py:219
 #: apps/usercp/avatar/views.py:219
 msgid "Request authorisation is invalid."
 msgid "Request authorisation is invalid."
-msgstr ""
+msgstr "Autoryzacja żądania jest nieprawidłowa."
 
 
 #: apps/usercp/avatar/views.py:78
 #: apps/usercp/avatar/views.py:78
 msgid "No avatar galleries are available at the moment."
 msgid "No avatar galleries are available at the moment."
-msgstr ""
+msgstr "Niestety nie ma aktualnie dostępnych żadnych galerii awatarów."
 
 
 #: apps/usercp/avatar/views.py:90
 #: apps/usercp/avatar/views.py:90
 msgid "Your avatar has been changed to one from gallery."
 msgid "Your avatar has been changed to one from gallery."
-msgstr ""
+msgstr "Ustawiono awatar z galerii."
 
 
 #: apps/usercp/avatar/views.py:92
 #: apps/usercp/avatar/views.py:92
 msgid "Selected Avatar is incorrect."
 msgid "Selected Avatar is incorrect."
-msgstr ""
+msgstr "Wskazany awatar jest niepoprawny."
 
 
 #: apps/usercp/avatar/views.py:150
 #: apps/usercp/avatar/views.py:150
 msgid "Your avatar has changed."
 msgid "Your avatar has changed."
-msgstr ""
+msgstr "Twój awatar został zmieniony."
 
 
 #: apps/usercp/avatar/views.py:155
 #: apps/usercp/avatar/views.py:155
 msgid "Only gif, jpeg and png files are allowed for member avatars."
 msgid "Only gif, jpeg and png files are allowed for member avatars."
-msgstr ""
+msgstr "Dozwolone rozszerzenia to png, jpeg oraz gif."
 
 
 #: apps/usercp/avatar/views.py:176
 #: apps/usercp/avatar/views.py:176
 msgid ""
 msgid ""
 "Crop Avatar option is avaiable only when you use uploaded image as your "
 "Crop Avatar option is avaiable only when you use uploaded image as your "
 "avatar."
 "avatar."
 msgstr ""
 msgstr ""
+"Opcja przycinania awatara jest dostępna tylko w przypadku przesyłania "
+"obrazka z komputera."
 
 
 #: apps/usercp/avatar/views.py:214
 #: apps/usercp/avatar/views.py:214
 msgid "Your avatar has been cropped."
 msgid "Your avatar has been cropped."
-msgstr ""
+msgstr "Twój awatar został przycięty."
 
 
 #: apps/usercp/avatar/views.py:217 forms/forms.py:158
 #: apps/usercp/avatar/views.py:217 forms/forms.py:158
 msgid "Form contains errors."
 msgid "Form contains errors."
-msgstr ""
+msgstr "Formularz zawiera błędy."
 
 
 #: apps/usercp/credentials/forms.py:18
 #: apps/usercp/credentials/forms.py:18
 msgid "New E-mail"
 msgid "New E-mail"
-msgstr ""
+msgstr "Nowy adres e-mail"
 
 
 #: apps/usercp/credentials/forms.py:18
 #: apps/usercp/credentials/forms.py:18
 msgid ""
 msgid ""
 "Enter new e-mail address or leave this field empty if you want only to "
 "Enter new e-mail address or leave this field empty if you want only to "
 "change your password."
 "change your password."
 msgstr ""
 msgstr ""
+"Wprowadź swój nowy adres e-mail, lub pozostaw to pole puste, aby zmienić "
+"tylko hasło."
 
 
 #: apps/usercp/credentials/forms.py:19
 #: apps/usercp/credentials/forms.py:19
 msgid "New Password"
 msgid "New Password"
-msgstr ""
+msgstr "Nowe hasło"
 
 
 #: apps/usercp/credentials/forms.py:19
 #: apps/usercp/credentials/forms.py:19
 msgid ""
 msgid ""
 "Enter new password or leave this empty if you only want to change your e-"
 "Enter new password or leave this empty if you only want to change your e-"
 "mail address."
 "mail address."
 msgstr ""
 msgstr ""
+"Wprowadź swoje nowe hasło, lub pozostaw to pole puste, aby zmienić tylko "
+"adres e-mail."
 
 
 #: apps/usercp/credentials/forms.py:20
 #: apps/usercp/credentials/forms.py:20
 msgid "Current Password"
 msgid "Current Password"
-msgstr ""
+msgstr "Obecne hasło"
 
 
 #: apps/usercp/credentials/forms.py:20
 #: apps/usercp/credentials/forms.py:20
-msgid "Confirm changes by entering new password."
-msgstr ""
+msgid "Confirm changes by entering your current password."
+msgstr "Potwierdź zmiany, wpisując swoje aktualne hasło."
 
 
 #: apps/usercp/credentials/forms.py:29
 #: apps/usercp/credentials/forms.py:29
 msgid "New e-mail is same as your current e-mail."
 msgid "New e-mail is same as your current e-mail."
-msgstr ""
+msgstr "Nowy adres e-mail jest taki sam jak obecny."
 
 
 #: apps/usercp/credentials/forms.py:32
 #: apps/usercp/credentials/forms.py:32
 msgid "New e-mail address is already in use by other member."
 msgid "New e-mail address is already in use by other member."
-msgstr ""
+msgstr "Podany nowy adres e-mail jest już używany przez innego użytkownika."
 
 
 #: apps/usercp/credentials/forms.py:45
 #: apps/usercp/credentials/forms.py:45
 msgid "You have entered wrong password."
 msgid "You have entered wrong password."
-msgstr ""
+msgstr "Wprowadzono błędne hasło."
 
 
 #: apps/usercp/credentials/forms.py:51
 #: apps/usercp/credentials/forms.py:51
 msgid "You have to enter either new e-mail address or new password."
 msgid "You have to enter either new e-mail address or new password."
 msgstr ""
 msgstr ""
+"Nie podano ani nowego hasła, ani nowego adresu e-mail. Przynajmniej jedno z "
+"nich jest wymagane."
 
 
 #: apps/usercp/credentials/usercp.py:4
 #: apps/usercp/credentials/usercp.py:4
 msgid "Change E-mail or Password"
 msgid "Change E-mail or Password"
-msgstr ""
+msgstr "Zmień adres e-mail lub hasło"
 
 
 #: apps/usercp/credentials/views.py:23
 #: apps/usercp/credentials/views.py:23
 msgid "Activate new Sign-In Credentials"
 msgid "Activate new Sign-In Credentials"
-msgstr ""
+msgstr "Aktywacja nowych danych do logowania"
 
 
 #: apps/usercp/credentials/views.py:34
 #: apps/usercp/credentials/views.py:34
 msgid ""
 msgid ""
@@ -3980,6 +4343,10 @@ msgid ""
 "click to confirm change of your sign-in credentials. This link will be valid "
 "click to confirm change of your sign-in credentials. This link will be valid "
 "only for duration of this session, do not sign out until you confirm change!"
 "only for duration of this session, do not sign out until you confirm change!"
 msgstr ""
 msgstr ""
+"Wysłaliśmy wiadomość potwierdzającą zmianę danych logowania na Twój nowy "
+"adres e-mail. Kliknij na znajdujący się w niej link. Uwaga, link w "
+"wiadomości będzie aktualny tylko na czas trwania aktualnej sesji. Nie "
+"wylogowuj się, dopóki nie potwierdzisz zmian!"
 
 
 #: apps/usercp/credentials/views.py:36
 #: apps/usercp/credentials/views.py:36
 msgid ""
 msgid ""
@@ -3987,41 +4354,48 @@ msgid ""
 "click to confirm change of your sign-in credentials. This link will be valid "
 "click to confirm change of your sign-in credentials. This link will be valid "
 "only for duration of this session, do not sign out until you confirm change!"
 "only for duration of this session, do not sign out until you confirm change!"
 msgstr ""
 msgstr ""
+"Wysłaliśmy wiadomość potwierdzającą zmianę danych logowania na Twój adres e-"
+"mail. Kliknij na znajdujący się w niej link. Uwaga, link w wiadomości będzie "
+"aktualny tylko na czas trwania aktualnej sesji. Nie wylogowuj się, dopóki "
+"nie potwierdzisz zmian!"
 
 
 #: apps/usercp/credentials/views.py:66
 #: apps/usercp/credentials/views.py:66
 #, python-format
 #, python-format
 msgid "%(username)s, your Sign-In credentials have been changed."
 msgid "%(username)s, your Sign-In credentials have been changed."
-msgstr ""
+msgstr "%(username)s, Twoje dane do logowania zostały zmienione."
 
 
 #: apps/usercp/credentials/views.py:71
 #: apps/usercp/credentials/views.py:71
 msgid "Your new credentials have been invalidated. Please try again."
 msgid "Your new credentials have been invalidated. Please try again."
 msgstr ""
 msgstr ""
+"Niestety, nowe dane logowania już wygasły. Prosimy powtórzyć cała procedurę "
+"ich zmiany - unikając tym razem przerwania sesji."
 
 
 #: apps/usercp/options/forms.py:10
 #: apps/usercp/options/forms.py:10
 msgid "Show my presence to everyone"
 msgid "Show my presence to everyone"
-msgstr ""
+msgstr "Pokazuj każdemu moją obecność"
 
 
 #: apps/usercp/options/forms.py:11
 #: apps/usercp/options/forms.py:11
 msgid "Show my presence to people I follow"
 msgid "Show my presence to people I follow"
-msgstr ""
+msgstr "Pokazuj obecność tylko osobom, które obserwuję"
 
 
 #: apps/usercp/options/forms.py:12
 #: apps/usercp/options/forms.py:12
 msgid "Show my presence to nobody"
 msgid "Show my presence to nobody"
-msgstr ""
+msgstr "Nie pokazuj nikomu mojej obecności"
 
 
 #: apps/usercp/options/forms.py:15 apps/usercp/options/forms.py:20
 #: apps/usercp/options/forms.py:15 apps/usercp/options/forms.py:20
-#: fixtures/accountssetings.py:74 fixtures/accountssetings.py:85
+#: fixtures/accountssetings.py:75 fixtures/accountssetings.py:86
 msgid "Don't watch"
 msgid "Don't watch"
-msgstr ""
+msgstr "Nie obserwuj"
 
 
 #: apps/usercp/options/forms.py:16 apps/usercp/options/forms.py:21
 #: apps/usercp/options/forms.py:16 apps/usercp/options/forms.py:21
-#: fixtures/accountssetings.py:75 fixtures/accountssetings.py:86
+#: fixtures/accountssetings.py:76 fixtures/accountssetings.py:87
 msgid "Put on watched threads list"
 msgid "Put on watched threads list"
-msgstr ""
+msgstr "Umieść na liście obserwowanych tematów"
 
 
 #: apps/usercp/options/forms.py:17 apps/usercp/options/forms.py:22
 #: apps/usercp/options/forms.py:17 apps/usercp/options/forms.py:22
 msgid "Put on watched threads list and e-mail me when somebody replies"
 msgid "Put on watched threads list and e-mail me when somebody replies"
 msgstr ""
 msgstr ""
+"Umieść na liście obserwowanych tematów i wyślij mi e-mail, gdy ktoś odpisze"
 
 
 #: apps/usercp/options/forms.py:25
 #: apps/usercp/options/forms.py:25
 msgid "From everyone"
 msgid "From everyone"
@@ -4057,13 +4431,16 @@ msgstr ""
 
 
 #: apps/usercp/options/forms.py:36
 #: apps/usercp/options/forms.py:36
 msgid "Allow Private Threads Invitations"
 msgid "Allow Private Threads Invitations"
-msgstr ""
+msgstr "Pozwalaj na zaproszenia do prywatnych dyskusji"
 
 
 #: apps/usercp/options/forms.py:36
 #: apps/usercp/options/forms.py:36
 msgid ""
 msgid ""
 "If you wish, you can restrict who can invite you to private threads. Keep in "
 "If you wish, you can restrict who can invite you to private threads. Keep in "
 "mind some groups or members may be allowed to override this preference."
 "mind some groups or members may be allowed to override this preference."
 msgstr ""
 msgstr ""
+"Jeżeli chcesz, możesz ograniczyć liczbę użytkowników, którzy mogą zaprosić "
+"Cię do prywatnych dyskusji. Miej na uwadzę, ograniczenie może nie dotyczyć "
+"niektórych grup (np. administratorów)."
 
 
 #: apps/usercp/options/forms.py:40 apps/usercp/options/usercp.py:4
 #: apps/usercp/options/forms.py:40 apps/usercp/options/usercp.py:4
 msgid "Forum Options"
 msgid "Forum Options"
@@ -4086,7 +4463,7 @@ msgid ""
 "On occasion board administrator may want to send e-mail message to multiple "
 "On occasion board administrator may want to send e-mail message to multiple "
 "members."
 "members."
 msgstr ""
 msgstr ""
-"Czasami, administrator forum może chcieć rozesłać wiadomość e-mail do "
+"Czasami administrator forum może chcieć rozesłać wiadomość e-mail do "
 "członków forum."
 "członków forum."
 
 
 #: apps/usercp/options/forms.py:43
 #: apps/usercp/options/forms.py:43
@@ -4123,7 +4500,7 @@ msgstr "Twoja sygnaturka została zmieniona."
 
 
 #: apps/usercp/username/forms.py:15
 #: apps/usercp/username/forms.py:15
 msgid "Change Username to"
 msgid "Change Username to"
-msgstr "Zmien nazwę użytkownika na"
+msgstr "Zmień nazwę użytkownika na"
 
 
 #: apps/usercp/username/forms.py:15
 #: apps/usercp/username/forms.py:15
 msgid "Enter new desired username."
 msgid "Enter new desired username."
@@ -4156,141 +4533,146 @@ msgstr ""
 
 
 #: fixtures/accountssetings.py:7
 #: fixtures/accountssetings.py:7
 msgid "Users Accounts Settings"
 msgid "Users Accounts Settings"
-msgstr "Ustawienia profili użytkowników"
+msgstr "Profile użytkowników"
 
 
 #: fixtures/accountssetings.py:8
 #: fixtures/accountssetings.py:8
 msgid "Those settings allow you to increase security of your members accounts."
 msgid "Those settings allow you to increase security of your members accounts."
 msgstr "Dane ustawienia mogą zwiększyć bezpieczeństwo członków Twojego forum."
 msgstr "Dane ustawienia mogą zwiększyć bezpieczeństwo członków Twojego forum."
 
 
-#: fixtures/accountssetings.py:13
+#: fixtures/accountssetings.py:14
 msgid "No validation required"
 msgid "No validation required"
-msgstr ""
+msgstr "Nie wymagaj walidacji"
 
 
-#: fixtures/accountssetings.py:13
+#: fixtures/accountssetings.py:14
 msgid "Activation Token sent to User"
 msgid "Activation Token sent to User"
-msgstr ""
+msgstr "Aktywacja poprzez potwierdzenie z wiadomości e-mail."
 
 
-#: fixtures/accountssetings.py:13
+#: fixtures/accountssetings.py:14
 msgid "Activation by Administrator"
 msgid "Activation by Administrator"
-msgstr ""
+msgstr "Aktywacja przez administratora"
 
 
-#: fixtures/accountssetings.py:13
+#: fixtures/accountssetings.py:14
 msgid "Dont allow new registrations"
 msgid "Dont allow new registrations"
 msgstr "Nie pozwalaj na nowe rejestracje"
 msgstr "Nie pozwalaj na nowe rejestracje"
 
 
-#: fixtures/accountssetings.py:14 models/usermodel.py:189
+#: fixtures/accountssetings.py:15 models/usermodel.py:189
 msgid "Users Registrations"
 msgid "Users Registrations"
 msgstr "Rejestracje użytkowników"
 msgstr "Rejestracje użytkowników"
 
 
-#: fixtures/accountssetings.py:15
+#: fixtures/accountssetings.py:16
 msgid "New accounts validation"
 msgid "New accounts validation"
 msgstr "Walidacja nowych kont"
 msgstr "Walidacja nowych kont"
 
 
-#: fixtures/accountssetings.py:22
+#: fixtures/accountssetings.py:23
 msgid "Default Timezone"
 msgid "Default Timezone"
 msgstr "Domyślna strefa czasowa"
 msgstr "Domyślna strefa czasowa"
 
 
-#: fixtures/accountssetings.py:23
+#: fixtures/accountssetings.py:24
 msgid "Used by guests, crawlers and newly registered users."
 msgid "Used by guests, crawlers and newly registered users."
 msgstr ""
 msgstr ""
 "Używana przez gości, boty wyszukiwarek i nowo zarejestrowanych użytkowników."
 "Używana przez gości, boty wyszukiwarek i nowo zarejestrowanych użytkowników."
 
 
-#: fixtures/accountssetings.py:30
+#: fixtures/accountssetings.py:31
 msgid "Users Names"
 msgid "Users Names"
 msgstr "Nazwy użytkowników"
 msgstr "Nazwy użytkowników"
 
 
-#: fixtures/accountssetings.py:31
+#: fixtures/accountssetings.py:32
 msgid "Minimum allowed username length"
 msgid "Minimum allowed username length"
 msgstr "Minimalna, dozwolona długość nazwy uzytkownika"
 msgstr "Minimalna, dozwolona długość nazwy uzytkownika"
 
 
-#: fixtures/accountssetings.py:38
+#: fixtures/accountssetings.py:39
 msgid "Maxim allowed username length"
 msgid "Maxim allowed username length"
 msgstr "Maksymalna, dozwolona długość nazwy uzytkownika"
 msgstr "Maksymalna, dozwolona długość nazwy uzytkownika"
 
 
-#: fixtures/accountssetings.py:45
+#: fixtures/accountssetings.py:46
 msgid "Users Passwords"
 msgid "Users Passwords"
-msgstr ""
+msgstr "Hasła użytkowników"
 
 
-#: fixtures/accountssetings.py:46
+#: fixtures/accountssetings.py:47
 msgid "Minimum user password length"
 msgid "Minimum user password length"
-msgstr ""
+msgstr "Minimalna możliwa dlugość hasła"
 
 
-#: fixtures/accountssetings.py:52
+#: fixtures/accountssetings.py:53
 msgid "Require mixed Case"
 msgid "Require mixed Case"
-msgstr ""
+msgstr "Wymagane wielkie i małe i litery"
 
 
-#: fixtures/accountssetings.py:52
+#: fixtures/accountssetings.py:53
 msgid "Require digits"
 msgid "Require digits"
-msgstr ""
+msgstr "Wymagane cyfry"
 
 
-#: fixtures/accountssetings.py:52
+#: fixtures/accountssetings.py:53
 msgid "Require special characters"
 msgid "Require special characters"
-msgstr ""
+msgstr "Wymagane znaki specjalne"
 
 
-#: fixtures/accountssetings.py:53
+#: fixtures/accountssetings.py:54
 msgid "Password Complexity"
 msgid "Password Complexity"
-msgstr ""
+msgstr "Złożoność hasła"
 
 
-#: fixtures/accountssetings.py:60
+#: fixtures/accountssetings.py:61
 msgid "Password Lifetime"
 msgid "Password Lifetime"
-msgstr ""
+msgstr "Długość życia hasła"
 
 
-#: fixtures/accountssetings.py:61
+#: fixtures/accountssetings.py:62
 msgid ""
 msgid ""
 "Enter number of days since password was set to force member to change it "
 "Enter number of days since password was set to force member to change it "
 "with new one, or 0 to dont force your members to change their passwords."
 "with new one, or 0 to dont force your members to change their passwords."
 msgstr ""
 msgstr ""
+"Wprowadź liczbę dni, po których użytkownicy będą zmuszeni do zamiany swojego "
+"hasła na nowe. Wprowadzenie zera wyłącza wymuszanie zmiany haseł."
 
 
-#: fixtures/accountssetings.py:67
+#: fixtures/accountssetings.py:68
 msgid "Include User Password in Welcoming E-mail"
 msgid "Include User Password in Welcoming E-mail"
-msgstr ""
+msgstr "Umieść hasło użytkownika w powitalnym e-mailu"
 
 
-#: fixtures/accountssetings.py:68
+#: fixtures/accountssetings.py:69
 msgid ""
 msgid ""
 "If you want to, Misago can include new user password in welcoming e-mail "
 "If you want to, Misago can include new user password in welcoming e-mail "
 "that is sent to new users after successful account creation."
 "that is sent to new users after successful account creation."
 msgstr ""
 msgstr ""
+"Jeżeli chcesz, Misago może umieścić hasło nowego użytkownika w powitalnym e-"
+"mailu, którego ten otrzyma po pomyślnej rejestracji."
 
 
-#: fixtures/accountssetings.py:76 fixtures/accountssetings.py:87
+#: fixtures/accountssetings.py:77 fixtures/accountssetings.py:88
 msgid "Put on watched threads list and e-mail user when somebody replies"
 msgid "Put on watched threads list and e-mail user when somebody replies"
 msgstr ""
 msgstr ""
+"Umieść na liście obserwowanych tematów i wyślij e-mail, gdy ktoś odpisze"
 
 
-#: fixtures/accountssetings.py:78
+#: fixtures/accountssetings.py:79
 msgid "Default Watching Preferences"
 msgid "Default Watching Preferences"
-msgstr ""
+msgstr "Domyślne ustawienia obserwowania tematów"
 
 
-#: fixtures/accountssetings.py:79
+#: fixtures/accountssetings.py:80
 msgid "Watch threads user started"
 msgid "Watch threads user started"
-msgstr ""
+msgstr "Obserwowanie tematów rozpoczętych przez użytkownika"
 
 
-#: fixtures/accountssetings.py:89
+#: fixtures/accountssetings.py:90
 msgid "Watch threads user replied in"
 msgid "Watch threads user replied in"
-msgstr ""
+msgstr "Obserwowanie tematów w których użytkownik odpisał"
 
 
-#: fixtures/accountssetings.py:97
+#: fixtures/accountssetings.py:98
 msgid "Number of Profiles Per Page"
 msgid "Number of Profiles Per Page"
-msgstr ""
+msgstr "Liczba profili na stronę"
 
 
 #: fixtures/avatarssettings.py:7
 #: fixtures/avatarssettings.py:7
 msgid "Users Avatars Settings"
 msgid "Users Avatars Settings"
-msgstr ""
+msgstr "Awatary użytkowników"
 
 
 #: fixtures/avatarssettings.py:8
 #: fixtures/avatarssettings.py:8
 msgid "Those settings allow you to control your users avatars."
 msgid "Those settings allow you to control your users avatars."
-msgstr ""
+msgstr "Te ustawienia pozwalają na kontrolę awatarów użytkowników."
 
 
 #: fixtures/avatarssettings.py:14 fixtures/avatarssettings.py:23
 #: fixtures/avatarssettings.py:14 fixtures/avatarssettings.py:23
 #: templates/cranefly/usercp/avatar.html:26
 #: templates/cranefly/usercp/avatar.html:26
 msgid "Gravatar"
 msgid "Gravatar"
-msgstr ""
+msgstr "Gravatar"
 
 
 #: fixtures/avatarssettings.py:14 templates/cranefly/usercp/avatar.html:22
 #: fixtures/avatarssettings.py:14 templates/cranefly/usercp/avatar.html:22
 msgid "Uploaded Avatar"
 msgid "Uploaded Avatar"
-msgstr "Przesłany avatar"
+msgstr "Przesłany awatar"
 
 
 #: fixtures/avatarssettings.py:14
 #: fixtures/avatarssettings.py:14
 msgid "Avatars Gallery"
 msgid "Avatars Gallery"
-msgstr "Galeria avatarów"
+msgstr "Galeria awatarów"
 
 
 #: fixtures/avatarssettings.py:15
 #: fixtures/avatarssettings.py:15
 msgid "General Settings"
 msgid "General Settings"
@@ -4298,19 +4680,19 @@ msgstr "Ogólne ustawienia"
 
 
 #: fixtures/avatarssettings.py:16
 #: fixtures/avatarssettings.py:16
 msgid "Allowed Avatars"
 msgid "Allowed Avatars"
-msgstr "Dozwolone avatary"
+msgstr "Dozwolone awatary"
 
 
 #: fixtures/avatarssettings.py:17
 #: fixtures/avatarssettings.py:17
 msgid "Select Avatar types allowed on your forum."
 msgid "Select Avatar types allowed on your forum."
-msgstr "Wybierz typy avatarów, dozwolone na tym forum."
+msgstr "Wybierz typy awatarów, dozwolone na tym forum."
 
 
 #: fixtures/avatarssettings.py:23
 #: fixtures/avatarssettings.py:23
 msgid "Random Avatar from Gallery"
 msgid "Random Avatar from Gallery"
-msgstr "Losowy avatar z galerii"
+msgstr "Losowy awatar z galerii"
 
 
 #: fixtures/avatarssettings.py:24
 #: fixtures/avatarssettings.py:24
 msgid "Default Avatar"
 msgid "Default Avatar"
-msgstr "Domyślny avatar"
+msgstr "Domyślny awatar"
 
 
 #: fixtures/avatarssettings.py:25
 #: fixtures/avatarssettings.py:25
 msgid ""
 msgid ""
@@ -4319,14 +4701,14 @@ msgid ""
 "regular gallery. If no avatar can be picked from gallery, Gravatar will be "
 "regular gallery. If no avatar can be picked from gallery, Gravatar will be "
 "used."
 "used."
 msgstr ""
 msgstr ""
-"Domyślny avatar przypisany do nowych użytkowników. Jeżeli utworzysz katalog "
+"Domyślny awatar przypisany do nowych użytkowników. Jeżeli utworzysz katalog "
 "i nazwiesz go \"_default\", forum wybierze losowy avatar z tego katalogu "
 "i nazwiesz go \"_default\", forum wybierze losowy avatar z tego katalogu "
 "zamiast z normalnej galerii. Jeżeli nie uda się wybrać avatara z galerii, "
 "zamiast z normalnej galerii. Jeżeli nie uda się wybrać avatara z galerii, "
 "zostanie użyty Gravatar."
 "zostanie użyty Gravatar."
 
 
 #: fixtures/avatarssettings.py:32
 #: fixtures/avatarssettings.py:32
 msgid "Avatar Upload Settings"
 msgid "Avatar Upload Settings"
-msgstr "Ustawienia wgrywania avatara"
+msgstr "Ustawienia wgrywania awatara"
 
 
 #: fixtures/avatarssettings.py:33
 #: fixtures/avatarssettings.py:33
 msgid "Maxmimum size of uploaded file"
 msgid "Maxmimum size of uploaded file"
@@ -4358,7 +4740,7 @@ msgstr ""
 
 
 #: fixtures/basicsettings.py:26
 #: fixtures/basicsettings.py:26
 msgid "Board Header Postscript"
 msgid "Board Header Postscript"
-msgstr ""
+msgstr "Dodatkowa informacja w nagłówku"
 
 
 #: fixtures/basicsettings.py:27
 #: fixtures/basicsettings.py:27
 msgid "Additional text displayed in some themes board header after board name."
 msgid "Additional text displayed in some themes board header after board name."
@@ -4396,56 +4778,62 @@ msgstr "Ogon forum"
 
 
 #: fixtures/basicsettings.py:46
 #: fixtures/basicsettings.py:46
 msgid "Custom Credit"
 msgid "Custom Credit"
-msgstr ""
+msgstr "Dodatkowa informacja"
 
 
 #: fixtures/basicsettings.py:47
 #: fixtures/basicsettings.py:47
 msgid ""
 msgid ""
 "Custom Credit to display in board footer above software and theme copyright "
 "Custom Credit to display in board footer above software and theme copyright "
 "information. You can use HTML."
 "information. You can use HTML."
 msgstr ""
 msgstr ""
+"Dodatkowa informacja w ogonie forum, ponad tą o Misago i ew. autorze stylu. "
+"Można używać HTML."
 
 
 #: fixtures/basicsettings.py:52
 #: fixtures/basicsettings.py:52
 msgid "Board E-Mails"
 msgid "Board E-Mails"
-msgstr ""
+msgstr "Wiadomości e-mail od forum"
 
 
 #: fixtures/basicsettings.py:53
 #: fixtures/basicsettings.py:53
 msgid "Custom Footnote in HTML E-mails"
 msgid "Custom Footnote in HTML E-mails"
-msgstr ""
+msgstr "Niestandardowa stopka wiadomości HTML"
 
 
 #: fixtures/basicsettings.py:54
 #: fixtures/basicsettings.py:54
 msgid "Custom Footnote to display in HTML e-mail messages sent by board."
 msgid "Custom Footnote to display in HTML e-mail messages sent by board."
-msgstr ""
+msgstr "Niestandardowa stopka dołączana do wiadomości HTML."
 
 
 #: fixtures/basicsettings.py:59
 #: fixtures/basicsettings.py:59
 msgid "Custom Footnote in plain text E-mails"
 msgid "Custom Footnote in plain text E-mails"
-msgstr ""
+msgstr "Niestandardowa stopka wiadomości w czystym tekście"
 
 
 #: fixtures/basicsettings.py:60
 #: fixtures/basicsettings.py:60
 msgid "Custom Footnote to display in plain text e-mail messages sent by board."
 msgid "Custom Footnote to display in plain text e-mail messages sent by board."
 msgstr ""
 msgstr ""
+"Niestandardowa stopka dołączana do wiadomości w czystym tekście (gdy klient "
+"pocztowy nie obsługuje HTML)."
 
 
 #: fixtures/bruteforcesettings.py:7 fixtures/bruteforcesettings.py:15
 #: fixtures/bruteforcesettings.py:7 fixtures/bruteforcesettings.py:15
 msgid "Brute-force Countermeasures"
 msgid "Brute-force Countermeasures"
-msgstr ""
+msgstr "Zapobieganie atakom typu brute-force"
 
 
 #: fixtures/bruteforcesettings.py:8
 #: fixtures/bruteforcesettings.py:8
 msgid ""
 msgid ""
 "Those settings allow you to protect your forum from brute-force attacks."
 "Those settings allow you to protect your forum from brute-force attacks."
-msgstr ""
+msgstr "Ustawienia pomagające chronić forum przed atakami typu brute-force."
 
 
 #: fixtures/bruteforcesettings.py:16
 #: fixtures/bruteforcesettings.py:16
 msgid "IP invalid attempts limit"
 msgid "IP invalid attempts limit"
-msgstr ""
+msgstr "Limit nieudanych prób na IP"
 
 
 #: fixtures/bruteforcesettings.py:17
 #: fixtures/bruteforcesettings.py:17
 msgid ""
 msgid ""
 "Enter maximal number of allowed attempts before IP address \"jams\". "
 "Enter maximal number of allowed attempts before IP address \"jams\". "
 "Defautly forum records only failed sign-in attempts."
 "Defautly forum records only failed sign-in attempts."
 msgstr ""
 msgstr ""
+"Wprowadź maksymalną liczbę nieudanych prób, po których adres IP zostanie "
+"zablokowany. Domyślnie forum uwzględnia tylko nieudane próby logowania."
 
 
 #: fixtures/bruteforcesettings.py:24
 #: fixtures/bruteforcesettings.py:24
 msgid "Protect register form"
 msgid "Protect register form"
-msgstr ""
+msgstr "Chroń formularz rejestracji"
 
 
 #: fixtures/bruteforcesettings.py:25
 #: fixtures/bruteforcesettings.py:25
 msgid ""
 msgid ""
@@ -4454,16 +4842,26 @@ msgid ""
 "against spam-bots, however same protection may cause problems for users with "
 "against spam-bots, however same protection may cause problems for users with "
 "disabilities or ones that have problems understanding Q&A challenge."
 "disabilities or ones that have problems understanding Q&A challenge."
 msgstr ""
 msgstr ""
+"Ustawienie tej opcji na \"Tak\" spowoduje wliczanie nieudanych rejestracji "
+"do limitu. Większośc nieudanych rejestracji jest spowodowana zabezpieczeniem "
+"CAPTCHA, przeciwko spam-botom. Niemniej, ta opcja może poważnie utrudnić, a "
+"nawet uniemożliwić rejestrację użytkownikom niepełnosprawnym, albo "
+"posiadającym problemy z odnalezieniem odpowiedzi na pytanie rejestracyjne "
+"(jeżeli istnieje)."
 
 
 #: fixtures/bruteforcesettings.py:32
 #: fixtures/bruteforcesettings.py:32
 msgid "Automaticaly unlock jammed IPs"
 msgid "Automaticaly unlock jammed IPs"
-msgstr ""
+msgstr "Automatycznie odblokowuj zablokowane adresy IP"
 
 
 #: fixtures/bruteforcesettings.py:33
 #: fixtures/bruteforcesettings.py:33
 msgid ""
 msgid ""
 "Enter number of minutes since IP address \"jams\" to automatically unlock "
 "Enter number of minutes since IP address \"jams\" to automatically unlock "
 "it, or 0 to never unlock jammed IP adresses. Jams don't count as bans."
 "it, or 0 to never unlock jammed IP adresses. Jams don't count as bans."
 msgstr ""
 msgstr ""
+"Blokady adresów IP nie liczą się jako bany i powinny być ostatecznie "
+"automatycznie zdejmowane. Wprowadź liczbę minut, po których adres IP "
+"zostanie odblokowany. Wpisz 0, aby nigdy nie zdejmować blokady adresów IP, "
+"aczkolwiek nie jest to zalecane. "
 
 
 #: fixtures/captchasettings.py:7
 #: fixtures/captchasettings.py:7
 msgid "Spam Countermeasures"
 msgid "Spam Countermeasures"
@@ -4474,6 +4872,8 @@ msgid ""
 "Those settings allow you to combat automatic registrations and spam messages "
 "Those settings allow you to combat automatic registrations and spam messages "
 "on your forum."
 "on your forum."
 msgstr ""
 msgstr ""
+"Te ustawienia pomagają zwalczać automatyczne rejestracje botów i spam na "
+"forum."
 
 
 #: fixtures/captchasettings.py:14
 #: fixtures/captchasettings.py:14
 msgid "No protection"
 msgid "No protection"
@@ -4481,19 +4881,19 @@ msgstr "Bez zabezpieczeń"
 
 
 #: fixtures/captchasettings.py:14 fixtures/captchasettings.py:22
 #: fixtures/captchasettings.py:14 fixtures/captchasettings.py:22
 msgid "reCaptcha"
 msgid "reCaptcha"
-msgstr ""
+msgstr "Zabezpieczenie reCaptcha"
 
 
 #: fixtures/captchasettings.py:14
 #: fixtures/captchasettings.py:14
 msgid "Question & Answer"
 msgid "Question & Answer"
-msgstr ""
+msgstr "Pytanie i odpowiedź (Q&A)"
 
 
 #: fixtures/captchasettings.py:15
 #: fixtures/captchasettings.py:15
 msgid "Spambots Registrations"
 msgid "Spambots Registrations"
-msgstr ""
+msgstr "Rejestracje spambotów"
 
 
 #: fixtures/captchasettings.py:16
 #: fixtures/captchasettings.py:16
 msgid "CAPTCHA type"
 msgid "CAPTCHA type"
-msgstr ""
+msgstr "Typ CAPTCHA"
 
 
 #: fixtures/captchasettings.py:17
 #: fixtures/captchasettings.py:17
 msgid ""
 msgid ""
@@ -4501,6 +4901,9 @@ msgid ""
 "Computers and Humans Apart\". Its type of test developed on purpose of "
 "Computers and Humans Apart\". Its type of test developed on purpose of "
 "blocking automatic registrations."
 "blocking automatic registrations."
 msgstr ""
 msgstr ""
+"CAPTCHA to test służący do odróżniania botów (programów) od prawdziwych "
+"użytkowników. Misago wykorzystuje go do blokowania automatycznych "
+"rejestracji."
 
 
 #: fixtures/captchasettings.py:23
 #: fixtures/captchasettings.py:23
 msgid "Public Key"
 msgid "Public Key"
@@ -4509,6 +4912,7 @@ msgstr "Klucz publiczny"
 #: fixtures/captchasettings.py:24
 #: fixtures/captchasettings.py:24
 msgid "Enter public API key that you have received from reCaptcha."
 msgid "Enter public API key that you have received from reCaptcha."
 msgstr ""
 msgstr ""
+"Wprowadź publiczny klucz API (public API key), który otrzymałeś od reCaptcha."
 
 
 #: fixtures/captchasettings.py:29
 #: fixtures/captchasettings.py:29
 msgid "Private Key"
 msgid "Private Key"
@@ -4517,18 +4921,21 @@ msgstr "Klucz prywatny"
 #: fixtures/captchasettings.py:30
 #: fixtures/captchasettings.py:30
 msgid "Enter private API key that you have received from reCaptcha."
 msgid "Enter private API key that you have received from reCaptcha."
 msgstr ""
 msgstr ""
+"Wprowadź prywatny klucz API (private API key), który otrzymałeś od reCaptcha."
 
 
 #: fixtures/captchasettings.py:36
 #: fixtures/captchasettings.py:36
 msgid "Use SSL in reCaptcha"
 msgid "Use SSL in reCaptcha"
-msgstr ""
+msgstr "Używaj SSL w reCaptcha"
 
 
 #: fixtures/captchasettings.py:37
 #: fixtures/captchasettings.py:37
 msgid "Do you want forum to use SSL when making requests to reCaptha servers?"
 msgid "Do you want forum to use SSL when making requests to reCaptha servers?"
 msgstr ""
 msgstr ""
+"Czy chcesz, aby forum używało szyfrowania SSL przy wykonywaniu żądań do "
+"serwerów reCaptha?"
 
 
 #: fixtures/captchasettings.py:42
 #: fixtures/captchasettings.py:42
 msgid "Question and Answer Test"
 msgid "Question and Answer Test"
-msgstr ""
+msgstr "Test typu \"Pytanie i odpowiedź\""
 
 
 #: fixtures/captchasettings.py:43
 #: fixtures/captchasettings.py:43
 msgid "Question"
 msgid "Question"
@@ -4562,55 +4969,57 @@ msgstr ""
 msgid "Full Access"
 msgid "Full Access"
 msgstr "Pełny dostęp"
 msgstr "Pełny dostęp"
 
 
-#: fixtures/forumsroles.py:45
+#: fixtures/forumsroles.py:47
 msgid "Standard Access and Upload"
 msgid "Standard Access and Upload"
-msgstr "Standardowy dostęp i przysyłanie plików"
+msgstr "Standardowy dostęp i wgrywanie plików"
 
 
-#: fixtures/forumsroles.py:68
+#: fixtures/forumsroles.py:70
 msgid "Standard Access"
 msgid "Standard Access"
 msgstr "Standardowy dostęp"
 msgstr "Standardowy dostęp"
 
 
-#: fixtures/forumsroles.py:88
+#: fixtures/forumsroles.py:90
 msgid "Read and Download"
 msgid "Read and Download"
 msgstr "Czytanie i pobieranie"
 msgstr "Czytanie i pobieranie"
 
 
-#: fixtures/forumsroles.py:99
+#: fixtures/forumsroles.py:101
 msgid "Threads list only"
 msgid "Threads list only"
 msgstr "Tylko lista tematów"
 msgstr "Tylko lista tematów"
 
 
-#: fixtures/forumsroles.py:107
+#: fixtures/forumsroles.py:109
 msgid "Read only"
 msgid "Read only"
 msgstr "Tylko odczyt"
 msgstr "Tylko odczyt"
 
 
 #: fixtures/privatethreadssettings.py:7
 #: fixtures/privatethreadssettings.py:7
 msgid "Private Threads Settings"
 msgid "Private Threads Settings"
-msgstr "Ustawienia prywatnych tematów"
+msgstr "Ustawienia prywatnych dyskusji"
 
 
 #: fixtures/privatethreadssettings.py:8
 #: fixtures/privatethreadssettings.py:8
 msgid "Those settings control your forum's private threads."
 msgid "Those settings control your forum's private threads."
-msgstr ""
+msgstr "Te ustawienia wpływaja na prywatne dyskusje na tym forum."
 
 
 #: fixtures/privatethreadssettings.py:15
 #: fixtures/privatethreadssettings.py:15
 msgid "Enable Private Threads"
 msgid "Enable Private Threads"
-msgstr ""
+msgstr "Uaktywnij prywatne dyskusje"
 
 
 #: fixtures/rankingsettings.py:7
 #: fixtures/rankingsettings.py:7
 msgid "Members Ranking"
 msgid "Members Ranking"
-msgstr ""
+msgstr "Ranking użytkowników"
 
 
 #: fixtures/rankingsettings.py:8
 #: fixtures/rankingsettings.py:8
 msgid ""
 msgid ""
 "Those settings control mechanisms of members activity ranking which allows "
 "Those settings control mechanisms of members activity ranking which allows "
 "you to gamificate your forum."
 "you to gamificate your forum."
 msgstr ""
 msgstr ""
+"Te ustawienia wpływają na ranking użytkowników, który skłania użytkowników "
+"do aktywnego uczestnictwa w życiu forum."
 
 
 #: fixtures/rankingsettings.py:15
 #: fixtures/rankingsettings.py:15
 msgid "Basic Ranking Settings"
 msgid "Basic Ranking Settings"
-msgstr ""
+msgstr "Podstawowe ustawienia rankingu"
 
 
 #: fixtures/rankingsettings.py:16
 #: fixtures/rankingsettings.py:16
 msgid "Ranking Inflation"
 msgid "Ranking Inflation"
-msgstr ""
+msgstr "Inflacja w rankingu"
 
 
 #: fixtures/rankingsettings.py:17
 #: fixtures/rankingsettings.py:17
 msgid ""
 msgid ""
@@ -4619,6 +5028,10 @@ msgid ""
 "inactivity and requiring users to remain active in order to remain high in "
 "inactivity and requiring users to remain active in order to remain high in "
 "ranking."
 "ranking."
 msgstr ""
 msgstr ""
+"Wprowadź w procentach ilość punktów, którą użytkownik będzie stopniowo "
+"tracił. Inflacja wyników jest ważnym mechanizmem, który kara za "
+"nieaktywność. Aby utrzymać wysoką pozycję w rankingu, użytkownik musi "
+"wypowiadać się na forum."
 
 
 #: fixtures/rankingsettings.py:23
 #: fixtures/rankingsettings.py:23
 msgid "Don't Keep Users Ranking Positions Secret"
 msgid "Don't Keep Users Ranking Positions Secret"
@@ -4670,17 +5083,20 @@ msgstr ""
 
 
 #: fixtures/rankingsettings.py:53
 #: fixtures/rankingsettings.py:53
 msgid "Reward Cooldown"
 msgid "Reward Cooldown"
-msgstr ""
+msgstr "Minimalny odstep czasowy pomiędzy nagrodami"
 
 
 #: fixtures/rankingsettings.py:54
 #: fixtures/rankingsettings.py:54
 msgid ""
 msgid ""
 "Minimal time (in seconds) that has to pass between postings for new message "
 "Minimal time (in seconds) that has to pass between postings for new message "
 "to receive karma vote. This is useful to combat flood."
 "to receive karma vote. This is useful to combat flood."
 msgstr ""
 msgstr ""
+"Minimalny odstęp czasu (w sekundach), który musi upłynąć pomiędzy kolejnymi "
+"postami użytkownika, aby ten otrzymał punkty. Pomaga zwalczać nabijanie "
+"punktów serią postów."
 
 
 #: fixtures/rankingsettings.py:61
 #: fixtures/rankingsettings.py:61
 msgid "Karma System"
 msgid "Karma System"
-msgstr ""
+msgstr "System punktów"
 
 
 #: fixtures/rankingsettings.py:62
 #: fixtures/rankingsettings.py:62
 msgid "Upvote Reward"
 msgid "Upvote Reward"
@@ -4697,20 +5113,21 @@ msgstr "Kara za "
 #: fixtures/rankingsettings.py:71
 #: fixtures/rankingsettings.py:71
 msgid "Score user will lose every time his post receives downvote."
 msgid "Score user will lose every time his post receives downvote."
 msgstr ""
 msgstr ""
+"Ilość punktów, które utraci użytkownik, gdy jego post otrzyma głos w dół."
 
 
 #: fixtures/ranks.py:17
 #: fixtures/ranks.py:17
 msgid "Most Valuable Posters"
 msgid "Most Valuable Posters"
-msgstr "Najbardziej wartościowi "
+msgstr "Przyjaciele forum"
 
 
 #: fixtures/ranks.py:19
 #: fixtures/ranks.py:19
 msgid "MVP"
 msgid "MVP"
-msgstr "MVP"
+msgstr "Przyjaciel forum"
 
 
 #: fixtures/ranks.py:27
 #: fixtures/ranks.py:27
 msgid "Top Posters"
 msgid "Top Posters"
-msgstr ""
+msgstr "Najaktywniejsi"
 
 
-#: fixtures/ranks.py:37 templates/cranefly/index.html:127
+#: fixtures/ranks.py:37 templates/cranefly/index.html:137
 msgid "Members"
 msgid "Members"
 msgstr "Członkowie"
 msgstr "Członkowie"
 
 
@@ -4741,15 +5158,30 @@ msgstr ""
 "Sprawia, że sesje są bardziej bezpieczne, ale może spowodować problemy z "
 "Sprawia, że sesje są bardziej bezpieczne, ale może spowodować problemy z "
 "serwerami proxy i VPN."
 "serwerami proxy i VPN."
 
 
-#: fixtures/signingsettings.py:22
+#: fixtures/signingsettings.py:23
+msgid "Online Tracker Updates Frequency"
+msgstr "Częstotliwość aktualizacji listy online"
+
+#: fixtures/signingsettings.py:24
+msgid ""
+"How often do you want online tracker to synchronize itself with database? "
+"Low numbers provide good accuracy at cost of database traffic while great "
+"number provides your users with general idea how many are currently online "
+"while at same time keeping stress off your database."
+msgstr ""
+"Jak często ma być aktualizowana lista online? Niskie liczby dają bardzo "
+"dobrą dokładność liczby użytkowników online, kosztem pracy bazy danych. Duże "
+"liczby zmniejszają obciążenie bazy, dając jedynie podglądowe wyniki."
+
+#: fixtures/signingsettings.py:30
 msgid "\"Remember Me\" Feature"
 msgid "\"Remember Me\" Feature"
 msgstr "Opcja \"Zapamiętaj mnie\""
 msgstr "Opcja \"Zapamiętaj mnie\""
 
 
-#: fixtures/signingsettings.py:23
+#: fixtures/signingsettings.py:31
 msgid "Enable \"Remember Me\" feature"
 msgid "Enable \"Remember Me\" feature"
 msgstr "Włącz opcję \"Zapamiętaj mnie\""
 msgstr "Włącz opcję \"Zapamiętaj mnie\""
 
 
-#: fixtures/signingsettings.py:24
+#: fixtures/signingsettings.py:32
 msgid ""
 msgid ""
 "Turning this option on allows users to sign in on to your board using cookie-"
 "Turning this option on allows users to sign in on to your board using cookie-"
 "based tokens. This may result in account compromisation when user fails to "
 "based tokens. This may result in account compromisation when user fails to "
@@ -4761,26 +5193,32 @@ msgstr ""
 "profilu użytkownika, gdy ten zapomni się wylogować na współdzielonym "
 "profilu użytkownika, gdy ten zapomni się wylogować na współdzielonym "
 "komputerze, lub dopuści do kradzieży swoich ciasteczek (cookies)."
 "komputerze, lub dopuści do kradzieży swoich ciasteczek (cookies)."
 
 
-#: fixtures/signingsettings.py:30
+#: fixtures/signingsettings.py:38
 msgid "\"Remember Me\" token lifetime"
 msgid "\"Remember Me\" token lifetime"
-msgstr ""
+msgstr "Czas życia tokenu opcji \"Zapamiętaj mnie\""
 
 
-#: fixtures/signingsettings.py:31
+#: fixtures/signingsettings.py:39
 msgid ""
 msgid ""
 "Number of days since either last use or creation of \"Remember Me\" token to "
 "Number of days since either last use or creation of \"Remember Me\" token to "
 "its expiration."
 "its expiration."
 msgstr ""
 msgstr ""
+"Liczba dni od ostatniego użycia lub utworzenia tokenu opcji \"Zapamiętaj mnie"
+"\" do jego wygaśnięcia."
 
 
-#: fixtures/signingsettings.py:37
+#: fixtures/signingsettings.py:45
 msgid "Allow \"Remember Me\" tokens refreshing"
 msgid "Allow \"Remember Me\" tokens refreshing"
-msgstr ""
+msgstr "Pozwól na automatycznie odświeżanie tokenu opcji \"Zapamiętaj mnie\""
 
 
-#: fixtures/signingsettings.py:38
+#: fixtures/signingsettings.py:46
 msgid ""
 msgid ""
 "Set this setting to off if you want to force your users to periodically "
 "Set this setting to off if you want to force your users to periodically "
 "update their \"Remember Me\" tokens by signing in. If this option is on, "
 "update their \"Remember Me\" tokens by signing in. If this option is on, "
 "Tokens are updated when they are used to open new session."
 "Tokens are updated when they are used to open new session."
 msgstr ""
 msgstr ""
+"Wyłączenie tej opcji zmusza użytkowników do okresowego zaktualizowania "
+"swoich tokenów opcji \"Zapamiętaj mnie\" poprzez ręczne zalogowanie się. "
+"Jeżeli ta opcja jest włączona, tokeny zostaną przedłużone od samego wejścia "
+"na forum."
 
 
 #: fixtures/threadssettings.py:7
 #: fixtures/threadssettings.py:7
 msgid "Threads and Posts Settings"
 msgid "Threads and Posts Settings"
@@ -4816,14 +5254,14 @@ msgstr "Liczba tematów wyświetlana na stronie w widoku forum."
 
 
 #: fixtures/threadssettings.py:39
 #: fixtures/threadssettings.py:39
 msgid "Display avatars on threads list"
 msgid "Display avatars on threads list"
-msgstr "Wyświetlaj avatary na liście tematów"
+msgstr "Wyświetlaj awatary na liście tematów"
 
 
 #: fixtures/threadssettings.py:40
 #: fixtures/threadssettings.py:40
 msgid ""
 msgid ""
 "Unlike basic user data, avatars are not cached - turning this option on will "
 "Unlike basic user data, avatars are not cached - turning this option on will "
 "cause one extra query on threads lists."
 "cause one extra query on threads lists."
 msgstr ""
 msgstr ""
-"W przeciwieństwie do zwykłych danych użytkownika, avatary nie są trzymane w "
+"W przeciwieństwie do zwykłych danych użytkownika, awatary nie są trzymane w "
 "pamięci podręcznej. Włączenie tej opcji może minimalnie przedłużyć czas "
 "pamięci podręcznej. Włączenie tej opcji może minimalnie przedłużyć czas "
 "generowania listy tematów. "
 "generowania listy tematów. "
 
 
@@ -4840,10 +5278,12 @@ msgid ""
 "Enter number of threads to be displayed on \"Popular Threads\" list on board "
 "Enter number of threads to be displayed on \"Popular Threads\" list on board "
 "index or 0 to don't display any threads there."
 "index or 0 to don't display any threads there."
 msgstr ""
 msgstr ""
+"Wprowadź liczbę tematów wyświetlanych na liście \"popularnych tematów\" na "
+"stronie głównej forum, lub wprowadź 0 aby wyłączyć tą listę."
 
 
 #: fixtures/threadssettings.py:56
 #: fixtures/threadssettings.py:56
 msgid "Ranking Update Frequency"
 msgid "Ranking Update Frequency"
-msgstr "Częstotliwośc aktualizacji rankingu"
+msgstr "Częstotliwość aktualizacji rankingu"
 
 
 #: fixtures/threadssettings.py:57
 #: fixtures/threadssettings.py:57
 msgid ""
 msgid ""
@@ -4876,16 +5316,20 @@ msgstr ""
 
 
 #: fixtures/threadssettings.py:80
 #: fixtures/threadssettings.py:80
 msgid "Score inflation"
 msgid "Score inflation"
-msgstr ""
+msgstr "Stopniowa utrata punktów"
 
 
 #: fixtures/threadssettings.py:81
 #: fixtures/threadssettings.py:81
 #, python-format
 #, python-format
 msgid ""
 msgid ""
 "Thread popularity system requires inflation to be defined in order to be "
 "Thread popularity system requires inflation to be defined in order to be "
 "effective. updatethreadranking task will lower thread scores by percent "
 "effective. updatethreadranking task will lower thread scores by percent "
-"defined here on every launch. For example, yf you enter 5, thread scores "
-"will be lowered by 5% on every update. Enter zero to disable inflation."
+"defined here on every launch. For example, if you enter 5, thread scores "
+"will be lowered by 5%% on every update. Enter zero to disable inflation."
 msgstr ""
 msgstr ""
+"System popularności tematów wymaga zdefiniowanej wartości stopniowej utraty "
+"punktów do skutecznego działania. Wartość w procentach.  Dla przykładu, "
+"jeżeli wpiszesz tutaj 5, co każdą aktualizację, każdy temat utraci 5%% "
+"swoich punktów. Wpisanie zera wyłącza utratę punktów."
 
 
 #: fixtures/threadssettings.py:89
 #: fixtures/threadssettings.py:89
 msgid "Min. Post Length"
 msgid "Min. Post Length"
@@ -4973,15 +5417,15 @@ msgstr ""
 msgid "Administrator"
 msgid "Administrator"
 msgstr "Administrator"
 msgstr "Administrator"
 
 
-#: fixtures/userroles.py:29
+#: fixtures/userroles.py:30
 msgid "Moderator"
 msgid "Moderator"
 msgstr "Moderator"
 msgstr "Moderator"
 
 
-#: fixtures/userroles.py:51
+#: fixtures/userroles.py:53
 msgid "Registered"
 msgid "Registered"
 msgstr "Zarejestrowany"
 msgstr "Zarejestrowany"
 
 
-#: fixtures/userroles.py:67 templates/admin/online/list.html:13
+#: fixtures/userroles.py:69 templates/admin/online/list.html:13
 msgid "Guest"
 msgid "Guest"
 msgstr "Gość"
 msgstr "Gość"
 
 
@@ -5009,16 +5453,16 @@ msgstr "Musisz uzupełnić wszystkie pola."
 msgid "Entered values differ from each other."
 msgid "Entered values differ from each other."
 msgstr "Wprowadzone wartości różnią się."
 msgstr "Wprowadzone wartości różnią się."
 
 
-#: markdown/factory.py:112
+#: markdown/factory.py:81
 #, python-format
 #, python-format
 msgid "Posted by %(user)s"
 msgid "Posted by %(user)s"
 msgstr "%(user)s napisał/a"
 msgstr "%(user)s napisał/a"
 
 
-#: markdown/factory.py:114
+#: markdown/factory.py:83
 msgid "Quote"
 msgid "Quote"
 msgstr "Cytat"
 msgstr "Cytat"
 
 
-#: middleware/user.py:24
+#: middleware/user.py:31
 #, python-format
 #, python-format
 msgid ""
 msgid ""
 "Welcome back, %(username)s! We've signed you in automatically for your "
 "Welcome back, %(username)s! We've signed you in automatically for your "
@@ -5027,19 +5471,19 @@ msgstr ""
 "Witaj ponownie, %(username)s! Dla Twojej wygody, zalogowaliśmy Cię "
 "Witaj ponownie, %(username)s! Dla Twojej wygody, zalogowaliśmy Cię "
 "automatycznie."
 "automatycznie."
 
 
-#: models/forummodel.py:175
+#: models/forummodel.py:176
 msgid "Reports"
 msgid "Reports"
 msgstr "Raporty"
 msgstr "Raporty"
 
 
-#: models/forummodel.py:177
+#: models/forummodel.py:178
 msgid "Root Category"
 msgid "Root Category"
-msgstr ""
+msgstr "Główny korzeń"
 
 
-#: models/postmodel.py:42
+#: models/postmodel.py:43
 msgid "New Posts"
 msgid "New Posts"
 msgstr "Nowe posty"
 msgstr "Nowe posty"
 
 
-#: models/postmodel.py:92
+#: models/postmodel.py:96
 #, python-format
 #, python-format
 msgid "%(username)s has mentioned you in his reply in thread %(thread)s"
 msgid "%(username)s has mentioned you in his reply in thread %(thread)s"
 msgstr "%(username)s zwrócił się do Ciebie w odpowiedzi w temacie %(thread)s"
 msgstr "%(username)s zwrócił się do Ciebie w odpowiedzi w temacie %(thread)s"
@@ -5047,17 +5491,19 @@ msgstr "%(username)s zwrócił się do Ciebie w odpowiedzi w temacie %(thread)s"
 #: models/pruningpolicymodel.py:20
 #: models/pruningpolicymodel.py:20
 msgid "Pruning policy must have at least one pruning criteria set to be valid."
 msgid "Pruning policy must have at least one pruning criteria set to be valid."
 msgstr ""
 msgstr ""
+"Zasada usuwania użytkowników musi zawierać przynajmniej jedno kryterium, aby "
+"była poprawna."
 
 
 #: models/themeadjustmentmodel.py:7
 #: models/themeadjustmentmodel.py:7
 msgid "User agents for this theme are already defined."
 msgid "User agents for this theme are already defined."
-msgstr ""
+msgstr "User agenty dla tego szablonu zostały już wcześniej przypisane."
 
 
 #: models/threadmodel.py:83 templates/cranefly/layout.html:42
 #: models/threadmodel.py:83 templates/cranefly/layout.html:42
 #: templates/cranefly/new_threads.html:10
 #: templates/cranefly/new_threads.html:10
 msgid "New Threads"
 msgid "New Threads"
 msgstr "Nowe tematy"
 msgstr "Nowe tematy"
 
 
-#: models/threadmodel.py:167
+#: models/threadmodel.py:169
 #, python-format
 #, python-format
 msgid "New reply in thread \"%(thread)s\""
 msgid "New reply in thread \"%(thread)s\""
 msgstr "Nowa odpowiedź w temacie \"%(thread)s\""
 msgstr "Nowa odpowiedź w temacie \"%(thread)s\""
@@ -5100,7 +5546,7 @@ msgstr "Ta wiadomość została wysłana przez %(board_name)s."
 #: templates/_email/private_thread_invite.html:3
 #: templates/_email/private_thread_invite.html:3
 #: templates/_email/private_thread_invite.txt:3
 #: templates/_email/private_thread_invite.txt:3
 msgid "You've been invited to private thread"
 msgid "You've been invited to private thread"
-msgstr "Zostałeś zaproszony do prywatnego tematu"
+msgstr "Zostałeś zaproszony do prywatnej dyskusji"
 
 
 #: templates/_email/private_thread_invite.html:6
 #: templates/_email/private_thread_invite.html:6
 #: templates/_email/private_thread_invite.txt:6
 #: templates/_email/private_thread_invite.txt:6
@@ -5110,12 +5556,12 @@ msgid ""
 "you to participate in private thread \"%(thread)s\"."
 "you to participate in private thread \"%(thread)s\"."
 msgstr ""
 msgstr ""
 "%(username)s, otrzymujesz tą wiadomość, ponieważ %(author)s zaprosił Cię do "
 "%(username)s, otrzymujesz tą wiadomość, ponieważ %(author)s zaprosił Cię do "
-"uczestnictwa w prywatnym temacie \"%(thread)s\"."
+"uczestnictwa w prywatnej dyskusji \"%(thread)s\"."
 
 
 #: templates/_email/private_thread_invite.html:7
 #: templates/_email/private_thread_invite.html:7
 #: templates/_email/private_thread_invite.txt:8
 #: templates/_email/private_thread_invite.txt:8
 msgid "You can see this thread by clicking link below:"
 msgid "You can see this thread by clicking link below:"
-msgstr "Możesz zobaczyć ten temat, klikajac na poniższy link:"
+msgstr "Możesz zobaczyć tę dyskusję, klikajac na poniższy link:"
 
 
 #: templates/_email/private_thread_reply_notification.html:3
 #: templates/_email/private_thread_reply_notification.html:3
 #: templates/_email/private_thread_reply_notification.txt:3
 #: templates/_email/private_thread_reply_notification.txt:3
@@ -5194,20 +5640,11 @@ msgstr ""
 "pasek adresu swojej przeglądarki internetowej:"
 "pasek adresu swojej przeglądarki internetowej:"
 
 
 #: templates/_email/users/activation/admin.html:6
 #: templates/_email/users/activation/admin.html:6
-msgid ""
-"Your account will remain inactive until Board Administrator accepts it. "
-"Depending on number of new registrations this may take few minutes or few "
-"days. Thanks for your patience!"
-msgstr ""
-"Twoje konto pozostanie nieaktywne do czasu akceptacji przez administratora "
-"forum. W zależności od ilości nowych rejestracji, może to potrwać od kilku "
-"minut, do kilku dni. Dziękujemy za cierpliwość!"
-
 #: templates/_email/users/activation/admin.txt:6
 #: templates/_email/users/activation/admin.txt:6
 msgid ""
 msgid ""
 "Your account will remain inactive until Board Administrator accepts it. "
 "Your account will remain inactive until Board Administrator accepts it. "
 "Depending on number of new registrations this may take few minutes or few "
 "Depending on number of new registrations this may take few minutes or few "
-"days. Thanks for patience!"
+"days. Thanks for your patience!"
 msgstr ""
 msgstr ""
 "Twoje konto pozostanie nieaktywne do czasu akceptacji przez administratora "
 "Twoje konto pozostanie nieaktywne do czasu akceptacji przez administratora "
 "forum. W zależności od ilości nowych rejestracji, może to potrwać od kilku "
 "forum. W zależności od ilości nowych rejestracji, może to potrwać od kilku "
@@ -5462,7 +5899,7 @@ msgstr[2] "%(total)s administratorów online"
 #: templates/admin/index.html:31
 #: templates/admin/index.html:31
 #, python-format
 #, python-format
 msgid "started %(start)s ago from %(ip)s"
 msgid "started %(start)s ago from %(ip)s"
-msgstr "rozpoczęty %(start)s temu z adresu IP %(ip)s"
+msgstr "zalogowany %(start)s temu z adresu IP %(ip)s"
 
 
 #: templates/admin/index.html:31
 #: templates/admin/index.html:31
 #, python-format
 #, python-format
@@ -5521,6 +5958,7 @@ msgstr ""
 "funkcjonalności."
 "funkcjonalności."
 
 
 #: templates/admin/admin/acl_form.html:15
 #: templates/admin/admin/acl_form.html:15
+#: templates/debug_toolbar/panels/acl.html:8
 msgid "Permission"
 msgid "Permission"
 msgstr "Zezwolenie"
 msgstr "Zezwolenie"
 
 
@@ -5534,11 +5972,11 @@ msgstr "Zapisz i dodaj kolejne"
 
 
 #: templates/admin/admin/acl_form.html:43 templates/admin/admin/form.html:38
 #: templates/admin/admin/acl_form.html:43 templates/admin/admin/form.html:38
 #: templates/admin/prune/apply.html:14
 #: templates/admin/prune/apply.html:14
-#: templates/cranefly/threads/merge.html:52
-#: templates/cranefly/threads/move_posts.html:53
-#: templates/cranefly/threads/move_thread.html:54
-#: templates/cranefly/threads/move_threads.html:52
-#: templates/cranefly/threads/split.html:53
+#: templates/cranefly/threads/merge.html:50
+#: templates/cranefly/threads/move_posts.html:51
+#: templates/cranefly/threads/move_thread.html:52
+#: templates/cranefly/threads/move_threads.html:50
+#: templates/cranefly/threads/split.html:51
 #: templates/cranefly/usercp/avatar_crop.html:17
 #: templates/cranefly/usercp/avatar_crop.html:17
 #: templates/cranefly/usercp/avatar_upload.html:25
 #: templates/cranefly/usercp/avatar_upload.html:25
 #: templates/cranefly/usercp/options.html:26
 #: templates/cranefly/usercp/options.html:26
@@ -5602,7 +6040,7 @@ msgstr "Ignorowanie subskrypcji"
 
 
 #: templates/admin/online/list.html:15
 #: templates/admin/online/list.html:15
 msgid "Registered Member"
 msgid "Registered Member"
-msgstr "Zarejestrowany członek"
+msgstr "Zarejestrowany"
 
 
 #: templates/admin/online/list.html:15
 #: templates/admin/online/list.html:15
 #: templates/cranefly/profiles/details.html:47
 #: templates/cranefly/profiles/details.html:47
@@ -5619,7 +6057,7 @@ msgstr "Użytkownicy do usunięcia"
 
 
 #: templates/admin/ranks/list.html:9
 #: templates/admin/ranks/list.html:9
 msgid "Order"
 msgid "Order"
-msgstr "Kolejność"
+msgstr "Istotność"
 
 
 #: templates/admin/ranks/list.html:14
 #: templates/admin/ranks/list.html:14
 msgid "Special"
 msgid "Special"
@@ -5627,7 +6065,7 @@ msgstr "Specjalna"
 
 
 #: templates/admin/ranks/list.html:14
 #: templates/admin/ranks/list.html:14
 msgid "Tab"
 msgid "Tab"
-msgstr "Zakładka"
+msgstr "Ma kartę"
 
 
 #: templates/admin/ranks/list.html:14
 #: templates/admin/ranks/list.html:14
 msgid "On Index"
 msgid "On Index"
@@ -5709,7 +6147,7 @@ msgstr "Nieaktywny"
 #: templates/cranefly/alerts.html:14 templates/cranefly/alerts.html.py:16
 #: templates/cranefly/alerts.html:14 templates/cranefly/alerts.html.py:16
 #: templates/cranefly/alerts.html:39 templates/cranefly/layout.html:53
 #: templates/cranefly/alerts.html:39 templates/cranefly/layout.html:53
 msgid "Your Notifications"
 msgid "Your Notifications"
-msgstr "Twoje powiadomienia"
+msgstr "Powiadomienia"
 
 
 #: templates/cranefly/alerts.html:30
 #: templates/cranefly/alerts.html:30
 msgid "Looks like you don't have any notifications... yet."
 msgid "Looks like you don't have any notifications... yet."
@@ -5735,33 +6173,41 @@ msgstr "Stare powiadomienie"
 msgid "Image could not be loaded."
 msgid "Image could not be loaded."
 msgstr "Nie udało się wczytać obrazka."
 msgstr "Nie udało się wczytać obrazka."
 
 
-#: templates/cranefly/category.html:49 templates/cranefly/index.html:35
-#: templates/cranefly/threads/list.html:53
+#: templates/cranefly/category.html:47 templates/cranefly/index.html:35
+#: templates/cranefly/threads/list.html:51
 msgid "This forum is empty"
 msgid "This forum is empty"
 msgstr "To forum jest puste"
 msgstr "To forum jest puste"
 
 
-#: templates/cranefly/category.html:52 templates/cranefly/index.html:38
-#: templates/cranefly/threads/list.html:56
+#: templates/cranefly/category.html:50 templates/cranefly/index.html:38
+#: templates/cranefly/threads/list.html:54
 msgid "This forum is protected"
 msgid "This forum is protected"
 msgstr "To forum jest chronione"
 msgstr "To forum jest chronione"
 
 
-#: templates/cranefly/category.html:58 templates/cranefly/index.html:44
-#: templates/cranefly/threads/list.html:62
+#: templates/cranefly/category.html:56 templates/cranefly/index.html:44
+#: templates/cranefly/threads/list.html:60
 #, python-format
 #, python-format
-msgid "%(clicks)s clicks"
-msgstr "%(clicks)s kliknięć"
+msgid "%(clicks)s click"
+msgid_plural "%(clicks)s clicks"
+msgstr[0] "%(clicks)s kliknięcie"
+msgstr[1] "%(clicks)s kliknięcia"
+msgstr[2] "%(clicks)s kliknięć"
 
 
-#: templates/cranefly/category.html:64 templates/cranefly/index.html:50
-#: templates/cranefly/threads/list.html:68
+#: templates/cranefly/category.html:63 templates/cranefly/index.html:51
+#: templates/cranefly/threads/list.html:67
 msgid "Subforums"
 msgid "Subforums"
 msgstr "Subfora"
 msgstr "Subfora"
 
 
-#: templates/cranefly/category.html:83 templates/cranefly/index.html:69
-#: templates/cranefly/threads/list.html:87
+#: templates/cranefly/category.html:74 templates/cranefly/index.html:62
+#: templates/cranefly/threads/list.html:78
+msgid "Click to go to this subforum"
+msgstr "Kliknij, aby przejść do tego subforum"
+
+#: templates/cranefly/category.html:85 templates/cranefly/index.html:73
+#: templates/cranefly/threads/list.html:89
 msgid "Clicks"
 msgid "Clicks"
 msgstr "Kliknięcia"
 msgstr "Kliknięcia"
 
 
-#: templates/cranefly/category.html:94
+#: templates/cranefly/category.html:96
 msgid "Looks like there are no forums to display in this category."
 msgid "Looks like there are no forums to display in this category."
 msgstr ""
 msgstr ""
 "Wygląda na to, że w tej kategorii nie ma żadnych forów do wyświetlenia."
 "Wygląda na to, że w tej kategorii nie ma żadnych forów do wyświetlenia."
@@ -5864,7 +6310,7 @@ msgid "Looks like no forums exist that you have permission to see."
 msgstr "Wygląda na to, że nie istnieje żadne forum, które możesz zobaczyć."
 msgstr "Wygląda na to, że nie istnieje żadne forum, które możesz zobaczyć."
 
 
 #: templates/cranefly/forum_map.html:90
 #: templates/cranefly/forum_map.html:90
-#: templates/cranefly/threads/list.html:210
+#: templates/cranefly/threads/list.html:212
 #, python-format
 #, python-format
 msgid "%(posts)s post - last in %(thread)s"
 msgid "%(posts)s post - last in %(thread)s"
 msgid_plural "%(posts)s posts - last in %(thread)s"
 msgid_plural "%(posts)s posts - last in %(thread)s"
@@ -5873,7 +6319,7 @@ msgstr[1] "%(posts)s posty - ostatni w %(thread)s"
 msgstr[2] "%(posts)s postów - ostatni w %(thread)s"
 msgstr[2] "%(posts)s postów - ostatni w %(thread)s"
 
 
 #: templates/cranefly/forum_map.html:92
 #: templates/cranefly/forum_map.html:92
-#: templates/cranefly/threads/list.html:212
+#: templates/cranefly/threads/list.html:214
 #, python-format
 #, python-format
 msgid "%(posts)s post"
 msgid "%(posts)s post"
 msgid_plural "%(posts)s posts"
 msgid_plural "%(posts)s posts"
@@ -5882,7 +6328,7 @@ msgstr[1] "%(posts)s posty"
 msgstr[2] "%(posts)s postów"
 msgstr[2] "%(posts)s postów"
 
 
 #: templates/cranefly/forum_map.html:101
 #: templates/cranefly/forum_map.html:101
-#: templates/cranefly/threads/list.html:221
+#: templates/cranefly/threads/list.html:223
 #, python-format
 #, python-format
 msgid "%(redirects)s click"
 msgid "%(redirects)s click"
 msgid_plural "%(redirects)s clicks"
 msgid_plural "%(redirects)s clicks"
@@ -5890,17 +6336,17 @@ msgstr[0] "jedno kliknięcie"
 msgstr[1] "%(redirects)s kliknięcia"
 msgstr[1] "%(redirects)s kliknięcia"
 msgstr[2] "%(redirects)s kliknięć"
 msgstr[2] "%(redirects)s kliknięć"
 
 
-#: templates/cranefly/index.html:89
+#: templates/cranefly/index.html:93
 #, python-format
 #, python-format
 msgid "%(rank_name)s Online"
 msgid "%(rank_name)s Online"
 msgstr "%(rank_name)s Online"
 msgstr "%(rank_name)s Online"
 
 
-#: templates/cranefly/index.html:106 templates/cranefly/layout.html:41
+#: templates/cranefly/index.html:116 templates/cranefly/layout.html:41
 #: templates/cranefly/popular_threads.html:10
 #: templates/cranefly/popular_threads.html:10
 msgid "Popular Threads"
 msgid "Popular Threads"
 msgstr "Popularne tematy"
 msgstr "Popularne tematy"
 
 
-#: templates/cranefly/index.html:138
+#: templates/cranefly/index.html:148
 msgid "Mark forums read"
 msgid "Mark forums read"
 msgstr "Oznacz wszystkie fora jako przeczytane"
 msgstr "Oznacz wszystkie fora jako przeczytane"
 
 
@@ -5918,7 +6364,7 @@ msgstr "Strona główna forum"
 
 
 #: templates/cranefly/layout.html:44
 #: templates/cranefly/layout.html:44
 msgid "Search Community"
 msgid "Search Community"
-msgstr "Przeszukaj forum"
+msgstr "Wyszukiwanie zaawansowane"
 
 
 #: templates/cranefly/layout.html:52
 #: templates/cranefly/layout.html:52
 msgid "Go to your profile"
 msgid "Go to your profile"
@@ -5930,11 +6376,11 @@ msgstr "Masz nowe powiadomienia!"
 
 
 #: templates/cranefly/layout.html:55
 #: templates/cranefly/layout.html:55
 msgid "There are unread Private Threads!"
 msgid "There are unread Private Threads!"
-msgstr "Masz nieprzeczytane prywatne tematy!"
+msgstr "Masz nieprzeczytane prywatne dyskusje!"
 
 
 #: templates/cranefly/layout.html:55
 #: templates/cranefly/layout.html:55
 msgid "Your Private Threads"
 msgid "Your Private Threads"
-msgstr "Twoje prywatne tematy"
+msgstr "Prywatne dyskusje"
 
 
 #: templates/cranefly/layout.html:57 templates/cranefly/newsfeed.html:10
 #: templates/cranefly/layout.html:57 templates/cranefly/newsfeed.html:10
 msgid "Your News Feed"
 msgid "Your News Feed"
@@ -5942,19 +6388,19 @@ msgstr "Nowości dla Ciebie"
 
 
 #: templates/cranefly/layout.html:58 templates/cranefly/watched.html:10
 #: templates/cranefly/layout.html:58 templates/cranefly/watched.html:10
 msgid "Threads you are watching"
 msgid "Threads you are watching"
-msgstr "Tematy obserwowane przez Ciebie"
+msgstr "Obserwowane tematy"
 
 
 #: templates/cranefly/layout.html:59
 #: templates/cranefly/layout.html:59
 msgid "Edit your profile options"
 msgid "Edit your profile options"
-msgstr "Zmodyfikuj ustawienia swojego profilu"
+msgstr "Ustawienia profilu"
 
 
 #: templates/cranefly/layout.html:60
 #: templates/cranefly/layout.html:60
 msgid "Sign Out and browse as guest"
 msgid "Sign Out and browse as guest"
 msgstr "Wyloguj się i przegladaj forum jako gość"
 msgstr "Wyloguj się i przegladaj forum jako gość"
 
 
-#: templates/cranefly/layout.html:64
-msgid "Sign In using your account data"
-msgstr "Zaloguj się na swoje konto"
+#: templates/cranefly/layout.html:64 templates/cranefly/signin.html:13
+msgid "Sign In to Your Account"
+msgstr "Formularz logowania"
 
 
 #: templates/cranefly/layout.html:65 templates/cranefly/register.html:13
 #: templates/cranefly/layout.html:65 templates/cranefly/register.html:13
 msgid "Register new account"
 msgid "Register new account"
@@ -5973,14 +6419,14 @@ msgstr "Strona %(page)s"
 #: templates/cranefly/popular_threads.html:20
 #: templates/cranefly/popular_threads.html:20
 #: templates/cranefly/watched.html:32
 #: templates/cranefly/watched.html:32
 #: templates/cranefly/private_threads/list.html:41
 #: templates/cranefly/private_threads/list.html:41
-#: templates/cranefly/threads/list.html:116
+#: templates/cranefly/threads/list.html:118
 msgid "Thread"
 msgid "Thread"
 msgstr "Temat"
 msgstr "Temat"
 
 
 #: templates/cranefly/new_threads.html:21
 #: templates/cranefly/new_threads.html:21
 #: templates/cranefly/popular_threads.html:21
 #: templates/cranefly/popular_threads.html:21
 #: templates/cranefly/private_threads/list.html:42
 #: templates/cranefly/private_threads/list.html:42
-#: templates/cranefly/threads/list.html:117
+#: templates/cranefly/threads/list.html:119
 msgid "Rating"
 msgid "Rating"
 msgstr "Ocena"
 msgstr "Ocena"
 
 
@@ -5988,7 +6434,7 @@ msgstr "Ocena"
 #: templates/cranefly/popular_threads.html:22
 #: templates/cranefly/popular_threads.html:22
 #: templates/cranefly/watched.html:33
 #: templates/cranefly/watched.html:33
 #: templates/cranefly/private_threads/list.html:43
 #: templates/cranefly/private_threads/list.html:43
-#: templates/cranefly/threads/list.html:118
+#: templates/cranefly/threads/list.html:120
 msgid "Activity"
 msgid "Activity"
 msgstr "Aktywność"
 msgstr "Aktywność"
 
 
@@ -5996,7 +6442,7 @@ msgstr "Aktywność"
 #: templates/cranefly/popular_threads.html:33
 #: templates/cranefly/popular_threads.html:33
 #: templates/cranefly/watched.html:41
 #: templates/cranefly/watched.html:41
 #: templates/cranefly/private_threads/list.html:54
 #: templates/cranefly/private_threads/list.html:54
-#: templates/cranefly/threads/list.html:129
+#: templates/cranefly/threads/list.html:131
 msgid "Click to see first unread post"
 msgid "Click to see first unread post"
 msgstr "Kliknij, żeby zobaczyć ostatni nieprzeczytany post"
 msgstr "Kliknij, żeby zobaczyć ostatni nieprzeczytany post"
 
 
@@ -6004,7 +6450,7 @@ msgstr "Kliknij, żeby zobaczyć ostatni nieprzeczytany post"
 #: templates/cranefly/popular_threads.html:35
 #: templates/cranefly/popular_threads.html:35
 #: templates/cranefly/watched.html:43
 #: templates/cranefly/watched.html:43
 #: templates/cranefly/private_threads/list.html:56
 #: templates/cranefly/private_threads/list.html:56
-#: templates/cranefly/threads/list.html:131
+#: templates/cranefly/threads/list.html:133
 msgid "Click to see last post"
 msgid "Click to see last post"
 msgstr "Kliknij, aby zobaczyć ostatni post"
 msgstr "Kliknij, aby zobaczyć ostatni post"
 
 
@@ -6013,26 +6459,26 @@ msgstr "Kliknij, aby zobaczyć ostatni post"
 #: templates/cranefly/watched.html:47
 #: templates/cranefly/watched.html:47
 #, python-format
 #, python-format
 msgid "by %(user)s in %(forum)s %(start)s"
 msgid "by %(user)s in %(forum)s %(start)s"
-msgstr "przez %(user)s w %(forum)s %(start)s"
+msgstr "od %(user)s w %(forum)s %(start)s"
 
 
 #: templates/cranefly/new_threads.html:43
 #: templates/cranefly/new_threads.html:43
 #: templates/cranefly/popular_threads.html:43
 #: templates/cranefly/popular_threads.html:43
 #: templates/cranefly/private_threads/list.html:70
 #: templates/cranefly/private_threads/list.html:70
-#: templates/cranefly/threads/list.html:145
+#: templates/cranefly/threads/list.html:147
 msgid "This thread is an annoucement"
 msgid "This thread is an annoucement"
 msgstr "Ten temat jest ogłoszeniem"
 msgstr "Ten temat jest ogłoszeniem"
 
 
 #: templates/cranefly/new_threads.html:46
 #: templates/cranefly/new_threads.html:46
 #: templates/cranefly/popular_threads.html:46
 #: templates/cranefly/popular_threads.html:46
 #: templates/cranefly/private_threads/list.html:73
 #: templates/cranefly/private_threads/list.html:73
-#: templates/cranefly/threads/list.html:148
+#: templates/cranefly/threads/list.html:150
 msgid "This thread is sticky"
 msgid "This thread is sticky"
 msgstr "Ten temat jest przyklejony"
 msgstr "Ten temat jest przyklejony"
 
 
 #: templates/cranefly/new_threads.html:49
 #: templates/cranefly/new_threads.html:49
 #: templates/cranefly/popular_threads.html:49
 #: templates/cranefly/popular_threads.html:49
 #: templates/cranefly/private_threads/list.html:82
 #: templates/cranefly/private_threads/list.html:82
-#: templates/cranefly/threads/list.html:157
+#: templates/cranefly/threads/list.html:159
 msgid "This thread is closed"
 msgid "This thread is closed"
 msgstr "Ten temat jest zamknięty"
 msgstr "Ten temat jest zamknięty"
 
 
@@ -6040,10 +6486,10 @@ msgstr "Ten temat jest zamknięty"
 #: templates/cranefly/popular_threads.html:60
 #: templates/cranefly/popular_threads.html:60
 #: templates/cranefly/watched.html:52
 #: templates/cranefly/watched.html:52
 #: templates/cranefly/private_threads/list.html:93
 #: templates/cranefly/private_threads/list.html:93
-#: templates/cranefly/threads/list.html:168
+#: templates/cranefly/threads/list.html:170
 #, python-format
 #, python-format
 msgid "last by %(user)s %(last)s"
 msgid "last by %(user)s %(last)s"
-msgstr "ostatni przez %(user)s %(last)s"
+msgstr "ostatnia od %(user)s %(last)s"
 
 
 #: templates/cranefly/new_threads.html:70
 #: templates/cranefly/new_threads.html:70
 msgid "No new threads were started in last 48 hours."
 msgid "No new threads were started in last 48 hours."
@@ -6053,11 +6499,11 @@ msgstr "W ciągu ostatnich 48 godzin nie rozpoczęto żadnych nowych tematów."
 #: templates/cranefly/popular_threads.html:76
 #: templates/cranefly/popular_threads.html:76
 #: templates/cranefly/watched.html:96
 #: templates/cranefly/watched.html:96
 #: templates/cranefly/private_threads/list.html:138
 #: templates/cranefly/private_threads/list.html:138
-#: templates/cranefly/threads/list.html:229
+#: templates/cranefly/threads/list.html:231
 #, python-format
 #, python-format
 msgid "%(replies)s reply"
 msgid "%(replies)s reply"
 msgid_plural "%(replies)s replies"
 msgid_plural "%(replies)s replies"
-msgstr[0] "jedna odpowiedź"
+msgstr[0] "%(replies)s odpowiedź"
 msgstr[1] "%(replies)s odpowiedzi"
 msgstr[1] "%(replies)s odpowiedzi"
 msgstr[2] "%(replies)s odpowiedzi"
 msgstr[2] "%(replies)s odpowiedzi"
 
 
@@ -6108,10 +6554,6 @@ msgstr "Operacja zmiany hasła"
 msgid "Reset Password"
 msgid "Reset Password"
 msgstr "Zresetuj hasło"
 msgstr "Zresetuj hasło"
 
 
-#: templates/cranefly/signin.html:13
-msgid "Sign In to Your Account"
-msgstr "Formularz logowania"
-
 #: templates/cranefly/signin.html:22
 #: templates/cranefly/signin.html:22
 msgid "Click here if you forgot your sign in credentials."
 msgid "Click here if you forgot your sign in credentials."
 msgstr "Kliknij tutaj, jeżeli zapomniałeś danych do swojego konta."
 msgstr "Kliknij tutaj, jeżeli zapomniałeś danych do swojego konta."
@@ -6180,7 +6622,7 @@ msgstr "Nowsze tematy"
 
 
 #: templates/cranefly/watched.html:123 templates/cranefly/watched.html:124
 #: templates/cranefly/watched.html:123 templates/cranefly/watched.html:124
 #: templates/cranefly/private_threads/list.html:154
 #: templates/cranefly/private_threads/list.html:154
-#: templates/cranefly/threads/list.html:245
+#: templates/cranefly/threads/list.html:247
 msgid "Older Threads"
 msgid "Older Threads"
 msgstr "Starsze tematy"
 msgstr "Starsze tematy"
 
 
@@ -6188,10 +6630,10 @@ msgstr "Starsze tematy"
 #: templates/cranefly/private_threads/changelog.html:19
 #: templates/cranefly/private_threads/changelog.html:19
 #: templates/cranefly/private_threads/changelog_diff.html:9
 #: templates/cranefly/private_threads/changelog_diff.html:9
 #: templates/cranefly/private_threads/changelog_diff.html:20
 #: templates/cranefly/private_threads/changelog_diff.html:20
-#: templates/cranefly/threads/changelog.html:11
-#: templates/cranefly/threads/changelog.html:21
-#: templates/cranefly/threads/changelog_diff.html:11
-#: templates/cranefly/threads/changelog_diff.html:22
+#: templates/cranefly/threads/changelog.html:9
+#: templates/cranefly/threads/changelog.html:19
+#: templates/cranefly/threads/changelog_diff.html:9
+#: templates/cranefly/threads/changelog_diff.html:20
 #, python-format
 #, python-format
 msgid "Post #%(post)s Changelog"
 msgid "Post #%(post)s Changelog"
 msgstr "Historia zmian postu #%(post)s"
 msgstr "Historia zmian postu #%(post)s"
@@ -6199,17 +6641,17 @@ msgstr "Historia zmian postu #%(post)s"
 #: templates/cranefly/private_threads/changelog.html:23
 #: templates/cranefly/private_threads/changelog.html:23
 #: templates/cranefly/private_threads/details.html:23
 #: templates/cranefly/private_threads/details.html:23
 #: templates/cranefly/private_threads/posting.html:144
 #: templates/cranefly/private_threads/posting.html:144
-#: templates/cranefly/private_threads/thread.html:105
-#: templates/cranefly/private_threads/thread.html:107
-#: templates/cranefly/private_threads/thread.html:190
-#: templates/cranefly/private_threads/thread.html:192
-#: templates/cranefly/threads/changelog.html:25
-#: templates/cranefly/threads/details.html:25
-#: templates/cranefly/threads/posting.html:153
-#: templates/cranefly/threads/thread.html:99
-#: templates/cranefly/threads/thread.html:101
-#: templates/cranefly/threads/thread.html:184
-#: templates/cranefly/threads/thread.html:186
+#: templates/cranefly/private_threads/thread.html:101
+#: templates/cranefly/private_threads/thread.html:103
+#: templates/cranefly/private_threads/thread.html:174
+#: templates/cranefly/private_threads/thread.html:176
+#: templates/cranefly/threads/changelog.html:23
+#: templates/cranefly/threads/details.html:23
+#: templates/cranefly/threads/posting.html:151
+#: templates/cranefly/threads/thread.html:93
+#: templates/cranefly/threads/thread.html:95
+#: templates/cranefly/threads/thread.html:162
+#: templates/cranefly/threads/thread.html:164
 #, python-format
 #, python-format
 msgid "One edit"
 msgid "One edit"
 msgid_plural "%(edits)s edits"
 msgid_plural "%(edits)s edits"
@@ -6219,20 +6661,20 @@ msgstr[2] "%(edits)s edycji"
 
 
 #: templates/cranefly/private_threads/changelog.html:24
 #: templates/cranefly/private_threads/changelog.html:24
 #: templates/cranefly/private_threads/details.html:24
 #: templates/cranefly/private_threads/details.html:24
-#: templates/cranefly/private_threads/thread.html:209
-#: templates/cranefly/threads/changelog.html:26
-#: templates/cranefly/threads/details.html:26
-#: templates/cranefly/threads/thread.html:203
+#: templates/cranefly/private_threads/thread.html:193
+#: templates/cranefly/threads/changelog.html:24
+#: templates/cranefly/threads/details.html:24
+#: templates/cranefly/threads/thread.html:177
 msgid "Protected"
 msgid "Protected"
 msgstr "Chroniony"
 msgstr "Chroniony"
 
 
 #: templates/cranefly/private_threads/changelog.html:36
 #: templates/cranefly/private_threads/changelog.html:36
-#: templates/cranefly/threads/changelog.html:38
+#: templates/cranefly/threads/changelog.html:36
 msgid "Change Log"
 msgid "Change Log"
 msgstr "Historia zmian"
 msgstr "Historia zmian"
 
 
 #: templates/cranefly/private_threads/changelog.html:56
 #: templates/cranefly/private_threads/changelog.html:56
-#: templates/cranefly/threads/changelog.html:58
+#: templates/cranefly/threads/changelog.html:56
 #, python-format
 #, python-format
 msgid "Added one character to post."
 msgid "Added one character to post."
 msgid_plural "Added %(chars)s characters to post."
 msgid_plural "Added %(chars)s characters to post."
@@ -6241,7 +6683,7 @@ msgstr[1] "Dodano %(chars)s znaki do treści postu."
 msgstr[2] "Dodano %(chars)s znaków do treści postu."
 msgstr[2] "Dodano %(chars)s znaków do treści postu."
 
 
 #: templates/cranefly/private_threads/changelog.html:58
 #: templates/cranefly/private_threads/changelog.html:58
-#: templates/cranefly/threads/changelog.html:60
+#: templates/cranefly/threads/changelog.html:58
 #, python-format
 #, python-format
 msgid "Removed one character from post."
 msgid "Removed one character from post."
 msgid_plural "Removed %(chars)s characters from post."
 msgid_plural "Removed %(chars)s characters from post."
@@ -6250,43 +6692,43 @@ msgstr[1] "Usunięto %(chars)s znaki z treści postu."
 msgstr[2] "Usunięto %(chars)s znaków z treści postu."
 msgstr[2] "Usunięto %(chars)s znaków z treści postu."
 
 
 #: templates/cranefly/private_threads/changelog.html:60
 #: templates/cranefly/private_threads/changelog.html:60
-#: templates/cranefly/threads/changelog.html:62
+#: templates/cranefly/threads/changelog.html:60
 msgid "No change in message's length."
 msgid "No change in message's length."
 msgstr "Bez zmian w długości postu."
 msgstr "Bez zmian w długości postu."
 
 
 #: templates/cranefly/private_threads/changelog.html:61
 #: templates/cranefly/private_threads/changelog.html:61
-#: templates/cranefly/threads/changelog.html:63
+#: templates/cranefly/threads/changelog.html:61
 #, python-format
 #, python-format
 msgid "Changed thread name from \"%(old)s\" to \"%(new)s\"."
 msgid "Changed thread name from \"%(old)s\" to \"%(new)s\"."
 msgstr "Zmieniono nazwę tematu z \"%(old)s\" na \"%(new)s\"."
 msgstr "Zmieniono nazwę tematu z \"%(old)s\" na \"%(new)s\"."
 
 
 #: templates/cranefly/private_threads/changelog.html:61
 #: templates/cranefly/private_threads/changelog.html:61
-#: templates/cranefly/threads/changelog.html:63
+#: templates/cranefly/threads/changelog.html:61
 #, python-format
 #, python-format
 msgid "Renamed thread from \"%(old)s\" to \"%(new)s\"."
 msgid "Renamed thread from \"%(old)s\" to \"%(new)s\"."
 msgstr "Zmieniono nazwę tematu z \"%(old)s\" na \"%(new)s\"."
 msgstr "Zmieniono nazwę tematu z \"%(old)s\" na \"%(new)s\"."
 
 
 #: templates/cranefly/private_threads/changelog.html:63
 #: templates/cranefly/private_threads/changelog.html:63
-#: templates/cranefly/threads/changelog.html:65
+#: templates/cranefly/threads/changelog.html:63
 #, python-format
 #, python-format
 msgid "By %(user)s %(date)s"
 msgid "By %(user)s %(date)s"
 msgstr "Przez %(user)s %(date)s"
 msgstr "Przez %(user)s %(date)s"
 
 
 #: templates/cranefly/private_threads/changelog.html:72
 #: templates/cranefly/private_threads/changelog.html:72
-#: templates/cranefly/threads/changelog.html:74
+#: templates/cranefly/threads/changelog.html:72
 msgid "This post was never edited."
 msgid "This post was never edited."
 msgstr "Ten post nigdy nie był zmieniany."
 msgstr "Ten post nigdy nie był zmieniany."
 
 
 #: templates/cranefly/private_threads/changelog_diff.html:10
 #: templates/cranefly/private_threads/changelog_diff.html:10
 #: templates/cranefly/private_threads/changelog_diff.html:20
 #: templates/cranefly/private_threads/changelog_diff.html:20
-#: templates/cranefly/threads/changelog_diff.html:12
-#: templates/cranefly/threads/changelog_diff.html:22
+#: templates/cranefly/threads/changelog_diff.html:10
+#: templates/cranefly/threads/changelog_diff.html:20
 #, python-format
 #, python-format
 msgid "Edit from %(date)s"
 msgid "Edit from %(date)s"
 msgstr "Zmiana z %(date)s"
 msgstr "Zmiana z %(date)s"
 
 
 #: templates/cranefly/private_threads/changelog_diff.html:29
 #: templates/cranefly/private_threads/changelog_diff.html:29
-#: templates/cranefly/threads/changelog_diff.html:31
+#: templates/cranefly/threads/changelog_diff.html:29
 #, python-format
 #, python-format
 msgid "Added one character"
 msgid "Added one character"
 msgid_plural "Added %(chars)s characters"
 msgid_plural "Added %(chars)s characters"
@@ -6295,7 +6737,7 @@ msgstr[1] "Dodano %(chars)s znaki"
 msgstr[2] "Dodano %(chars)s znaków"
 msgstr[2] "Dodano %(chars)s znaków"
 
 
 #: templates/cranefly/private_threads/changelog_diff.html:31
 #: templates/cranefly/private_threads/changelog_diff.html:31
-#: templates/cranefly/threads/changelog_diff.html:33
+#: templates/cranefly/threads/changelog_diff.html:31
 #, python-format
 #, python-format
 msgid "Removed one character"
 msgid "Removed one character"
 msgid_plural "Removed %(chars)s characters"
 msgid_plural "Removed %(chars)s characters"
@@ -6304,53 +6746,53 @@ msgstr[1] "Usunięto %(chars)s znaki"
 msgstr[2] "Usunięto %(chars)s znaków"
 msgstr[2] "Usunięto %(chars)s znaków"
 
 
 #: templates/cranefly/private_threads/changelog_diff.html:55
 #: templates/cranefly/private_threads/changelog_diff.html:55
-#: templates/cranefly/threads/changelog_diff.html:57
+#: templates/cranefly/threads/changelog_diff.html:55
 msgid "Revert this edit"
 msgid "Revert this edit"
 msgstr "Cofnij tą zmianę"
 msgstr "Cofnij tą zmianę"
 
 
 #: templates/cranefly/private_threads/details.html:9
 #: templates/cranefly/private_threads/details.html:9
 #: templates/cranefly/private_threads/details.html:19
 #: templates/cranefly/private_threads/details.html:19
-#: templates/cranefly/threads/details.html:11
-#: templates/cranefly/threads/details.html:21
+#: templates/cranefly/threads/details.html:9
+#: templates/cranefly/threads/details.html:19
 #, python-format
 #, python-format
 msgid "Post #%(post)s Info"
 msgid "Post #%(post)s Info"
 msgstr "Informacje o poście #%(post)s"
 msgstr "Informacje o poście #%(post)s"
 
 
 #: templates/cranefly/private_threads/details.html:32
 #: templates/cranefly/private_threads/details.html:32
-#: templates/cranefly/threads/details.html:34
+#: templates/cranefly/threads/details.html:32
 msgid "UserAgent"
 msgid "UserAgent"
 msgstr "UserAgent"
 msgstr "UserAgent"
 
 
 #: templates/cranefly/private_threads/list.html:33
 #: templates/cranefly/private_threads/list.html:33
 #: templates/cranefly/private_threads/list.html:129
 #: templates/cranefly/private_threads/list.html:129
-#: templates/cranefly/threads/list.html:108
-#: templates/cranefly/threads/list.html:198
+#: templates/cranefly/threads/list.html:110
+#: templates/cranefly/threads/list.html:200
 msgid "New Thread"
 msgid "New Thread"
 msgstr "Załóż nowy temat"
 msgstr "Załóż nowy temat"
 
 
 #: templates/cranefly/private_threads/list.html:60
 #: templates/cranefly/private_threads/list.html:60
-#: templates/cranefly/threads/list.html:135
+#: templates/cranefly/threads/list.html:137
 #, python-format
 #, python-format
 msgid "by %(user)s %(start)s"
 msgid "by %(user)s %(start)s"
 msgstr "przez %(user)s %(start)s"
 msgstr "przez %(user)s %(start)s"
 
 
 #: templates/cranefly/private_threads/list.html:64
 #: templates/cranefly/private_threads/list.html:64
-#: templates/cranefly/threads/list.html:139
+#: templates/cranefly/threads/list.html:141
 msgid "This thread has reported replies"
 msgid "This thread has reported replies"
 msgstr "Ten temat ma odpowiedzi, które zostały zgłoszone do moderacji"
 msgstr "Ten temat ma odpowiedzi, które zostały zgłoszone do moderacji"
 
 
 #: templates/cranefly/private_threads/list.html:67
 #: templates/cranefly/private_threads/list.html:67
-#: templates/cranefly/threads/list.html:142
+#: templates/cranefly/threads/list.html:144
 msgid "This thread has unreviewed replies"
 msgid "This thread has unreviewed replies"
 msgstr "Ten temat ma odpowiedzi, które oczekują na akceptację"
 msgstr "Ten temat ma odpowiedzi, które oczekują na akceptację"
 
 
 #: templates/cranefly/private_threads/list.html:76
 #: templates/cranefly/private_threads/list.html:76
-#: templates/cranefly/threads/list.html:151
+#: templates/cranefly/threads/list.html:153
 msgid "This thread awaits review"
 msgid "This thread awaits review"
 msgstr "Ten temat oczekuje na akceptację"
 msgstr "Ten temat oczekuje na akceptację"
 
 
 #: templates/cranefly/private_threads/list.html:79
 #: templates/cranefly/private_threads/list.html:79
-#: templates/cranefly/threads/list.html:154
+#: templates/cranefly/threads/list.html:156
 msgid "This thread is deleted"
 msgid "This thread is deleted"
 msgstr "Ten temat został usunięty"
 msgstr "Ten temat został usunięty"
 
 
@@ -6360,31 +6802,31 @@ msgstr "Aktualnie nie uczestniczysz w żadnej prywatnej dyskusji."
 
 
 #: templates/cranefly/private_threads/list.html:106
 #: templates/cranefly/private_threads/list.html:106
 msgid "There are no unread private threads."
 msgid "There are no unread private threads."
-msgstr "Nie masz nieprzeczytanych prywatnych tematów."
+msgstr "Nie masz nieprzeczytanych prywatnych dyskusji."
 
 
 #: templates/cranefly/private_threads/list.html:108
 #: templates/cranefly/private_threads/list.html:108
 msgid "You have started no private threads."
 msgid "You have started no private threads."
-msgstr "Nie rozpocząłeś żadnych prywatnych tematów."
+msgstr "Nie rozpocząłeś żadnych prywatnych dyskusji."
 
 
 #: templates/cranefly/private_threads/list.html:154
 #: templates/cranefly/private_threads/list.html:154
-#: templates/cranefly/threads/list.html:245
+#: templates/cranefly/threads/list.html:247
 msgid "First Page"
 msgid "First Page"
 msgstr "Pierwsza strona"
 msgstr "Pierwsza strona"
 
 
 #: templates/cranefly/private_threads/list.html:154
 #: templates/cranefly/private_threads/list.html:154
-#: templates/cranefly/private_threads/thread.html:496
-#: templates/cranefly/threads/list.html:245
-#: templates/cranefly/threads/thread.html:477
+#: templates/cranefly/private_threads/thread.html:511
+#: templates/cranefly/threads/list.html:247
+#: templates/cranefly/threads/thread.html:484
 msgid "First"
 msgid "First"
 msgstr "Pierwszy"
 msgstr "Pierwszy"
 
 
 #: templates/cranefly/private_threads/list.html:154
 #: templates/cranefly/private_threads/list.html:154
-#: templates/cranefly/threads/list.html:245
+#: templates/cranefly/threads/list.html:247
 msgid "Newest Threads"
 msgid "Newest Threads"
 msgstr "Nowsze tematy"
 msgstr "Nowsze tematy"
 
 
 #: templates/cranefly/private_threads/list.html:170
 #: templates/cranefly/private_threads/list.html:170
-#: templates/cranefly/threads/list.html:271
+#: templates/cranefly/threads/list.html:273
 msgid ""
 msgid ""
 "Are you sure you want to delete selected threads? This action is not "
 "Are you sure you want to delete selected threads? This action is not "
 "reversible!"
 "reversible!"
@@ -6394,49 +6836,49 @@ msgstr ""
 
 
 #: templates/cranefly/private_threads/posting.html:81
 #: templates/cranefly/private_threads/posting.html:81
 #: templates/cranefly/private_threads/posting.html:178
 #: templates/cranefly/private_threads/posting.html:178
-#: templates/cranefly/threads/posting.html:90
-#: templates/cranefly/threads/posting.html:187
+#: templates/cranefly/threads/posting.html:88
+#: templates/cranefly/threads/posting.html:185
 msgid "Preview"
 msgid "Preview"
 msgstr "Podgląd"
 msgstr "Podgląd"
 
 
 #: templates/cranefly/private_threads/posting.html:127
 #: templates/cranefly/private_threads/posting.html:127
-#: templates/cranefly/threads/posting.html:136
+#: templates/cranefly/threads/posting.html:134
 msgid "Post New Thread"
 msgid "Post New Thread"
-msgstr "Zakładanie nowego tematu"
+msgstr "Nowy temat"
 
 
 #: templates/cranefly/private_threads/posting.html:129
 #: templates/cranefly/private_threads/posting.html:129
-#: templates/cranefly/threads/posting.html:138
+#: templates/cranefly/threads/posting.html:136
 msgid "Edit Thread"
 msgid "Edit Thread"
 msgstr "Modyfikacja tematu"
 msgstr "Modyfikacja tematu"
 
 
 #: templates/cranefly/private_threads/posting.html:131
 #: templates/cranefly/private_threads/posting.html:131
-#: templates/cranefly/threads/posting.html:140
+#: templates/cranefly/threads/posting.html:138
 msgid "Post New Reply"
 msgid "Post New Reply"
-msgstr "Tworzenie nowej odpowiedzi"
+msgstr "Nowa odpowiedź"
 
 
 #: templates/cranefly/private_threads/posting.html:133
 #: templates/cranefly/private_threads/posting.html:133
-#: templates/cranefly/threads/posting.html:142
+#: templates/cranefly/threads/posting.html:140
 msgid "Edit Reply"
 msgid "Edit Reply"
 msgstr "Edycja odpowiedzi"
 msgstr "Edycja odpowiedzi"
 
 
 #: templates/cranefly/private_threads/posting.html:140
 #: templates/cranefly/private_threads/posting.html:140
 #: templates/cranefly/private_threads/posting.html:149
 #: templates/cranefly/private_threads/posting.html:149
 #: templates/cranefly/private_threads/thread.html:22
 #: templates/cranefly/private_threads/thread.html:22
-#: templates/cranefly/threads/posting.html:149
-#: templates/cranefly/threads/posting.html:158
-#: templates/cranefly/threads/thread.html:24
+#: templates/cranefly/threads/posting.html:147
+#: templates/cranefly/threads/posting.html:156
+#: templates/cranefly/threads/thread.html:22
 msgid "Not Reviewed"
 msgid "Not Reviewed"
 msgstr "Oczekuje na akceptację"
 msgstr "Oczekuje na akceptację"
 
 
 #: templates/cranefly/private_threads/posting.html:146
 #: templates/cranefly/private_threads/posting.html:146
-#: templates/cranefly/threads/posting.html:155
+#: templates/cranefly/threads/posting.html:153
 msgid "First edit"
 msgid "First edit"
 msgstr "Pierwsza edycja"
 msgstr "Pierwsza edycja"
 
 
 #: templates/cranefly/private_threads/posting.html:157
 #: templates/cranefly/private_threads/posting.html:157
 #: templates/cranefly/private_threads/thread.html:26
 #: templates/cranefly/private_threads/thread.html:26
-#: templates/cranefly/threads/posting.html:166
-#: templates/cranefly/threads/thread.html:28
+#: templates/cranefly/threads/posting.html:164
+#: templates/cranefly/threads/thread.html:26
 #, python-format
 #, python-format
 msgid "One reply"
 msgid "One reply"
 msgid_plural "%(replies)s replies"
 msgid_plural "%(replies)s replies"
@@ -6446,249 +6888,258 @@ msgstr[2] "%(replies)s odpowiedzi"
 
 
 #: templates/cranefly/private_threads/posting.html:159
 #: templates/cranefly/private_threads/posting.html:159
 #: templates/cranefly/private_threads/thread.html:28
 #: templates/cranefly/private_threads/thread.html:28
-#: templates/cranefly/threads/posting.html:168
-#: templates/cranefly/threads/thread.html:30
+#: templates/cranefly/threads/posting.html:166
+#: templates/cranefly/threads/thread.html:28
 msgid "No replies"
 msgid "No replies"
 msgstr "Brak odpowiedzi"
 msgstr "Brak odpowiedzi"
 
 
 #: templates/cranefly/private_threads/posting.html:162
 #: templates/cranefly/private_threads/posting.html:162
 #: templates/cranefly/private_threads/thread.html:30
 #: templates/cranefly/private_threads/thread.html:30
-#: templates/cranefly/threads/posting.html:171
-#: templates/cranefly/threads/thread.html:32
+#: templates/cranefly/threads/posting.html:169
+#: templates/cranefly/threads/thread.html:30
 msgid "Locked"
 msgid "Locked"
 msgstr "Zamknięty"
 msgstr "Zamknięty"
 
 
 #: templates/cranefly/private_threads/posting.html:168
 #: templates/cranefly/private_threads/posting.html:168
-#: templates/cranefly/threads/posting.html:177
+#: templates/cranefly/threads/posting.html:175
 msgid "Post Thread"
 msgid "Post Thread"
 msgstr "Załóż temat"
 msgstr "Załóż temat"
 
 
 #: templates/cranefly/private_threads/posting.html:170
 #: templates/cranefly/private_threads/posting.html:170
-#: templates/cranefly/threads/posting.html:179
+#: templates/cranefly/threads/posting.html:177
 msgid "Post Reply"
 msgid "Post Reply"
 msgstr "Wyślij odpowiedź"
 msgstr "Wyślij odpowiedź"
 
 
 #: templates/cranefly/private_threads/thread.html:55
 #: templates/cranefly/private_threads/thread.html:55
-#: templates/cranefly/private_threads/thread.html:260
-#: templates/cranefly/private_threads/thread.html:364
-#: templates/cranefly/threads/thread.html:48
-#: templates/cranefly/threads/thread.html:293
-#: templates/cranefly/threads/thread.html:392
+#: templates/cranefly/private_threads/thread.html:244
+#: templates/cranefly/private_threads/thread.html:375
+#: templates/cranefly/threads/thread.html:46
+#: templates/cranefly/threads/thread.html:267
+#: templates/cranefly/threads/thread.html:395
 msgid "Reply"
 msgid "Reply"
-msgstr "Odpowiedź"
+msgstr "Odpowiedz"
 
 
 #: templates/cranefly/private_threads/thread.html:58
 #: templates/cranefly/private_threads/thread.html:58
-#: templates/cranefly/threads/thread.html:51
+#: templates/cranefly/threads/thread.html:49
 msgid "Remove thread from watched list"
 msgid "Remove thread from watched list"
 msgstr "Zaprzestań obserwacji tego tematu"
 msgstr "Zaprzestań obserwacji tego tematu"
 
 
 #: templates/cranefly/private_threads/thread.html:60
 #: templates/cranefly/private_threads/thread.html:60
-#: templates/cranefly/threads/thread.html:53
+#: templates/cranefly/threads/thread.html:51
 msgid "Don't e-mail me anymore if anyone replies to this thread"
 msgid "Don't e-mail me anymore if anyone replies to this thread"
 msgstr "Nie informuj mnie już e-mailowo o odpowiedziach w tym temacie"
 msgstr "Nie informuj mnie już e-mailowo o odpowiedziach w tym temacie"
 
 
 #: templates/cranefly/private_threads/thread.html:62
 #: templates/cranefly/private_threads/thread.html:62
-#: templates/cranefly/threads/thread.html:55
+#: templates/cranefly/threads/thread.html:53
 msgid "E-mail me if anyone replies"
 msgid "E-mail me if anyone replies"
 msgstr "Poinformuj mnie mailowo, gdy ktoś odpisze"
 msgstr "Poinformuj mnie mailowo, gdy ktoś odpisze"
 
 
 #: templates/cranefly/private_threads/thread.html:65
 #: templates/cranefly/private_threads/thread.html:65
-#: templates/cranefly/threads/thread.html:58
+#: templates/cranefly/threads/thread.html:56
 msgid "Add thread to watched list"
 msgid "Add thread to watched list"
 msgstr "Dodaj ten temat do listy obserwowanych"
 msgstr "Dodaj ten temat do listy obserwowanych"
 
 
 #: templates/cranefly/private_threads/thread.html:66
 #: templates/cranefly/private_threads/thread.html:66
-#: templates/cranefly/threads/thread.html:59
+#: templates/cranefly/threads/thread.html:57
 msgid "Add thread to watched list and e-mail me if anyone replies"
 msgid "Add thread to watched list and e-mail me if anyone replies"
 msgstr ""
 msgstr ""
 "Dodaj ten temat do listy obserwowanych i poinformuj mnie mailowo, gdy ktoś "
 "Dodaj ten temat do listy obserwowanych i poinformuj mnie mailowo, gdy ktoś "
 "odpisze"
 "odpisze"
 
 
 #: templates/cranefly/private_threads/thread.html:69
 #: templates/cranefly/private_threads/thread.html:69
-#: templates/cranefly/threads/thread.html:62
+#: templates/cranefly/threads/thread.html:60
 msgid "Show Hidden Replies"
 msgid "Show Hidden Replies"
 msgstr "Pokaż ukryte odpowiedzi"
 msgstr "Pokaż ukryte odpowiedzi"
 
 
 #: templates/cranefly/private_threads/thread.html:94
 #: templates/cranefly/private_threads/thread.html:94
-#: templates/cranefly/private_threads/thread.html:179
-#: templates/cranefly/threads/thread.html:88
-#: templates/cranefly/threads/thread.html:173
+#: templates/cranefly/private_threads/thread.html:163
+#: templates/cranefly/threads/thread.html:86
+#: templates/cranefly/threads/thread.html:155
 msgid "Unregistered"
 msgid "Unregistered"
 msgstr "Niezarejestrowany"
 msgstr "Niezarejestrowany"
 
 
-#: templates/cranefly/private_threads/thread.html:105
-#: templates/cranefly/private_threads/thread.html:190
-#: templates/cranefly/threads/thread.html:99
-#: templates/cranefly/threads/thread.html:184
+#: templates/cranefly/private_threads/thread.html:101
+#: templates/cranefly/private_threads/thread.html:174
+#: templates/cranefly/threads/thread.html:93
+#: templates/cranefly/threads/thread.html:162
 msgid "Show changelog"
 msgid "Show changelog"
 msgstr "Pokaż historię zmian"
 msgstr "Pokaż historię zmian"
 
 
-#: templates/cranefly/private_threads/thread.html:116
-#: templates/cranefly/private_threads/thread.html:150
-#: templates/cranefly/private_threads/thread.html:204
-#: templates/cranefly/threads/thread.html:110
-#: templates/cranefly/threads/thread.html:144
-#: templates/cranefly/threads/thread.html:198
+#: templates/cranefly/private_threads/thread.html:108
+#: templates/cranefly/private_threads/thread.html:134
+#: templates/cranefly/private_threads/thread.html:188
+#: templates/cranefly/threads/thread.html:100
+#: templates/cranefly/threads/thread.html:126
+#: templates/cranefly/threads/thread.html:172
 msgid "Direct link to this post"
 msgid "Direct link to this post"
 msgstr "Bezpośredni link do tego postu"
 msgstr "Bezpośredni link do tego postu"
 
 
-#: templates/cranefly/private_threads/thread.html:121
-#: templates/cranefly/private_threads/thread.html:155
-#: templates/cranefly/private_threads/thread.html:233
-#: templates/cranefly/threads/thread.html:115
-#: templates/cranefly/threads/thread.html:149
-#: templates/cranefly/threads/thread.html:227
+#: templates/cranefly/private_threads/thread.html:113
+#: templates/cranefly/private_threads/thread.html:139
+#: templates/cranefly/private_threads/thread.html:217
+#: templates/cranefly/threads/thread.html:105
+#: templates/cranefly/threads/thread.html:131
+#: templates/cranefly/threads/thread.html:201
 msgid "New"
 msgid "New"
 msgstr "Nowy"
 msgstr "Nowy"
 
 
-#: templates/cranefly/private_threads/thread.html:128
-#: templates/cranefly/threads/thread.html:122
+#: templates/cranefly/private_threads/thread.html:120
+#: templates/cranefly/threads/thread.html:112
 #, python-format
 #, python-format
 msgid "%(user)s has deleted this reply %(date)s"
 msgid "%(user)s has deleted this reply %(date)s"
 msgstr "%(user)s usunął tą odpowiedź %(date)s"
 msgstr "%(user)s usunął tą odpowiedź %(date)s"
 
 
-#: templates/cranefly/private_threads/thread.html:162
-#: templates/cranefly/threads/thread.html:156
+#: templates/cranefly/private_threads/thread.html:146
+#: templates/cranefly/threads/thread.html:138
 msgid "This reply was posted by user that is on your ignored list."
 msgid "This reply was posted by user that is on your ignored list."
 msgstr ""
 msgstr ""
 "Ten post został napisany przez użytkownika, który znajduje się na twojej "
 "Ten post został napisany przez użytkownika, który znajduje się na twojej "
 "liście ignorowanych."
 "liście ignorowanych."
 
 
-#: templates/cranefly/private_threads/thread.html:215
-#: templates/cranefly/threads/thread.html:209
+#: templates/cranefly/private_threads/thread.html:199
+#: templates/cranefly/threads/thread.html:183
 msgid "Deleted"
 msgid "Deleted"
 msgstr "Usunięty"
 msgstr "Usunięty"
 
 
-#: templates/cranefly/private_threads/thread.html:221
-#: templates/cranefly/threads/thread.html:215
+#: templates/cranefly/private_threads/thread.html:205
+#: templates/cranefly/threads/thread.html:189
 msgid "Unreviewed"
 msgid "Unreviewed"
 msgstr "Oczekuje na akceptację"
 msgstr "Oczekuje na akceptację"
 
 
-#: templates/cranefly/private_threads/thread.html:227
-#: templates/cranefly/threads/thread.html:221
+#: templates/cranefly/private_threads/thread.html:211
+#: templates/cranefly/threads/thread.html:195
 msgid "Reported"
 msgid "Reported"
 msgstr "Zgłoszony"
 msgstr "Zgłoszony"
 
 
-#: templates/cranefly/private_threads/thread.html:253
-#: templates/cranefly/threads/thread.html:286
+#: templates/cranefly/private_threads/thread.html:237
+#: templates/cranefly/threads/thread.html:260
 msgid "Info"
 msgid "Info"
 msgstr "Info"
 msgstr "Info"
 
 
-#: templates/cranefly/private_threads/thread.html:256
-#: templates/cranefly/private_threads/thread.html:258
-#: templates/cranefly/threads/thread.html:289
-#: templates/cranefly/threads/thread.html:291
+#: templates/cranefly/private_threads/thread.html:240
+#: templates/cranefly/private_threads/thread.html:242
+#: templates/cranefly/threads/thread.html:263
+#: templates/cranefly/threads/thread.html:265
 msgid "Edit"
 msgid "Edit"
 msgstr "Edytuj"
 msgstr "Edytuj"
 
 
-#: templates/cranefly/private_threads/thread.html:267
-#: templates/cranefly/private_threads/thread.html:275
-#: templates/cranefly/threads/thread.html:300
-#: templates/cranefly/threads/thread.html:308
-msgid "Delete thread:"
-msgstr "Usuń temat"
+#: templates/cranefly/private_threads/thread.html:252
+#: templates/cranefly/threads/thread.html:275
+msgid "Make this thread visible to other users"
+msgstr "Uczyń ten temat widocznym dla innych użytkowników"
+
+#: templates/cranefly/private_threads/thread.html:252
+#: templates/cranefly/private_threads/thread.html:274
+#: templates/cranefly/private_threads/thread.html:327
+#: templates/cranefly/threads/thread.html:275
+#: templates/cranefly/threads/thread.html:297
+#: templates/cranefly/threads/thread.html:347
+msgid "Restore"
+msgstr "Przywróć"
+
+#: templates/cranefly/private_threads/thread.html:257
+#: templates/cranefly/threads/thread.html:280
+msgid "Hide this thread from other users"
+msgstr "Ukryj ten temat dla innych użytkowników"
+
+#: templates/cranefly/private_threads/thread.html:257
+#: templates/cranefly/private_threads/thread.html:279
+#: templates/cranefly/private_threads/thread.html:332
+#: templates/cranefly/threads/thread.html:280
+#: templates/cranefly/threads/thread.html:302
+#: templates/cranefly/threads/thread.html:352
+msgid "Hide"
+msgstr "Ukryj"
 
 
-#: templates/cranefly/private_threads/thread.html:268
-#: templates/cranefly/threads/thread.html:301
+#: templates/cranefly/private_threads/thread.html:264
+#: templates/cranefly/threads/thread.html:287
 msgid "Delete this thread for good"
 msgid "Delete this thread for good"
 msgstr "Usuń ten temat na stałe"
 msgstr "Usuń ten temat na stałe"
 
 
-#: templates/cranefly/private_threads/thread.html:268
-#: templates/cranefly/private_threads/thread.html:287
-#: templates/cranefly/threads/thread.html:301
-#: templates/cranefly/threads/thread.html:320
-msgid "Hard"
-msgstr "Na stałe"
-
-#: templates/cranefly/private_threads/thread.html:277
-#: templates/cranefly/threads/thread.html:310
-msgid "Hide this thread from other users"
-msgstr "Ukryj ten temat dla innych użytkowników"
-
-#: templates/cranefly/private_threads/thread.html:277
-#: templates/cranefly/private_threads/thread.html:296
-#: templates/cranefly/threads/thread.html:310
-#: templates/cranefly/threads/thread.html:329
-msgid "Soft"
-msgstr "Odwracalnie"
+#: templates/cranefly/private_threads/thread.html:264
+#: templates/cranefly/private_threads/thread.html:286
+#: templates/cranefly/private_threads/thread.html:339
+#: templates/cranefly/threads/thread.html:287
+#: templates/cranefly/threads/thread.html:309
+#: templates/cranefly/threads/thread.html:359
+msgid "Delete"
+msgstr "Usuń"
+
+#: templates/cranefly/private_threads/thread.html:274
+#: templates/cranefly/threads/thread.html:297
+msgid "Make this reply visible to other users"
+msgstr "Uczyń tą odpowiedź widoczną dla innych użytkowników"
+
+#: templates/cranefly/private_threads/thread.html:279
+#: templates/cranefly/threads/thread.html:302
+msgid "Hide this reply from other users"
+msgstr "Ukryj odpowiedź "
 
 
 #: templates/cranefly/private_threads/thread.html:286
 #: templates/cranefly/private_threads/thread.html:286
-#: templates/cranefly/private_threads/thread.html:294
-#: templates/cranefly/threads/thread.html:319
-#: templates/cranefly/threads/thread.html:327
-msgid "Delete reply:"
-msgstr "Usuń odpowiedź:"
-
-#: templates/cranefly/private_threads/thread.html:287
-#: templates/cranefly/threads/thread.html:320
+#: templates/cranefly/threads/thread.html:309
 msgid "Delete this reply for good"
 msgid "Delete this reply for good"
 msgstr "Usuń odpowiedź na dobre"
 msgstr "Usuń odpowiedź na dobre"
 
 
-#: templates/cranefly/private_threads/thread.html:296
-#: templates/cranefly/threads/thread.html:329
-msgid "Hide this reply from other users"
-msgstr "Ukryj odpowiedź "
-
-#: templates/cranefly/private_threads/thread.html:314
-#: templates/cranefly/threads/thread.html:348
+#: templates/cranefly/private_threads/thread.html:304
+#: templates/cranefly/threads/thread.html:328
 msgid "This thread has reached its post limit and has been closed."
 msgid "This thread has reached its post limit and has been closed."
 msgstr "Ten temat osiągnął limit postów i został automatycznie zamknięty."
 msgstr "Ten temat osiągnął limit postów i został automatycznie zamknięty."
 
 
-#: templates/cranefly/private_threads/thread.html:316
-#: templates/cranefly/threads/thread.html:350
+#: templates/cranefly/private_threads/thread.html:306
+#: templates/cranefly/threads/thread.html:330
 #, python-format
 #, python-format
 msgid "%(user)s accepted this thread %(date)s"
 msgid "%(user)s accepted this thread %(date)s"
 msgstr "%(user)s zaakceptował ten temat %(date)s"
 msgstr "%(user)s zaakceptował ten temat %(date)s"
 
 
-#: templates/cranefly/private_threads/thread.html:318
-#: templates/cranefly/threads/thread.html:352
+#: templates/cranefly/private_threads/thread.html:308
+#: templates/cranefly/threads/thread.html:332
 #, python-format
 #, python-format
 msgid "%(user)s closed this thread %(date)s"
 msgid "%(user)s closed this thread %(date)s"
 msgstr "%(user)s zamknął ten temat %(date)s"
 msgstr "%(user)s zamknął ten temat %(date)s"
 
 
-#: templates/cranefly/private_threads/thread.html:320
-#: templates/cranefly/threads/thread.html:354
+#: templates/cranefly/private_threads/thread.html:310
+#: templates/cranefly/threads/thread.html:334
 #, python-format
 #, python-format
 msgid "%(user)s opened this thread %(date)s"
 msgid "%(user)s opened this thread %(date)s"
 msgstr "%(user)s otworzył ten temat %(date)s"
 msgstr "%(user)s otworzył ten temat %(date)s"
 
 
-#: templates/cranefly/private_threads/thread.html:322
-#: templates/cranefly/threads/thread.html:356
+#: templates/cranefly/private_threads/thread.html:312
+#: templates/cranefly/threads/thread.html:336
 #, python-format
 #, python-format
 msgid "%(user)s deleted this thread %(date)s"
 msgid "%(user)s deleted this thread %(date)s"
 msgstr "%(user)s usunął ten temat %(date)s"
 msgstr "%(user)s usunął ten temat %(date)s"
 
 
-#: templates/cranefly/private_threads/thread.html:324
-#: templates/cranefly/threads/thread.html:358
+#: templates/cranefly/private_threads/thread.html:314
+#: templates/cranefly/threads/thread.html:338
 #, python-format
 #, python-format
 msgid "%(user)s restored this thread %(date)s"
 msgid "%(user)s restored this thread %(date)s"
 msgstr "%(user)s przywrócił ten temat %(date)s"
 msgstr "%(user)s przywrócił ten temat %(date)s"
 
 
-#: templates/cranefly/private_threads/thread.html:326
+#: templates/cranefly/private_threads/thread.html:316
 #, python-format
 #, python-format
 msgid "%(user)s added %(invited)s to thread %(date)s"
 msgid "%(user)s added %(invited)s to thread %(date)s"
 msgstr "%(user)s dodał %(invited)s do tematu %(date)s"
 msgstr "%(user)s dodał %(invited)s do tematu %(date)s"
 
 
-#: templates/cranefly/private_threads/thread.html:328
+#: templates/cranefly/private_threads/thread.html:318
 #, python-format
 #, python-format
 msgid "%(user)s removed %(removed)s from thread %(date)s"
 msgid "%(user)s removed %(removed)s from thread %(date)s"
 msgstr "%(user)s usunął %(removed)s z tematu %(date)s"
 msgstr "%(user)s usunął %(removed)s z tematu %(date)s"
 
 
-#: templates/cranefly/private_threads/thread.html:330
+#: templates/cranefly/private_threads/thread.html:320
 #, python-format
 #, python-format
 msgid "%(user)s left thread %(date)s"
 msgid "%(user)s left thread %(date)s"
 msgstr "%(user)s opuścił temat %(date)s"
 msgstr "%(user)s opuścił temat %(date)s"
 
 
-#: templates/cranefly/private_threads/thread.html:366
+#: templates/cranefly/private_threads/thread.html:377
 msgid "This thread has no participants."
 msgid "This thread has no participants."
 msgstr "Ten temat nie ma żadnych uczestników."
 msgstr "Ten temat nie ma żadnych uczestników."
 
 
-#: templates/cranefly/private_threads/thread.html:375
-#: templates/cranefly/threads/thread.html:403
+#: templates/cranefly/private_threads/thread.html:386
+#: templates/cranefly/threads/thread.html:406
 msgid "Your Avatar"
 msgid "Your Avatar"
-msgstr "Twój avatar"
+msgstr "Twój awatar"
 
 
-#: templates/cranefly/private_threads/thread.html:384
+#: templates/cranefly/private_threads/thread.html:395
 #, python-format
 #, python-format
 msgid "One participant"
 msgid "One participant"
 msgid_plural "%(participants)s participants"
 msgid_plural "%(participants)s participants"
@@ -6696,15 +7147,15 @@ msgstr[0] "Jeden uczestnik"
 msgstr[1] "%(participants)s uczestników"
 msgstr[1] "%(participants)s uczestników"
 msgstr[2] "%(participants)s uczestników"
 msgstr[2] "%(participants)s uczestników"
 
 
-#: templates/cranefly/private_threads/thread.html:390
+#: templates/cranefly/private_threads/thread.html:401
 msgid "Leave this thread"
 msgid "Leave this thread"
 msgstr "Wypisz się z tego tematu"
 msgstr "Wypisz się z tego tematu"
 
 
-#: templates/cranefly/private_threads/thread.html:390
+#: templates/cranefly/private_threads/thread.html:401
 msgid "Remove from this thread"
 msgid "Remove from this thread"
 msgstr "Usuń z tego tematu"
 msgstr "Usuń z tego tematu"
 
 
-#: templates/cranefly/private_threads/thread.html:402
+#: templates/cranefly/private_threads/thread.html:413
 msgid ""
 msgid ""
 "This thread has too few participants. Invite other users to open it for new "
 "This thread has too few participants. Invite other users to open it for new "
 "replies."
 "replies."
@@ -6712,46 +7163,46 @@ msgstr ""
 "Ten temat ma zbyt mało uczestników. Zaproś innych użytkowników, aby "
 "Ten temat ma zbyt mało uczestników. Zaproś innych użytkowników, aby "
 "umożliwić dyskusję."
 "umożliwić dyskusję."
 
 
-#: templates/cranefly/private_threads/thread.html:405
+#: templates/cranefly/private_threads/thread.html:416
 msgid "Invite User"
 msgid "Invite User"
 msgstr "Zaproś użytkownika"
 msgstr "Zaproś użytkownika"
 
 
-#: templates/cranefly/private_threads/thread.html:434
+#: templates/cranefly/private_threads/thread.html:445
 msgid ""
 msgid ""
 "Are you sure you want to leave this thread? It will be deleted after you "
 "Are you sure you want to leave this thread? It will be deleted after you "
 "leave!"
 "leave!"
 msgstr "Czy aby na pewno chcesz opuścić ten temat? Zostanie po tym usunięty!"
 msgstr "Czy aby na pewno chcesz opuścić ten temat? Zostanie po tym usunięty!"
 
 
-#: templates/cranefly/private_threads/thread.html:436
+#: templates/cranefly/private_threads/thread.html:447
 msgid "Are you sure you want to leave this thread?"
 msgid "Are you sure you want to leave this thread?"
 msgstr "Czy aby na pewno chcesz opuścić ten temat?"
 msgstr "Czy aby na pewno chcesz opuścić ten temat?"
 
 
-#: templates/cranefly/private_threads/thread.html:441
+#: templates/cranefly/private_threads/thread.html:452
 msgid "Are you sure you want to remove this member from this thread?"
 msgid "Are you sure you want to remove this member from this thread?"
 msgstr "Czy aby na pewno chcesz usunąć tego użytkownika z dyskusji?"
 msgstr "Czy aby na pewno chcesz usunąć tego użytkownika z dyskusji?"
 
 
-#: templates/cranefly/private_threads/thread.html:446
-#: templates/cranefly/threads/thread.html:426
+#: templates/cranefly/private_threads/thread.html:457
+#: templates/cranefly/threads/thread.html:429
 msgid ""
 msgid ""
 "Are you sure you want to delete this thread? This action is not reversible!"
 "Are you sure you want to delete this thread? This action is not reversible!"
 msgstr ""
 msgstr ""
 "Czy aby na pewno chcesz usunąć ten temat? Nie będzie można już tego cofnąć!"
 "Czy aby na pewno chcesz usunąć ten temat? Nie będzie można już tego cofnąć!"
 
 
-#: templates/cranefly/private_threads/thread.html:458
-#: templates/cranefly/threads/thread.html:438
+#: templates/cranefly/private_threads/thread.html:469
+#: templates/cranefly/threads/thread.html:441
 msgid "You have to select at least two posts you want to merge."
 msgid "You have to select at least two posts you want to merge."
 msgstr "Musisz zaznaczyć przynajmniej dwa posty, aby móc je połączyć."
 msgstr "Musisz zaznaczyć przynajmniej dwa posty, aby móc je połączyć."
 
 
-#: templates/cranefly/private_threads/thread.html:461
-#: templates/cranefly/threads/thread.html:441
+#: templates/cranefly/private_threads/thread.html:472
+#: templates/cranefly/threads/thread.html:444
 msgid ""
 msgid ""
 "Are you sure you want to merge selected posts? This action is not reversible!"
 "Are you sure you want to merge selected posts? This action is not reversible!"
 msgstr ""
 msgstr ""
 "Czy aby na pewno chcesz połączyć zaznaczone posty? Nie będzie można już tego "
 "Czy aby na pewno chcesz połączyć zaznaczone posty? Nie będzie można już tego "
 "cofnąć!"
 "cofnąć!"
 
 
-#: templates/cranefly/private_threads/thread.html:465
-#: templates/cranefly/threads/thread.html:445
+#: templates/cranefly/private_threads/thread.html:476
+#: templates/cranefly/threads/thread.html:448
 msgid ""
 msgid ""
 "Are you sure you want to delete selected posts? This action is not "
 "Are you sure you want to delete selected posts? This action is not "
 "reversible!"
 "reversible!"
@@ -6759,55 +7210,60 @@ msgstr ""
 "Czy aby na pewno chcesz usunąć zaznaczone posty? Nie będzie można już tego "
 "Czy aby na pewno chcesz usunąć zaznaczone posty? Nie będzie można już tego "
 "cofnąć!"
 "cofnąć!"
 
 
-#: templates/cranefly/private_threads/thread.html:471
-#: templates/cranefly/threads/thread.html:451
+#: templates/cranefly/private_threads/thread.html:482
+#: templates/cranefly/threads/thread.html:454
 msgid "Are you sure you want to delete this thread?"
 msgid "Are you sure you want to delete this thread?"
 msgstr "Czy aby na pewno chcesz usunąć ten temat?"
 msgstr "Czy aby na pewno chcesz usunąć ten temat?"
 
 
-#: templates/cranefly/private_threads/thread.html:475
-#: templates/cranefly/threads/thread.html:455
+#: templates/cranefly/private_threads/thread.html:486
+#: templates/cranefly/threads/thread.html:458
 msgid "Are you sure you want to delete this post?"
 msgid "Are you sure you want to delete this post?"
 msgstr "Czy aby na pewno chcesz usunąć ten post?"
 msgstr "Czy aby na pewno chcesz usunąć ten post?"
 
 
-#: templates/cranefly/private_threads/thread.html:496
-#: templates/cranefly/threads/thread.html:477
+#: templates/cranefly/private_threads/thread.html:490
+#: templates/cranefly/threads/thread.html:462
+msgid "Are you sure you want to delete this checkpoint?"
+msgstr "Czy aby na pewno chcesz usunąć tę etykietę zdarzenia?"
+
+#: templates/cranefly/private_threads/thread.html:511
+#: templates/cranefly/threads/thread.html:484
 msgid "Go to first page"
 msgid "Go to first page"
 msgstr "Idź do pierwszej strony"
 msgstr "Idź do pierwszej strony"
 
 
-#: templates/cranefly/private_threads/thread.html:496
+#: templates/cranefly/private_threads/thread.html:511
 #: templates/cranefly/profiles/posts.html:42
 #: templates/cranefly/profiles/posts.html:42
 #: templates/cranefly/profiles/threads.html:38
 #: templates/cranefly/profiles/threads.html:38
-#: templates/cranefly/threads/thread.html:477
+#: templates/cranefly/threads/thread.html:484
 msgid "Older Posts"
 msgid "Older Posts"
 msgstr "Starsze posty"
 msgstr "Starsze posty"
 
 
-#: templates/cranefly/private_threads/thread.html:496
-#: templates/cranefly/threads/thread.html:477
+#: templates/cranefly/private_threads/thread.html:511
+#: templates/cranefly/threads/thread.html:484
 msgid "Newest Posts"
 msgid "Newest Posts"
 msgstr "Nowsze posty"
 msgstr "Nowsze posty"
 
 
-#: templates/cranefly/private_threads/thread.html:496
-#: templates/cranefly/threads/thread.html:477
+#: templates/cranefly/private_threads/thread.html:511
+#: templates/cranefly/threads/thread.html:484
 msgid "Go to last page"
 msgid "Go to last page"
 msgstr "Idź do ostatniej strony"
 msgstr "Idź do ostatniej strony"
 
 
-#: templates/cranefly/private_threads/thread.html:496
-#: templates/cranefly/threads/thread.html:477
+#: templates/cranefly/private_threads/thread.html:511
+#: templates/cranefly/threads/thread.html:484
 msgid "Last"
 msgid "Last"
 msgstr "Ostatni"
 msgstr "Ostatni"
 
 
-#: templates/cranefly/private_threads/thread.html:498
-#: templates/cranefly/threads/thread.html:479
+#: templates/cranefly/private_threads/thread.html:513
+#: templates/cranefly/threads/thread.html:486
 msgid "Go to first unread"
 msgid "Go to first unread"
 msgstr "Idź do pierwszego nieprzeczytanego posta"
 msgstr "Idź do pierwszego nieprzeczytanego posta"
 
 
-#: templates/cranefly/private_threads/thread.html:498
-#: templates/cranefly/threads/thread.html:479
+#: templates/cranefly/private_threads/thread.html:513
+#: templates/cranefly/threads/thread.html:486
 msgid "First Unread"
 msgid "First Unread"
 msgstr "Pierwszy nieprzeczytany post"
 msgstr "Pierwszy nieprzeczytany post"
 
 
-#: templates/cranefly/private_threads/thread.html:533
-#: templates/cranefly/threads/thread.html:507
+#: templates/cranefly/private_threads/thread.html:548
+#: templates/cranefly/threads/thread.html:523
 msgid "Full Editor"
 msgid "Full Editor"
 msgstr "Pełny edytor"
 msgstr "Pełny edytor"
 
 
@@ -6817,7 +7273,7 @@ msgstr "Szczegóły profilu"
 
 
 #: templates/cranefly/profiles/details.html:33
 #: templates/cranefly/profiles/details.html:33
 msgid "Member Since"
 msgid "Member Since"
-msgstr "Członek forum od"
+msgstr "Dołączył"
 
 
 #: templates/cranefly/profiles/details.html:41
 #: templates/cranefly/profiles/details.html:41
 msgid "Last Seen"
 msgid "Last Seen"
@@ -6837,7 +7293,7 @@ msgstr "Rozpoczętych tematów"
 
 
 #: templates/cranefly/profiles/details.html:81
 #: templates/cranefly/profiles/details.html:81
 msgid "Votes Cast"
 msgid "Votes Cast"
-msgstr ""
+msgstr "Głosów w ankietach"
 
 
 #: templates/cranefly/profiles/details.html:97
 #: templates/cranefly/profiles/details.html:97
 msgid "Ranking Performance"
 msgid "Ranking Performance"
@@ -6845,7 +7301,7 @@ msgstr "Ranking"
 
 
 #: templates/cranefly/profiles/details.html:107
 #: templates/cranefly/profiles/details.html:107
 msgid "Not Ranked"
 msgid "Not Ranked"
-msgstr ""
+msgstr "Nie dotyczy"
 
 
 #: templates/cranefly/profiles/details.html:113
 #: templates/cranefly/profiles/details.html:113
 msgid "Ranking Position"
 msgid "Ranking Position"
@@ -6857,20 +7313,19 @@ msgstr "Wynik"
 
 
 #: templates/cranefly/profiles/details.html:125
 #: templates/cranefly/profiles/details.html:125
 msgid "Karma Received"
 msgid "Karma Received"
-msgstr ""
+msgstr "Otrzymane głosy"
 
 
 #: templates/cranefly/profiles/details.html:133
 #: templates/cranefly/profiles/details.html:133
 msgid "Karma Given"
 msgid "Karma Given"
-msgstr ""
+msgstr "Oddane głosy"
 
 
 #: templates/cranefly/profiles/details.html:146
 #: templates/cranefly/profiles/details.html:146
 msgid "Interactions"
 msgid "Interactions"
-msgstr "Interkacje"
+msgstr "Interakcje"
 
 
 #: templates/cranefly/profiles/details.html:161
 #: templates/cranefly/profiles/details.html:161
-#: templates/cranefly/profiles/profile.html:51
 msgid "Following"
 msgid "Following"
-msgstr "Obserwuje"
+msgstr "Obserwowany"
 
 
 #: templates/cranefly/profiles/details.html:182
 #: templates/cranefly/profiles/details.html:182
 msgid "Registration Details"
 msgid "Registration Details"
@@ -6928,8 +7383,7 @@ msgstr "Użytkownicy, których obserwuje %(username)s"
 
 
 #: templates/cranefly/profiles/list.html:17
 #: templates/cranefly/profiles/list.html:17
 msgid "Browse notable user groups or find specific user"
 msgid "Browse notable user groups or find specific user"
-msgstr ""
-"Przejrzyj najistotniejsze grupy, albo wyszukaj konkretnego użytkownika."
+msgstr "Najważniejsze rangi i wyszukiwanie użytkowników"
 
 
 #: templates/cranefly/profiles/list.html:47
 #: templates/cranefly/profiles/list.html:47
 msgid ""
 msgid ""
@@ -6994,7 +7448,7 @@ msgstr "Nowsze posty"
 
 
 #: templates/cranefly/profiles/profile.html:13
 #: templates/cranefly/profiles/profile.html:13
 msgid "Click to jump to your Avatar Settings"
 msgid "Click to jump to your Avatar Settings"
-msgstr "Kliknij, aby przejść do ustawień swojego avatara"
+msgstr "Kliknij, aby przejść do ustawień swojego awatara"
 
 
 #: templates/cranefly/profiles/profile.html:22
 #: templates/cranefly/profiles/profile.html:22
 msgid "Online, hidden"
 msgid "Online, hidden"
@@ -7022,17 +7476,30 @@ msgstr "nigdy nie zalogowany"
 msgid "hiding activity"
 msgid "hiding activity"
 msgstr "ukrywanie aktywności"
 msgstr "ukrywanie aktywności"
 
 
-#: templates/cranefly/profiles/profile.html:51
-msgid "Follow"
-msgstr "Obserwuj"
+#: templates/cranefly/profiles/profile.html:50
+#, python-format
+msgid "Remove %(user)s from ignored"
+msgstr "Przestań ignorować %(user)s"
 
 
-#: templates/cranefly/profiles/profile.html:60
-msgid "Ignoring"
-msgstr ""
+#: templates/cranefly/profiles/profile.html:50
+#, python-format
+msgid "Add %(user)s to ignored"
+msgstr "Dodaj %(user)s do listy ignorowanych"
+
+#: templates/cranefly/profiles/profile.html:59
+#, python-format
+msgid "Stop following %(user)s"
+msgstr "Przestań obserwować %(user)s"
+
+#: templates/cranefly/profiles/profile.html:59
+#, python-format
+msgid "Start following %(user)s"
+msgstr "Zacznij obserwować %(user)s"
 
 
-#: templates/cranefly/profiles/profile.html:60
-msgid "Ignore"
-msgstr "Ignoruj"
+#: templates/cranefly/profiles/profile.html:66
+#, python-format
+msgid "Start private thread with %(user)s"
+msgstr "Rozpocznij prywatną dyskusję z %(user)s"
 
 
 #: templates/cranefly/profiles/threads.html:9
 #: templates/cranefly/profiles/threads.html:9
 #, python-format
 #, python-format
@@ -7047,13 +7514,13 @@ msgstr[2] "%(username)s rozpoczął %(total)s tematów"
 msgid "%(username)s started no threads"
 msgid "%(username)s started no threads"
 msgstr "%(username)s nie rozpoczął żadnych tematów"
 msgstr "%(username)s nie rozpoczął żadnych tematów"
 
 
-#: templates/cranefly/threads/karmas.html:11
-#: templates/cranefly/threads/karmas.html:21
+#: templates/cranefly/threads/karmas.html:9
+#: templates/cranefly/threads/karmas.html:19
 #, python-format
 #, python-format
 msgid "Post #%(post)s Votes"
 msgid "Post #%(post)s Votes"
 msgstr "Głosy postu #%(post)s "
 msgstr "Głosy postu #%(post)s "
 
 
-#: templates/cranefly/threads/karmas.html:34
+#: templates/cranefly/threads/karmas.html:32
 #, python-format
 #, python-format
 msgid "One like"
 msgid "One like"
 msgid_plural "%(votes)s likes"
 msgid_plural "%(votes)s likes"
@@ -7061,11 +7528,11 @@ msgstr[0] "Jedna osoba polubiła ten post"
 msgstr[1] "%(votes)s osoby polubiły ten post"
 msgstr[1] "%(votes)s osoby polubiły ten post"
 msgstr[2] "%(votes)s osób poubiło ten post"
 msgstr[2] "%(votes)s osób poubiło ten post"
 
 
-#: templates/cranefly/threads/karmas.html:54
+#: templates/cranefly/threads/karmas.html:52
 msgid "Nobody liked this post."
 msgid "Nobody liked this post."
 msgstr "Nikt nie polubił tego postu."
 msgstr "Nikt nie polubił tego postu."
 
 
-#: templates/cranefly/threads/karmas.html:61
+#: templates/cranefly/threads/karmas.html:59
 #, python-format
 #, python-format
 msgid "One hate"
 msgid "One hate"
 msgid_plural "%(votes)s hates"
 msgid_plural "%(votes)s hates"
@@ -7073,118 +7540,123 @@ msgstr[0] "Jedna osoba nie lubi tego postu"
 msgstr[1] "%(votes)s osoby nie lubią tego postu"
 msgstr[1] "%(votes)s osoby nie lubią tego postu"
 msgstr[2] "%(votes)s osób nie lubi tego postu"
 msgstr[2] "%(votes)s osób nie lubi tego postu"
 
 
-#: templates/cranefly/threads/karmas.html:81
+#: templates/cranefly/threads/karmas.html:79
 msgid "Nobody hated this post."
 msgid "Nobody hated this post."
 msgstr "Nikt nie wyraził niechęci do tego postu."
 msgstr "Nikt nie wyraził niechęci do tego postu."
 
 
-#: templates/cranefly/threads/karmas.html:98
+#: templates/cranefly/threads/karmas.html:96
 #, python-format
 #, python-format
 msgid "From %(ip)s"
 msgid "From %(ip)s"
 msgstr "Z %(ip)s"
 msgstr "Z %(ip)s"
 
 
-#: templates/cranefly/threads/list.html:36
+#: templates/cranefly/threads/list.html:34
 msgid "Child forums"
 msgid "Child forums"
 msgstr "Subfora"
 msgstr "Subfora"
 
 
-#: templates/cranefly/threads/list.html:178
+#: templates/cranefly/threads/list.html:180
 msgid "There are no threads in this forum."
 msgid "There are no threads in this forum."
 msgstr "Aktualnie nie ma żadnych tematów na tym forum."
 msgstr "Aktualnie nie ma żadnych tematów na tym forum."
 
 
-#: templates/cranefly/threads/list.html:200
+#: templates/cranefly/threads/list.html:202
 msgid "Sign in or register to start threads."
 msgid "Sign in or register to start threads."
-msgstr "Zaloguj się lub zarejestruj, aby rozpocząć nowe tematy."
+msgstr "Zaloguj się lub zarejestruj, aby rozpoczynać nowe tematy."
 
 
-#: templates/cranefly/threads/merge.html:12
-#: templates/cranefly/threads/merge.html:22
-#: templates/cranefly/threads/merge.html:32
-#: templates/cranefly/threads/merge.html:51
+#: templates/cranefly/threads/merge.html:10
+#: templates/cranefly/threads/merge.html:20
+#: templates/cranefly/threads/merge.html:30
+#: templates/cranefly/threads/merge.html:49
 msgid "Merge Threads"
 msgid "Merge Threads"
 msgstr "Połącz tematy"
 msgstr "Połącz tematy"
 
 
-#: templates/cranefly/threads/move_posts.html:12
-#: templates/cranefly/threads/move_posts.html:22
-#: templates/cranefly/threads/move_posts.html:32
-#: templates/cranefly/threads/move_posts.html:52
+#: templates/cranefly/threads/move_posts.html:10
+#: templates/cranefly/threads/move_posts.html:20
+#: templates/cranefly/threads/move_posts.html:30
+#: templates/cranefly/threads/move_posts.html:50
 msgid "Move Posts"
 msgid "Move Posts"
 msgstr "Przenieś posty"
 msgstr "Przenieś posty"
 
 
-#: templates/cranefly/threads/move_thread.html:12
-#: templates/cranefly/threads/move_thread.html:22
-#: templates/cranefly/threads/move_thread.html:32
-#: templates/cranefly/threads/move_thread.html:53
+#: templates/cranefly/threads/move_thread.html:10
+#: templates/cranefly/threads/move_thread.html:20
+#: templates/cranefly/threads/move_thread.html:30
+#: templates/cranefly/threads/move_thread.html:51
 msgid "Move Thread"
 msgid "Move Thread"
 msgstr "Przenieś temat"
 msgstr "Przenieś temat"
 
 
-#: templates/cranefly/threads/move_thread.html:49
+#: templates/cranefly/threads/move_thread.html:47
 msgid "Move Thread To"
 msgid "Move Thread To"
 msgstr "Przenieś temat do"
 msgstr "Przenieś temat do"
 
 
-#: templates/cranefly/threads/move_thread.html:49
+#: templates/cranefly/threads/move_thread.html:47
 msgid "Select forum you want to move this thread to."
 msgid "Select forum you want to move this thread to."
 msgstr "Wskaż forum, do którego chcesz przenieść ten temat."
 msgstr "Wskaż forum, do którego chcesz przenieść ten temat."
 
 
-#: templates/cranefly/threads/move_threads.html:12
-#: templates/cranefly/threads/move_threads.html:22
-#: templates/cranefly/threads/move_threads.html:32
-#: templates/cranefly/threads/move_threads.html:51
+#: templates/cranefly/threads/move_threads.html:10
+#: templates/cranefly/threads/move_threads.html:20
+#: templates/cranefly/threads/move_threads.html:30
+#: templates/cranefly/threads/move_threads.html:49
 msgid "Move Threads"
 msgid "Move Threads"
 msgstr "Przenieś tematy"
 msgstr "Przenieś tematy"
 
 
-#: templates/cranefly/threads/posting.html:76
+#: templates/cranefly/threads/posting.html:74
 msgid "Thread Status"
 msgid "Thread Status"
 msgstr "Typ tematu"
 msgstr "Typ tematu"
 
 
-#: templates/cranefly/threads/split.html:12
-#: templates/cranefly/threads/split.html:22
-#: templates/cranefly/threads/split.html:32
-#: templates/cranefly/threads/split.html:52
+#: templates/cranefly/threads/split.html:10
+#: templates/cranefly/threads/split.html:20
+#: templates/cranefly/threads/split.html:30
+#: templates/cranefly/threads/split.html:50
 msgid "Split Thread"
 msgid "Split Thread"
 msgstr "Podziel temat"
 msgstr "Podziel temat"
 
 
-#: templates/cranefly/threads/thread.html:256
+#: templates/cranefly/threads/thread.html:230
 msgid "Like"
 msgid "Like"
 msgstr "Lubię"
 msgstr "Lubię"
 
 
-#: templates/cranefly/threads/thread.html:259
+#: templates/cranefly/threads/thread.html:233
 msgid "Likes"
 msgid "Likes"
 msgstr "Lubi"
 msgstr "Lubi"
 
 
-#: templates/cranefly/threads/thread.html:269
+#: templates/cranefly/threads/thread.html:243
 msgid "Hate"
 msgid "Hate"
 msgstr "Nie lubię"
 msgstr "Nie lubię"
 
 
-#: templates/cranefly/threads/thread.html:272
+#: templates/cranefly/threads/thread.html:246
 msgid "Hates"
 msgid "Hates"
 msgstr "Nie lubi"
 msgstr "Nie lubi"
 
 
-#: templates/cranefly/threads/thread.html:277
+#: templates/cranefly/threads/thread.html:251
 msgid "Show Votes"
 msgid "Show Votes"
 msgstr "Zobacz głosy"
 msgstr "Zobacz głosy"
 
 
-#: templates/cranefly/threads/thread.html:394
+#: templates/cranefly/threads/thread.html:340
+#, python-format
+msgid "%(user)s moved this thread from %(forum)s %(date)s"
+msgstr "%(user)s przeniósł ten temat z %(forum)s %(date)s"
+
+#: templates/cranefly/threads/thread.html:397
 msgid "Sign in or register to reply."
 msgid "Sign in or register to reply."
-msgstr "Zaloguj się lub zarejestruj, aby odpowiedzieć."
+msgstr "Zaloguj się lub zarejestruj, aby odpowiadać."
 
 
-#: templates/cranefly/threads/thread.html:480
+#: templates/cranefly/threads/thread.html:487
 msgid "Go to first post awaiting review"
 msgid "Go to first post awaiting review"
 msgstr "Idź do pierwszego postu oczekującego na akceptację"
 msgstr "Idź do pierwszego postu oczekującego na akceptację"
 
 
-#: templates/cranefly/threads/thread.html:480
+#: templates/cranefly/threads/thread.html:487
 msgid "First Unreviewed"
 msgid "First Unreviewed"
 msgstr "Pierwszy nieprzejrzany"
 msgstr "Pierwszy nieprzejrzany"
 
 
-#: templates/cranefly/threads/thread.html:481
+#: templates/cranefly/threads/thread.html:488
 msgid "Go to first reported post"
 msgid "Go to first reported post"
 msgstr "Idź do pierwszego zgłoszonego postu"
 msgstr "Idź do pierwszego zgłoszonego postu"
 
 
-#: templates/cranefly/threads/thread.html:481
+#: templates/cranefly/threads/thread.html:488
 msgid "First Reported"
 msgid "First Reported"
 msgstr "Pierwszy zgłoszony"
 msgstr "Pierwszy zgłoszony"
 
 
 #: templates/cranefly/usercp/avatar.html:24
 #: templates/cranefly/usercp/avatar.html:24
 #: templates/cranefly/usercp/avatar_gallery.html:28
 #: templates/cranefly/usercp/avatar_gallery.html:28
 msgid "Gallery Avatar"
 msgid "Gallery Avatar"
-msgstr "Avatar z galerii"
+msgstr "Awatar z galerii"
 
 
 #: templates/cranefly/usercp/avatar.html:30
 #: templates/cranefly/usercp/avatar.html:30
 msgid "Use Gravatar"
 msgid "Use Gravatar"
@@ -7193,45 +7665,45 @@ msgstr "Użyj Gravatara"
 #: templates/cranefly/usercp/avatar.html:31
 #: templates/cranefly/usercp/avatar.html:31
 #: templates/cranefly/usercp/avatar_gallery.html:10
 #: templates/cranefly/usercp/avatar_gallery.html:10
 msgid "Pick Avatar from Gallery"
 msgid "Pick Avatar from Gallery"
-msgstr "Wybierz avatar z galerii"
+msgstr "Wybierz awatar z galerii"
 
 
 #: templates/cranefly/usercp/avatar.html:32
 #: templates/cranefly/usercp/avatar.html:32
 msgid "Crop Your Avatar"
 msgid "Crop Your Avatar"
-msgstr "Przytnij swój avatar"
+msgstr "Przytnij swój awatar"
 
 
 #: templates/cranefly/usercp/avatar.html:33
 #: templates/cranefly/usercp/avatar.html:33
 #: templates/cranefly/usercp/avatar_upload.html:11
 #: templates/cranefly/usercp/avatar_upload.html:11
 #: templates/cranefly/usercp/avatar_upload.html:24
 #: templates/cranefly/usercp/avatar_upload.html:24
 msgid "Upload Avatar"
 msgid "Upload Avatar"
-msgstr "Prześlij avatar"
+msgstr "Prześlij awatar"
 
 
 #: templates/cranefly/usercp/avatar_banned.html:10
 #: templates/cranefly/usercp/avatar_banned.html:10
 #: templates/cranefly/usercp/avatar_crop.html:16
 #: templates/cranefly/usercp/avatar_crop.html:16
 #: templates/cranefly/usercp/avatar_gallery.html:10
 #: templates/cranefly/usercp/avatar_gallery.html:10
 #: templates/cranefly/usercp/avatar_upload.html:11
 #: templates/cranefly/usercp/avatar_upload.html:11
 msgid "Change your Avatar"
 msgid "Change your Avatar"
-msgstr "Zmień swój avatar"
+msgstr "Zmień swój awatar"
 
 
 #: templates/cranefly/usercp/avatar_banned.html:14
 #: templates/cranefly/usercp/avatar_banned.html:14
 #, python-format
 #, python-format
 msgid ""
 msgid ""
 "%(username)s, your ability to change your avatar has been removed for "
 "%(username)s, your ability to change your avatar has been removed for "
 "following reason:"
 "following reason:"
-msgstr "%(username)s, nie możesz już zmienić avatara z następującego powodu:"
+msgstr "%(username)s, nie możesz już zmienić awatara z następującego powodu:"
 
 
 #: templates/cranefly/usercp/avatar_banned.html:19
 #: templates/cranefly/usercp/avatar_banned.html:19
 #, python-format
 #, python-format
 msgid "%(username)s, your ability to change your avatar has been removed."
 msgid "%(username)s, your ability to change your avatar has been removed."
-msgstr "%(username)s, administrator odebrał Ci możliwość zmiany avatara."
+msgstr "%(username)s, administrator odebrał Ci możliwość zmiany awatara."
 
 
 #: templates/cranefly/usercp/avatar_crop.html:14
 #: templates/cranefly/usercp/avatar_crop.html:14
 msgid "Avatar Preview"
 msgid "Avatar Preview"
-msgstr "Podgląd avatara"
+msgstr "Podgląd awatara"
 
 
 #: templates/cranefly/usercp/avatar_crop.html:16
 #: templates/cranefly/usercp/avatar_crop.html:16
 #: templates/cranefly/usercp/avatar_crop.html:18
 #: templates/cranefly/usercp/avatar_crop.html:18
 msgid "Crop Avatar"
 msgid "Crop Avatar"
-msgstr "Przycinanie avatara"
+msgstr "Przycinanie awatara"
 
 
 #: templates/cranefly/usercp/avatar_crop.html:33
 #: templates/cranefly/usercp/avatar_crop.html:33
 msgid "Uploaded Image"
 msgid "Uploaded Image"
@@ -7296,6 +7768,10 @@ msgstr[2] "Możesz zmienić swoją nazwę użytkownika jeszcze %(changes)s razy.
 msgid "You will be able to change your username on %(next_change)s"
 msgid "You will be able to change your username on %(next_change)s"
 msgstr "Będziesz mógł zmienić swoją nazwę użytkownika %(next_change)s"
 msgstr "Będziesz mógł zmienić swoją nazwę użytkownika %(next_change)s"
 
 
+#: templates/debug_toolbar/panels/acl.html:9
+msgid "Value"
+msgstr "Wartość"
+
 #: utils/datesformats.py:25 utils/datesformats.py:35 utils/datesformats.py:65
 #: utils/datesformats.py:25 utils/datesformats.py:35 utils/datesformats.py:65
 msgid "Never"
 msgid "Never"
 msgstr "Nigdy"
 msgstr "Nigdy"
@@ -7322,9 +7798,9 @@ msgstr "%(day)s, o %(hour)s"
 
 
 #: utils/datesformats.py:77
 #: utils/datesformats.py:77
 msgid "Just now"
 msgid "Just now"
-msgstr "Tuż przed chwilą"
+msgstr "Przed chwilą"
 
 
-#: utils/datesformats.py:80 utils/datesformats.py:85
+#: utils/datesformats.py:82
 #, python-format
 #, python-format
 msgid "Minute ago"
 msgid "Minute ago"
 msgid_plural "%(minutes)s minutes ago"
 msgid_plural "%(minutes)s minutes ago"
@@ -7332,15 +7808,15 @@ msgstr[0] "Minutę temu"
 msgstr[1] "%(minutes)s minuty temu"
 msgstr[1] "%(minutes)s minuty temu"
 msgstr[2] "%(minutes)s minut temu"
 msgstr[2] "%(minutes)s minut temu"
 
 
-#: utils/datesformats.py:95
+#: utils/datesformats.py:92
 #, python-format
 #, python-format
 msgid "Hour and %(minutes)s ago"
 msgid "Hour and %(minutes)s ago"
 msgid_plural "%(hours)s hours and %(minutes)s ago"
 msgid_plural "%(hours)s hours and %(minutes)s ago"
-msgstr[0] "Jedną godzinę i %(minutes)s temu"
+msgstr[0] "Godzinę i %(minutes)s temu"
 msgstr[1] "%(hours)s godziny i %(minutes)s temu"
 msgstr[1] "%(hours)s godziny i %(minutes)s temu"
 msgstr[2] "%(hours)s godzin i %(minutes)s temu"
 msgstr[2] "%(hours)s godzin i %(minutes)s temu"
 
 
-#: utils/datesformats.py:98
+#: utils/datesformats.py:95
 #, python-format
 #, python-format
 msgid "%(minutes)s minute"
 msgid "%(minutes)s minute"
 msgid_plural "%(minutes)s minutes"
 msgid_plural "%(minutes)s minutes"
@@ -7348,12 +7824,12 @@ msgstr[0] "%(minutes)s minutę"
 msgstr[1] "%(minutes)s minuty"
 msgstr[1] "%(minutes)s minuty"
 msgstr[2] "%(minutes)s minut"
 msgstr[2] "%(minutes)s minut"
 
 
-#: utils/datesformats.py:101
+#: utils/datesformats.py:98
 #, python-format
 #, python-format
 msgid "%(hours)s hours and %(minutes)s minutes ago"
 msgid "%(hours)s hours and %(minutes)s minutes ago"
 msgstr "%(hours)s godzin i %(minutes)s minut temu"
 msgstr "%(hours)s godzin i %(minutes)s minut temu"
 
 
-#: utils/datesformats.py:104
+#: utils/datesformats.py:101
 #, python-format
 #, python-format
 msgid "Hour ago"
 msgid "Hour ago"
 msgid_plural "%(hours)s hours ago"
 msgid_plural "%(hours)s hours ago"
@@ -7696,3 +8172,113 @@ msgstr ""
 #: utils/timezones.py:69
 #: utils/timezones.py:69
 msgid "(UTC+14:00) Nuku'alofa"
 msgid "(UTC+14:00) Nuku'alofa"
 msgstr ""
 msgstr ""
+
+#~ msgid "Follow"
+#~ msgstr "Obserwuj"
+
+#~ msgid "Ignoring"
+#~ msgstr "Ignorowany"
+
+#~ msgid "Ignore"
+#~ msgstr "Ignoruj"
+
+#~ msgid "Added on %(date)s:"
+#~ msgstr "Dodano %(date)s:"
+
+#, fuzzy
+#~ msgid "Archive pruned forums?"
+#~ msgstr "Wyczyść forum"
+
+#~ msgid "Post Content"
+#~ msgstr "Treść posta"
+
+#~ msgid "Subforums List Attributes"
+#~ msgstr "Atrybuty stylu listy subforów"
+
+#~ msgid "Are you sure you want to delete all content from selected forums?"
+#~ msgstr "Czy ta pewno chcesz usunąć całą zawartość zaznaczonych forów?"
+
+#~ msgid "Selected forums have been pruned successfully."
+#~ msgstr "Wskazane fora zostały pomyślnie wyczyszczone."
+
+#~ msgid "Save Category"
+#~ msgstr "Zapisz kategorię"
+
+#~ msgid "Save Forum"
+#~ msgstr "Zapisz forum"
+
+#~ msgid ""
+#~ "You have to create at least one category before you will be able to "
+#~ "create forums."
+#~ msgstr ""
+#~ "Aby utworzyć forum, musisz najpierw posiadać przynajmniej jedną kategorię."
+
+#~ msgid ""
+#~ "You have to create at least one category before you will be able to "
+#~ "create redirects."
+#~ msgstr ""
+#~ "Aby utworzyć przekierowanie, musisz najpierw posiadać przynajmniej jedną "
+#~ "kategorię."
+
+#~ msgid "Hidden Sessions"
+#~ msgstr "Ukryte sesje"
+
+#~ msgid "Create new category"
+#~ msgstr "Utwórz nową kategorię"
+
+#~ msgid "New Forum"
+#~ msgstr "Nowe forum"
+
+#~ msgid "Create new redirect"
+#~ msgstr "Utwórz nowe przekierowanie"
+
+#~ msgid "Soft delete posts"
+#~ msgstr "Odwracalnie usuń posty"
+
+#~ msgid "Hard delete posts"
+#~ msgstr "Usuń posty na stałe"
+
+#~ msgid "Soft delete this thread"
+#~ msgstr "Usuń przywracalnie ten temat"
+
+#~ msgid "Hard delete this thread"
+#~ msgstr "Usuń ten temat na stałe"
+
+#~ msgid "Undelete threads"
+#~ msgstr "Przywróć tematy"
+
+#~ msgid "Soft delete threads"
+#~ msgstr "Usuń nietrwale tematy"
+
+#~ msgid "Hard delete threads"
+#~ msgstr "Usuń tematy na stałe"
+
+#~ msgid "Selected threads have been undeleted."
+#~ msgstr "Zaznaczone tematy zostały przywrócone."
+
+#~ msgid "Selected threads have been softly deleted."
+#~ msgstr "Zaznaczone tematy zostały nietrwale usunięte."
+
+#~ msgid "Thread has been undeleted."
+#~ msgstr "Temat został przywrócony."
+
+#~ msgid ""
+#~ "Your account will remain inactive until Board Administrator accepts it. "
+#~ "Depending on number of new registrations this may take few minutes or few "
+#~ "days. Thanks for patience!"
+#~ msgstr ""
+#~ "Twoje konto pozostanie nieaktywne do czasu akceptacji przez "
+#~ "administratora forum. W zależności od ilości nowych rejestracji, może to "
+#~ "potrwać od kilku minut, do kilku dni. Dziękujemy za cierpliwość!"
+
+#~ msgid "Sign In using your account data"
+#~ msgstr "Zaloguj się na swoje konto"
+
+#~ msgid "Hard"
+#~ msgstr "Na stałe"
+
+#~ msgid "Soft"
+#~ msgstr "Odwracalnie"
+
+#~ msgid "Delete reply:"
+#~ msgstr "Usuń odpowiedź:"

+ 2 - 1
misago/management/commands/clearattempts.py

@@ -1,4 +1,5 @@
 from datetime import timedelta
 from datetime import timedelta
+from django.core.management.base import BaseCommand
 from django.utils import timezone
 from django.utils import timezone
 from misago.models import SignInAttempt
 from misago.models import SignInAttempt
 
 
@@ -7,6 +8,6 @@ class Command(BaseCommand):
     This command is intended to work as CRON job fired every few days to remove failed sign-in attempts
     This command is intended to work as CRON job fired every few days to remove failed sign-in attempts
     """
     """
     help = 'Clears sign-in attempts log'
     help = 'Clears sign-in attempts log'
-    def handle(self):
+    def handle(self, *args, **options):
         SignInAttempt.objects.filter(date__lte=timezone.now() - timedelta(hours=24)).delete()
         SignInAttempt.objects.filter(date__lte=timezone.now() - timedelta(hours=24)).delete()
         self.stdout.write('Failed Sign-In attempts older than 24h have been removed.\n')
         self.stdout.write('Failed Sign-In attempts older than 24h have been removed.\n')

+ 9 - 0
misago/management/commands/clearmonitor.py

@@ -0,0 +1,9 @@
+from django.core.management.base import BaseCommand
+from misago.models import MonitorItem
+
+class Command(BaseCommand):
+    help = 'Clears forum monitor'
+
+    def handle(self, *args, **options):
+        MonitorItem.objects.filter(_value__isnull=True).delete()
+        self.stdout.write('\nForum monitor has been cleared.\n')

+ 13 - 0
misago/management/commands/countreports.py

@@ -0,0 +1,13 @@
+from django.core.management.base import BaseCommand
+from misago.models import Post
+from misago.monitor import Monitor
+
+class Command(BaseCommand):
+    """
+    This command is intended to work as CRON job fired every few minutes/hours to count reported posts
+    """
+    help = 'Counts reported posts'
+    def handle(self, *args, **options):
+        monitor = Monitor()
+        monitor['reported_posts'] = Post.objects.filter(reported=True).count()
+        self.stdout.write('Reported posts were recounted.\n')

+ 0 - 289
misago/management/commands/migratefrom01.py

@@ -1,289 +0,0 @@
-from django.conf import settings
-from django.core.management import CommandError
-from django.core.management.base import BaseCommand
-from django.db import connections
-from misago.models import *
-
-class Command(BaseCommand):
-    """
-    Small utility that migrates data from Misago 0.1 instalation to 0.2
-    """
-    help = 'Updates Popular Threads ranking'
-    def handle(self, *args, **options):
-        self.cursor = connections['deprecated'].cursor()
-        self.stdout.write('\nBeginning migration from Misago 0.1...\n')
-
-        self.mig_settings()
-        self.mig_monitor()
-
-        self.users = {}
-        self.forums = {}
-        self.threads = {}
-
-        self.mig_users()
-        self.mig_users_roles()
-        self.mig_users_relations()
-        
-        Karma.objects.all().delete()
-        Change.objects.all().delete()
-        Checkpoint.objects.all().delete()
-        Post.objects.all().delete()
-        Thread.objects.all().delete()
-
-        self.mig_forums()
-        
-        self.mig_threads()
-        self.mig_posts()
-        self.mig_subs()
-
-        self.sync_threads()
-        self.sync_forums()
-        
-        self.stdout.write('\nData was migrated.\n')
-
-    def mig_settings(self):
-        self.stdout.write('Migrating Database Settings...')
-        self.cursor.execute("SELECT setting, value FROM settings_setting");
-        for row in self.dictfetchall():
-            Setting.objects.filter(setting=row['setting']).update(value=row['value'])
-
-    def mig_monitor(self):
-        self.stdout.write('Migrating Forum Monitor...')
-        self.cursor.execute("SELECT id, value, updated FROM monitor_item");
-        for row in self.dictfetchall():
-            MonitorItem.objects.filter(id=row['id']).update(value=row['value'], updated=row['updated'])
-
-    def mig_users(self):
-        User.objects.all().delete()
-        self.stdout.write('Migrating Users...')
-        self.cursor.execute("SELECT * FROM users_user");
-        for row in self.dictfetchall():
-            self.users[row['id']] = User.objects.create(
-                                                        username=row['username'],
-                                                        username_slug=row['username_slug'],
-                                                        email=row['email'],
-                                                        email_hash=row['email_hash'],
-                                                        password=row['password'],
-                                                        password_date=row['password_date'],
-                                                        avatar_type=row['avatar_type'],
-                                                        avatar_image=row['avatar_image'],
-                                                        avatar_original=row['avatar_original'],
-                                                        avatar_temp=row['avatar_temp'],
-                                                        signature=row['signature'],
-                                                        signature_preparsed=row['signature_preparsed'],
-                                                        join_date=row['join_date'],
-                                                        join_ip=row['join_ip'],
-                                                        join_agent=row['join_agent'],
-                                                        last_date=row['last_date'],
-                                                        last_ip=row['last_ip'],
-                                                        last_agent=row['last_agent'],
-                                                        hide_activity=row['hide_activity'],
-                                                        subscribe_start=row['subscribe_start'],
-                                                        subscribe_reply=row['subscribe_reply'],
-                                                        receive_newsletters=row['receive_newsletters'],
-                                                        threads=row['threads'],
-                                                        posts=row['posts'],
-                                                        votes=row['votes'],
-                                                        karma_given_p=row['karma_given_p'],
-                                                        karma_given_n=row['karma_given_n'],
-                                                        karma_p=row['karma_p'],
-                                                        karma_n=row['karma_n'],
-                                                        following=row['following'],
-                                                        followers=row['followers'],
-                                                        score=row['score'],
-                                                        ranking=row['ranking'],
-                                                        last_sync=row['last_sync'],
-                                                        title=row['title'],
-                                                        last_post=row['last_post'],
-                                                        last_search=row['last_search'],
-                                                        alerts=row['alerts'],
-                                                        alerts_date=row['alerts_date'],
-                                                        activation=row['activation'],
-                                                        token=row['token'],
-                                                        avatar_ban=row['avatar_ban'],
-                                                        avatar_ban_reason_user=row['avatar_ban_reason_user'],
-                                                        avatar_ban_reason_admin=row['avatar_ban_reason_admin'],
-                                                        signature_ban=row['signature_ban'],
-                                                        signature_ban_reason_user=row['signature_ban_reason_user'],
-                                                        signature_ban_reason_admin=row['signature_ban_reason_admin'],
-                                                        timezone=row['timezone'],
-                                                        is_team=row['is_team'],
-                                                        acl_key=row['acl_key'],
-                                                        )
-
-    def mig_users_roles(self):
-        self.stdout.write('Migrating Users Roles...')
-        self.cursor.execute("SELECT * FROM users_user_roles");
-        for row in self.dictfetchall():
-            self.users[row['user_id']].roles.add(Role.objects.get(id=row['role_id']))
-
-    def mig_users_relations(self):
-        self.stdout.write('Migrating Users Relations...')
-        self.cursor.execute("SELECT * FROM users_user_follows");
-        for row in self.dictfetchall():
-            self.users[row['from_user_id']].follows.add(self.users[row['to_user_id']])
-        self.cursor.execute("SELECT * FROM users_user_ignores");
-        for row in self.dictfetchall():
-            self.users[row['from_user_id']].ignores.add(self.users[row['to_user_id']])
-
-    def mig_forums(self):
-        self.stdout.write('Migrating Forums...')
-        self.forums[4] = Forum.objects.get(special='root')
-        for forum in self.forums[4].get_descendants():
-            forum.delete()
-        self.forums[4] = Forum.objects.get(special='root')
-        self.cursor.execute("SELECT * FROM forums_forum WHERE level > 0 ORDER BY lft");
-        for row in self.dictfetchall():
-            self.forums[row['id']] = Forum(
-                                           type=row['type'],
-                                           special=row['token'],
-                                           name=row['name'],
-                                           slug=row['slug'],
-                                           description=row['description'],
-                                           description_preparsed=row['description_preparsed'],
-                                           redirect=row['redirect'],
-                                           redirects=row['redirects'],
-                                           attrs=row['attrs'],
-                                           show_details=row['show_details'],
-                                           style=row['style'],
-                                           closed=row['closed'],
-                                           )
-            self.forums[row['id']].insert_at(self.forums[row['parent_id']], position='last-child', save=True)
-            Forum.objects.populate_tree(True)
-
-    def mig_threads(self):
-        self.stdout.write('Migrating Threads...')
-        self.cursor.execute("SELECT * FROM threads_thread");
-        for row in self.dictfetchall():
-            self.threads[row['id']] = Thread.objects.create(                
-                                                            forum=self.forums[row['forum_id']],
-                                                            weight=row['weight'],
-                                                            name=row['name'],
-                                                            slug=row['slug'],
-                                                            merges=row['merges'],
-                                                            score=row['score'],
-                                                            upvotes=row['upvotes'],
-                                                            downvotes=row['downvotes'],
-                                                            start=row['start'],
-                                                            start_poster_name='a',
-                                                            start_poster_slug='a',
-                                                            last=row['last'],
-                                                            deleted=row['deleted'],
-                                                            closed=row['closed'],
-                                                            )
-
-    def mig_posts(self):
-        self.stdout.write('Migrating Posts...')
-        self.cursor.execute("SELECT * FROM threads_post");
-        for row in self.dictfetchall():
-            post = Post.objects.create(
-                                       forum=self.forums[row['forum_id']],
-                                       thread=self.threads[row['thread_id']],
-                                       merge=row['merge'],
-                                       user=(self.users[row['user_id']] if row['user_id'] else None),
-                                       user_name=row['user_name'],
-                                       ip=row['ip'],
-                                       agent=row['agent'],
-                                       post=row['post'],
-                                       post_preparsed=row['post_preparsed'],
-                                       upvotes=row['upvotes'],
-                                       downvotes=row['downvotes'],
-                                       checkpoints=row['checkpoints'],
-                                       date=row['date'],
-                                       edits=row['edits'],
-                                       edit_date=row['edit_date'],
-                                       edit_reason=row['edit_reason'],
-                                       edit_user=(self.users[row['edit_user_id']] if row['edit_user_id'] else None),
-                                       edit_user_name=row['edit_user_name'],
-                                       edit_user_slug=row['edit_user_slug'],
-                                       reported=row['reported'],
-                                       moderated=row['moderated'],
-                                       deleted=row['deleted'],
-                                       protected=row['protected'],
-                                       )
-
-            # Migrate post checkpoints
-            self.cursor.execute("SELECT * FROM threads_checkpoint WHERE post_id = %s" % row['id']);
-            for related in self.dictfetchall():
-                Checkpoint.objects.create(
-                                          forum=self.forums[row['forum_id']],
-                                          thread=self.threads[row['thread_id']],
-                                          post=post,
-                                          action=related['action'],
-                                          user=(self.users[related['user_id']] if related['user_id'] else None),
-                                          user_name=related['user_name'],
-                                          user_slug=related['user_slug'],
-                                          date=related['date'],
-                                          ip=related['ip'],
-                                          agent=related['agent'],
-                                          )
-
-            # Migrate post edits
-            self.cursor.execute("SELECT * FROM threads_change WHERE post_id = %s" % row['id']);
-            for related in self.dictfetchall():
-                Change.objects.create(
-                                      forum=self.forums[row['forum_id']],
-                                      thread=self.threads[row['thread_id']],
-                                      post=post,
-                                      user=(self.users[related['user_id']] if related['user_id'] else None),
-                                      user_name=related['user_name'],
-                                      user_slug=related['user_slug'],
-                                      date=related['date'],
-                                      ip=related['ip'],
-                                      agent=related['agent'],
-                                      reason=related['reason'],
-                                      thread_name_new=related['thread_name_new'],
-                                      thread_name_old=related['thread_name_old'],
-                                      post_content=related['post_content'],
-                                      size=related['size'],
-                                      change=related['change'],
-                                      )
-
-            # Migrate post karmas
-            self.cursor.execute("SELECT * FROM threads_karma WHERE post_id = %s" % row['id']);
-            for related in self.dictfetchall():
-                Karma.objects.create(
-                                     forum=self.forums[row['forum_id']],
-                                     thread=self.threads[row['thread_id']],
-                                     post=post,
-                                     user=(self.users[related['user_id']] if related['user_id'] else None),
-                                     user_name=related['user_name'],
-                                     user_slug=related['user_slug'],
-                                     date=related['date'],
-                                     ip=related['ip'],
-                                     agent=related['agent'],
-                                     score=related['score'],
-                                     )
-
-            # Migrate mentions
-            self.cursor.execute("SELECT * FROM threads_post_mentions WHERE post_id = %s" % row['id']);
-            for related in self.dictfetchall():
-                post.mentions.add(self.users[related['user_id']])
-
-    def mig_subs(self):
-        self.stdout.write('Migrating Subscribtions...')
-        self.cursor.execute("SELECT * FROM watcher_threadwatch");
-        for row in self.dictfetchall():
-            WatchedThread.objects.create( 
-                                         user=self.users[row['user_id']],
-                                         forum=self.forums[row['forum_id']],
-                                         thread=self.threads[row['thread_id']],
-                                         last_read=row['last_read'],
-                                         email=row['email'],
-                                         )
-
-    def sync_threads(self):
-        self.stdout.write('Synchronising Threads...')
-        for thread in Thread.objects.all():
-            thread.sync()
-            thread.save(force_update=True)
-
-    def sync_forums(self):
-        self.stdout.write('Synchronising Forums...')
-        for forum in Forum.objects.all():
-            forum.sync()
-            forum.save(force_update=True)
-
-    def dictfetchall(self):
-        desc = self.cursor.description
-        return [dict(zip([col[0] for col in desc], row)) for row in self.cursor.fetchall()]

+ 20 - 5
misago/management/commands/pruneforums.py

@@ -10,20 +10,35 @@ class Command(BaseCommand):
     """
     """
     help = 'Updates Popular Threads ranking'
     help = 'Updates Popular Threads ranking'
     def handle(self, *args, **options):
     def handle(self, *args, **options):
+        sync_forums = []
         for forum in Forum.objects.all():
         for forum in Forum.objects.all():
+            archive = forum.pruned_archive
             deleted = 0
             deleted = 0
             if forum.prune_start:
             if forum.prune_start:
                 for thread in forum.thread_set.filter(weight=0).filter(start__lte=timezone.now() - timedelta(days=forum.prune_start)):
                 for thread in forum.thread_set.filter(weight=0).filter(start__lte=timezone.now() - timedelta(days=forum.prune_start)):
-                    thread.delete()
+                    if archive:
+                        thread.move_to(archive)
+                        thread.save(force_update=True)
+                    else:
+                        thread.delete()                        
                     deleted += 1
                     deleted += 1
             if forum.prune_last:
             if forum.prune_last:
                 for thread in forum.thread_set.filter(weight=0).filter(last__lte=timezone.now() - timedelta(days=forum.prune_last)):
                 for thread in forum.thread_set.filter(weight=0).filter(last__lte=timezone.now() - timedelta(days=forum.prune_last)):
-                    thread.delete()
+                    if archive:
+                        thread.move_to(archive)
+                        thread.save(force_update=True)
+                    else:
+                        thread.delete()
                     deleted += 1
                     deleted += 1
             if deleted:
             if deleted:
-                forum.sync()
-                forum.save(force_update=True)
+                if forum not in sync_forums:
+                    sync_forums.append(forum)
+                if archive and archive not in sync_forums:
+                    sync_forums.append(archive)
+        for forum in sync_forums:
+            forum.sync()
+            forum.save(force_update=True)
         monitor = Monitor()
         monitor = Monitor()
-        monitor['threads'] = Post.objects.count()
+        monitor['threads'] = Thread.objects.count()
         monitor['posts'] = Post.objects.count()
         monitor['posts'] = Post.objects.count()
         self.stdout.write('Forums were pruned.\n')
         self.stdout.write('Forums were pruned.\n')

+ 1 - 2
misago/management/commands/rebuildacls.py

@@ -5,6 +5,5 @@ class Command(BaseCommand):
     help = 'Rebuilds ACLs for all users'
     help = 'Rebuilds ACLs for all users'
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
-        monitor = Monitor()
-        monitor['acl_version'] = int(monitor['acl_version']) + 1
+        Monitor().increase('acl_version')
         self.stdout.write('\nUser ACLs cache has been set as outdated and will be rebuild when needed.\n')
         self.stdout.write('\nUser ACLs cache has been set as outdated and will be rebuild when needed.\n')

+ 22 - 0
misago/management/commands/reparseposts.py

@@ -0,0 +1,22 @@
+from django.core.management.base import BaseCommand
+from misago.markdown import post_markdown
+from misago.models import Post
+
+class Command(BaseCommand):
+    help = 'Reparse markdown for all forum posts'
+
+    def handle(self, *args, **options):
+        count = 0
+        total = Post.objects.count()
+        last = 0
+
+        self.stdout.write('\nReparsing posts...')
+        for post in Post.objects.iterator():
+            md, post.post_preparsed = post_markdown(post.post)
+            post.save(force_update=True)
+            count += 1
+            progress = (count * 100 / total)
+            if not progress % 10 and progress > last and progress < 100:
+                self.stdout.write('Reparsed %s out of %s posts...' % (count, total))
+                last = progress
+        self.stdout.write('\n%s posts have been reparsed.\n' % count)

+ 6 - 3
misago/management/commands/updateranking.py

@@ -30,7 +30,10 @@ class Command(BaseCommand):
 
 
         # Inflate scores
         # Inflate scores
         settings = DBSettings()
         settings = DBSettings()
-        inflation = float(100 - settings['ranking_inflation']) / 100
-        User.objects.all().update(score=F('score') * inflation, ranking=0)
+        if settings['ranking_inflation']:
+            inflation = float(100 - settings['ranking_inflation']) / 100
+            User.objects.all().update(acl_key=None, score=F('score') * inflation, ranking=0)
+        else:
+            User.objects.all().update(acl_key=None)
 
 
-        self.stdout.write('Users ranking for has been updated.\n')
+        self.stdout.write('Users ranking has been updated.\n')

+ 20 - 0
misago/markdown/extensions/bbcodes.py

@@ -0,0 +1,20 @@
+import re
+import markdown
+from markdown.inlinepatterns import SimpleTagPattern
+
+EMPHASIS_RE = r'\[i\]([^*]+)\[/i\]'
+STRONG_RE = r'\[b\]([^*]+)\[/b\]'
+STRONG_ALT_RE = r'\[u\]([^*]+)\[/u\]'
+
+class BBCodesExtension(markdown.Extension):
+    def extendMarkdown(self, md):
+        md.registerExtension(self)
+        md.inlinePatterns.add('mi_bb_bold_alt',
+                              SimpleTagPattern(STRONG_ALT_RE, 'strong'),
+                              '>strong')
+        md.inlinePatterns.add('mi_bb_bold',
+                              SimpleTagPattern(STRONG_RE, 'strong'),
+                              '>strong')
+        md.inlinePatterns.add('mi_bb_italics',
+                              SimpleTagPattern(EMPHASIS_RE, 'em'),
+                              '>emphasis')

+ 37 - 0
misago/markdown/extensions/cleanlinks.py

@@ -0,0 +1,37 @@
+import markdown
+from markdown.util import etree
+from misago.utils.urls import is_url, is_inner, clean_inner
+
+class CleanLinksExtension(markdown.Extension):
+    def extendMarkdown(self, md):
+        md.registerExtension(self)
+        md.treeprocessors.add('mi_cleanlinks',
+                              CleanLinksTreeprocessor(md),
+                              '_end')
+
+
+class CleanLinksTreeprocessor(markdown.treeprocessors.Treeprocessor):
+    def run(self, root):
+        self.inurl = False
+        return self.walk_tree(root)
+
+    def walk_tree(self, node):
+        if node.tag == 'a':
+            self.inurl = True
+            if is_inner(node.get('href')):
+                node.set('href', clean_inner(node.get('href')))
+            else:
+                node.set('rel', 'nofollow')
+        if node.tag == 'img':
+            if is_inner(node.get('src')):
+                node.set('src', '%s' % clean_inner(node.get('src')))
+
+        try:
+            if self.inurl and node.text and is_url(node.text) and is_inner(node.text):
+                node.text = clean_inner(node.text)[1:]
+        except TypeError:
+            pass
+            
+        for i in node:
+            self.walk_tree(i)
+        self.inurl = False

+ 194 - 0
misago/markdown/extensions/emoji.py

@@ -0,0 +1,194 @@
+import markdown
+from markdown.inlinepatterns import SubstituteTagPattern
+from markdown.util import etree
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+
+EMOJIS = (
+    '+1', '-1', '100', '1234', '8ball', 'a', 'ab', 'abc', 'abcd', 'accept',
+    'aerial_tramway', 'airplane', 'alarm_clock', 'alien', 'ambulance',
+    'anchor', 'angel', 'anger', 'angry', 'anguished', 'ant', 'apple',
+    'aquarius', 'aries', 'arrows_clockwise', 'arrows_counterclockwise',
+    'arrow_backward', 'arrow_double_down', 'arrow_double_up', 'arrow_down',
+    'arrow_down_small', 'arrow_forward', 'arrow_heading_down',
+    'arrow_heading_up', 'arrow_left', 'arrow_lower_left', 'arrow_lower_right',
+    'arrow_right', 'arrow_right_hook', 'arrow_up', 'arrow_upper_left',
+    'arrow_upper_right', 'arrow_up_down', 'arrow_up_small', 'art',
+    'articulated_lorry', 'astonished', 'atm', 'b', 'baby', 'baby_bottle',
+    'baby_chick', 'baby_symbol', 'baggage_claim', 'balloon',
+    'ballot_box_with_check', 'bamboo', 'banana', 'bangbang', 'bank', 'barber',
+    'bar_chart', 'baseball', 'basketball', 'bath', 'bathtub', 'battery',
+    'bear', 'bee', 'beer', 'beers', 'beetle', 'beginner', 'bell', 'bento',
+    'bicyclist', 'bike', 'bikini', 'bird', 'birthday', 'black_circle',
+    'black_joker', 'black_nib', 'black_square', 'black_square_button',
+    'blossom', 'blowfish', 'blue_book', 'blue_car', 'blue_heart', 'blush',
+    'boar', 'boat', 'bomb', 'book', 'bookmark', 'bookmark_tabs', 'books',
+    'boom', 'boot', 'bouquet', 'bow', 'bowling', 'bowtie', 'boy', 'bread',
+    'bride_with_veil', 'bridge_at_night', 'briefcase', 'broken_heart', 'bug',
+    'bulb', 'bullettrain_front', 'bullettrain_side', 'bus', 'busstop',
+    'busts_in_silhouette', 'bust_in_silhouette', 'cactus', 'cake', 'calendar',
+    'calling', 'camel', 'camera', 'cancer', 'candy', 'capital_abcd',
+    'capricorn', 'car', 'card_index', 'carousel_horse', 'cat', 'cat2', 'cd',
+    'chart', 'chart_with_downwards_trend', 'chart_with_upwards_trend',
+    'checkered_flag', 'cherries', 'cherry_blossom', 'chestnut', 'chicken',
+    'children_crossing', 'chocolate_bar', 'christmas_tree', 'church', 'cinema',
+    'circus_tent', 'city_sunrise', 'city_sunset', 'cl', 'clap', 'clapper',
+    'clipboard', 'clock1', 'clock10', 'clock1030', 'clock11', 'clock1130',
+    'clock12', 'clock1230', 'clock130', 'clock2', 'clock230', 'clock3',
+    'clock330', 'clock4', 'clock430', 'clock5', 'clock530', 'clock6',
+    'clock630', 'clock7', 'clock730', 'clock8', 'clock830', 'clock9',
+    'clock930', 'closed_book', 'closed_lock_with_key', 'closed_umbrella',
+    'cloud', 'clubs', 'cn', 'cocktail', 'coffee', 'cold_sweat', 'collision',
+    'computer', 'confetti_ball', 'confounded', 'confused', 'congratulations',
+    'construction', 'construction_worker', 'convenience_store', 'cookie',
+    'cool', 'cop', 'copyright', 'corn', 'couple', 'couplekiss',
+    'couple_with_heart', 'cow', 'cow2', 'credit_card', 'crocodile',
+    'crossed_flags', 'crown', 'cry', 'crying_cat_face', 'crystal_ball',
+    'cupid', 'curly_loop', 'currency_exchange', 'curry', 'custard', 'customs',
+    'cyclone', 'dancer', 'dancers', 'dango', 'dart', 'dash', 'date', 'de',
+    'deciduous_tree', 'department_store', 'diamonds',
+    'diamond_shape_with_a_dot_inside', 'disappointed', 'disappointed_relieved',
+    'dizzy', 'dizzy_face', 'dog', 'dog2', 'dollar', 'dolls', 'dolphin', 'door',
+    'doughnut', 'do_not_litter', 'dragon', 'dragon_face', 'dress',
+    'dromedary_camel', 'droplet', 'dvd', 'e-mail', 'ear', 'earth_africa',
+    'earth_americas', 'earth_asia', 'ear_of_rice', 'egg', 'eggplant', 'eight',
+    'eight_pointed_black_star', 'eight_spoked_asterisk', 'electric_plug',
+    'elephant', 'email', 'end', 'envelope', 'es', 'euro', 'european_castle',
+    'european_post_office', 'evergreen_tree', 'exclamation', 'expressionless',
+    'eyeglasses', 'eyes', 'facepunch', 'factory', 'fallen_leaf', 'family',
+    'fast_forward', 'fax', 'fearful', 'feelsgood', 'feet', 'ferris_wheel',
+    'file_folder', 'finnadie', 'fire', 'fireworks', 'fire_engine',
+    'first_quarter_moon', 'first_quarter_moon_with_face', 'fish',
+    'fishing_pole_and_fish', 'fish_cake', 'fist', 'five', 'flags',
+    'flashlight', 'floppy_disk', 'flower_playing_cards', 'flushed', 'foggy',
+    'football', 'fork_and_knife', 'fountain', 'four', 'four_leaf_clover', 'fr',
+    'free', 'fried_shrimp', 'fries', 'frog', 'frowning', 'fu', 'fuelpump',
+    'full_moon', 'full_moon_with_face', 'game_die', 'gb', 'gem', 'gemini',
+    'ghost', 'gift', 'gift_heart', 'girl', 'globe_with_meridians', 'goat',
+    'goberserk', 'godmode', 'golf', 'grapes', 'green_apple', 'green_book',
+    'green_heart', 'grey_exclamation', 'grey_question', 'grimacing', 'grin',
+    'grinning', 'guardsman', 'guitar', 'gun', 'haircut', 'hamburger', 'hammer',
+    'hamster', 'hand', 'handbag', 'hankey', 'hash', 'hatched_chick',
+    'hatching_chick', 'headphones', 'heart', 'heartbeat', 'heartpulse',
+    'hearts', 'heart_decoration', 'heart_eyes', 'heart_eyes_cat',
+    'hear_no_evil', 'heavy_check_mark', 'heavy_division_sign',
+    'heavy_dollar_sign', 'heavy_exclamation_mark', 'heavy_minus_sign',
+    'heavy_multiplication_x', 'heavy_plus_sign', 'helicopter', 'herb',
+    'hibiscus', 'high_brightness', 'high_heel', 'hocho', 'honeybee',
+    'honey_pot', 'horse', 'horse_racing', 'hospital', 'hotel', 'hotsprings',
+    'hourglass', 'hourglass_flowing_sand', 'house', 'house_with_garden',
+    'hurtrealbad', 'hushed', 'icecream', 'ice_cream', 'id',
+    'ideograph_advantage', 'imp', 'inbox_tray', 'incoming_envelope',
+    'information_desk_person', 'information_source', 'innocent', 'interrobang',
+    'iphone', 'it', 'izakaya_lantern', 'jack_o_lantern', 'japan',
+    'japanese_castle', 'japanese_goblin', 'japanese_ogre', 'jeans', 'joy',
+    'joy_cat', 'jp', 'key', 'keycap_ten', 'kimono', 'kiss', 'kissing',
+    'kissing_cat', 'kissing_closed_eyes', 'kissing_face', 'kissing_heart',
+    'kissing_smiling_eyes', 'koala', 'koko', 'kr', 'large_blue_circle',
+    'large_blue_diamond', 'large_orange_diamond', 'last_quarter_moon',
+    'last_quarter_moon_with_face', 'laughing', 'leaves', 'ledger',
+    'leftwards_arrow_with_hook', 'left_luggage', 'left_right_arrow', 'lemon',
+    'leo', 'leopard', 'libra', 'light_rail', 'link', 'lips', 'lipstick',
+    'lock', 'lock_with_ink_pen', 'lollipop', 'loop', 'loudspeaker',
+    'love_hotel', 'love_letter', 'low_brightness', 'm', 'mag', 'mag_right',
+    'mahjong', 'mailbox', 'mailbox_closed', 'mailbox_with_mail',
+    'mailbox_with_no_mail', 'man', 'mans_shoe', 'man_with_gua_pi_mao',
+    'man_with_turban', 'maple_leaf', 'mask', 'massage', 'meat_on_bone', 'mega',
+    'melon', 'memo', 'mens', 'metal', 'metro', 'microphone', 'microscope',
+    'milky_way', 'minibus', 'minidisc', 'mobile_phone_off', 'moneybag',
+    'money_with_wings', 'monkey', 'monkey_face', 'monorail', 'moon',
+    'mortar_board', 'mountain_bicyclist', 'mountain_cableway',
+    'mountain_railway', 'mount_fuji', 'mouse', 'mouse2', 'movie_camera',
+    'moyai', 'muscle', 'mushroom', 'musical_keyboard', 'musical_note',
+    'musical_score', 'mute', 'nail_care', 'name_badge', 'neckbeard', 'necktie',
+    'negative_squared_cross_mark', 'neutral_face', 'new', 'newspaper',
+    'new_moon', 'new_moon_with_face', 'ng', 'nine', 'non-potable_water',
+    'nose', 'notebook', 'notebook_with_decorative_cover', 'notes', 'no_bell',
+    'no_bicycles', 'no_entry', 'no_entry_sign', 'no_good', 'no_mobile_phones',
+    'no_mouth', 'no_pedestrians', 'no_smoking', 'nut_and_bolt', 'o', 'o2',
+    'ocean', 'octocat', 'octopus', 'oden', 'office', 'ok', 'ok_hand',
+    'ok_woman', 'older_man', 'older_woman', 'on', 'oncoming_automobile',
+    'oncoming_bus', 'oncoming_police_car', 'oncoming_taxi', 'one',
+    'open_file_folder', 'open_hands', 'open_mouth', 'ophiuchus', 'orange_book',
+    'outbox_tray', 'ox', 'pager', 'page_facing_up', 'page_with_curl',
+    'palm_tree', 'panda_face', 'paperclip', 'parking', 'partly_sunny',
+    'part_alternation_mark', 'passport_control', 'paw_prints', 'peach', 'pear',
+    'pencil', 'pencil2', 'penguin', 'pensive', 'performing_arts', 'persevere',
+    'person_frowning', 'person_with_blond_hair', 'person_with_pouting_face',
+    'phone', 'pig', 'pig2', 'pig_nose', 'pill', 'pineapple', 'pisces', 'pizza',
+    'plus1', 'point_down', 'point_left', 'point_right', 'point_up',
+    'point_up_2', 'police_car', 'poodle', 'poop', 'postal_horn', 'postbox',
+    'post_office', 'potable_water', 'pouch', 'poultry_leg', 'pound',
+    'pouting_cat', 'pray', 'princess', 'punch', 'purple_heart', 'purse',
+    'pushpin', 'put_litter_in_its_place', 'question', 'rabbit', 'rabbit2',
+    'racehorse', 'radio', 'radio_button', 'rage', 'rage1', 'rage2', 'rage3',
+    'rage4', 'railway_car', 'rainbow', 'raised_hand', 'raised_hands',
+    'raising_hand', 'ram', 'ramen', 'rat', 'recycle', 'red_car', 'red_circle',
+    'registered', 'relaxed', 'relieved', 'repeat', 'repeat_one', 'restroom',
+    'revolving_hearts', 'rewind', 'ribbon', 'rice', 'rice_ball',
+    'rice_cracker', 'rice_scene', 'ring', 'rocket', 'roller_coaster',
+    'rooster', 'rose', 'rotating_light', 'round_pushpin', 'rowboat', 'ru',
+    'rugby_football', 'runner', 'running', 'running_shirt_with_sash', 'sa',
+    'sagittarius', 'sailboat', 'sake', 'sandal', 'santa', 'satellite',
+    'satisfied', 'saxophone', 'school', 'school_satchel', 'scissors',
+    'scorpius', 'scream', 'scream_cat', 'scroll', 'seat', 'secret', 'seedling',
+    'see_no_evil', 'seven', 'shaved_ice', 'sheep', 'shell', 'ship', 'shipit',
+    'shirt', 'shit', 'shoe', 'shower', 'signal_strength', 'six',
+    'six_pointed_star', 'ski', 'skull', 'sleeping', 'sleepy', 'slot_machine',
+    'small_blue_diamond', 'small_orange_diamond', 'small_red_triangle',
+    'small_red_triangle_down', 'smile', 'smiley', 'smiley_cat', 'smile_cat',
+    'smiling_imp', 'smirk', 'smirk_cat', 'smoking', 'snail', 'snake',
+    'snowboarder', 'snowflake', 'snowman', 'sob', 'soccer', 'soon', 'sos',
+    'sound', 'space_invader', 'spades', 'spaghetti', 'sparkler', 'sparkles',
+    'sparkling_heart', 'speaker', 'speak_no_evil', 'speech_balloon',
+    'speedboat', 'squirrel', 'star', 'star2', 'stars', 'station',
+    'statue_of_liberty', 'steam_locomotive', 'stew', 'straight_ruler',
+    'strawberry', 'stuck_out_tongue', 'stuck_out_tongue_closed_eyes',
+    'stuck_out_tongue_winking_eye', 'sunflower', 'sunglasses', 'sunny',
+    'sunrise', 'sunrise_over_mountains', 'sun_with_face', 'surfer', 'sushi',
+    'suspect', 'suspension_railway', 'sweat', 'sweat_drops', 'sweat_smile',
+    'sweet_potato', 'swimmer', 'symbols', 'syringe', 'tada', 'tanabata_tree',
+    'tangerine', 'taurus', 'taxi', 'tea', 'telephone', 'telephone_receiver',
+    'telescope', 'tennis', 'tent', 'thought_balloon', 'three', 'thumbsdown',
+    'thumbsup', 'ticket', 'tiger', 'tiger2', 'tired_face', 'tm', 'toilet',
+    'tokyo_tower', 'tomato', 'tongue', 'top', 'tophat', 'tractor',
+    'traffic_light', 'train', 'train2', 'tram', 'triangular_flag_on_post',
+    'triangular_ruler', 'trident', 'triumph', 'trolleybus', 'trollface',
+    'trophy', 'tropical_drink', 'tropical_fish', 'truck', 'trumpet', 'tshirt',
+    'tulip', 'turtle', 'tv', 'twisted_rightwards_arrows', 'two', 'two_hearts',
+    'two_men_holding_hands', 'two_women_holding_hands', 'u5272', 'u5408',
+    'u55b6', 'u6307', 'u6708', 'u6709', 'u6e80', 'u7121', 'u7533', 'u7981',
+    'u7a7a', 'uk', 'umbrella', 'unamused', 'underage', 'unlock', 'up', 'us',
+    'v', 'vertical_traffic_light', 'vhs', 'vibration_mode', 'video_camera',
+    'video_game', 'violin', 'virgo', 'volcano', 'vs', 'walking',
+    'waning_crescent_moon', 'waning_gibbous_moon', 'warning', 'watch',
+    'watermelon', 'water_buffalo', 'wave', 'wavy_dash', 'waxing_crescent_moon',
+    'waxing_gibbous_moon', 'wc', 'weary', 'wedding', 'whale', 'whale2',
+    'wheelchair', 'white_check_mark', 'white_circle', 'white_flower',
+    'white_square', 'white_square_button', 'wind_chime', 'wine_glass', 'wink',
+    'wolf', 'woman', 'womans_clothes', 'womans_hat', 'womens', 'worried',
+    'wrench', 'x', 'yellow_heart', 'yen', 'yum', 'zap', 'zero', 'zzz',
+)
+
+
+class EmojiPattern(SubstituteTagPattern):
+    def __init__ (self, emo):
+        super(SubstituteTagPattern, self).__init__(r':%s:' % emo, '')
+        self.emo = emo
+
+    def handleMatch (self, m):
+        element = etree.Element('img')
+        element.set('class', 'emoji')
+        element.set('alt', ':%s:' % self.emo)
+        element.set('title', ':%s:' % self.emo)
+        element.set('src', '%semojis/%s.png' % (settings.STATIC_URL, self.emo))
+        return element
+
+
+class EmojiExtension(markdown.Extension):
+    def extendMarkdown(self, md):
+        md.registerExtension(self)
+        for emo in EMOJIS:
+            md.inlinePatterns.add('mi_emoji_%s' % emo,
+                                  EmojiPattern(emo),
+                                  '_end')

+ 8 - 9
misago/markdown/extensions/magiclinks.py

@@ -4,6 +4,8 @@ import markdown
 from markdown.inlinepatterns import LinkPattern
 from markdown.inlinepatterns import LinkPattern
 from markdown.postprocessors import RawHtmlPostprocessor
 from markdown.postprocessors import RawHtmlPostprocessor
 from markdown.util import etree
 from markdown.util import etree
+from misago.utils.strings import html_escape
+from misago.utils.urls import is_inner, clean_inner
 
 
 # Global vars
 # Global vars
 MAGICLINKS_RE = re.compile(r'(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))', re.UNICODE)
 MAGICLINKS_RE = re.compile(r'(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))', re.UNICODE)
@@ -25,8 +27,11 @@ class MagicLinksTreeprocessor(markdown.treeprocessors.Treeprocessor):
             link = LinkPattern(MAGICLINKS_RE, self.markdown)
             link = LinkPattern(MAGICLINKS_RE, self.markdown)
             href = link.sanitize_url(link.unescape(matchobj.group(0).strip()))
             href = link.sanitize_url(link.unescape(matchobj.group(0).strip()))
             if href:
             if href:
-                href = self.escape(href)
-                return self.markdown.htmlStash.store('<a href="%(href)s">%(href)s</a>' % {'href': href}, safe=True)
+                if is_inner(href):
+                    clean = clean_inner(href)
+                    return self.markdown.htmlStash.store('<a href="%s">%s</a>' % (clean, clean[1:]), safe=True)
+                else:
+                    return self.markdown.htmlStash.store('<a href="%(href)s" rel="nofollow">%(href)s</a>' % {'href': href}, safe=True)
             else:
             else:
                 return matchobj.group(0)
                 return matchobj.group(0)
 
 
@@ -36,10 +41,4 @@ class MagicLinksTreeprocessor(markdown.treeprocessors.Treeprocessor):
             if node.tail and unicode(node.tail).strip():
             if node.tail and unicode(node.tail).strip():
                 node.tail = MAGICLINKS_RE.sub(parse_link, unicode(node.tail))
                 node.tail = MAGICLINKS_RE.sub(parse_link, unicode(node.tail))
             for i in node:
             for i in node:
-                self.walk_tree(i)
-
-    def escape(self, html):
-        html = html.replace('&', '&amp;')
-        html = html.replace('<', '&lt;')
-        html = html.replace('>', '&gt;')
-        return html.replace('"', '&quot;')
+                self.walk_tree(i)

+ 32 - 0
misago/markdown/extensions/shorthandimgs.py

@@ -0,0 +1,32 @@
+#-*- coding: utf-8 -*-
+import re
+import markdown
+from markdown.inlinepatterns import LinkPattern
+from misago.utils.strings import html_escape
+from misago.utils.urls import is_inner, clean_inner
+from markdown.util import etree
+
+IMAGES_RE =  r'\!(\s?)\((<.*?>|([^\)]*))\)'
+
+class ShorthandImagesExtension(markdown.Extension):
+    def extendMarkdown(self, md):
+        md.registerExtension(self)
+        md.inlinePatterns.add('mi_shorthand_imgs',
+                              ShorthandImagePattern(IMAGES_RE, md),
+                              '_end')
+
+
+class ShorthandImagePattern(LinkPattern):
+    def handleMatch(self, m):
+        img_src = m.groups()[2].strip()
+        if is_inner(img_src):
+            img_src = self.sanitize_url(clean_inner(img_src))
+        if img_src:
+            a = etree.Element("a")
+            a.set('href', img_src)
+            a.set('rel', 'nofollow')
+            a.set('target', '_blank')
+            img = etree.SubElement(a, "img")
+            img.set('src', img_src)
+            img.set('alt', img_src)
+            return a

+ 1 - 1
misago/markdown/extensions/strikethrough.py

@@ -3,7 +3,7 @@ import markdown
 from markdown.inlinepatterns import SimpleTagPattern
 from markdown.inlinepatterns import SimpleTagPattern
 
 
 # Global vars
 # Global vars
-STRIKETHROUGH_RE = r'~~(.+?)~~'
+STRIKETHROUGH_RE = r'(~{2})(.+?)\2'
 
 
 class StrikethroughExtension(markdown.Extension):
 class StrikethroughExtension(markdown.Extension):
     def extendMarkdown(self, md):
     def extendMarkdown(self, md):

+ 24 - 43
misago/markdown/factory.py

@@ -1,48 +1,15 @@
 import re
 import re
 import markdown
 import markdown
-from HTMLParser import HTMLParser
 from django.conf import settings
 from django.conf import settings
 from django.utils.importlib import import_module
 from django.utils.importlib import import_module
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.utils.strings import random_string
 from misago.utils.strings import random_string
-
-class ClearHTMLParser(HTMLParser):
-    def __init__(self):
-        HTMLParser.__init__(self)
-        self.clean_text = ''
-        self.lookback = []
-        
-    def handle_entityref(self, name):
-        if name == 'gt':
-            self.clean_text += '>'
-        if name == 'lt':
-            self.clean_text += '<'
-
-    def handle_starttag(self, tag, attrs):
-        self.lookback.append(tag)
-
-    def handle_endtag(self, tag):
-        try:
-            if self.lookback[-1] == tag:
-                self.lookback.pop()
-        except IndexError:
-            pass
-        
-    def handle_data(self, data):
-        # String does not repeat itself
-        if self.clean_text[-len(data):] != data:
-            # String is not "QUOTE"
-            try:
-                if self.lookback[-1] in ('strong', 'em'):
-                    self.clean_text += data
-                elif not (data == 'Quote' and self.lookback[-1] == 'h3' and self.lookback[-2] == 'blockquote'):
-                    self.clean_text += data
-            except IndexError:
-                self.clean_text += data
-
+from misago.markdown.extensions.cleanlinks import CleanLinksExtension
+from misago.markdown.extensions.emoji import EmojiExtension
+from misago.markdown.parsers import RemoveHTMLParser
 
 
 def clear_markdown(text):
 def clear_markdown(text):
-    parser = ClearHTMLParser()
+    parser = RemoveHTMLParser()
     parser.feed(text)
     parser.feed(text)
     return parser.clean_text
     return parser.clean_text
 
 
@@ -62,12 +29,17 @@ def signature_markdown(acl, text):
                            extensions=['nl2br'])
                            extensions=['nl2br'])
 
 
     remove_unsupported(md)
     remove_unsupported(md)
+    cleanlinks = CleanLinksExtension()
+    cleanlinks.extendMarkdown(md)
 
 
     if not acl.usercp.allow_signature_links():
     if not acl.usercp.allow_signature_links():
         del md.inlinePatterns['link']
         del md.inlinePatterns['link']
         del md.inlinePatterns['autolink']
         del md.inlinePatterns['autolink']
     if not acl.usercp.allow_signature_images():
     if not acl.usercp.allow_signature_images():
         del md.inlinePatterns['image_link']
         del md.inlinePatterns['image_link']
+    else:
+        emojis = EmojiExtension()
+        emojis.extendMarkdown(md)
 
 
     del md.parser.blockprocessors['hashheader']
     del md.parser.blockprocessors['hashheader']
     del md.parser.blockprocessors['setextheader']
     del md.parser.blockprocessors['setextheader']
@@ -80,7 +52,7 @@ def signature_markdown(acl, text):
     return md.convert(text)
     return md.convert(text)
 
 
 
 
-def post_markdown(request, text):
+def post_markdown(text):
     md = markdown.Markdown(
     md = markdown.Markdown(
                            safe_mode='escape',
                            safe_mode='escape',
                            output_format=settings.OUTPUT_FORMAT,
                            output_format=settings.OUTPUT_FORMAT,
@@ -96,13 +68,14 @@ def post_markdown(request, text):
         ext = attr()
         ext = attr()
         ext.extendMarkdown(md)
         ext.extendMarkdown(md)
     text = md.convert(text)
     text = md.convert(text)
-    return tidy_markdown(md, text)
+    md, text = tidy_markdown(md, text)
+    return md, text
 
 
 
 
 def tidy_markdown(md, text):
 def tidy_markdown(md, text):
-    text = text.replace('<p><h3><quotetitle>', '<h3><quotetitle>')
-    text = text.replace('</quotetitle></h3></p>', '</quotetitle></h3>')
-    text = text.replace('</quotetitle></h3><br>\r\n', '</quotetitle></h3>\r\n<p>')
+    text = text.replace('<p><h3><quotetitle>', '<article><header><quotetitle>')
+    text = text.replace('</quotetitle></h3></p>', '</quotetitle></header></article>')
+    text = text.replace('</quotetitle></h3><br>\r\n', '</quotetitle></header></article>\r\n<p>')
     text = text.replace('\r\n<p></p>', '')
     text = text.replace('\r\n<p></p>', '')
     return md, text
     return md, text
 
 
@@ -112,4 +85,12 @@ def finalize_markdown(text):
         return _("Posted by %(user)s") % {'user': match.group('content')}
         return _("Posted by %(user)s") % {'user': match.group('content')}
     text = re.sub(r'<quotetitle>(?P<content>.+)</quotetitle>', trans_quotetitle, text)
     text = re.sub(r'<quotetitle>(?P<content>.+)</quotetitle>', trans_quotetitle, text)
     text = re.sub(r'<quotesingletitle>', _("Quote"), text)
     text = re.sub(r'<quotesingletitle>', _("Quote"), text)
-    return text
+    text = re.sub(r'<imgalt>', _("Posted image"), text)
+    return text
+
+
+def emojis():
+    if 'misago.markdown.extensions.emoji.EmojiExtension' in settings.MARKDOWN_EXTENSIONS:
+        from misago.markdown.extensions.emoji import EMOJIS
+        return EMOJIS
+    return []

+ 51 - 0
misago/markdown/parsers.py

@@ -0,0 +1,51 @@
+from HTMLParser import HTMLParser
+from urlparse import urlparse
+from django.conf import settings
+from misago.utils.strings import random_string
+
+class RemoveHTMLParser(HTMLParser):
+    def __init__(self):
+        HTMLParser.__init__(self)
+        self.clean_text = ''
+        self.lookback = []
+
+    def handle_entityref(self, name):
+        if name == 'gt':
+            self.clean_text += '>'
+        if name == 'lt':
+            self.clean_text += '<'
+
+    def handle_starttag(self, tag, attrs):
+        if tag == 'img':
+            self.handle_startendtag(tag, attrs)
+        else:
+            self.lookback.append(tag)
+
+    def handle_endtag(self, tag):
+        try:
+            if self.lookback[-1] == tag:
+                self.lookback.pop()
+        except IndexError:
+            pass
+
+    def handle_startendtag(self, tag, attrs):
+        try:
+            if tag == 'img':
+                for attr in attrs:
+                    if attr[0] == 'alt':
+                        self.clean_text += attr[1]
+                        break
+        except KeyError:
+            pass
+        
+    def handle_data(self, data):
+        # String does not repeat itself
+        if self.clean_text[-len(data):] != data:
+            # String is not "QUOTE"
+            try:
+                if self.lookback[-1] in ('strong', 'em'):
+                    self.clean_text += data
+                elif not (data == 'Quote' and self.lookback[-1] == 'h3' and self.lookback[-2] == 'blockquote'):
+                    self.clean_text += data
+            except IndexError:
+                self.clean_text += data

+ 1 - 1
misago/middleware/heartbeat.py

@@ -3,6 +3,6 @@ from django.http import HttpResponse
 
 
 class HeartbeatMiddleware(object):
 class HeartbeatMiddleware(object):
     def process_request(self, request):
     def process_request(self, request):
-        request.heartbeat = settings.HEARTBEAT_PATH and settings.HEARTBEAT_PATH == request.path
+        request.heartbeat = settings.HEARTBEAT_PATH and settings.HEARTBEAT_PATH == request.path[1:]
         if request.heartbeat:
         if request.heartbeat:
             return HttpResponse('BATTLECRUISER OPERATIONAL')
             return HttpResponse('BATTLECRUISER OPERATIONAL')

+ 17 - 0
misago/middleware/mailsqueue.py

@@ -0,0 +1,17 @@
+from django.conf import settings
+from django.core import mail
+
+class MailsQueueMiddleware(object):
+    def process_request(self, request):
+        request.mails_queue = []
+
+    def process_response(self, request, response):
+        try:
+            if request.mails_queue:
+                connection = mail.get_connection(fail_silently=settings.DEBUG)
+                connection.open()
+                connection.send_messages(request.mails_queue)
+                connection.close()
+        except AttributeError:
+            pass
+        return response

+ 1 - 3
misago/middleware/session.py

@@ -12,9 +12,6 @@ class SessionMiddleware(object):
             request.session = HumanSession(request)
             request.session = HumanSession(request)
             request.user = request.session.get_user()
             request.user = request.session.get_user()
 
 
-            if request.user.is_authenticated():
-                request.session.set_hidden(request.user.hide_activity > 0)
-
     def process_response(self, request, response):
     def process_response(self, request, response):
         try:
         try:
             # Sync last visit date
             # Sync last visit date
@@ -24,6 +21,7 @@ class SessionMiddleware(object):
                     request.session['visit_sync'] = timezone.now()
                     request.session['visit_sync'] = timezone.now()
                     request.user.last_date = timezone.now()
                     request.user.last_date = timezone.now()
                     request.user.save(force_update=True)
                     request.user.save(force_update=True)
+            request.session.match()
             request.session.save()
             request.session.save()
         except AttributeError:
         except AttributeError:
             pass
             pass

+ 12 - 15
misago/middleware/theme.py

@@ -1,24 +1,21 @@
+from urlparse import urlparse
 from django.conf import settings
 from django.conf import settings
 from django.core.cache import cache
 from django.core.cache import cache
 from misago.theme import Theme
 from misago.theme import Theme
-from misago.models import ThemeAdjustment
 
 
 class ThemeMiddleware(object):
 class ThemeMiddleware(object):
     def process_request(self, request):
     def process_request(self, request):
         if not settings.INSTALLED_THEMES:
         if not settings.INSTALLED_THEMES:
             raise ValueError('There are no themes installed!')
             raise ValueError('There are no themes installed!')
         request.theme = Theme(settings.INSTALLED_THEMES[0])
         request.theme = Theme(settings.INSTALLED_THEMES[0])
-        
-        # Adjust theme for specific client?
-        if request.META.get('HTTP_USER_AGENT'):
-            adjustments = cache.get('client_adjustments', 'nada')
-            if adjustments == 'nada':
-                adjustments = ThemeAdjustment.objects.all()
-                cache.set('client_adjustments', adjustments)
-            if adjustments:
-                user_agent = request.META.get('HTTP_USER_AGENT').lower()
-                for item in adjustments:
-                    if item.adjust_theme(user_agent):
-                        request.theme = Theme(item.theme)
-                        break
-            
+
+        if settings.MOBILE_SUBDOMAIN and settings.MOBILE_TEMPLATES:
+            if settings.MOBILE_SUBDOMAIN == '*':
+                request.theme = Theme(settings.MOBILE_TEMPLATES)
+            else:
+                mobile_domain = '%s.%s/' % (settings.MOBILE_SUBDOMAIN, urlparse(settings.BOARD_ADDRESS).netloc)
+                current_domain = '%s.%s/' % (settings.MOBILE_SUBDOMAIN, urlparse(request.META.get('HTTP_HOST')).netloc)
+                
+                if current_domain == mobile_domain:
+                    request.theme = Theme(settings.MOBILE_TEMPLATES)
+                    

+ 9 - 4
misago/middleware/user.py

@@ -2,6 +2,7 @@ from django.conf import settings
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.messages import Message
 from misago.messages import Message
+from misago.onlines import MembersOnline
 
 
 def set_timezone(new_tz):
 def set_timezone(new_tz):
     if settings.USE_TZ:
     if settings.USE_TZ:
@@ -15,14 +16,18 @@ def set_timezone(new_tz):
 class UserMiddleware(object):
 class UserMiddleware(object):
     def process_request(self, request):
     def process_request(self, request):
         if request.user.is_authenticated():
         if request.user.is_authenticated():
-            # Set user timezone and rank
             request.session.rank = request.user.rank_id
             request.session.rank = request.user.rank_id
             set_timezone(request.user.timezone)
             set_timezone(request.user.timezone)
-
-            # Display "welcome back!" message
             if request.session.remember_me:
             if request.session.remember_me:
                 request.messages.set_message(Message(_("Welcome back, %(username)s! We've signed you in automatically for your convenience.") % {'username': request.user.username}), 'info')
                 request.messages.set_message(Message(_("Welcome back, %(username)s! We've signed you in automatically for your convenience.") % {'username': request.user.username}), 'info')
         else:
         else:
-            # Set guest's timezone and empty rank
             set_timezone(request.settings['default_timezone'])
             set_timezone(request.settings['default_timezone'])
             request.session.rank = None
             request.session.rank = None
+        request.onlines = MembersOnline(request.settings['online_counting'], request.monitor, request.settings['online_counting_frequency'])
+
+    def process_response(self, request, response):
+        try:
+            request.onlines.sync()
+        except AttributeError:
+            pass
+        return response

+ 396 - 0
misago/migrations/0002_auto__del_field_session_hidden.py

@@ -0,0 +1,396 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Deleting field 'Session.hidden'
+        db.delete_column(u'misago_session', 'hidden')
+
+
+    def backwards(self, orm):
+        # Adding field 'Session.hidden'
+        db.add_column(u'misago_session', 'hidden',
+                      self.gf('django.db.models.fields.BooleanField')(default=False),
+                      keep_default=False)
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.themeadjustment': {
+            'Meta': {'object_name': 'ThemeAdjustment'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'theme': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'useragents': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 415 - 0
misago/migrations/0003_auto__add_field_checkpoint_old_forum__add_field_checkpoint_old_forum_n.py

@@ -0,0 +1,415 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Checkpoint.old_forum'
+        db.add_column(u'misago_checkpoint', 'old_forum',
+                      self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='+', null=True, to=orm['misago.Forum']),
+                      keep_default=False)
+
+        # Adding field 'Checkpoint.old_forum_name'
+        db.add_column(u'misago_checkpoint', 'old_forum_name',
+                      self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
+                      keep_default=False)
+
+        # Adding field 'Checkpoint.old_forum_slug'
+        db.add_column(u'misago_checkpoint', 'old_forum_slug',
+                      self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Checkpoint.old_forum'
+        db.delete_column(u'misago_checkpoint', 'old_forum_id')
+
+        # Deleting field 'Checkpoint.old_forum_name'
+        db.delete_column(u'misago_checkpoint', 'old_forum_name')
+
+        # Deleting field 'Checkpoint.old_forum_slug'
+        db.delete_column(u'misago_checkpoint', 'old_forum_slug')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.themeadjustment': {
+            'Meta': {'object_name': 'ThemeAdjustment'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'theme': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'useragents': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 400 - 0
misago/migrations/0004_auto__add_field_checkpoint_deleted.py

@@ -0,0 +1,400 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Checkpoint.deleted'
+        db.add_column(u'misago_checkpoint', 'deleted',
+                      self.gf('django.db.models.fields.BooleanField')(default=False),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Checkpoint.deleted'
+        db.delete_column(u'misago_checkpoint', 'deleted')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.themeadjustment': {
+            'Meta': {'object_name': 'ThemeAdjustment'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'theme': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'useragents': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 401 - 0
misago/migrations/0005_auto__add_field_forum_pruned_archive.py

@@ -0,0 +1,401 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Forum.pruned_archive'
+        db.add_column(u'misago_forum', 'pruned_archive',
+                      self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='+', null=True, on_delete=models.SET_NULL, to=orm['misago.Forum']),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Forum.pruned_archive'
+        db.delete_column(u'misago_forum', 'pruned_archive_id')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.themeadjustment': {
+            'Meta': {'object_name': 'ThemeAdjustment'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'theme': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'useragents': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 405 - 0
misago/migrations/0006_auto.py

@@ -0,0 +1,405 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding M2M table for field roles on 'Rank'
+        db.create_table(u'misago_rank_roles', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('rank', models.ForeignKey(orm['misago.rank'], null=False)),
+            ('role', models.ForeignKey(orm['misago.role'], null=False))
+        ))
+        db.create_unique(u'misago_rank_roles', ['rank_id', 'role_id'])
+
+
+    def backwards(self, orm):
+        # Removing M2M table for field roles on 'Rank'
+        db.delete_table('misago_rank_roles')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.themeadjustment': {
+            'Meta': {'object_name': 'ThemeAdjustment'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'theme': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'useragents': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 394 - 0
misago/migrations/0007_removethemeadjustments.py

@@ -0,0 +1,394 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        db.delete_table('misago_themeadjustment')
+
+    def backwards(self, orm):
+        db.create_table(u'misago_themeadjustment', (
+            ('theme', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
+            ('useragents', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+        ))
+        db.send_create_signal('misago', ['ThemeAdjustment'])
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 397 - 0
misago/migrations/0008_auto__add_field_thread_report_for.py

@@ -0,0 +1,397 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Thread.report_for'
+        db.add_column(u'misago_thread', 'report_for',
+                      self.gf('django.db.models.fields.related.ForeignKey')(blank=True, db_index= True, related_name='reports', null=True, to=orm['misago.Post']),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Thread.report_for'
+        db.delete_column(u'misago_thread', 'report_for_id')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'reports'", 'null': 'True', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 404 - 0
misago/migrations/0009_auto__chg_field_thread_report_for__add_field_monitoritem_type.py

@@ -0,0 +1,404 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+
+        # Changing field 'Thread.report_for'
+        db.alter_column(u'misago_thread', 'report_for_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, on_delete=models.SET_NULL, to=orm['misago.Post']))
+        # Adding field 'MonitorItem.type'
+        db.add_column(u'misago_monitoritem', 'type',
+                      self.gf('django.db.models.fields.CharField')(default='int', max_length=255),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+
+        # Changing field 'Thread.report_for'
+        db.alter_column(u'misago_thread', 'report_for_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['misago.Post']))
+        # Deleting field 'MonitorItem.type'
+        db.delete_column(u'misago_monitoritem', 'type')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'string'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'reports'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 396 - 0
misago/migrations/0010_auto__del_field_checkpoint_post__del_field_monitoritem_value__add_fiel.py

@@ -0,0 +1,396 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Deleting field 'Checkpoint.post'
+        db.delete_column(u'misago_checkpoint', 'post_id')
+
+        # Deleting field 'Post.checkpoints'
+        db.delete_column(u'misago_post', 'checkpoints')
+
+
+    def backwards(self, orm):
+        raise RuntimeError("Cannot reverse this migration.")
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'int'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'report_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 394 - 0
misago/migrations/0011_auto__del_field_thread_merges__del_field_post_merge.py

@@ -0,0 +1,394 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Deleting field 'Thread.merges'
+        db.delete_column(u'misago_thread', 'merges')
+
+        # Deleting field 'Post.merge'
+        db.delete_column(u'misago_post', 'merge')
+
+
+    def backwards(self, orm):
+        raise RuntimeError("Cannot reverse this migration.")
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'int'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'report_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 396 - 0
misago/migrations/0012_auto__add_field_post_current_date.py

@@ -0,0 +1,396 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+from django.utils import timezone
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Post.current_date'
+        db.add_column(u'misago_post', 'current_date',
+                      self.gf('django.db.models.fields.DateTimeField')(default=timezone.now(), db_index=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Post.current_date'
+        db.delete_column(u'misago_post', 'current_date')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'int'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'current_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'report_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 391 - 0
misago/migrations/0013_set_posts_current_date.py

@@ -0,0 +1,391 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        for post in orm.Post.objects.all(): 
+            post.current_date = post.edit_date or post.date 
+            post.save()
+
+    def backwards(self, orm):
+        raise RuntimeError("Cannot reverse this migration.")
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'int'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'current_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'report_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']
+    symmetrical = True

+ 391 - 0
misago/migrations/0014_auto__del_field_post_edit_date.py

@@ -0,0 +1,391 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Deleting field 'Post.edit_date'
+        db.delete_column(u'misago_post', 'edit_date')
+
+
+    def backwards(self, orm):
+        raise RuntimeError("Cannot reverse this migration.")
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'int'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'current_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'report_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 388 - 0
misago/migrations/0015_remove_users_reported.py

@@ -0,0 +1,388 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        orm.MonitorItem.objects.filter(pk='users_reported').delete()
+
+    def backwards(self, orm):
+        raise RuntimeError("Cannot reverse this migration.")
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'int'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'current_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'report_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']
+    symmetrical = True

+ 0 - 1
misago/models/__init__.py

@@ -17,7 +17,6 @@ from misago.models.sessionmodel import Session
 from misago.models.settingmodel import Setting
 from misago.models.settingmodel import Setting
 from misago.models.settingsgroupmodel import SettingsGroup
 from misago.models.settingsgroupmodel import SettingsGroup
 from misago.models.signinattemptmodel import SignInAttempt
 from misago.models.signinattemptmodel import SignInAttempt
-from misago.models.themeadjustmentmodel import ThemeAdjustment
 from misago.models.threadmodel import Thread
 from misago.models.threadmodel import Thread
 from misago.models.threadreadmodel import ThreadRead
 from misago.models.threadreadmodel import ThreadRead
 from misago.models.tokenmodel import Token
 from misago.models.tokenmodel import Token

+ 14 - 21
misago/models/checkpointmodel.py

@@ -1,11 +1,10 @@
 from django.db import models
 from django.db import models
 from misago.signals import (merge_post, merge_thread, move_forum_content,
 from misago.signals import (merge_post, merge_thread, move_forum_content,
-                            move_post, move_thread, rename_user)
+                            move_post, move_thread, rename_forum, rename_user)
 
 
 class Checkpoint(models.Model):
 class Checkpoint(models.Model):
     forum = models.ForeignKey('Forum')
     forum = models.ForeignKey('Forum')
     thread = models.ForeignKey('Thread')
     thread = models.ForeignKey('Thread')
-    post = models.ForeignKey('Post')
     action = models.CharField(max_length=255)
     action = models.CharField(max_length=255)
     user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
     user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
     user_name = models.CharField(max_length=255)
     user_name = models.CharField(max_length=255)
@@ -13,14 +12,27 @@ class Checkpoint(models.Model):
     target_user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL, related_name='+')
     target_user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL, related_name='+')
     target_user_name = models.CharField(max_length=255, null=True, blank=True)
     target_user_name = models.CharField(max_length=255, null=True, blank=True)
     target_user_slug = models.CharField(max_length=255, null=True, blank=True)
     target_user_slug = models.CharField(max_length=255, null=True, blank=True)
+    old_forum = models.ForeignKey('Forum', null=True, blank=True, related_name='+')
+    old_forum_name = models.CharField(max_length=255, null=True, blank=True)
+    old_forum_slug = models.CharField(max_length=255, null=True, blank=True)
     date = models.DateTimeField()
     date = models.DateTimeField()
     ip = models.GenericIPAddressField()
     ip = models.GenericIPAddressField()
     agent = models.CharField(max_length=255)
     agent = models.CharField(max_length=255)
+    deleted = models.BooleanField(default=False)
 
 
     class Meta:
     class Meta:
         app_label = 'misago'
         app_label = 'misago'
 
 
 
 
+def rename_forum_handler(sender, **kwargs):
+    Checkpoint.objects.filter(old_forum=sender).update(
+                                                  old_forum_name=sender.name,
+                                                  old_forum_slug=sender.slug,
+                                                  )
+
+rename_forum.connect(rename_forum_handler, dispatch_uid="rename_forum_checkpoints")
+
+
 def rename_user_handler(sender, **kwargs):
 def rename_user_handler(sender, **kwargs):
     Checkpoint.objects.filter(user=sender).update(
     Checkpoint.objects.filter(user=sender).update(
                                                   user_name=sender.username,
                                                   user_name=sender.username,
@@ -46,22 +58,3 @@ def merge_thread_handler(sender, **kwargs):
     Checkpoint.objects.filter(thread=sender).delete()
     Checkpoint.objects.filter(thread=sender).delete()
 
 
 merge_thread.connect(merge_thread_handler, dispatch_uid="merge_threads_checkpoints")
 merge_thread.connect(merge_thread_handler, dispatch_uid="merge_threads_checkpoints")
-
-
-def move_posts_handler(sender, **kwargs):
-    if sender.checkpoints:
-        prev_post = Post.objects.filter(thread=sender.thread_id).filter(merge__lte=sender.merge).exclude(id=sender.pk).order_by('merge', '-id')[:1][0]
-        Checkpoint.objects.filter(post=sender).update(post=prev_post)
-        prev_post.checkpoints = True
-        prev_post.save(force_update=True)
-    sender.checkpoints = False
-
-move_post.connect(move_posts_handler, dispatch_uid="move_posts_checkpoints")
-
-
-def merge_posts_handler(sender, **kwargs):
-    Checkpoint.objects.filter(post=sender).update(post=kwargs['new_post'])
-    if sender.checkpoints:
-        kwargs['new_post'].checkpoints = True
-
-merge_post.connect(merge_posts_handler, dispatch_uid="merge_posts_checkpoints")

+ 74 - 8
misago/models/forummodel.py

@@ -1,22 +1,36 @@
 import urlparse
 import urlparse
+import threading
 from mptt.managers import TreeManager
 from mptt.managers import TreeManager
 from mptt.models import MPTTModel, TreeForeignKey
 from mptt.models import MPTTModel, TreeForeignKey
 from django.conf import settings
 from django.conf import settings
 from django.core.cache import cache
 from django.core.cache import cache
+from django.core.urlresolvers import reverse
 from django.db import models
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
-from misago.signals import delete_forum_content, move_forum_content, rename_user
+from misago.signals import delete_forum_content, move_forum_content, rename_forum, rename_user
+
+thread_local = threading.local()
 
 
 class ForumManager(TreeManager):
 class ForumManager(TreeManager):
-    forums_tree = None
+    @property
+    def forums_tree(self):
+        try:
+            return thread_local.misago_forums_tree
+        except AttributeError:
+            thread_local.misago_forums_tree = None
+        return thread_local.misago_forums_tree
+
+    @forums_tree.setter
+    def forums_tree(self, value):
+        thread_local.misago_forums_tree = value
 
 
     def special_pk(self, name):
     def special_pk(self, name):
         self.populate_tree()
         self.populate_tree()
-        return self.forums_tree[name].pk
+        return self.forums_tree.get(name).pk
 
 
     def special_model(self, name):
     def special_model(self, name):
         self.populate_tree()
         self.populate_tree()
-        return self.forums_tree[name]
+        return self.forums_tree.get(name)
 
 
     def populate_tree(self, force=False):
     def populate_tree(self, force=False):
         if not self.forums_tree:
         if not self.forums_tree:
@@ -123,6 +137,21 @@ class ForumManager(TreeManager):
                 readable.append(forum.pk)
                 readable.append(forum.pk)
         return readable
         return readable
 
 
+    def forum_by_name(self, forum, acl):
+        forums = self.readable_forums(acl, True)
+        forum = forum.lower()
+        for f in forums:
+            f = self.forums_tree[f]
+            if forum == unicode(f).lower():
+                return f
+        forum_len = len(forum)
+        for f in forums:
+            f = self.forums_tree[f]
+            name = unicode(f).lower()
+            if forum == unicode(f).lower()[0:forum_len]:
+                return f
+        return None
+
 
 
 class Forum(MPTTModel):
 class Forum(MPTTModel):
     parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
     parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
@@ -148,6 +177,7 @@ class Forum(MPTTModel):
     last_poster_style = models.CharField(max_length=255, null=True, blank=True)
     last_poster_style = models.CharField(max_length=255, null=True, blank=True)
     prune_start = models.PositiveIntegerField(default=0)
     prune_start = models.PositiveIntegerField(default=0)
     prune_last = models.PositiveIntegerField(default=0)
     prune_last = models.PositiveIntegerField(default=0)
+    pruned_archive = models.ForeignKey('self', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
     redirect = models.CharField(max_length=255, null=True, blank=True)
     redirect = models.CharField(max_length=255, null=True, blank=True)
     attrs = models.CharField(max_length=255, null=True, blank=True)
     attrs = models.CharField(max_length=255, null=True, blank=True)
     show_details = models.BooleanField(default=True)
     show_details = models.BooleanField(default=True)
@@ -177,6 +207,36 @@ class Forum(MPTTModel):
            return unicode(_('Root Category'))
            return unicode(_('Root Category'))
         return unicode(self.name)
         return unicode(self.name)
 
 
+    @property
+    def url(self):
+        if self.special == 'private_threads':
+           reverse('private_threads')
+        if self.special == 'reports':
+           reverse('reports')
+        if self.type == 'category':
+            return reverse('category', kwargs={'forum': self.pk, 'slug': self.slug})
+        if self.type == 'redirect':
+            return reverse('redirect', kwargs={'forum': self.pk, 'slug': self.slug})
+        return reverse('forum', kwargs={'forum': self.pk, 'slug': self.slug})
+
+    def thread_link(self, extra):
+        if self.special == 'private_threads':
+           route_prefix = 'private_thread'
+        if self.special == 'reports':
+           route_prefix = 'report'
+        else:
+            route_prefix = 'thread'
+        if extra:
+            return '%s_%s' % (route_prefix, extra) if extra else route_prefix
+        return route_prefix
+
+    def thread_url(self, thread, route=None):
+        route_prefix = 'thread'
+        if self.special:
+            route_prefix = self.special[0:-1]
+        link = '%s_%s' % (route_prefix, route) if route else route_prefix
+        return reverse(link, kwargs={'thread': thread.pk, 'slug': thread.slug})
+
     def set_description(self, description):
     def set_description(self, description):
         self.description = description.strip()
         self.description = description.strip()
         self.description_preparsed = ''
         self.description_preparsed = ''
@@ -199,6 +259,9 @@ class Forum(MPTTModel):
     def move_content(self, target):
     def move_content(self, target):
         move_forum_content.send(sender=self, move_to=target)
         move_forum_content.send(sender=self, move_to=target)
 
 
+    def sync_name(self):
+        rename_forum.send(sender=self)
+
     def attr(self, att):
     def attr(self, att):
         if self.attrs:
         if self.attrs:
             return att in self.attrs.split()
             return att in self.attrs.split()
@@ -221,9 +284,7 @@ class Forum(MPTTModel):
         self.last_poster_slug = thread.last_poster_slug
         self.last_poster_slug = thread.last_poster_slug
         self.last_poster_style = thread.last_poster_style
         self.last_poster_style = thread.last_poster_style
 
 
-    def sync(self):
-        self.threads = self.thread_set.filter(moderated=False).filter(deleted=False).count()
-        self.posts = self.post_set.filter(moderated=False).count()
+    def sync_last(self):
         self.last_poster = None
         self.last_poster = None
         self.last_poster_name = None
         self.last_poster_name = None
         self.last_poster_slug = None
         self.last_poster_slug = None
@@ -233,7 +294,7 @@ class Forum(MPTTModel):
         self.last_thread_name = None
         self.last_thread_name = None
         self.last_thread_slug = None
         self.last_thread_slug = None
         try:
         try:
-            last_thread = self.thread_set.filter(moderated=False).filter(deleted=False).order_by('-last').all()[0:][0]
+            last_thread = self.thread_set.filter(moderated=False).filter(deleted=False).order_by('-last').all()[:1][0]
             self.last_poster_name = last_thread.last_poster_name
             self.last_poster_name = last_thread.last_poster_name
             self.last_poster_slug = last_thread.last_poster_slug
             self.last_poster_slug = last_thread.last_poster_slug
             self.last_poster_style = last_thread.last_poster_style
             self.last_poster_style = last_thread.last_poster_style
@@ -246,6 +307,11 @@ class Forum(MPTTModel):
         except (IndexError, AttributeError):
         except (IndexError, AttributeError):
             pass
             pass
 
 
+    def sync(self):
+        self.threads = self.thread_set.filter(moderated=False).filter(deleted=False).count()
+        self.posts = self.post_set.filter(moderated=False).count()
+        self.sync_last()
+
     def prune(self):
     def prune(self):
         pass
         pass
 
 

+ 1 - 1
misago/models/forumreadmodel.py

@@ -17,7 +17,7 @@ class ForumRead(models.Model):
         from misago.models import ThreadRead
         from misago.models import ThreadRead
         
         
         threads = {}
         threads = {}
-        for thread in ThreadRead.objects.filter(user=self.user, forum=self.forum, updated__gte=(timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH))):
+        for thread in ThreadRead.objects.filter(user_id=self.user_id, forum_id=self.forum_id, updated__gte=(timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH))):
             threads[thread.thread_id] = thread
             threads[thread.thread_id] = thread
         return threads
         return threads
 
 

+ 4 - 0
misago/models/karmamodel.py

@@ -64,6 +64,10 @@ def sync_user_handler(sender, **kwargs):
     sender.karma_given_p = sender.karma_set.filter(score__gt=0).count()
     sender.karma_given_p = sender.karma_set.filter(score__gt=0).count()
     sender.karma_given_n = sender.karma_set.filter(score__lt=0).count()
     sender.karma_given_n = sender.karma_set.filter(score__lt=0).count()
     sender.karma_p = sender.post_set.all().aggregate(Sum('upvotes'))['upvotes__sum']
     sender.karma_p = sender.post_set.all().aggregate(Sum('upvotes'))['upvotes__sum']
+    if not sender.karma_p:
+        sender.karma_p = 0
     sender.karma_n = sender.post_set.all().aggregate(Sum('downvotes'))['downvotes__sum']
     sender.karma_n = sender.post_set.all().aggregate(Sum('downvotes'))['downvotes__sum']
+    if not sender.karma_n:
+        sender.karma_n = 0
 
 
 sync_user_profile.connect(sync_user_handler, dispatch_uid="sync_user_karmas")
 sync_user_profile.connect(sync_user_handler, dispatch_uid="sync_user_karmas")

+ 18 - 1
misago/models/monitoritemmodel.py

@@ -2,8 +2,25 @@ from django.db import models
 
 
 class MonitorItem(models.Model):
 class MonitorItem(models.Model):
     id = models.CharField(max_length=255, primary_key=True)
     id = models.CharField(max_length=255, primary_key=True)
-    value = models.TextField(blank=True, null=True)
+    _value = models.TextField(db_column="value", blank=True, null=True)
+    type = models.CharField(max_length=255, default="int")
     updated = models.DateTimeField(blank=True, null=True)
     updated = models.DateTimeField(blank=True, null=True)
 
 
     class Meta:
     class Meta:
         app_label = 'misago'
         app_label = 'misago'
+
+    @property
+    def value(self):
+        if self.type in ("int", "integer"):
+            return int(self._value)
+        if self.type == "float":
+            return float(self._value)
+        return self._value
+
+    @value.setter
+    def value(self, v):
+        if self.type in ("int", "integer"):
+            self._value = int(v)
+        if self.type == "float":
+            self._value = float(v)
+        self._value = v

+ 37 - 39
misago/models/postmodel.py

@@ -1,7 +1,9 @@
 from django.db import models
 from django.db import models
 from django.db.models import F
 from django.db.models import F
+from django.db.models.signals import pre_save, pre_delete
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
+from misago.markdown import clear_markdown
 from misago.signals import (delete_user_content, merge_post, merge_thread,
 from misago.signals import (delete_user_content, merge_post, merge_thread,
                             move_forum_content, move_post, move_thread,
                             move_forum_content, move_post, move_thread,
                             rename_user, sync_user_profile)
                             rename_user, sync_user_profile)
@@ -15,7 +17,6 @@ class PostManager(models.Manager):
 class Post(models.Model):
 class Post(models.Model):
     forum = models.ForeignKey('Forum')
     forum = models.ForeignKey('Forum')
     thread = models.ForeignKey('Thread')
     thread = models.ForeignKey('Thread')
-    merge = models.PositiveIntegerField(default=0)
     user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
     user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
     user_name = models.CharField(max_length=255)
     user_name = models.CharField(max_length=255)
     ip = models.GenericIPAddressField()
     ip = models.GenericIPAddressField()
@@ -25,15 +26,14 @@ class Post(models.Model):
     upvotes = models.PositiveIntegerField(default=0)
     upvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
     mentions = models.ManyToManyField('User', related_name="mention_set")
     mentions = models.ManyToManyField('User', related_name="mention_set")
-    checkpoints = models.BooleanField(default=False)
     date = models.DateTimeField()
     date = models.DateTimeField()
+    current_date = models.DateTimeField(db_index=True)
     edits = models.PositiveIntegerField(default=0)
     edits = models.PositiveIntegerField(default=0)
-    edit_date = models.DateTimeField(null=True, blank=True)
     edit_reason = models.CharField(max_length=255, null=True, blank=True)
     edit_reason = models.CharField(max_length=255, null=True, blank=True)
     edit_user = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
     edit_user = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
     edit_user_name = models.CharField(max_length=255, null=True, blank=True)
     edit_user_name = models.CharField(max_length=255, null=True, blank=True)
     edit_user_slug = models.SlugField(max_length=255, null=True, blank=True)
     edit_user_slug = models.SlugField(max_length=255, null=True, blank=True)
-    reported = models.BooleanField(default=False)
+    reported = models.BooleanField(default=False, db_index=True)
     moderated = models.BooleanField(default=False)
     moderated = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     protected = models.BooleanField(default=False)
     protected = models.BooleanField(default=False)
@@ -45,9 +45,30 @@ class Post(models.Model):
     class Meta:
     class Meta:
         app_label = 'misago'
         app_label = 'misago'
 
 
+    def delete(self, *args, **kwargs):
+        """
+        FUGLY HAX for weird stuff that happens with
+        relations on model deletion in MySQL
+        """
+        if self.reported:
+            self.report_set.update(report_for=None)
+        return super(Post, self).delete(*args, **kwargs)
+
     def get_date(self):
     def get_date(self):
         return self.date
         return self.date
 
 
+    def quote(self):
+        quote = []
+        quote.append('@%s' % self.user_name)
+        for line in self.post.splitlines():
+            quote.append('> %s' % line)
+        quote.append('\r\n')
+        return '\r\n'.join(quote)
+
+    @property
+    def post_clean(self):
+        return clear_markdown(self.post_preparsed)
+
     def move_to(self, thread):
     def move_to(self, thread):
         move_post.send(sender=self, move_to=thread)
         move_post.send(sender=self, move_to=thread)
         self.thread = thread
         self.thread = thread
@@ -57,25 +78,6 @@ class Post(models.Model):
         post.post = '%s\n- - -\n%s' % (post.post, self.post)
         post.post = '%s\n- - -\n%s' % (post.post, self.post)
         merge_post.send(sender=self, new_post=post)
         merge_post.send(sender=self, new_post=post)
 
 
-    def set_checkpoint(self, request, action, user=None):
-        if request.user.is_authenticated():
-            self.checkpoints = True
-            self.checkpoint_set.create(
-                                       forum=self.forum,
-                                       thread=self.thread,
-                                       post=self,
-                                       action=action,
-                                       user=request.user,
-                                       user_name=request.user.username,
-                                       user_slug=request.user.username_slug,
-                                       date=timezone.now(),
-                                       ip=request.session.get_ip(request),
-                                       agent=request.META.get('HTTP_USER_AGENT'),
-                                       target_user=user,
-                                       target_user_name=(user.username if user else None),
-                                       target_user_slug=(user.username_slug if user else None),
-                                       )
-            
     def notify_mentioned(self, request, thread_type, users):
     def notify_mentioned(self, request, thread_type, users):
         from misago.acl.builder import acl
         from misago.acl.builder import acl
         from misago.acl.exceptions import ACLError403, ACLError404
         from misago.acl.exceptions import ACLError403, ACLError404
@@ -97,6 +99,15 @@ class Post(models.Model):
                 except (ACLError403, ACLError404):
                 except (ACLError403, ACLError404):
                     pass
                     pass
 
 
+    def is_reported(self):
+        self.reported = self.report_set.filter(weight=2).count() > 0
+
+    def live_report(self):
+        try:
+            return self.report_set.filter(weight=2)[0]
+        except IndexError:
+            return None
+
 
 
 def rename_user_handler(sender, **kwargs):
 def rename_user_handler(sender, **kwargs):
     Post.objects.filter(user=sender).update(
     Post.objects.filter(user=sender).update(
@@ -114,24 +125,11 @@ def delete_user_content_handler(sender, **kwargs):
     from misago.models import Thread
     from misago.models import Thread
 
 
     threads = []
     threads = []
-    prev_posts = []
-
-    for post in sender.post_set.filter(checkpoints=True):
-        threads.append(post.thread_id)
-        prev_post = Post.objects.filter(thread=post.thread_id).exclude(merge__gt=post.merge).exclude(user=sender).order_by('merge', '-id')[:1][0]
-        post.checkpoint_set.update(post=prev_post)
-        if not prev_post.pk in prev_posts:
-            prev_posts.append(prev_post.pk)
-
-    sender.post_set.all().delete()
-    Post.objects.filter(id__in=prev_posts).update(checkpoints=True)
-
     for post in sender.post_set.distinct().values('thread_id').iterator():
     for post in sender.post_set.distinct().values('thread_id').iterator():
         if not post['thread_id'] in threads:
         if not post['thread_id'] in threads:
             threads.append(post['thread_id'])
             threads.append(post['thread_id'])
 
 
-    for post in Post.objects.filter(user=sender):
-        post.delete()
+    sender.post_set.all().delete()
 
 
     for thread in Thread.objects.filter(id__in=threads):
     for thread in Thread.objects.filter(id__in=threads):
         thread.sync()
         thread.sync()
@@ -153,7 +151,7 @@ move_thread.connect(move_thread_handler, dispatch_uid="move_thread_posts")
 
 
 
 
 def merge_thread_handler(sender, **kwargs):
 def merge_thread_handler(sender, **kwargs):
-    Post.objects.filter(thread=sender).update(thread=kwargs['new_thread'], merge=F('merge') + kwargs['merge'])
+    Post.objects.filter(thread=sender).update(thread=kwargs['new_thread'])
 
 
 merge_thread.connect(merge_thread_handler, dispatch_uid="merge_threads_posts")
 merge_thread.connect(merge_thread_handler, dispatch_uid="merge_threads_posts")
 
 
@@ -161,4 +159,4 @@ merge_thread.connect(merge_thread_handler, dispatch_uid="merge_threads_posts")
 def sync_user_handler(sender, **kwargs):
 def sync_user_handler(sender, **kwargs):
     sender.posts = sender.post_set.count()
     sender.posts = sender.post_set.count()
 
 
-sync_user_profile.connect(sync_user_handler, dispatch_uid="sync_user_posts")
+sync_user_profile.connect(sync_user_handler, dispatch_uid="sync_user_posts")

+ 7 - 6
misago/models/rankmodel.py

@@ -18,6 +18,7 @@ class Rank(models.Model):
     on_index = models.BooleanField(default=False)
     on_index = models.BooleanField(default=False)
     order = models.IntegerField(default=0)
     order = models.IntegerField(default=0)
     criteria = models.CharField(max_length=255, null=True, blank=True)
     criteria = models.CharField(max_length=255, null=True, blank=True)
+    roles = models.ManyToManyField('Role')
 
 
     class Meta:
     class Meta:
         app_label = 'misago'
         app_label = 'misago'
@@ -52,10 +53,10 @@ class Rank(models.Model):
                 if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2'
                 if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2'
                     or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
                     or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
                     if special_ranks:
                     if special_ranks:
-                        cursor.execute('''UPDATE users_user
+                        cursor.execute('''UPDATE misago_user
                             FROM (
                             FROM (
                                 SELECT id
                                 SELECT id
-                                FROM users_user
+                                FROM misago_user
                                 WHERE rank_id NOT IN (%s)
                                 WHERE rank_id NOT IN (%s)
                                 ORDER BY score DESC LIMIT %s
                                 ORDER BY score DESC LIMIT %s
                                 ) AS updateable
                                 ) AS updateable
@@ -63,10 +64,10 @@ class Rank(models.Model):
                             WHERE id = updateable.id
                             WHERE id = updateable.id
                             RETURNING *''' % (self.id, special_ranks, criteria))
                             RETURNING *''' % (self.id, special_ranks, criteria))
                     else:
                     else:
-                        cursor.execute('''UPDATE users_user
+                        cursor.execute('''UPDATE misago_user
                             FROM (
                             FROM (
                                 SELECT id
                                 SELECT id
-                                FROM users_user
+                                FROM misago_user
                                 ORDER BY score DESC LIMIT %s
                                 ORDER BY score DESC LIMIT %s
                                 ) AS updateable
                                 ) AS updateable
                             SET rank_id=%s
                             SET rank_id=%s
@@ -78,13 +79,13 @@ class Rank(models.Model):
                     or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3'
                     or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3'
                     or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.oracle'):
                     or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.oracle'):
                     if special_ranks:
                     if special_ranks:
-                        cursor.execute('''UPDATE users_user
+                        cursor.execute('''UPDATE misago_user
                             SET rank_id=%s
                             SET rank_id=%s
                             WHERE rank_id NOT IN (%s)
                             WHERE rank_id NOT IN (%s)
                             ORDER BY score DESC
                             ORDER BY score DESC
                             LIMIT %s''' % (self.id, special_ranks, criteria))
                             LIMIT %s''' % (self.id, special_ranks, criteria))
                     else:
                     else:
-                        cursor.execute('''UPDATE users_user
+                        cursor.execute('''UPDATE misago_user
                         SET rank_id=%s
                         SET rank_id=%s
                         ORDER BY score DESC
                         ORDER BY score DESC
                         LIMIT %s''', [self.id, criteria])
                         LIMIT %s''', [self.id, criteria])

+ 0 - 1
misago/models/sessionmodel.py

@@ -13,7 +13,6 @@ class Session(models.Model):
     rank = models.ForeignKey('Rank', related_name='sessions', null=True, on_delete=models.SET_NULL)
     rank = models.ForeignKey('Rank', related_name='sessions', null=True, on_delete=models.SET_NULL)
     admin = models.BooleanField(default=False)
     admin = models.BooleanField(default=False)
     matched = models.BooleanField(default=False)
     matched = models.BooleanField(default=False)
-    hidden = models.BooleanField(default=False)
 
 
     class Meta:
     class Meta:
         app_label = 'misago'
         app_label = 'misago'

+ 0 - 25
misago/models/themeadjustmentmodel.py

@@ -1,25 +0,0 @@
-from django.core.cache import cache
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
-
-class ThemeAdjustment(models.Model):
-    theme = models.CharField(max_length=255, unique=True,
-                             error_messages={'unique': _("User agents for this theme are already defined.")})
-    useragents = models.TextField(null=True, blank=True)
-    
-    class Meta:
-        app_label = 'misago'
-
-    def adjust_theme(self, useragent):
-        for string in self.useragents.splitlines():
-            if string in useragent:
-                return True
-        return False
-    
-    def save(self, *args, **kwargs):
-        cache.delete('client_adjustments')
-        super(ThemeAdjustment, self).save(*args, **kwargs)
-
-    def delete(self, *args, **kwargs):
-        cache.delete('client_adjustments')
-        super(ThemeAdjustment, self).delete(*args, **kwargs)

+ 92 - 15
misago/models/threadmodel.py

@@ -1,7 +1,7 @@
 from datetime import timedelta
 from datetime import timedelta
 from django.conf import settings
 from django.conf import settings
 from django.db import models
 from django.db import models
-from django.db.models.signals import pre_delete
+from django.db.models.signals import pre_save, pre_delete
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.signals import (delete_user_content, merge_thread, move_forum_content,
 from misago.signals import (delete_user_content, merge_thread, move_forum_content,
@@ -57,7 +57,6 @@ class Thread(models.Model):
     replies_reported = models.PositiveIntegerField(default=0)
     replies_reported = models.PositiveIntegerField(default=0)
     replies_moderated = models.PositiveIntegerField(default=0)
     replies_moderated = models.PositiveIntegerField(default=0)
     replies_deleted = models.PositiveIntegerField(default=0)
     replies_deleted = models.PositiveIntegerField(default=0)
-    merges = models.PositiveIntegerField(default=0)
     score = models.PositiveIntegerField(default=30)
     score = models.PositiveIntegerField(default=30)
     upvotes = models.PositiveIntegerField(default=0)
     upvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
@@ -74,6 +73,7 @@ class Thread(models.Model):
     last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
     last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
     last_poster_style = models.CharField(max_length=255, null=True, blank=True)
     last_poster_style = models.CharField(max_length=255, null=True, blank=True)
     participants = models.ManyToManyField('User', related_name='private_thread_set')
     participants = models.ManyToManyField('User', related_name='private_thread_set')
+    report_for = models.ForeignKey('Post', related_name='report_set', null=True, blank=True, on_delete=models.SET_NULL)
     moderated = models.BooleanField(default=False)
     moderated = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     closed = models.BooleanField(default=False)
     closed = models.BooleanField(default=False)
@@ -85,9 +85,55 @@ class Thread(models.Model):
     class Meta:
     class Meta:
         app_label = 'misago'
         app_label = 'misago'
 
 
+    def delete(self, *args, **kwargs):
+        """
+        FUGLY HAX for weird stuff that happens with
+        relations on model deletion in MySQL
+        """
+        if self.replies_reported:
+            clear_reports = [post.pk for post in self.post_set.filter(reported=True)]
+            if clear_reports:
+                Thread.objects.filter(report_for__in=clear_reports).update(report_for=None)
+        return super(Thread, self).delete(*args, **kwargs)
+
     def get_date(self):
     def get_date(self):
         return self.start
         return self.start
 
 
+    def set_checkpoints(self, show_all, posts, stop=None):
+        qs = self.checkpoint_set.filter(date__gte=posts[0].date)
+        if not show_all:
+            qs = qs.filter(deleted=False)
+        if stop:
+            qs = qs.filter(date__lte=stop)
+        checkpoints = [i for i in qs]
+
+        i_max = len(posts) - 1
+        for i, post in enumerate(posts):
+            post.checkpoints_visible = []
+            for c in checkpoints:
+                if c.date >= post.date and (i == i_max or c.date < posts[i+1].date):
+                    post.checkpoints_visible.append(c)
+
+    def set_checkpoint(self, request, action, user=None, forum=None):
+        if request.user.is_authenticated():
+            self.checkpoint_set.create(
+                                       forum=self.forum,
+                                       thread=self,
+                                       action=action,
+                                       user=request.user,
+                                       user_name=request.user.username,
+                                       user_slug=request.user.username_slug,
+                                       date=timezone.now(),
+                                       ip=request.session.get_ip(request),
+                                       agent=request.META.get('HTTP_USER_AGENT'),
+                                       target_user=user,
+                                       target_user_name=(user.username if user else None),
+                                       target_user_slug=(user.username_slug if user else None),
+                                       old_forum=forum,
+                                       old_forum_name=(forum.name if forum else None),
+                                       old_forum_slug=(forum.slug if forum else None),
+                                       )
+
     def new_start_post(self, post):
     def new_start_post(self, post):
         self.start = post.date
         self.start = post.date
         self.start_post = post
         self.start_post = post
@@ -110,8 +156,8 @@ class Thread(models.Model):
         move_thread.send(sender=self, move_to=move_to)
         move_thread.send(sender=self, move_to=move_to)
         self.forum = move_to
         self.forum = move_to
 
 
-    def merge_with(self, thread, merge):
-        merge_thread.send(sender=self, new_thread=thread, merge=merge)
+    def merge_with(self, thread):
+        merge_thread.send(sender=self, new_thread=thread)
 
 
     def sync(self):
     def sync(self):
         # Counters
         # Counters
@@ -122,7 +168,7 @@ class Thread(models.Model):
         self.replies_moderated = self.post_set.filter(moderated=True).count()
         self.replies_moderated = self.post_set.filter(moderated=True).count()
         self.replies_deleted = self.post_set.filter(deleted=True).count()
         self.replies_deleted = self.post_set.filter(deleted=True).count()
         # First post
         # First post
-        start_post = self.post_set.order_by('merge', 'id')[0:][0]
+        start_post = self.post_set.order_by('id')[0:][0]
         self.start = start_post.date
         self.start = start_post.date
         self.start_post = start_post
         self.start_post = start_post
         self.start_poster = start_post.user
         self.start_poster = start_post.user
@@ -133,7 +179,7 @@ class Thread(models.Model):
         self.downvotes = start_post.downvotes
         self.downvotes = start_post.downvotes
         # Last visible post
         # Last visible post
         if self.replies > 0:
         if self.replies > 0:
-            last_post = self.post_set.order_by('-merge', '-id').filter(moderated=False)[0:][0]
+            last_post = self.post_set.order_by('-id').filter(moderated=False)[0:][0]
         else:
         else:
             last_post = start_post
             last_post = start_post
         self.last = last_post.date
         self.last = last_post.date
@@ -145,14 +191,14 @@ class Thread(models.Model):
         # Flags
         # Flags
         self.moderated = start_post.moderated
         self.moderated = start_post.moderated
         self.deleted = start_post.deleted
         self.deleted = start_post.deleted
-        self.merges = last_post.merge
         
         
     def email_watchers(self, request, thread_type, post):
     def email_watchers(self, request, thread_type, post):
         from misago.acl.builder import acl
         from misago.acl.builder import acl
         from misago.acl.exceptions import ACLError403, ACLError404
         from misago.acl.exceptions import ACLError403, ACLError404
         from misago.models import ThreadRead, WatchedThread
         from misago.models import ThreadRead, WatchedThread
 
 
-        for watch in WatchedThread.objects.filter(thread=self).filter(email=True).filter(last_read__gte=self.previous_last.date):
+        notified = []
+        for watch in WatchedThread.objects.filter(thread=self).filter(last_read__gte=self.previous_last.date):
             user = watch.user
             user = watch.user
             if user.pk != request.user.pk:
             if user.pk != request.user.pk:
                 try:
                 try:
@@ -161,14 +207,17 @@ class Thread(models.Model):
                     user_acl.threads.allow_thread_view(user, self)
                     user_acl.threads.allow_thread_view(user, self)
                     user_acl.threads.allow_post_view(user, self, post)
                     user_acl.threads.allow_post_view(user, self, post)
                     if not user.is_ignoring(request.user):
                     if not user.is_ignoring(request.user):
-                        user.email_user(
-                                        request,
-                                        '%s_reply_notification' % thread_type,
-                                        _('New reply in thread "%(thread)s"') % {'thread': self.name},
-                                        {'author': request.user, 'post': post, 'thread': self}
-                                        )
+                        if watch.email:
+                            user.email_user(
+                                            request,
+                                            '%s_reply_notification' % thread_type,
+                                            _('New reply in thread "%(thread)s"') % {'thread': self.name},
+                                            {'author': request.user, 'post': post, 'thread': self}
+                                            )
+                        notified.append(user)
                 except (ACLError403, ACLError404):
                 except (ACLError403, ACLError404):
                     pass
                     pass
+        return notified
 
 
 
 
 def rename_user_handler(sender, **kwargs):
 def rename_user_handler(sender, **kwargs):
@@ -184,6 +233,34 @@ def rename_user_handler(sender, **kwargs):
 rename_user.connect(rename_user_handler, dispatch_uid="rename_user_threads")
 rename_user.connect(rename_user_handler, dispatch_uid="rename_user_threads")
 
 
 
 
+def report_update_handler(sender, **kwargs):
+    if sender == Thread:
+        thread = kwargs.get('instance')
+        if thread.weight < 2 and thread.report_for_id:
+            reported_post = thread.report_for
+            if reported_post.reported:
+                reported_post.reported = False
+                reported_post.save(force_update=True)
+                reported_post.thread.replies_reported -= 1
+                reported_post.thread.save(force_update=True)
+
+pre_save.connect(report_update_handler, dispatch_uid="sync_post_reports_on_update")
+
+
+def report_delete_handler(sender, **kwargs):
+    if sender == Thread:
+        thread = kwargs.get('instance')
+        if thread.report_for_id:
+            reported_post = thread.report_for
+            if reported_post.reported:
+                reported_post.reported = False
+                reported_post.save(force_update=True)
+                reported_post.thread.replies_reported -= 1
+                reported_post.thread.save(force_update=True)
+
+pre_delete.connect(report_delete_handler, dispatch_uid="sync_post_reports_on_delete")
+
+
 def delete_user_content_handler(sender, **kwargs):
 def delete_user_content_handler(sender, **kwargs):
     for thread in Thread.objects.filter(start_poster=sender):
     for thread in Thread.objects.filter(start_poster=sender):
         thread.delete()
         thread.delete()
@@ -211,4 +288,4 @@ pre_delete.connect(delete_user_handler, dispatch_uid="delete_user_participations
 def sync_user_handler(sender, **kwargs):
 def sync_user_handler(sender, **kwargs):
     sender.threads = sender.thread_set.count()
     sender.threads = sender.thread_set.count()
 
 
-sync_user_profile.connect(sync_user_handler, dispatch_uid="sync_user_threads")
+sync_user_profile.connect(sync_user_handler, dispatch_uid="sync_user_threads")

+ 17 - 10
misago/models/usermodel.py

@@ -55,7 +55,7 @@ class UserManager(models.Manager):
         # Get first rank
         # Get first rank
         try:
         try:
             from misago.models import Rank
             from misago.models import Rank
-            default_rank = Rank.objects.filter(special=0).order_by('order')[0]
+            default_rank = Rank.objects.filter(special=0).order_by('-order')[0]
         except IndexError:
         except IndexError:
             default_rank = None
             default_rank = None
 
 
@@ -98,12 +98,12 @@ class UserManager(models.Manager):
 
 
         # Update forum stats
         # Update forum stats
         if activation == 0:
         if activation == 0:
-            monitor['users'] = int(monitor['users']) + 1
+            monitor.increase('users')
             monitor['last_user'] = new_user.pk
             monitor['last_user'] = new_user.pk
             monitor['last_user_name'] = new_user.username
             monitor['last_user_name'] = new_user.username
             monitor['last_user_slug'] = new_user.username_slug
             monitor['last_user_slug'] = new_user.username_slug
         else:
         else:
-            monitor['users_inactive'] = int(monitor['users_inactive']) + 1
+            monitor.increase('users_inactive')
 
 
         # Return new user
         # Return new user
         return new_user
         return new_user
@@ -358,11 +358,10 @@ class User(models.Model):
         self.password_date = tz_util.now()
         self.password_date = tz_util.now()
         self.password = make_password(raw_password.strip())
         self.password = make_password(raw_password.strip())
 
 
-    def set_last_visit(self, ip, agent, hidden=False):
+    def set_last_visit(self, ip, agent):
         self.last_date = tz_util.now()
         self.last_date = tz_util.now()
         self.last_ip = ip
         self.last_ip = ip
         self.last_agent = agent
         self.last_agent = agent
-        self.last_hide = hidden
 
 
     def check_password(self, raw_password, mobile=False):
     def check_password(self, raw_password, mobile=False):
         """
         """
@@ -418,6 +417,8 @@ class User(models.Model):
         return True
         return True
 
 
     def get_roles(self):
     def get_roles(self):
+        if self.rank:
+            return self.roles.all() | self.rank.roles.all()
         return self.roles.all()
         return self.roles.all()
 
 
     def make_acl_key(self, force=False):
     def make_acl_key(self, force=False):
@@ -425,12 +426,18 @@ class User(models.Model):
             return self.acl_key
             return self.acl_key
         roles_ids = []
         roles_ids = []
         for role in self.roles.all():
         for role in self.roles.all():
-            roles_ids.append(str(role.pk))
-        self.acl_key = 'acl_%s' % hashlib.md5('_'.join(roles_ids)).hexdigest()[0:8]
+            roles_ids.append(role.pk)
+        for role in self.rank.roles.all():
+            if not role.pk in roles_ids:
+                roles_ids.append(role.pk)
+        roles_ids.sort()
+        self.acl_key = 'acl_%s' % hashlib.md5('_'.join(str(x) for x in roles_ids)).hexdigest()[0:8]
+        self.save(update_fields=('acl_key',))
         return self.acl_key
         return self.acl_key
 
 
     def acl(self, request):
     def acl(self, request):
         try:
         try:
+            self.make_acl_key()
             acl = cache.get(self.acl_key)
             acl = cache.get(self.acl_key)
             if acl.version != request.monitor.acl_version:
             if acl.version != request.monitor.acl_version:
                 raise InvalidCacheBackendError()
                 raise InvalidCacheBackendError()
@@ -488,10 +495,10 @@ class User(models.Model):
         else:
         else:
             recipient = self.email
             recipient = self.email
 
 
-        # Build and send message
+        # Build message and add it to queue
         email = EmailMultiAlternatives(subject, templates[0].render(context), settings.EMAIL_HOST_USER, [recipient])
         email = EmailMultiAlternatives(subject, templates[0].render(context), settings.EMAIL_HOST_USER, [recipient])
         email.attach_alternative(templates[1].render(context), "text/html")
         email.attach_alternative(templates[1].render(context), "text/html")
-        email.send()
+        request.mails_queue.append(email)
 
 
     def get_activation(self):
     def get_activation(self):
         activations = ['none', 'user', 'admin', 'credentials']
         activations = ['none', 'user', 'admin', 'credentials']
@@ -553,7 +560,7 @@ class Crawler(Guest):
         self.username = username
         self.username = username
 
 
     def is_anonymous(self):
     def is_anonymous(self):
-        return True
+        return False
 
 
     def is_authenticated(self):
     def is_authenticated(self):
         return False
         return False

+ 18 - 2
misago/monitor.py

@@ -1,3 +1,4 @@
+from datetime import timedelta
 from django.core.cache import cache
 from django.core.cache import cache
 from django.utils import timezone
 from django.utils import timezone
 from misago.models import MonitorItem
 from misago.models import MonitorItem
@@ -13,7 +14,7 @@ class Monitor(object):
         if not self._items:
         if not self._items:
             self._items = {}
             self._items = {}
             for i in MonitorItem.objects.all():
             for i in MonitorItem.objects.all():
-                self._items[i.id] = [i.value, i.updated]
+                self._items[i.id] = [i.value, i.updated, i.type]
             cache.set('monitor', self._items)
             cache.set('monitor', self._items)
 
 
     def __contains__(self, key):
     def __contains__(self, key):
@@ -24,14 +25,26 @@ class Monitor(object):
 
 
     def __setitem__(self, key, value):
     def __setitem__(self, key, value):
         self._items[key][0] = value
         self._items[key][0] = value
+        self._items[key][1] = timezone.now()
         cache.set('monitor', self._items)
         cache.set('monitor', self._items)
-        sync_item = MonitorItem(id=key, value=value, updated=timezone.now())
+        sync_item = MonitorItem(
+                                id=key,
+                                value=value,
+                                type=self._items[key][2],
+                                updated=timezone.now()
+                                )
         sync_item.save(force_update=True)
         sync_item.save(force_update=True)
         return value
         return value
 
 
     def __delitem__(self, key):
     def __delitem__(self, key):
         pass
         pass
 
 
+    def increase(self, key, i=1):
+        self[key] = self[key] + i
+
+    def decrease(self, key, i=1):
+        self[key] = self[key] - i
+
     def get(self, key, default=None):
     def get(self, key, default=None):
         if not key in self._items:
         if not key in self._items:
             return default
             return default
@@ -42,6 +55,9 @@ class Monitor(object):
             return self._items[key][1]
             return self._items[key][1]
         return None
         return None
 
 
+    def expired(self, key, seconds=5):
+        return self._items[key][1] < (timezone.now() - timedelta(seconds=seconds))
+
     def has_key(self, key):
     def has_key(self, key):
         return key in self._items
         return key in self._items
 
 

+ 62 - 0
misago/onlines.py

@@ -0,0 +1,62 @@
+from datetime import timedelta
+from django.core.cache import cache
+from django.utils import timezone
+from misago.models import Session
+
+class MembersOnline(object):
+    def __init__(self, mode, monitor, frequency=180):
+        self.monitor = monitor
+        self.frequency = frequency
+        self._mode = mode
+        self._members = int(monitor['online_members'])
+        self._all = int(monitor['online_all'])
+        self._om = self._members
+        self._oa = self._all
+        if (self._mode != 'no' or monitor.expired('online_all', frequency) or
+                monitor.expired('online_members', frequency)):
+            self.count_sessions()
+
+    def count_sessions(self):
+        queryset = Session.objects.filter(matched=True).filter(crawler__isnull=True).filter(last__gte=timezone.now() - timedelta(seconds=self.frequency))
+        self._all = queryset.count()
+        self._members = queryset.filter(user__isnull=False).count()
+        cache.delete_many(['team_users_online', 'ranks_online'])
+
+    def new_session(self):
+        self._all += 1
+
+    def sign_in(self):
+        self._members += 1
+
+    def sign_out(self):
+        if self._members:
+            self._members -= 1
+
+    @property
+    def all(self):
+        return self._all
+
+    @property
+    def members(self):
+        return self._members
+
+    def sync(self):
+        if self._mode == 'snap':
+            if self._members != self._om:
+                self.monitor['online_members'] = self._members
+            if self._all != self._oa:
+                self.monitor['online_all'] = self._all
+
+    def stats(self, request):
+        stat = {
+                'members': self.members,
+                'all': self.all,
+               }
+
+        if not request.user.is_crawler():
+            if not stat['members'] and request.user.is_authenticated():
+                stat['members'] += 1
+                stat['all'] += 1
+            if not stat['all']:
+                stat['all'] += 1        
+        return stat

+ 3 - 3
misago/readstrackers.py

@@ -67,12 +67,12 @@ class ThreadsTracker(object):
             except KeyError:
             except KeyError:
                 self.need_create = thread
                 self.need_create = thread
 
 
-    def unread_count(self, queryset):
+    def unread_count(self, queryset=None):
         try:
         try:
             return self.unread_threads
             return self.unread_threads
         except AttributeError:
         except AttributeError:
             self.unread_threads = 0
             self.unread_threads = 0
-            if not queryset:
+            if queryset == None:
                 queryset = self.default_queryset()
                 queryset = self.default_queryset()
             for thread in queryset.filter(last__gte=self.record.cleared):
             for thread in queryset.filter(last__gte=self.record.cleared):
                 if not self.is_read(thread):
                 if not self.is_read(thread):
@@ -81,7 +81,7 @@ class ThreadsTracker(object):
 
 
     def sync(self, queryset=None):
     def sync(self, queryset=None):
         now = timezone.now()
         now = timezone.now()
-        if not queryset:
+        if queryset == None:
             queryset = self.default_queryset()
             queryset = self.default_queryset()
 
 
         if self.need_create:
         if self.need_create:

+ 0 - 0
misago/search/__init__.py → misago/search.py


+ 18 - 0
misago/search_indexes.py

@@ -0,0 +1,18 @@
+from haystack import indexes
+from misago.models import Post
+
+class PostIndex(indexes.SearchIndex, indexes.Indexable):
+    text = indexes.CharField(document=True, use_template=True)
+    forum = indexes.CharField(model_attr='forum')
+    thread = indexes.CharField(model_attr='thread')
+    user = indexes.CharField(model_attr='user_name')
+    date = indexes.DateTimeField(model_attr='date')
+
+    def get_model(self):
+        return Post
+
+    def get_updated_field(self):
+        return 'current_date'
+
+    def index_queryset(self, using=None):
+        return self.get_model().objects.all()

+ 33 - 25
misago/sessions.py

@@ -2,6 +2,7 @@ from hashlib import md5
 from datetime import timedelta
 from datetime import timedelta
 from django.conf import settings
 from django.conf import settings
 from django.contrib.sessions.backends.base import SessionBase, CreateError
 from django.contrib.sessions.backends.base import SessionBase, CreateError
+from django.db import IntegrityError
 from django.db.models.loading import cache as model_cache
 from django.db.models.loading import cache as model_cache
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.crypto import salted_hmac
 from django.utils.crypto import salted_hmac
@@ -41,6 +42,12 @@ class MisagoSession(SessionBase):
         """We use sessions to track onlines so sorry, only sessions cleaner may delete sessions"""
         """We use sessions to track onlines so sorry, only sessions cleaner may delete sessions"""
         pass
         pass
 
 
+    def created(self):
+        try:
+            return self.started
+        except AttributeError:
+            return False
+
     def flush(self):
     def flush(self):
         """We use sessions to track onlines so sorry, only sessions cleaner may delete sessions"""
         """We use sessions to track onlines so sorry, only sessions cleaner may delete sessions"""
         pass
         pass
@@ -51,12 +58,6 @@ class MisagoSession(SessionBase):
     def session_expired(self):
     def session_expired(self):
         return False
         return False
 
 
-    def get_hidden(self):
-        return False
-
-    def set_hidden(self, hidden=False):
-        pass
-
     def get_ip(self, request):
     def get_ip(self, request):
         return request.META.get('HTTP_X_FORWARDED_FOR', '') or request.META.get('REMOTE_ADDR')
         return request.META.get('HTTP_X_FORWARDED_FOR', '') or request.META.get('REMOTE_ADDR')
 
 
@@ -77,13 +78,17 @@ class MisagoSession(SessionBase):
         else:
         else:
             self._session_rk.save(force_insert=True)
             self._session_rk.save(force_insert=True)
 
 
+    def match(self):
+        self._session_rk.matched = True
+
 
 
 class CrawlerSession(MisagoSession):
 class CrawlerSession(MisagoSession):
     """
     """
     Crawler Session controller
     Crawler Session controller
     """
     """
     def __init__(self, request):
     def __init__(self, request):
-        self.hidden = False
+        self.matched = True
+        self.started = False
         self.team = False
         self.team = False
         self._ip = self.get_ip(request)
         self._ip = self.get_ip(request)
         self._session_key = md5('%s-%s' % (request.user.username, self._ip)).hexdigest()
         self._session_key = md5('%s-%s' % (request.user.username, self._ip)).hexdigest()
@@ -109,6 +114,8 @@ class CrawlerSession(MisagoSession):
                 self._session_rk.save(force_insert=True)
                 self._session_rk.save(force_insert=True)
                 break
                 break
             except CreateError:
             except CreateError:
+                continue
+            except IntegrityError:
                 try:
                 try:
                     self._session_rk =  Session.objects.get(id=self._session_key)                    
                     self._session_rk =  Session.objects.get(id=self._session_key)                    
                 except Session.DoesNotExist:
                 except Session.DoesNotExist:
@@ -123,8 +130,9 @@ class HumanSession(MisagoSession):
     Human Session controller
     Human Session controller
     """
     """
     def __init__(self, request):
     def __init__(self, request):
+        self.started = False
+        self.matched = False
         self.expired = False
         self.expired = False
-        self.hidden = False
         self.team = False
         self.team = False
         self.rank = None
         self.rank = None
         self.remember_me = None
         self.remember_me = None
@@ -140,10 +148,13 @@ class HumanSession(MisagoSession):
             if self._cookie_sid not in request.COOKIES or len(request.COOKIES[self._cookie_sid]) != 42:
             if self._cookie_sid not in request.COOKIES or len(request.COOKIES[self._cookie_sid]) != 42:
                 raise IncorrectSessionException()
                 raise IncorrectSessionException()
             self._session_key = request.COOKIES[self._cookie_sid]
             self._session_key = request.COOKIES[self._cookie_sid]
-            self._session_rk = Session.objects.select_related().get(
-                                                                    pk=self._session_key,
-                                                                    admin=request.firewall.admin
-                                                                    )
+            self._session_rk = Session.objects.select_related('user', 'rank')
+            if settings.USER_EXTENSIONS_PRELOAD:
+                self._session_rk = self._session_rk.select_related(*settings.USER_EXTENSIONS_PRELOAD)
+            self._session_rk = self._session_rk.get(
+                                                    pk=self._session_key,
+                                                    admin=request.firewall.admin
+                                                    )
             # IP invalid
             # IP invalid
             if request.settings.sessions_validate_ip and self._session_rk.ip != self._ip:
             if request.settings.sessions_validate_ip and self._session_rk.ip != self._ip:
                 raise IncorrectSessionException()
                 raise IncorrectSessionException()
@@ -153,16 +164,20 @@ class HumanSession(MisagoSession):
                 self.expired = True
                 self.expired = True
                 raise IncorrectSessionException()
                 raise IncorrectSessionException()
             
             
-            # Change session to matched and extract session user and hidden flag
-            self._session_rk.matched = True
+            # Change session to matched and extract session user
+            if self._session_rk.matched:
+                self.matched = True
+            else:
+                self.started = True
             self._user = self._session_rk.user
             self._user = self._session_rk.user
-            self.hidden = self._session_rk.hidden
             self.team = self._session_rk.team
             self.team = self._session_rk.team
         except (Session.DoesNotExist, IncorrectSessionException):
         except (Session.DoesNotExist, IncorrectSessionException):
             # Attempt autolog
             # Attempt autolog
             try:
             try:
                 self.remember_me = auth_remember(request, self.get_ip(request))
                 self.remember_me = auth_remember(request, self.get_ip(request))
                 self.create(request, user=self.remember_me.user)
                 self.create(request, user=self.remember_me.user)
+                self.started = True
+                self._session_rk.matched = True
             except AuthException as e:
             except AuthException as e:
                 # Autolog failed
                 # Autolog failed
                 self.create(request)
                 self.create(request)
@@ -190,12 +205,13 @@ class HumanSession(MisagoSession):
                                          admin=request.firewall.admin,
                                          admin=request.firewall.admin,
                                          )
                                          )
                 self._session_rk.save(force_insert=True)
                 self._session_rk.save(force_insert=True)
+                if settings.USER_EXTENSIONS_PRELOAD:
+                    self._session_rk = self._session_rk.select_related(*settings.USER_EXTENSIONS_PRELOAD)
                 if user:
                 if user:
                     # Update user data
                     # Update user data
                     user.set_last_visit(
                     user.set_last_visit(
                                         self.get_ip(request),
                                         self.get_ip(request),
-                                        request.META.get('HTTP_USER_AGENT', ''),
-                                        hidden=self.hidden
+                                        request.META.get('HTTP_USER_AGENT', '')
                                         )
                                         )
                     user.save(force_update=True)
                     user.save(force_update=True)
                 break
                 break
@@ -205,7 +221,6 @@ class HumanSession(MisagoSession):
 
 
     def save(self):
     def save(self):
         self._session_rk.user = self._user
         self._session_rk.user = self._user
-        self._session_rk.hidden = self.hidden
         self._session_rk.team = self.team
         self._session_rk.team = self.team
         self._session_rk.rank_id = self.rank
         self._session_rk.rank_id = self.rank
         super(HumanSession, self).save()
         super(HumanSession, self).save()
@@ -233,18 +248,11 @@ class HumanSession(MisagoSession):
                         if len(request.COOKIES[cookie_token]) > 0:
                         if len(request.COOKIES[cookie_token]) > 0:
                             Token.objects.filter(id=request.COOKIES[cookie_token]).delete()
                             Token.objects.filter(id=request.COOKIES[cookie_token]).delete()
                         request.cookiejar.delete('TOKEN')
                         request.cookiejar.delete('TOKEN')
-                self.hidden = False
                 self._user = None
                 self._user = None
                 request.user = Guest()
                 request.user = Guest()
         except AttributeError:
         except AttributeError:
             pass
             pass
 
 
-    def get_hidden(self):
-        return self.hidden
-
-    def set_hidden(self, hidden=False):
-        self.hidden = hidden
-
 
 
 class SessionMock(object):
 class SessionMock(object):
     def get_ip(self, request):
     def get_ip(self, request):

+ 28 - 3
misago/settings_base.py

@@ -10,6 +10,12 @@ ALLOWED_HOSTS = ['*']
 # Leave this setting empty
 # Leave this setting empty
 ADMIN_PATH = ''
 ADMIN_PATH = ''
 
 
+# Enable mobile subdomain for mobile stuff
+MOBILE_SUBDOMAIN = ''
+
+# Templates used by mobile version
+MOBILE_TEMPLATES = ''
+
 # Default format of Misago generated HTML
 # Default format of Misago generated HTML
 OUTPUT_FORMAT = 'html5'
 OUTPUT_FORMAT = 'html5'
 
 
@@ -83,6 +89,9 @@ TEMPLATE_CONTEXT_PROCESSORS = (
     'misago.context_processors.admin',
     'misago.context_processors.admin',
 )
 )
 
 
+# Template middlewares
+TEMPLATE_MIDDLEWARES = ()
+
 # Jinja2 Template Extensions
 # Jinja2 Template Extensions
 JINJA2_EXTENSIONS = (
 JINJA2_EXTENSIONS = (
     'jinja2.ext.do',
     'jinja2.ext.do',
@@ -105,6 +114,7 @@ MIDDLEWARE_CLASSES = (
     'misago.middleware.banning.BanningMiddleware',
     'misago.middleware.banning.BanningMiddleware',
     'misago.middleware.messages.MessagesMiddleware',
     'misago.middleware.messages.MessagesMiddleware',
     'misago.middleware.user.UserMiddleware',
     'misago.middleware.user.UserMiddleware',
+    'misago.middleware.mailsqueue.MailsQueueMiddleware',
     'misago.middleware.acl.ACLMiddleware',
     'misago.middleware.acl.ACLMiddleware',
     'misago.middleware.privatethreads.PrivateThreadsMiddleware',
     'misago.middleware.privatethreads.PrivateThreadsMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.middleware.common.CommonMiddleware',
@@ -113,10 +123,12 @@ MIDDLEWARE_CLASSES = (
 # List of application permission providers
 # List of application permission providers
 PERMISSION_PROVIDERS = (
 PERMISSION_PROVIDERS = (
     'misago.acl.permissions.usercp',
     'misago.acl.permissions.usercp',
+    'misago.acl.permissions.search',
     'misago.acl.permissions.users',
     'misago.acl.permissions.users',
     'misago.acl.permissions.forums',
     'misago.acl.permissions.forums',
     'misago.acl.permissions.threads',
     'misago.acl.permissions.threads',
     'misago.acl.permissions.privatethreads',
     'misago.acl.permissions.privatethreads',
+    'misago.acl.permissions.reports',
     'misago.acl.permissions.special',
     'misago.acl.permissions.special',
 )
 )
 
 
@@ -138,12 +150,24 @@ PROFILE_EXTENSIONS = (
     'misago.apps.profiles.details',
     'misago.apps.profiles.details',
 )
 )
 
 
+# List of User Model relations that should be loaded by session handler
+USER_EXTENSIONS_PRELOAD = ()
+
+# List of User Model relations that should be loaded when displaying users profiles
+PROFILE_EXTENSIONS_PRELOAD = ()
+
 # List of Markdown Extensions
 # List of Markdown Extensions
 MARKDOWN_EXTENSIONS = (
 MARKDOWN_EXTENSIONS = (
     'misago.markdown.extensions.strikethrough.StrikethroughExtension',
     'misago.markdown.extensions.strikethrough.StrikethroughExtension',
     'misago.markdown.extensions.quotes.QuoteTitlesExtension',
     'misago.markdown.extensions.quotes.QuoteTitlesExtension',
     'misago.markdown.extensions.mentions.MentionsExtension',
     'misago.markdown.extensions.mentions.MentionsExtension',
     'misago.markdown.extensions.magiclinks.MagicLinksExtension',
     'misago.markdown.extensions.magiclinks.MagicLinksExtension',
+    'misago.markdown.extensions.cleanlinks.CleanLinksExtension',
+    'misago.markdown.extensions.shorthandimgs.ShorthandImagesExtension',
+    # Uncomment for EXPERIMENTAL BBCode support
+    #'misago.markdown.extensions.bbcodes.BBCodesExtension',
+    # Uncomment for emoji support, requires emoji directory in static dir.
+    #'misago.markdown.extensions.emoji.EmojiExtension',
 )
 )
 
 
 # Name of root urls configuration
 # Name of root urls configuration
@@ -152,12 +176,13 @@ ROOT_URLCONF = 'misago.urls'
 #Installed applications
 #Installed applications
 INSTALLED_APPS = (
 INSTALLED_APPS = (
     # Applications that have no dependencies first!
     # Applications that have no dependencies first!
-    'south',
-    'coffin',
+    'south', # Database schema building and updating
+    'coffin', # Jinja2 integration
     'django.contrib.staticfiles',
     'django.contrib.staticfiles',
     'django.contrib.humanize',
     'django.contrib.humanize',
     'mptt', # Modified Pre-order Tree Transversal - allows us to nest forums 
     'mptt', # Modified Pre-order Tree Transversal - allows us to nest forums 
-    'debug_toolbar', # Debug toolbar
+    'haystack', # Search engines bridge
+    'debug_toolbar', # Debug toolbar'
     'misago', # Misago Forum App
     'misago', # Misago Forum App
 )
 )
 
 

+ 2 - 1
misago/signals.py

@@ -3,9 +3,10 @@ import django.dispatch
 delete_forum_content = django.dispatch.Signal()
 delete_forum_content = django.dispatch.Signal()
 delete_user_content = django.dispatch.Signal()
 delete_user_content = django.dispatch.Signal()
 merge_post = django.dispatch.Signal(providing_args=["new_post"])
 merge_post = django.dispatch.Signal(providing_args=["new_post"])
-merge_thread = django.dispatch.Signal(providing_args=["new_thread", "merge"])
+merge_thread = django.dispatch.Signal(providing_args=["new_thread"])
 move_forum_content = django.dispatch.Signal(providing_args=["move_to"])
 move_forum_content = django.dispatch.Signal(providing_args=["move_to"])
 move_post = django.dispatch.Signal(providing_args=["move_to"])
 move_post = django.dispatch.Signal(providing_args=["move_to"])
 move_thread = django.dispatch.Signal(providing_args=["move_to"])
 move_thread = django.dispatch.Signal(providing_args=["move_to"])
+rename_forum = django.dispatch.Signal()
 rename_user = django.dispatch.Signal()
 rename_user = django.dispatch.Signal()
 sync_user_profile = django.dispatch.Signal()
 sync_user_profile = django.dispatch.Signal()

+ 11 - 1
misago/templatetags/datetime.py

@@ -1,5 +1,5 @@
 from coffin.template import Library
 from coffin.template import Library
-from misago.utils.datesformats import date, reldate, reltimesince
+from misago.utils.datesformats import date, reldate, reltimesince, compact, relcompact
 
 
 register = Library()
 register = Library()
 
 
@@ -17,3 +17,13 @@ def reldate_filter(val, arg=""):
 @register.filter(name='reltimesince')
 @register.filter(name='reltimesince')
 def reltimesince_filter(val, arg=""):
 def reltimesince_filter(val, arg=""):
     return reltimesince(val, arg)
     return reltimesince(val, arg)
+
+
+@register.filter(name='compact')
+def compact_filter(val):
+    return compact(val)
+
+
+@register.filter(name='relcompact')
+def relcompact_filter(val):
+    return relcompact(val)

+ 10 - 1
misago/templatetags/utils.py

@@ -1,4 +1,5 @@
 from coffin.template import Library
 from coffin.template import Library
+from haystack.utils import Highlighter
 from misago.utils.strings import short_string
 from misago.utils.strings import short_string
 
 
 register = Library()
 register = Library()
@@ -14,4 +15,12 @@ def intersect(list_a, list_b):
 
 
 @register.filter(name='short_string')
 @register.filter(name='short_string')
 def make_short(string, length=16):
 def make_short(string, length=16):
-    return short_string(string, length)
+    return short_string(string, length)
+
+
+@register.filter(name='highlight')
+def highlight_result(text, query, length=500):
+    hl = Highlighter(query, html_tag='strong', max_length=length)
+    hl = hl.highlight(text)
+
+    return hl

+ 1 - 1
misago/tests/__init__.py

@@ -1 +1 @@
-from misago.tests.testuseradd import UserAddTestCase
+from misago.tests.user_manager_create_user import UserManagerCreateUserTestCase

+ 14 - 3
misago/tests/testuseradd.py → misago/tests/user_manager_create_user.py

@@ -1,17 +1,22 @@
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.management import call_command
 from django.core.management import call_command
 from django.test import TestCase
 from django.test import TestCase
-from misago.models import User, Rank, Role
+from misago.models import User
 from misago.monitor import Monitor
 from misago.monitor import Monitor
 
 
-class UserAddTestCase(TestCase):
+class UserManagerCreateUserTestCase(TestCase):
     def setUp(self):
     def setUp(self):
         call_command('startmisago', quiet=True)
         call_command('startmisago', quiet=True)
 
 
-    def test_user_from_model(self):
+    def test_create_user(self):
         """Test User.objects.create_user"""
         """Test User.objects.create_user"""
 
 
         user_a = User.objects.create_user('Lemmiwinks', 'lemm@sp.com', '123pass')
         user_a = User.objects.create_user('Lemmiwinks', 'lemm@sp.com', '123pass')
+        try:
+            user_from_db = User.objects.get(username=user_a.username)
+            user_from_db = User.objects.get(email=user_a.email)
+        except User.DoesNotExist:
+            raise AssertionError("User A was not saved in database!")
 
 
         monitor = Monitor()
         monitor = Monitor()
         self.assertEqual(int(monitor['users']), 1)
         self.assertEqual(int(monitor['users']), 1)
@@ -21,6 +26,12 @@ class UserAddTestCase(TestCase):
         self.assertEqual(monitor['last_user_slug'], user_a.username_slug)
         self.assertEqual(monitor['last_user_slug'], user_a.username_slug)
 
 
         user_b = User.objects.create_user('InactiveTest', 'lemsm@sp.com', '123pass', activation=User.ACTIVATION_USER)
         user_b = User.objects.create_user('InactiveTest', 'lemsm@sp.com', '123pass', activation=User.ACTIVATION_USER)
+        try:
+            user_from_db = User.objects.get(username=user_b.username)
+            user_from_db = User.objects.get(email=user_b.email)
+            self.assertEqual(user_from_db.activation, User.ACTIVATION_USER)
+        except User.DoesNotExist:
+            raise AssertionError("User B was not saved in database!")
 
 
         monitor = Monitor()
         monitor = Monitor()
         self.assertEqual(int(monitor['users']), 1)
         self.assertEqual(int(monitor['users']), 1)

+ 61 - 8
misago/theme.py

@@ -1,4 +1,5 @@
 from django.conf import settings
 from django.conf import settings
+from django.utils.importlib import import_module
 from coffin.shortcuts import render_to_response
 from coffin.shortcuts import render_to_response
 from coffin.template import dict_from_django_context
 from coffin.template import dict_from_django_context
 from coffin.template.loader import get_template, select_template, render_to_string
 from coffin.template.loader import get_template, select_template, render_to_string
@@ -10,8 +11,56 @@ if not hasattr(safestring, '__html__'):
     safestring.SafeUnicode.__html__ = lambda self: unicode(self)
     safestring.SafeUnicode.__html__ = lambda self: unicode(self)
 
 
 class Theme(object):
 class Theme(object):
+    middlewares = ()
+
     def __init__(self, theme):
     def __init__(self, theme):
         self.set_theme(theme);
         self.set_theme(theme);
+        self._mutex = None
+
+        if not self.middlewares:
+            self.load_middlewares(settings.TEMPLATE_MIDDLEWARES)
+
+    def load_middlewares(self, middlewares):
+        for extension in middlewares:
+            module = '.'.join(extension.split('.')[:-1])
+            extension = extension.split('.')[-1]
+            module = import_module(module)
+            middleware = getattr(module, extension)
+            self.middlewares += (middleware(), )
+
+    def merge_contexts(self, dictionary=None, context_instance=None):
+        dictionary = dictionary or {}
+        if context_instance:
+            context_instance.update(dictionary)
+        else:
+            context_instance = dictionary
+        return context_instance
+
+    def process_context(self, templates, context):
+        if self._mutex:
+            return context
+        self._mutex = True
+
+        for middleware in self.middlewares:
+            try:
+                new_context = middleware.process_context(self, templates, context)
+                if new_context:
+                    context = new_context
+            except AttributeError:
+                pass
+
+        self._mutex = None
+        return context
+
+    def process_template(self, templates, context):
+        for middleware in self.middlewares:
+            try:
+                new_templates = middleware.process_template(self, templates, context)
+                if new_templates:
+                    return new_templates
+            except AttributeError:
+                pass
+        return templates
 
 
     def set_theme(self, theme):
     def set_theme(self, theme):
         if theme not in settings.INSTALLED_THEMES:
         if theme not in settings.INSTALLED_THEMES:
@@ -26,7 +75,8 @@ class Theme(object):
     def get_theme(self):
     def get_theme(self):
         return self._theme
         return self._theme
 
 
-    def prefix_templates(self, templates):
+    def prefix_templates(self, templates, dictionary=None):
+        templates = self.process_template(templates, dictionary)
         if isinstance(templates, str):
         if isinstance(templates, str):
             return ('%s/%s' % (self._theme, templates), templates)
             return ('%s/%s' % (self._theme, templates), templates)
         else:
         else:
@@ -36,16 +86,19 @@ class Theme(object):
             prefixed += templates
             prefixed += templates
             return prefixed
             return prefixed
 
 
-    def render_to_string(self, templates, *args, **kwargs):
-        templates = self.prefix_templates(templates)
-        return render_to_string(templates, *args, **kwargs)
+    def render_to_string(self, templates, dictionary=None, context_instance=None):
+        dictionary = self.process_context(templates, self.merge_contexts(dictionary, context_instance))
+        templates = self.prefix_templates(templates, dictionary)
+        return render_to_string(templates, dictionary)
 
 
-    def render_to_response(self, templates, *args, **kwargs):
-        templates = self.prefix_templates(templates)
-        return render_to_response(templates, *args, **kwargs)
+    def render_to_response(self, templates, dictionary=None, context_instance=None,
+                       mimetype=None):
+        dictionary = self.process_context(templates, self.merge_contexts(dictionary, context_instance))
+        templates = self.prefix_templates(templates, dictionary)
+        return render_to_response(templates, dictionary=dictionary, mimetype=mimetype)
 
 
     def macro(self, templates, macro, dictionary={}, context_instance=None):
     def macro(self, templates, macro, dictionary={}, context_instance=None):
-        templates = self.prefix_templates(templates)
+        templates = self.prefix_templates(templates, dictionary)
         template = select_template(templates)
         template = select_template(templates)
         if context_instance:
         if context_instance:
             context_instance.update(dictionary)
             context_instance.update(dictionary)

+ 10 - 3
misago/urls.py

@@ -15,9 +15,9 @@ urlpatterns = patterns('misago.apps',
     url(r'^tos/$', 'tos.tos', name="tos"),
     url(r'^tos/$', 'tos.tos', name="tos"),
     url(r'^forum-map/$', 'forummap.forum_map', name="forum_map"),
     url(r'^forum-map/$', 'forummap.forum_map', name="forum_map"),
     url(r'^popular/$', 'popularthreads.popular_threads', name="popular_threads"),
     url(r'^popular/$', 'popularthreads.popular_threads', name="popular_threads"),
-    url(r'^popular/(?P<page>[0-9]+)/$', 'popularthreads.popular_threads', name="popular_threads"),
+    url(r'^popular/(?P<page>[1-9]([0-9]+)?)/$', 'popularthreads.popular_threads', name="popular_threads"),
     url(r'^new/$', 'newthreads.new_threads', name="new_threads"),
     url(r'^new/$', 'newthreads.new_threads', name="new_threads"),
-    url(r'^new/(?P<page>[0-9]+)/$', 'newthreads.new_threads', name="new_threads"),
+    url(r'^new/(?P<page>[1-9]([0-9]+)?)/$', 'newthreads.new_threads', name="new_threads"),
 )
 )
 
 
 urlpatterns += patterns('',
 urlpatterns += patterns('',
@@ -28,7 +28,8 @@ urlpatterns += patterns('',
     (r'^watched-threads/', include('misago.apps.watchedthreads.urls')),
     (r'^watched-threads/', include('misago.apps.watchedthreads.urls')),
     (r'^reset-password/', include('misago.apps.resetpswd.urls')),
     (r'^reset-password/', include('misago.apps.resetpswd.urls')),
     (r'^private-threads/', include('misago.apps.privatethreads.urls')),
     (r'^private-threads/', include('misago.apps.privatethreads.urls')),
-    #(r'^reports/', include('misago.apps.reports.urls')),
+    (r'^reports/', include('misago.apps.reports.urls')),
+    (r'^search/', include('misago.apps.search.urls')),
     (r'^', include('misago.apps.threads.urls')),
     (r'^', include('misago.apps.threads.urls')),
 )
 )
 
 
@@ -48,3 +49,9 @@ if settings.DEBUG:
 # Set error handlers
 # Set error handlers
 handler403 = 'misago.apps.errors.error403'
 handler403 = 'misago.apps.errors.error403'
 handler404 = 'misago.apps.errors.error404'
 handler404 = 'misago.apps.errors.error404'
+
+# Make sure people are not keeping uploads and app under same domain
+import warnings
+from urlparse import urlparse
+if not settings.DEBUG and not urlparse(settings.MEDIA_URL).netloc:
+    warnings.warn('Sharing same domain name between application and user uploaded media is a security risk. Create a subdomain pointing to your media directory (eg. "uploads.myforum.com") and change your MEDIA_URL.', RuntimeWarning)

+ 43 - 8
misago/utils/datesformats.py

@@ -75,17 +75,17 @@ def reltimesince(val, arg=""):
     if diff.seconds >= 0:
     if diff.seconds >= 0:
         if diff.seconds <= 5:
         if diff.seconds <= 5:
             return _("Just now")
             return _("Just now")
-            
-        if diff.seconds <= 60:
-            return _("Minute ago")
-            
-        if diff.seconds < 3600:
-            minutes = int(math.floor(diff.seconds / 60.0))
+                        
+        if diff.seconds < 3540:
+            minutes = int(math.ceil(diff.seconds / 60.0))
             return ungettext(
             return ungettext(
                     "Minute ago",
                     "Minute ago",
                     "%(minutes)s minutes ago",
                     "%(minutes)s minutes ago",
                 minutes) % {'minutes': minutes}
                 minutes) % {'minutes': minutes}
-                
+
+        if diff.seconds < 3660:
+            return _("Hour ago")
+        
         if diff.seconds < 10800:
         if diff.seconds < 10800:
             hours = int(math.floor(diff.seconds / 3600.0))
             hours = int(math.floor(diff.seconds / 3600.0))
             minutes = (diff.seconds - (hours * 3600)) / 60
             minutes = (diff.seconds - (hours * 3600)) / 60
@@ -106,4 +106,39 @@ def reltimesince(val, arg=""):
                 hours) % {'hours': hours}
                 hours) % {'hours': hours}
 
 
     # Fallback to reldate
     # Fallback to reldate
-    return reldate(val, arg)
+    return reldate(val, arg)
+
+
+def compact(val):
+    if not val:
+        return _("Never")
+    now = datetime.now(utc if is_aware(val) else None)
+    local = localtime(val)
+
+    if now.year == local.year:        
+        return format(localtime(val), _('j M'))
+    return format(localtime(val), _('j M y'))
+
+
+def relcompact(val):
+    if not val:
+        return _("Never")
+    now = datetime.now(utc if is_aware(val) else None)
+    diff = now - val
+    local = localtime(val)
+
+    # Difference is greater than day for sure
+    if diff.days != 0:
+        return compact(val)
+
+    if diff.seconds >= 0:
+        if diff.seconds <= 60:
+            return _("Now")
+        if diff.seconds < 3600:
+            minutes = int(math.ceil(diff.seconds / 60.0))
+            return pgettext("number of minutes", "%(minute)sm") % {'minute': minutes}
+        if diff.seconds < 86400:
+            hours = int(math.ceil(diff.seconds / 3600.0))
+            return pgettext("number of hours", "%(hour)sh") % {'hour': hours}
+
+    return compact(val)

+ 19 - 2
misago/utils/fixtures.py

@@ -104,6 +104,23 @@ def load_monitor_fixture(fixture):
     for id in fixture.keys():
     for id in fixture.keys():
         item = MonitorItem.objects.create(
         item = MonitorItem.objects.create(
                                           id=id,
                                           id=id,
-                                          value=fixture[id],
+                                          value=fixture[id][0],
+                                          type=fixture[id][1],
                                           updated=timezone.now()
                                           updated=timezone.now()
-                                          )
+                                          )
+
+
+def update_monitor_fixture(fixture):
+    for id in fixture.keys():
+        try:
+            item = MonitorItem.objects.get(id=id)
+            item.type = fixture[id][1]
+            item.updated = timezone.now()
+            item.save(force_update=True)
+        except MonitorItem.DoesNotExist:
+            MonitorItem.objects.create(
+                                       id=id,
+                                       value=fixture[id][0],
+                                       type=fixture[id][1],
+                                       updated=timezone.now()
+                                       )

+ 41 - 6
misago/utils/pagination.py

@@ -1,21 +1,31 @@
 import math
 import math
+from django.http import Http404
 
 
-def make_pagination(page, total, max):
+def make_pagination(page, total, per):
     pagination = {'start': 0, 'stop': 0, 'prev':-1, 'next':-1}
     pagination = {'start': 0, 'stop': 0, 'prev':-1, 'next':-1}
     page = int(page)
     page = int(page)
+
+    if page == 1:
+        # This is fugly abuse of "wrong page" handling
+        # It's done to combat "duplicate content" errors
+        # If page is 1 instead of 0, that suggests user came
+        # to page from somewhere/1/ instead of somewhere/
+        # when this happens We raise 404 to drop /1/ part from url
+        raise Http404()
+
     if page > 0:
     if page > 0:
-        pagination['start'] = (page - 1) * max
+        pagination['start'] = (page - 1) * per
 
 
     # Set page and total stat
     # Set page and total stat
-    pagination['page'] = int(pagination['start'] / max) + 1
-    pagination['total'] = int(math.ceil(total / float(max)))
+    pagination['page'] = int(pagination['start'] / per) + 1
+    pagination['total'] = int(math.ceil(total / float(per)))
 
 
     # Fix too large offset
     # Fix too large offset
     if pagination['start'] > total:
     if pagination['start'] > total:
         pagination['start'] = 0
         pagination['start'] = 0
 
 
     # Allow prev/next?
     # Allow prev/next?
-    if total > max:
+    if total > per:
         if pagination['page'] > 1:
         if pagination['page'] > 1:
             pagination['prev'] = pagination['page'] - 1
             pagination['prev'] = pagination['page'] - 1
         if pagination['page'] < pagination['total']:
         if pagination['page'] < pagination['total']:
@@ -26,5 +36,30 @@ def make_pagination(page, total, max):
         pagination['total'] = 1
         pagination['total'] = 1
 
 
     # Set stop offset
     # Set stop offset
-    pagination['stop'] = pagination['start'] + max
+    pagination['stop'] = pagination['start'] + per
+
+    # Put 1/5 of last page on current page...
+    if pagination['page'] + 1 == pagination['total']:
+        last_page = per + total - (pagination['total'] * per)
+        cutoff = int(per / 5)
+        if cutoff > 1 and last_page < cutoff:
+            pagination['stop'] += last_page
+            pagination['total'] -= 1
+            pagination['next'] = -1
+
+    # Raise 404 if page is out of range
+    if pagination['page'] > pagination['total']:
+        raise Http404()
+
+    # Return complete pager
     return pagination
     return pagination
+
+
+def page_number(item, total, per):
+    page_item = int(math.ceil(item / float(per)))
+    pages_total = int(math.ceil(total / float(per)))
+    last_page = total - ((pages_total - 1) * per)
+    cutoff = int(per / 5)
+    if cutoff > 1 and cutoff > last_page and pages_total == page_item and pages_total > 1:
+        page_item -= 1
+    return page_item

+ 7 - 3
misago/utils/strings.py

@@ -20,10 +20,14 @@ def random_string(length):
 def short_string(string, length=16):
 def short_string(string, length=16):
     if len(string) <= length:
     if len(string) <= length:
         return string;
         return string;
-    string = string[0:length]
+    string = string[0:length - 3]
     bits = string.split()
     bits = string.split()
-    if len(bits[-1]) > length:
-        bits[-1] = bits[-1][0:length]
     if len(bits[-1]) < 3:
     if len(bits[-1]) < 3:
         bits.pop()
         bits.pop()
     return '%s...' % (' '.join(bits))
     return '%s...' % (' '.join(bits))
+
+def html_escape(html):
+    html = html.replace('&', '&amp;')
+    html = html.replace('<', '&lt;')
+    html = html.replace('>', '&gt;')
+    return html.replace('"', '&quot;')

+ 24 - 0
misago/utils/urls.py

@@ -0,0 +1,24 @@
+#-*- coding: utf-8 -*-
+import re
+from urlparse import urlparse
+from django.conf import settings
+from misago.utils.strings import html_escape
+
+URL_RE = re.compile(r'^(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))$', re.UNICODE)
+
+def is_url(string):
+    return URL_RE.search(string.strip()) != None
+
+
+def is_inner(string):
+    return urlparse(string.strip()).netloc.lower() == urlparse(settings.BOARD_ADDRESS.lower()).netloc
+
+
+def clean_inner(string):
+    parsed = urlparse(string.strip())
+    href = parsed.path
+    if parsed.query:
+        href += '?%s' % parsed.query
+    if parsed.fragment:
+        href += '#%s' % parsed.fragment
+    return html_escape(href)

+ 5 - 2
misago/utils/views.py

@@ -9,14 +9,17 @@ def redirect_message(request, message, type='info', owner=None):
     return redirect(reverse('index'))
     return redirect(reverse('index'))
 
 
 
 
-def json_response(request, json={}, status=200, message=None):
+def json_response(request, json=None, status=200, message=None):
+    json = json or {}
     json.update({'code': status, 'message': unicode(message)})
     json.update({'code': status, 'message': unicode(message)})
     response = json_dumps(json, sort_keys=True,  ensure_ascii=False)
     response = json_dumps(json, sort_keys=True,  ensure_ascii=False)
     return HttpResponse(response, content_type='application/json', status=status)
     return HttpResponse(response, content_type='application/json', status=status)
 
 
 
 
-def ajax_response(request, template=None, macro=None, vars={}, json={}, status=200, message=None):
+def ajax_response(request, template=None, macro=None, vars=None, json=None, status=200, message=None):
     html = ''
     html = ''
+    vars = vars or {}    
+    json = json or {}
     if macro:
     if macro:
         html = request.theme.macro(template, macro, vars, context_instance=RequestContext(request));
         html = request.theme.macro(template, macro, vars, context_instance=RequestContext(request));
     return json_response(request, json.update({'html': html}), status, message)
     return json_response(request, json.update({'html': html}), status, message)

+ 3 - 1
requirements.txt

@@ -1,5 +1,6 @@
 django
 django
 django-debug-toolbar
 django-debug-toolbar
+django-haystack
 django-mptt
 django-mptt
 coffin
 coffin
 jinja2
 jinja2
@@ -9,4 +10,5 @@ Pillow
 pytz
 pytz
 recaptcha-client
 recaptcha-client
 South
 South
-Unidecode
+Unidecode
+whoosh

+ 177 - 120
static/cranefly/css/cranefly.css

@@ -221,7 +221,7 @@ input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio
 .control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#ffffff;}
 .control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#ffffff;}
 .control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#ffffff;}
 .control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#ffffff;}
 .control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#ffffff;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #ffffff;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #ffffff;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #ffffff;}
 .control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#ffffff;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #ffffff;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #ffffff;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #ffffff;}
-.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#ffffff;background-color:#fcf8e3;border-color:#ffffff;}
+.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#ffffff;background-color:#eb9213;border-color:#ffffff;}
 .control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#cf402e;}
 .control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#cf402e;}
 .control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#cf402e;}
 .control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#cf402e;}
 .control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#cf402e;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#a53325;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #e38b80;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #e38b80;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #e38b80;}
 .control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#cf402e;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#a53325;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #e38b80;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #e38b80;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #e38b80;}
@@ -310,11 +310,11 @@ table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span
 .table td.span12,.table th.span12{float:none;width:924px;margin-left:0;}
 .table td.span12,.table th.span12{float:none;width:924px;margin-left:0;}
 .table tbody tr.success>td{background-color:#529952;}
 .table tbody tr.success>td{background-color:#529952;}
 .table tbody tr.error>td{background-color:#c24a3b;}
 .table tbody tr.error>td{background-color:#c24a3b;}
-.table tbody tr.warning>td{background-color:#fcf8e3;}
+.table tbody tr.warning>td{background-color:#eb9213;}
 .table tbody tr.info>td{background-color:#1595ca;}
 .table tbody tr.info>td{background-color:#1595ca;}
 .table-hover tbody tr.success:hover>td{background-color:#498949;}
 .table-hover tbody tr.success:hover>td{background-color:#498949;}
 .table-hover tbody tr.error:hover>td{background-color:#af4235;}
 .table-hover tbody tr.error:hover>td{background-color:#af4235;}
-.table-hover tbody tr.warning:hover>td{background-color:#faf2cc;}
+.table-hover tbody tr.warning:hover>td{background-color:#d48311;}
 .table-hover tbody tr.info:hover>td{background-color:#1284b3;}
 .table-hover tbody tr.info:hover>td{background-color:#1284b3;}
 [class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;margin-top:1px;}
 [class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;margin-top:1px;}
 .icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png");}
 .icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png");}
@@ -567,7 +567,7 @@ button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-
 .btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;}
 .btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;}
 .btn-group-vertical>.btn-large:first-child{-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0;}
 .btn-group-vertical>.btn-large:first-child{-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0;}
 .btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;}
 .btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;}
-.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
+.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#eb9213;border:1px solid #dd6712;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
 .alert,.alert h4{color:#ffffff;}
 .alert,.alert h4{color:#ffffff;}
 .alert h4{margin:0;}
 .alert h4{margin:0;}
 .alert .close{position:relative;top:-2px;right:-21px;line-height:20px;}
 .alert .close{position:relative;top:-2px;right:-21px;line-height:20px;}
@@ -852,6 +852,7 @@ a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#ffffff;text-decor
 .header-primary .breadcrumb li a:hover,.header-primary .breadcrumb li a:active{color:#333333;}
 .header-primary .breadcrumb li a:hover,.header-primary .breadcrumb li a:active{color:#333333;}
 .header-primary .breadcrumb li .divider{padding-left:0px;padding-right:0px;}.header-primary .breadcrumb li .divider i{opacity:0.2;filter:alpha(opacity=20);position:relative;bottom:1px;}
 .header-primary .breadcrumb li .divider{padding-left:0px;padding-right:0px;}.header-primary .breadcrumb li .divider i{opacity:0.2;filter:alpha(opacity=20);position:relative;bottom:1px;}
 .header-primary h1{color:#555555;font-size:35px;font-weight:normal;}
 .header-primary h1{color:#555555;font-size:35px;font-weight:normal;}
+.header-primary.header-search h1{float:left;}.header-primary.header-search h1 form{float:right;margin:0px;}.header-primary.header-search h1 form input{margin-left:21px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;font-size:17.5px;}.header-primary.header-search h1 form input:focus,.header-primary.header-search h1 form input:active{border-color:#4dc3ff;-webkit-box-shadow:0px 0px 0px 3px #b3e5ff;-moz-box-shadow:0px 0px 0px 3px #b3e5ff;box-shadow:0px 0px 0px 3px #b3e5ff;}
 .header-primary .header-stats{overflow:visible;margin-bottom:0px;color:#999999;}.header-primary .header-stats li{float:left;margin-right:14px;}.header-primary .header-stats li>a{color:#999999;}.header-primary .header-stats li>a:hover,.header-primary .header-stats li>a:active{color:#333333;}
 .header-primary .header-stats{overflow:visible;margin-bottom:0px;color:#999999;}.header-primary .header-stats li{float:left;margin-right:14px;}.header-primary .header-stats li>a{color:#999999;}.header-primary .header-stats li>a:hover,.header-primary .header-stats li>a:active{color:#333333;}
 .header-primary .header-stats li>i{opacity:0.5;filter:alpha(opacity=50);}
 .header-primary .header-stats li>i{opacity:0.5;filter:alpha(opacity=50);}
 .header-primary .header-stats li.stats-form{float:right;}.header-primary .header-stats li.stats-form form{margin:0px;margin-bottom:-12px;padding:0px;}.header-primary .header-stats li.stats-form form button{position:relative;bottom:12px;}.header-primary .header-stats li.stats-form form button>i{position:relative;top:0px;}
 .header-primary .header-stats li.stats-form{float:right;}.header-primary .header-stats li.stats-form form{margin:0px;margin-bottom:-12px;padding:0px;}.header-primary .header-stats li.stats-form form button{position:relative;bottom:12px;}.header-primary .header-stats li.stats-form form button>i{position:relative;top:0px;}
@@ -863,15 +864,16 @@ a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#ffffff;text-decor
 .header-primary .header-tabs li a.btn{border:1px solid #cccccc;*border:0;border-radius:3px;margin:0px;padding:4px 12px;color:#333333;}.header-primary .header-tabs li a.btn i{position:relative;top:0px;}
 .header-primary .header-tabs li a.btn{border:1px solid #cccccc;*border:0;border-radius:3px;margin:0px;padding:4px 12px;color:#333333;}.header-primary .header-tabs li a.btn i{position:relative;top:0px;}
 .header-primary .header-tabs li a.btn:visited,.header-primary .header-tabs li a.btn:hover{background-color:#ffffff;border-color:#a6a6a6;}
 .header-primary .header-tabs li a.btn:visited,.header-primary .header-tabs li a.btn:hover{background-color:#ffffff;border-color:#a6a6a6;}
 html,body{height:100%;}
 html,body{height:100%;}
-#wrap{min-height:100%;height:auto !important;height:100%;margin:0 auto -100px;}#wrap .container-primary{padding-top:20px;padding-bottom:120px;}#wrap .container-primary .page-description{margin-bottom:20px;}
+#wrap{min-height:100%;height:auto !important;height:100%;margin:0 auto -102px;}#wrap .container-primary{padding-top:20px;padding-bottom:120px;}#wrap .container-primary .page-description{margin-bottom:20px;}
 #wrap .container-primary hr{border:none;border-top:1px solid #eeeeee;}
 #wrap .container-primary hr{border:none;border-top:1px solid #eeeeee;}
-footer{background-color:#eeeeee;border-top:1px solid #dadada;height:80px;padding:11px 19px;}footer hr{border-bottom:1px solid #dadada;margin:10px 0px;}
-footer .credits{color:#555555;font-size:90%;}footer .credits a:link,footer .credits a:active,footer .credits a:visited,footer .credits a:hover{color:#555555;}
+footer{background-color:#eeeeee;border-top:1px solid #dadada;height:90px;padding:11px 19px;padding-bottom:0px;}footer .container hr{border-bottom:1px solid #dadada;margin:10px 0px;}
+footer .container .credits p{margin-bottom:0px;color:#555555;font-size:90%;}footer .container .credits p a:link,footer .container .credits p a:active,footer .container .credits p a:visited,footer .container .credits p a:hover{color:#555555;}
 ::selection{background:#f89406;color:#ffffff;}
 ::selection{background:#f89406;color:#ffffff;}
 ::-moz-selection{background:#f89406;color:#ffffff;}
 ::-moz-selection{background:#f89406;color:#ffffff;}
 .navbar-header .navbar-inner{background:none;background-color:#f3f3f3;border-bottom:1px solid #dfdfdf;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}.navbar-header .navbar-inner .container{background:url("../img/logo.png");background-position:left center;background-repeat:no-repeat;}
 .navbar-header .navbar-inner{background:none;background-color:#f3f3f3;border-bottom:1px solid #dfdfdf;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}.navbar-header .navbar-inner .container{background:url("../img/logo.png");background-position:left center;background-repeat:no-repeat;}
 .navbar-header .navbar-inner .brand{margin-left:6px;text-shadow:none;}.navbar-header .navbar-inner .brand:link,.navbar-header .navbar-inner .brand:active,.navbar-header .navbar-inner .brand:visited,.navbar-header .navbar-inner .brand:hover{color:#c24a3b;font-size:200%;}.navbar-header .navbar-inner .brand:link span,.navbar-header .navbar-inner .brand:active span,.navbar-header .navbar-inner .brand:visited span,.navbar-header .navbar-inner .brand:hover span{color:#c0c0c0;}
 .navbar-header .navbar-inner .brand{margin-left:6px;text-shadow:none;}.navbar-header .navbar-inner .brand:link,.navbar-header .navbar-inner .brand:active,.navbar-header .navbar-inner .brand:visited,.navbar-header .navbar-inner .brand:hover{color:#c24a3b;font-size:200%;}.navbar-header .navbar-inner .brand:link span,.navbar-header .navbar-inner .brand:active span,.navbar-header .navbar-inner .brand:visited span,.navbar-header .navbar-inner .brand:hover span{color:#c0c0c0;}
-.navbar-header .navbar-inner .navbar-search-form{background-color:#ffffff;border:1px solid #dfdfdf;border-radius:3px;margin-top:9px;padding:1px 0px;color:#333333;}.navbar-header .navbar-inner .navbar-search-form input{border:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;margin:0px;}
+.navbar-header .navbar-inner .navbar-search-form{background-color:#ffffff;border:1px solid #dfdfdf;border-radius:3px;margin-top:9px;padding:1px 0px;color:#333333;}.navbar-header .navbar-inner .navbar-search-form.search-disabled{opacity:0.6;filter:alpha(opacity=60);}
+.navbar-header .navbar-inner .navbar-search-form input,.navbar-header .navbar-inner .navbar-search-form input:disabled{border:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;background:none;margin:0px;}
 .navbar-header .navbar-inner .navbar-search-form button{margin:0px;opacity:0.3;filter:alpha(opacity=30);}.navbar-header .navbar-inner .navbar-search-form button:hover,.navbar-header .navbar-inner .navbar-search-form button:active{opacity:0.8;filter:alpha(opacity=80);}
 .navbar-header .navbar-inner .navbar-search-form button{margin:0px;opacity:0.3;filter:alpha(opacity=30);}.navbar-header .navbar-inner .navbar-search-form button:hover,.navbar-header .navbar-inner .navbar-search-form button:active{opacity:0.8;filter:alpha(opacity=80);}
 .navbar-header .navbar-inner .navbar-blocks{margin-left:6px;}.navbar-header .navbar-inner .navbar-blocks li{margin-left:6px;}.navbar-header .navbar-inner .navbar-blocks li form{margin:0px;padding:0px;}
 .navbar-header .navbar-inner .navbar-blocks{margin-left:6px;}.navbar-header .navbar-inner .navbar-blocks li{margin-left:6px;}.navbar-header .navbar-inner .navbar-blocks li form{margin:0px;padding:0px;}
 .navbar-header .navbar-inner .navbar-blocks li a:link,.navbar-header .navbar-inner .navbar-blocks li a:visited,.navbar-header .navbar-inner .navbar-blocks li .btn-link{background-color:#f8f8f8;border:1px solid #dadada;border-radius:3px;padding:5px 8px;margin-top:9px;}.navbar-header .navbar-inner .navbar-blocks li a:link i,.navbar-header .navbar-inner .navbar-blocks li a:visited i,.navbar-header .navbar-inner .navbar-blocks li .btn-link i{opacity:0.7;filter:alpha(opacity=70);}
 .navbar-header .navbar-inner .navbar-blocks li a:link,.navbar-header .navbar-inner .navbar-blocks li a:visited,.navbar-header .navbar-inner .navbar-blocks li .btn-link{background-color:#f8f8f8;border:1px solid #dadada;border-radius:3px;padding:5px 8px;margin-top:9px;}.navbar-header .navbar-inner .navbar-blocks li a:link i,.navbar-header .navbar-inner .navbar-blocks li a:visited i,.navbar-header .navbar-inner .navbar-blocks li .btn-link i{opacity:0.7;filter:alpha(opacity=70);}
@@ -881,7 +883,20 @@ footer .credits{color:#555555;font-size:90%;}footer .credits a:link,footer .cred
 .navbar-header .navbar-inner .navbar-blocks li a:hover.fresh,.navbar-header .navbar-inner .navbar-blocks li a:active.fresh,.navbar-header .navbar-inner .navbar-blocks li .btn-link:hover.fresh,.navbar-header .navbar-inner .navbar-blocks li .btn-link:active.fresh{background-color:#46a546;border-color:#378137;}
 .navbar-header .navbar-inner .navbar-blocks li a:hover.fresh,.navbar-header .navbar-inner .navbar-blocks li a:active.fresh,.navbar-header .navbar-inner .navbar-blocks li .btn-link:hover.fresh,.navbar-header .navbar-inner .navbar-blocks li .btn-link:active.fresh{background-color:#46a546;border-color:#378137;}
 .navbar-header .navbar-inner .navbar-blocks li a:hover i,.navbar-header .navbar-inner .navbar-blocks li a:active i,.navbar-header .navbar-inner .navbar-blocks li .btn-link:hover i,.navbar-header .navbar-inner .navbar-blocks li .btn-link:active i{background-image:url("../img/glyphicons-halflings-white.png");opacity:1;filter:alpha(opacity=100);}
 .navbar-header .navbar-inner .navbar-blocks li a:hover i,.navbar-header .navbar-inner .navbar-blocks li a:active i,.navbar-header .navbar-inner .navbar-blocks li .btn-link:hover i,.navbar-header .navbar-inner .navbar-blocks li .btn-link:active i{background-image:url("../img/glyphicons-halflings-white.png");opacity:1;filter:alpha(opacity=100);}
 .navbar-header .navbar-inner .navbar-blocks li a:hover .label,.navbar-header .navbar-inner .navbar-blocks li a:active .label,.navbar-header .navbar-inner .navbar-blocks li .btn-link:hover .label,.navbar-header .navbar-inner .navbar-blocks li .btn-link:active .label{background-color:#eeeeee;color:#333333;}
 .navbar-header .navbar-inner .navbar-blocks li a:hover .label,.navbar-header .navbar-inner .navbar-blocks li a:active .label,.navbar-header .navbar-inner .navbar-blocks li .btn-link:hover .label,.navbar-header .navbar-inner .navbar-blocks li .btn-link:active .label{background-color:#eeeeee;color:#333333;}
-.navbar-header .navbar-inner .navbar-blocks li.user-profile a:link,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:visited,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:hover,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:active{background:none;border:none;margin-right:8px;margin-top:5px;font-weight:bold;text-shadow:none;}.navbar-header .navbar-inner .navbar-blocks li.user-profile a:link img,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:visited img,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:hover img,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:active img{border-radius:3px;margin-right:4px;width:32px;height:32px;position:relative;bottom:1px;}
+.navbar-header .navbar-inner .navbar-blocks li.user-profile a:link,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:visited,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:hover,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:active{background:none;border:none;margin-right:8px;margin-top:5px;font-weight:bold;text-shadow:none;}.navbar-header .navbar-inner .navbar-blocks li.user-profile a:link img,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:visited img,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:hover img,.navbar-header .navbar-inner .navbar-blocks li.user-profile a:active img{border-radius:3px;margin-right:6px;width:32px;height:32px;position:relative;bottom:1px;}
+.navbar-header .navbar-inner .navbar-compact{display:none;}.navbar-header .navbar-inner .navbar-compact li.user-profile>a:link,.navbar-header .navbar-inner .navbar-compact li.user-profile>a:visited{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;margin-top:0px;padding:14px;padding-top:10px;padding-bottom:8px;}.navbar-header .navbar-inner .navbar-compact li.user-profile>a:link img,.navbar-header .navbar-inner .navbar-compact li.user-profile>a:visited img{margin-right:0px;margin-left:6px;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile>a:link .caret-border,.navbar-header .navbar-inner .navbar-compact li.user-profile>a:visited .caret-border{background-color:#fbfbfb;border:1px solid #dfdfdf;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;margin-left:8px;padding:0px 4px;}.navbar-header .navbar-inner .navbar-compact li.user-profile>a:link .caret-border .caret,.navbar-header .navbar-inner .navbar-compact li.user-profile>a:visited .caret-border .caret{margin:0px;padding:0px;position:relative;top:13px;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile>a:hover,.navbar-header .navbar-inner .navbar-compact li.user-profile>a:active{background-color:#fbfbfb;}.navbar-header .navbar-inner .navbar-compact li.user-profile>a:hover .caret-border,.navbar-header .navbar-inner .navbar-compact li.user-profile>a:active .caret-border{border-color:#999999;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:link,.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:visited,.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:hover,.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:focus{background-color:#fbfbfb;}.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:link .caret-border,.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:visited .caret-border,.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:hover .caret-border,.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:focus .caret-border{background:#cf402e;border-color:#cf402e;}.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:link .caret-border .caret,.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:visited .caret-border .caret,.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:hover .caret-border .caret,.navbar-header .navbar-inner .navbar-compact li.user-profile.open .dropdown-toggle:focus .caret-border .caret{border-top-color:#ffffff;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu{border:none;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px;border-top:4px solid #cf402e;-webkit-box-shadow:0px 3px 4px #999999;-moz-box-shadow:0px 3px 4px #999999;box-shadow:0px 3px 4px #999999;margin:0px;margin-top:-8px;margin-right:1px;padding:4px 0px;width:270px;}.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu:before{display:none;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu:after{border-bottom:6px solid #cf402e;margin-top:-3px;margin-right:11px;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li{margin:0px;padding:0px;}.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .label{float:right;margin:0px;margin-top:2px;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li a,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;clear:none;display:block;float:none;margin:0px;padding:6px 12px;color:#333333;font-weight:normal;text-align:left;}.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li a i,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link i{opacity:1;filter:alpha(opacity=100);}
+.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li a:link .label,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link:link .label,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li a:active .label,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link:active .label,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li a:visited .label,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link:visited .label,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li a:hover .label,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link:hover .label{background-color:#cf402e;float:right;color:#ffffff;text-shadow:0px 1px 0px #7c261b;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li a:link,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li a:visited{opacity:0.8;filter:alpha(opacity=80);}
+.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li a:hover{background-color:#555555;opacity:1;filter:alpha(opacity=100);color:#ffffff;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link{background:none;border:none;width:100%;}.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link:hover,.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link:active{background:#cf402e;opacity:1;filter:alpha(opacity=100);color:#ffffff;text-shadow:0px 1px 0px #7c261b;}
+.navbar-header .navbar-inner .navbar-compact li.user-profile .dropdown-menu>li .btn-link i{position:relative;top:0px;}
 .navbar-header .navbar-inner .navbar-user-nav li .btn{padding:2px 8px;margin-left:8px;margin-top:13px;}.navbar-header .navbar-inner .navbar-user-nav li .btn:link,.navbar-header .navbar-inner .navbar-user-nav li .btn:active,.navbar-header .navbar-inner .navbar-user-nav li .btn:hover,.navbar-header .navbar-inner .navbar-user-nav li .btn:visited{color:#ffffff;}
 .navbar-header .navbar-inner .navbar-user-nav li .btn{padding:2px 8px;margin-left:8px;margin-top:13px;}.navbar-header .navbar-inner .navbar-user-nav li .btn:link,.navbar-header .navbar-inner .navbar-user-nav li .btn:active,.navbar-header .navbar-inner .navbar-user-nav li .btn:hover,.navbar-header .navbar-inner .navbar-user-nav li .btn:visited{color:#ffffff;}
 .navbar-header .navbar-inner .navbar-user-nav li .btn.btn-danger{text-shadow:0px 1px 0px #902d20;}.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-danger:hover,.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-danger:active,.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-danger:focus{background-color:#e82c15;border-color:#d12813;}
 .navbar-header .navbar-inner .navbar-user-nav li .btn.btn-danger{text-shadow:0px 1px 0px #902d20;}.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-danger:hover,.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-danger:active,.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-danger:focus{background-color:#e82c15;border-color:#d12813;}
 .navbar-header .navbar-inner .navbar-user-nav li .btn.btn-inverse{text-shadow:0px 1px 0px #0d0d0d;}.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-inverse:hover,.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-inverse:active,.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-inverse:focus{background-color:#262626;border-color:#1a1a1a;}
 .navbar-header .navbar-inner .navbar-user-nav li .btn.btn-inverse{text-shadow:0px 1px 0px #0d0d0d;}.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-inverse:hover,.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-inverse:active,.navbar-header .navbar-inner .navbar-user-nav li .btn.btn-inverse:focus{background-color:#262626;border-color:#1a1a1a;}
@@ -900,6 +915,7 @@ footer .breadcrumb li.active{color:#555555;}
 .messages-list a:active,.messages-list a:hover{opacity:1;filter:alpha(opacity=100);}
 .messages-list a:active,.messages-list a:hover{opacity:1;filter:alpha(opacity=100);}
 .messages-list .alert-info{text-shadow:0px 1px 0px #0b516e;}
 .messages-list .alert-info{text-shadow:0px 1px 0px #0b516e;}
 .messages-list .alert-success{text-shadow:0px 1px 0px #2e572e;}
 .messages-list .alert-success{text-shadow:0px 1px 0px #2e572e;}
+.messages-list .alert-warning{text-shadow:0px 1px 0px #ae510e;}
 .messages-list .alert-error{text-shadow:0px 1px 0px #742c23;}
 .messages-list .alert-error{text-shadow:0px 1px 0px #742c23;}
 .form-container{background:#ffffff;border:1px solid #e7e7e7;border-radius:3px;margin:0px -21px;margin-bottom:20px;padding:20px;padding-top:-15px;}.form-container .form-header{border-bottom:1px solid #e7e7e7;margin-top:-20px;margin-bottom:20px;padding:10px 0px;}.form-container .form-header h1{margin:0px;padding:0px;font-size:17.5px;}.form-container .form-header h1 small{font-size:13.125px;font-weight:bold;}
 .form-container{background:#ffffff;border:1px solid #e7e7e7;border-radius:3px;margin:0px -21px;margin-bottom:20px;padding:20px;padding-top:-15px;}.form-container .form-header{border-bottom:1px solid #e7e7e7;margin-top:-20px;margin-bottom:20px;padding:10px 0px;}.form-container .form-header h1{margin:0px;padding:0px;font-size:17.5px;}.form-container .form-header h1 small{font-size:13.125px;font-weight:bold;}
 .form-container .form-header .btn{margin-left:14px;position:relative;bottom:30px;}
 .form-container .form-header .btn{margin-left:14px;position:relative;bottom:30px;}
@@ -942,49 +958,53 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .error-page .error-protips{margin-top:40px;}.error-page .error-protips .go-back{display:none;}
 .error-page .error-protips{margin-top:40px;}.error-page .error-protips .go-back{display:none;}
 .error-page .error-protips a:link,.error-page .error-protips a:visited{background-color:#333333;border:1px solid #000000;border-radius:5px;margin:2px 10px;opacity:0.7;filter:alpha(opacity=70);padding:11px 19px;color:#ffffff;font-weight:bold;text-decoration:none;}
 .error-page .error-protips a:link,.error-page .error-protips a:visited{background-color:#333333;border:1px solid #000000;border-radius:5px;margin:2px 10px;opacity:0.7;filter:alpha(opacity=70);padding:11px 19px;color:#ffffff;font-weight:bold;text-decoration:none;}
 .error-page .error-protips a:hover,.error-page .error-protips a:active{opacity:1;filter:alpha(opacity=100);}
 .error-page .error-protips a:hover,.error-page .error-protips a:active{opacity:1;filter:alpha(opacity=100);}
-.markdown{margin:0px;overflow:auto;}.markdown>:first-child{margin-top:0px;}
-.markdown>:last-child{margin-bottom:0px;}
-.markdown h1{font-size:28px;}
-.markdown h2{font-size:23.8px;}
-.markdown h3{font-size:21px;}
-.markdown h4{font-size:16.8px;}
-.markdown hr{border:none;border-top:1px solid #eeeeee;margin:20px 0px;}
-.markdown blockquote{background-color:#fcfcfc;border:1px solid #f0f0f0;border-radius:3px;padding:14px;}.markdown blockquote>h3:first-child{margin:0px;margin-bottom:10px;padding:0px;font-size:14px;line-height:20px;}
-.markdown blockquote>:first-child{margin-top:0px;}
-.markdown blockquote>:last-child{margin-bottom:0px;}
-.markdown blockquote p{margin:0 0 10px;font-size:11.9px;}
-.markdown blockquote blockquote{background-color:#ffffff;border:1px solid #f2f2f2;border-radius:3px;padding:14px;}
-.markdown code{background-color:#333333;border:none;color:#eeeeee;font-size:14px;}
-.markdown pre{background-color:#222222;padding:7px 14px;}.markdown pre code{background:none;border:none;color:#eeeeee;font-size:11.9px;}
-.markdown img{border-radius:3px;box-shadow:0px 0px 4px #555555;margin:10px 0px;}
-.markdown .md-img{overflow:auto;}.markdown .md-img .md-img-span{margin:10px 0px;float:none;}.markdown .md-img .md-img-span .md-img-wrap{background-color:#eeeeee;border:1px solid #ffffff;border-radius:4px;box-shadow:0px 0px 2px #999999;margin:3px;}.markdown .md-img .md-img-span .md-img-wrap .md-img-bg{background-color:#ffffff;border-radius:3px;padding:10px;text-align:center;}.markdown .md-img .md-img-span .md-img-wrap .md-img-bg img{border-radius:3px;box-shadow:none;}
-.markdown .md-img .md-img-span .md-img-wrap .md-img-bg .md-img-error{background:url('../img/img_broken.jpg');border-radius:3px;padding:50px 0px;}.markdown .md-img .md-img-span .md-img-wrap .md-img-bg .md-img-error span{background-color:#333333;border-radius:5px;opacity:0.8;filter:alpha(opacity=80);padding:7px 14px;margin:0px auto;color:#ffffff;text-shadow:0px 1px 0px #000000;}
-.markdown .md-img .md-img-span .md-img-wrap .md-img-label{display:block;padding:7px 14px;color:#333333;}.markdown .md-img .md-img-span .md-img-wrap .md-img-label:hover,.markdown .md-img .md-img-span .md-img-wrap .md-img-label:active{color:#333333;text-decoration:none;}
-.markdown pre,.markdown blockquote,.markdown iframe{margin-top:20px;margin-bottom:20px;}.markdown pre>:first-child,.markdown blockquote>:first-child,.markdown iframe>:first-child{margin-top:0px;}
-.markdown pre>:last-child,.markdown blockquote>:last-child,.markdown iframe>:last-child{margin-bottom:0px;}
+.markdown,.markdown article{margin:0px;overflow:auto;}.markdown>:first-child,.markdown article>:first-child{margin-top:0px;}
+.markdown>:last-child,.markdown article>:last-child{margin-bottom:0px;}.markdown>:last-child img:last-child,.markdown article>:last-child img:last-child{margin-bottom:0px;}
+.markdown h1,.markdown article h1{font-size:23.8px;}
+.markdown h2,.markdown article h2{font-size:21px;}
+.markdown h3,.markdown article h3{font-size:16.8px;}
+.markdown h4,.markdown article h4{font-size:14px;}
+.markdown hr,.markdown article hr{border:none;border-top:1px solid #eeeeee;margin:20px 0px;}
+.markdown blockquote,.markdown article blockquote{border-left-color:#e1e1e1;padding:4.666666666666667px 14px;}.markdown blockquote header,.markdown article blockquote header{padding-bottom:10px;font-size:15.400000000000002px;font-weight:bold;line-height:20px;}
+.markdown blockquote p,.markdown article blockquote p{margin:0 0 10px;font-size:14px;}
+.markdown blockquote blockquote,.markdown article blockquote blockquote{opacity:0.85;filter:alpha(opacity=85);}
+.markdown code,.markdown article code{background-color:#333333;border:none;color:#eeeeee;font-size:14px;}
+.markdown pre,.markdown article pre{background-color:#222222;padding:7px 14px;}.markdown pre code,.markdown article pre code{background:none;border:none;color:#eeeeee;font-size:11.9px;}
+.markdown img,.markdown article img{background-color:#ffffff;border-radius:3px;margin:10px 0px;}
+.markdown pre,.markdown article pre,.markdown blockquote,.markdown article blockquote,.markdown iframe,.markdown article iframe{margin-top:20px;margin-bottom:20px;}.markdown pre>:first-child,.markdown article pre>:first-child,.markdown blockquote>:first-child,.markdown article blockquote>:first-child,.markdown iframe>:first-child,.markdown article iframe>:first-child{margin-top:0px;}
+.markdown pre>:last-child,.markdown article pre>:last-child,.markdown blockquote>:last-child,.markdown article blockquote>:last-child,.markdown iframe>:last-child,.markdown article iframe>:last-child{margin-bottom:0px;}
+.markdown .emoji,.markdown article .emoji{background:none;border-radius:0px;margin:0px;vertical-align:middle;}
+.markdown h1 .emoji,.markdown article h1 .emoji{width:27.8px;height:27.8px;}
+.markdown h2 .emoji,.markdown article h2 .emoji{width:25px;height:25px;}
+.markdown h3 .emoji,.markdown article h3 .emoji{width:20.8px;height:20.8px;}
+.markdown h4 .emoji,.markdown article h4 .emoji{width:18px;height:18px;}
+.markdown p .emoji,.markdown article p .emoji{width:18px;height:18px;}
+.markdown blockquote p .emoji,.markdown article blockquote p .emoji{width:13.9px;height:13.9px;}
 .index-sidebar{position:relative;bottom:9px;}
 .index-sidebar{position:relative;bottom:9px;}
-.index-category{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.index-category table{margin:0px;}.index-category table caption{background-color:#fbfbfb;border:1px solid #d5d5d5;border-radius:2px 2px 0px 0px;margin:-1px;padding:3.966666666666667px 9.9px;color:#333333;font-size:11.9px;font-weight:bold;text-align:left;}.index-category table caption small{margin-left:7px;color:#999999;font-size:11.9px;}
-.index-category table td{padding:14.75px 9.9px;padding-bottom:15.75px;}
-.index-category table .forum-icon{padding-right:2.95px;width:1%;}.index-category table .forum-icon .forum-icon-wrap{background-color:#555555;border:1px solid #3b3b3b;border-radius:3px;padding:3px 4px;}.index-category table .forum-icon .forum-icon-wrap.forum-icon-new{background-color:#cf402e;border:1px solid #a53325;}
-.index-category table .forum-icon .forum-icon-wrap.forum-icon-redirect{background-color:#9466c6;border:1px solid #7a43b6;}
-.index-category table .forum-main h3{float:left;margin:0px;padding:0px;font-size:17.5px;font-weight:normal;line-height:20px;}.index-category table .forum-main h3 a:link,.index-category table .forum-main h3 a:visited{color:#333333;}
-.index-category table .forum-main .dropdown{float:right;right:14px;}.index-category table .forum-main .dropdown .dropdown-toggle{padding:4px 8px;opacity:0.6;filter:alpha(opacity=60);color:#333333;font-weight:bold;}.index-category table .forum-main .dropdown .dropdown-toggle:hover,.index-category table .forum-main .dropdown .dropdown-toggle:active,.index-category table .forum-main .dropdown .dropdown-toggle:focus{opacity:1;filter:alpha(opacity=100);text-decoration:none;}
-.index-category table .forum-main .dropdown.open .dropdown-toggle{background-color:#eeeeee;border-radius:3px 3px 0px 0px;opacity:1;filter:alpha(opacity=100);padding-bottom:6px;text-decoration:none;}
-.index-category table .forum-main .dropdown .dropdown-menu{background:none;border:none;box-shadow:none;}.index-category table .forum-main .dropdown .dropdown-menu .dropdown-shadow{border-radius:3px;-webkit-box-shadow:0px 0px 3px #999999;-moz-box-shadow:0px 0px 3px #999999;box-shadow:0px 0px 3px #999999;width:256px;position:relative;right:0px;top:-4px;}.index-category table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul{background-color:#fbfbfb;border-radius:3px;margin:0px;padding:0px;}.index-category table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li{margin:0px;padding:0px;list-style:none;}.index-category table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a{border-bottom:1px dotted #d5d5d5;display:block;opacity:0.7;filter:alpha(opacity=70);padding:6px 8px;color:#333333;text-decoration:none;}.index-category table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:hover,.index-category table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:active{opacity:1;filter:alpha(opacity=100);}
-.index-category table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li:last-child a{border-bottom:none;}
-.index-category table .forum-main .forum-details{border-left:1px dotted #e0e0e0;float:right;margin:-10px 0px;margin-top:-8.1px;padding-left:14px;height:35px;width:220px;}.index-category table .forum-main .forum-details .thread-name a:link,.index-category table .forum-main .forum-details .thread-name a:active,.index-category table .forum-main .forum-details .thread-name a:visited,.index-category table .forum-main .forum-details .thread-name a:hover{margin-bottom:1px;color:#333333;font-size:11.9px;font-weight:bold;}
-.index-category table .forum-main .forum-details .muted{font-size:10.5px;line-height:10.5px;}.index-category table .forum-main .forum-details .muted .last-poster,.index-category table .forum-main .forum-details .muted a:link,.index-category table .forum-main .forum-details .muted a:active,.index-category table .forum-main .forum-details .muted a:visited,.index-category table .forum-main .forum-details .muted a:hover{color:#555555;}
-.index-category table .forum-main .forum-details em{position:relative;top:7.5px;color:#999999;}.index-category table .forum-main .forum-details em strong{color:#333333;font-weight:normal;}
-.index-category table .forum-main .forum-meta-tooltip .tooltip-inner{max-width:400px;text-align:left;}.index-category table .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats{color:#999999;font-size:10.5px;}.index-category table .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats strong{color:#ffffff;}
-.index-category table .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats span{margin-right:14px;}
-.index-category table .forum-main .forum-meta-tooltip .tooltip-inner .forum-description{clear:both;margin:0px;margin-bottom:7px;padding:0px;color:#eeeeee;font-size:14px;}
-.index-category.index-category-important caption{background-color:#cf402e;border:1px solid #a53325;color:#ffffff;text-shadow:0px 1px 0px #672017;}.index-category.index-category-important caption small{color:#280c09;text-shadow:none;}
-.index-category.index-category-inverse caption{background-color:#333333;border:1px solid #1a1a1a;color:#eeeeee;text-shadow:0px 1px 0px #000000;}.index-category.index-category-inverse caption small{color:#b3b3b3;text-shadow:none;}
-.index-category.index-category-info caption{background-color:#3c85a3;border:1px solid #2e677e;color:#ffffff;text-shadow:0px 1px 0px #1a3946;}.index-category.index-category-info caption small{color:#1a3946;text-shadow:none;}
+.index-category{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.index-category .header{background-color:#fbfbfb;border:1px solid #d5d5d5;border-radius:2px 2px 0px 0px;margin:-1px;margin-bottom:0px;padding:3.966666666666667px 9.9px;}.index-category .header h2{margin:0px;padding:0px;color:#333333;font-size:11.9px;font-weight:bold;line-height:20px;text-align:left;}.index-category .header h2 small{margin-left:7px;color:#999999;font-size:11.9px;}
+.index-category .forum{border-bottom:1px solid #d5d5d5;height:21px;overflow:visible;padding:14.75px 9.9px;}.index-category .forum.last{border-bottom:none;}
+.index-category .forum .forum-icon{float:left;}.index-category .forum .forum-icon .forum-icon-wrap{background-color:#555555;border:1px solid #3b3b3b;border-radius:3px;padding:1px 4px;position:relative;bottom:2px;}.index-category .forum .forum-icon .forum-icon-wrap.forum-icon-new{background-color:#cf402e;border:1px solid #a53325;}
+.index-category .forum .forum-icon .forum-icon-wrap.forum-icon-redirect{background-color:#9466c6;border:1px solid #7a43b6;}
+.index-category .forum .forum-main{margin-left:34px;}.index-category .forum .forum-main h3{float:left;margin:0px;padding:0px;font-size:17.5px;font-weight:normal;line-height:20px;}.index-category .forum .forum-main h3 a:link,.index-category .forum .forum-main h3 a:visited{color:#333333;}
+.index-category .forum .forum-main .dropdown{float:right;right:14px;}.index-category .forum .forum-main .dropdown .subforum:link,.index-category .forum .forum-main .dropdown .subforum:visited{color:#999999;font-weight:bold;}
+.index-category .forum .forum-main .dropdown .subforum:hover,.index-category .forum .forum-main .dropdown .subforum:active{color:#333333;}
+.index-category .forum .forum-main .dropdown .dropdown-toggle{padding:4px 8px;opacity:0.6;filter:alpha(opacity=60);color:#333333;font-weight:bold;}.index-category .forum .forum-main .dropdown .dropdown-toggle:hover,.index-category .forum .forum-main .dropdown .dropdown-toggle:active,.index-category .forum .forum-main .dropdown .dropdown-toggle:focus{opacity:1;filter:alpha(opacity=100);text-decoration:none;}
+.index-category .forum .forum-main .dropdown.open .dropdown-toggle{background-color:#eeeeee;border-radius:3px 3px 0px 0px;opacity:1;filter:alpha(opacity=100);padding-bottom:6px;text-decoration:none;}
+.index-category .forum .forum-main .dropdown .dropdown-menu{background:none;border:none;box-shadow:none;}.index-category .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow{border-radius:3px;-webkit-box-shadow:0px 0px 3px #999999;-moz-box-shadow:0px 0px 3px #999999;box-shadow:0px 0px 3px #999999;width:256px;position:relative;right:0px;top:-4px;}.index-category .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul{background-color:#fbfbfb;border-radius:3px;margin:0px;padding:0px;}.index-category .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li{margin:0px;padding:0px;list-style:none;}.index-category .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a{border-bottom:1px dotted #d5d5d5;display:block;opacity:0.7;filter:alpha(opacity=70);padding:6px 8px;color:#333333;text-decoration:none;}.index-category .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:hover,.index-category .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:active{opacity:1;filter:alpha(opacity=100);}
+.index-category .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li:last-child a{border-bottom:none;}
+.index-category .forum .forum-main .forum-details{border-left:1px dotted #e0e0e0;float:right;margin:-10px 0px;margin-top:-8.1px;padding-left:14px;height:35px;width:220px;}.index-category .forum .forum-main .forum-details .thread-name a:link,.index-category .forum .forum-main .forum-details .thread-name a:active,.index-category .forum .forum-main .forum-details .thread-name a:visited,.index-category .forum .forum-main .forum-details .thread-name a:hover{margin-bottom:1px;color:#333333;font-size:11.9px;font-weight:bold;}
+.index-category .forum .forum-main .forum-details .muted{font-size:10.5px;line-height:10.5px;}.index-category .forum .forum-main .forum-details .muted .last-poster,.index-category .forum .forum-main .forum-details .muted a:link,.index-category .forum .forum-main .forum-details .muted a:active,.index-category .forum .forum-main .forum-details .muted a:visited,.index-category .forum .forum-main .forum-details .muted a:hover{color:#555555;}
+.index-category .forum .forum-main .forum-details em{position:relative;top:7.5px;color:#999999;}.index-category .forum .forum-main .forum-details em strong{color:#333333;font-weight:normal;}
+.index-category .forum .forum-main .forum-meta-tooltip .tooltip-inner{max-width:400px;text-align:left;}.index-category .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats{color:#999999;font-size:10.5px;}.index-category .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats strong{color:#ffffff;}
+.index-category .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats span{margin-right:14px;}
+.index-category .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-description{clear:both;margin:0px;margin-bottom:7px;padding:0px;color:#eeeeee;font-size:14px;}
+.index-category.index-category-important .header{background-color:#cf402e;border:1px solid #a53325;}.index-category.index-category-important .header h2{color:#ffffff;text-shadow:0px 1px 0px #672017;}.index-category.index-category-important .header h2 small{color:#280c09;text-shadow:none;}
+.index-category.index-category-inverse .header{background-color:#333333;border:1px solid #1a1a1a;}.index-category.index-category-inverse .header h2{color:#eeeeee;text-shadow:0px 1px 0px #000000;}.index-category.index-category-inverse .header h2 small{color:#b3b3b3;text-shadow:none;}
+.index-category.index-category-info .header{background-color:#3c85a3;border:1px solid #2e677e;}.index-category.index-category-info .header h2{color:#ffffff;text-shadow:0px 1px 0px #1a3946;}.index-category.index-category-info .header h2 small{color:#1a3946;text-shadow:none;}
 .index-forums-read-all{margin:0px;padding:0px;}.index-forums-read-all .btn-link{margin:0px;opacity:0.5;filter:alpha(opacity=50);padding:0px;color:#333333;font-weight:bold;}.index-forums-read-all .btn-link:active,.index-forums-read-all .btn-link:hover{opacity:0.9;filter:alpha(opacity=90);}
 .index-forums-read-all{margin:0px;padding:0px;}.index-forums-read-all .btn-link{margin:0px;opacity:0.5;filter:alpha(opacity=50);padding:0px;color:#333333;font-weight:bold;}.index-forums-read-all .btn-link:active,.index-forums-read-all .btn-link:hover{opacity:0.9;filter:alpha(opacity=90);}
-.index-ranks-list h3{margin:0px;padding:0px;color:#999999;font-size:17.5px;font-weight:bold;}
+.index-ranks-list h3{margin:0px;padding:0px;color:#999999;font-size:17.5px;font-weight:bold;}.index-ranks-list h3 a:link,.index-ranks-list h3 a:active,.index-ranks-list h3 a:visited,.index-ranks-list h3 a:hover{color:#999999;font-size:17.5px;text-decoration:none;}
 .index-ranks-list ul{background-color:#ffffff;border:1px solid #e2e2e2;border-radius:3px;margin:0px;margin-bottom:20px;padding:0px;}.index-ranks-list ul li{border-bottom:1px dotted #e2e2e2;margin:0px;padding:6px 8px;font-weight:bold;}.index-ranks-list ul li img{background-color:#ffffff;border-radius:2px;width:28px;height:28px;}
 .index-ranks-list ul{background-color:#ffffff;border:1px solid #e2e2e2;border-radius:3px;margin:0px;margin-bottom:20px;padding:0px;}.index-ranks-list ul li{border-bottom:1px dotted #e2e2e2;margin:0px;padding:6px 8px;font-weight:bold;}.index-ranks-list ul li img{background-color:#ffffff;border-radius:2px;width:28px;height:28px;}
-.index-ranks-list ul li a:link,.index-ranks-list ul li a:active,.index-ranks-list ul li a:visited,.index-ranks-list ul li a:hover{position:relative;top:1.75px;margin:0px 4px;color:#333333;font-size:17.5px;}
+.index-ranks-list ul li .user-name:link,.index-ranks-list ul li .user-name:active,.index-ranks-list ul li .user-name:visited,.index-ranks-list ul li .user-name:hover{position:relative;top:1.75px;margin:0px 4px;color:#333333;font-size:17.5px;}
 .index-ranks-list ul li .label{float:right;position:relative;top:4.5px;}
 .index-ranks-list ul li .label{float:right;position:relative;top:4.5px;}
 .index-ranks-list ul li:last-child{border-bottom:none;}
 .index-ranks-list ul li:last-child{border-bottom:none;}
 .index-popular-threads h3{margin:0px;margin-bottom:-10px;padding:0px;color:#999999;font-size:17.5px;font-weight:bold;}
 .index-popular-threads h3{margin:0px;margin-bottom:-10px;padding:0px;color:#999999;font-size:17.5px;font-weight:bold;}
@@ -1004,24 +1024,20 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .usercp-avatar-select .usercp-avatar-gallery .usercp-avatar-select-form .usercp-btn-avatar:active img,.usercp-avatar-select .usercp-avatar-gallery .usercp-avatar-select-form .usercp-btn-avatar:hover img{border-color:#0088cc;-webkit-box-shadow:0px 0px 3px #0088cc;-moz-box-shadow:0px 0px 3px #0088cc;box-shadow:0px 0px 3px #0088cc;}
 .usercp-avatar-select .usercp-avatar-gallery .usercp-avatar-select-form .usercp-btn-avatar:active img,.usercp-avatar-select .usercp-avatar-gallery .usercp-avatar-select-form .usercp-btn-avatar:hover img{border-color:#0088cc;-webkit-box-shadow:0px 0px 3px #0088cc;-moz-box-shadow:0px 0px 3px #0088cc;box-shadow:0px 0px 3px #0088cc;}
 .usercp-avatar-crop .avatar-crop-preview{border-radius:5px;float:left;width:43.75px;height:43.75px;margin-right:14px;overflow:hidden;}
 .usercp-avatar-crop .avatar-crop-preview{border-radius:5px;float:left;width:43.75px;height:43.75px;margin-right:14px;overflow:hidden;}
 .usercp-avatar-crop .avatar-crop-target img{background-color:#ffffff;}
 .usercp-avatar-crop .avatar-crop-target img{background-color:#ffffff;}
-.forum-map-category{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.forum-map-category table{margin:0px;}.forum-map-category table caption{background-color:#fbfbfb;border:1px solid #d5d5d5;border-radius:2px 2px 0px 0px;margin:-1px;padding:3.966666666666667px 9.9px;color:#333333;font-size:11.9px;font-weight:bold;text-align:left;}.forum-map-category table caption small{margin-left:7px;color:#999999;font-size:11.9px;}
-.forum-map-category table .forum-map-forum h3,.forum-map-category table .forum-map-subforum h3{margin:0px;padding:0px;font-size:14px;line-height:20px;}.forum-map-category table .forum-map-forum h3 a:link,.forum-map-category table .forum-map-subforum h3 a:link,.forum-map-category table .forum-map-forum h3 a:visited,.forum-map-category table .forum-map-subforum h3 a:visited{color:#555555;}
-.forum-map-category table .forum-map-forum h3 a:active,.forum-map-category table .forum-map-subforum h3 a:active,.forum-map-category table .forum-map-forum h3 a:hover,.forum-map-category table .forum-map-subforum h3 a:hover{color:#333333;}
-.forum-map-category table .forum-map-forum .forum-details,.forum-map-category table .forum-map-subforum .forum-details{float:right;margin-top:-19px;color:#999999;font-size:11.9px;}.forum-map-category table .forum-map-forum .forum-details strong,.forum-map-category table .forum-map-subforum .forum-details strong,.forum-map-category table .forum-map-forum .forum-details a,.forum-map-category table .forum-map-subforum .forum-details a{color:#555555;font-weight:normal;}
-.forum-map-category table .forum-map-forum .forum-details a:hover,.forum-map-category table .forum-map-subforum .forum-details a:hover,.forum-map-category table .forum-map-forum .forum-details a:active,.forum-map-category table .forum-map-subforum .forum-details a:active{color:#333333;}
-.forum-map-category table .forum-map-forum .forum-details strong.stat-increment,.forum-map-category table .forum-map-subforum .forum-details strong.stat-increment{color:#46a546;}
-.forum-map-category table .forum-map-subforum{padding-left:15px;}.forum-map-category table .forum-map-subforum span.tree-t,.forum-map-category table .forum-map-subforum span.tree-l,.forum-map-category table .forum-map-subforum span.tree-s,.forum-map-category table .forum-map-subforum span.tree-i{display:inline-block;height:20px;width:10px;}
-.forum-map-category table .forum-map-subforum span.tree-t{border-left:1px solid #999999;margin-right:2px;}.forum-map-category table .forum-map-subforum span.tree-t span{border-top:1px solid #999999;display:inline-block;height:1px;width:100%;margin-bottom:3px;}
-.forum-map-category table .forum-map-subforum span.tree-l{margin-right:4px;}.forum-map-category table .forum-map-subforum span.tree-l span{border-left:1px solid #999999;border-bottom:1px solid #999999;display:inline-block;height:10px;width:100%;margin-bottom:3px;}
-.forum-map-category table .forum-map-subforum span.tree-i{border-left:1px solid #999999;position:relative;top:5px;margin-top:-5px;margin-right:4px;}
-.forum-map-category table .forum-map-subforum span.tree-s{height:1px;width:12px;margin-right:4px;}
-.forum-map-category.forum-map-category-important caption{background-color:#cf402e;border:1px solid #a53325;color:#ffffff;text-shadow:0px 1px 0px #672017;}.forum-map-category.forum-map-category-important caption small{color:#280c09;text-shadow:none;}
-.forum-map-category.forum-map-category-inverse caption{background-color:#333333;border:1px solid #1a1a1a;color:#eeeeee;text-shadow:0px 1px 0px #000000;}.forum-map-category.forum-map-category-inverse caption small{color:#b3b3b3;text-shadow:none;}
-.forum-map-category.forum-map-category-info caption{background-color:#3c85a3;border:1px solid #2e677e;color:#ffffff;text-shadow:0px 1px 0px #1a3946;}.forum-map-category.forum-map-category-info caption small{color:#1a3946;text-shadow:none;}
-.watched-threads table .watched-thread-flags{overflow:auto;}.watched-threads table .watched-thread-flags form{float:right;margin:0px;padding:0px;}.watched-threads table .watched-thread-flags form .btn{float:right;padding:3px 5px;padding-bottom:0px;margin-right:16px;}
-.watched-threads table .thread-replies{color:#999999;text-align:right;}
-.watched-threads table .thread-forum a:link,.watched-threads table .thread-forum a:visited{color:#555555;font-weight:bold;}
-.watched-threads table .thread-forum a:active,.watched-threads table .thread-forum a:hover{color:#333333;}
+.forum-map-category{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.forum-map-category .header{background-color:#fbfbfb;border:1px solid #d5d5d5;border-radius:2px 2px 0px 0px;margin:-1px;margin-bottom:0px;padding:3.966666666666667px 9.9px;}.forum-map-category .header h2{margin:0px;padding:0px;color:#333333;font-size:11.9px;font-weight:bold;line-height:20px;text-align:left;}.forum-map-category .header h2 small{margin-left:7px;color:#999999;font-size:11.9px;}
+.forum-map-category .forum-map-forum,.forum-map-category .forum-map-subforum{border-bottom:1px solid #d5d5d5;overflow:auto;padding:5.95px 9.9px;}.forum-map-category .forum-map-forum h3,.forum-map-category .forum-map-subforum h3{margin:0px;padding:0px;font-size:14px;line-height:20px;}.forum-map-category .forum-map-forum h3 a:link,.forum-map-category .forum-map-subforum h3 a:link,.forum-map-category .forum-map-forum h3 a:visited,.forum-map-category .forum-map-subforum h3 a:visited{color:#555555;}
+.forum-map-category .forum-map-forum h3 a:active,.forum-map-category .forum-map-subforum h3 a:active,.forum-map-category .forum-map-forum h3 a:hover,.forum-map-category .forum-map-subforum h3 a:hover{color:#333333;}
+.forum-map-category .forum-map-subforum{padding-left:15px;}.forum-map-category .forum-map-subforum span.tree-t,.forum-map-category .forum-map-subforum span.tree-l,.forum-map-category .forum-map-subforum span.tree-s,.forum-map-category .forum-map-subforum span.tree-i{display:inline-block;height:20px;width:10px;}
+.forum-map-category .forum-map-subforum span.tree-t{border-left:1px solid #999999;margin-right:2px;}.forum-map-category .forum-map-subforum span.tree-t span{border-top:1px solid #999999;display:inline-block;height:1px;width:100%;margin-bottom:3px;}
+.forum-map-category .forum-map-subforum span.tree-l{margin-right:4px;}.forum-map-category .forum-map-subforum span.tree-l span{border-left:1px solid #999999;border-bottom:1px solid #999999;display:inline-block;height:10px;width:100%;margin-bottom:3px;}
+.forum-map-category .forum-map-subforum span.tree-i{border-left:1px solid #999999;position:relative;top:5px;margin-top:-5px;margin-right:4px;}
+.forum-map-category .forum-map-subforum span.tree-s{height:1px;width:12px;margin-right:4px;}
+.forum-map-category>div:last-child{border-bottom:none;}
+.forum-map-category.forum-map-category-important .header{background-color:#cf402e;border:1px solid #a53325;}.forum-map-category.forum-map-category-important .header h2{color:#ffffff;text-shadow:0px 1px 0px #672017;}.forum-map-category.forum-map-category-important .header h2 small{color:#280c09;text-shadow:none;}
+.forum-map-category.forum-map-category-inverse .header{background-color:#333333;border:1px solid #1a1a1a;}.forum-map-category.forum-map-category-inverse .header h2{color:#eeeeee;text-shadow:0px 1px 0px #000000;}.forum-map-category.forum-map-category-inverse .header h2 small{color:#b3b3b3;text-shadow:none;}
+.forum-map-category.forum-map-category-info .header{background-color:#3c85a3;border:1px solid #2e677e;}.forum-map-category.forum-map-category-info .header h2{color:#ffffff;text-shadow:0px 1px 0px #1a3946;}.forum-map-category.forum-map-category-info .header h2 small{color:#1a3946;text-shadow:none;}
+.watched-threads .thread-last-reply{border-left:none !important;padding-left:0px !important;}
+.watched-threads .thread-options{float:right;overflow:auto;position:relative;top:8px;}.watched-threads .thread-options form{display:inline-block;float:left;margin:0px;padding:0px;overflow:auto;}.watched-threads .thread-options form .btn{float:right;padding:3px 5px;padding-bottom:0px;margin-left:16px;}
 .user-alerts td{vertical-align:middle;}.user-alerts td.alert-icon .label{background-color:#555555;border:1px solid #2f2f2f;border-radius:3px;padding:4px;padding-top:3px;}.user-alerts td.alert-icon .label i{background-image:url("../img/glyphicons-halflings-white.png");}
 .user-alerts td{vertical-align:middle;}.user-alerts td.alert-icon .label{background-color:#555555;border:1px solid #2f2f2f;border-radius:3px;padding:4px;padding-top:3px;}.user-alerts td.alert-icon .label i{background-image:url("../img/glyphicons-halflings-white.png");}
 .user-alerts td.alert-icon .label.label-warning{background-color:#cf402e;border:1px solid #902d20;}
 .user-alerts td.alert-icon .label.label-warning{background-color:#cf402e;border:1px solid #902d20;}
 .user-alerts td.alert-message{color:#555555;font-size:16.8px;}.user-alerts td.alert-message a:link,.user-alerts td.alert-message a:visited{color:#333333;font-weight:bold;}
 .user-alerts td.alert-message{color:#555555;font-size:16.8px;}.user-alerts td.alert-message a:link,.user-alerts td.alert-message a:visited{color:#333333;font-weight:bold;}
@@ -1030,24 +1046,26 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .news-feed .media .media-body{margin-left:66px;}.news-feed .media .media-body .post-preview:link,.news-feed .media .media-body .post-preview:active,.news-feed .media .media-body .post-preview:visited,.news-feed .media .media-body .post-preview:hover{display:block;margin-top:7px;color:#333333;font-size:16.8px;text-decoration:none;}
 .news-feed .media .media-body{margin-left:66px;}.news-feed .media .media-body .post-preview:link,.news-feed .media .media-body .post-preview:active,.news-feed .media .media-body .post-preview:visited,.news-feed .media .media-body .post-preview:hover{display:block;margin-top:7px;color:#333333;font-size:16.8px;text-decoration:none;}
 .news-feed .media .media-body .media-footer{margin:0px;color:#999999;font-size:10.5px;font-weight:normal;}.news-feed .media .media-body .media-footer a{color:#555555;}
 .news-feed .media .media-body .media-footer{margin:0px;color:#999999;font-size:10.5px;font-weight:normal;}.news-feed .media .media-body .media-footer a{color:#555555;}
 .news-feed hr{border:none;border-top:1px solid #eeeeee;margin:20px 0px;}
 .news-feed hr{border:none;border-top:1px solid #eeeeee;margin:20px 0px;}
-.category-forums-list{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.category-forums-list table{margin:0px;}.category-forums-list table tr:first-child td{border-top:none;}
-.category-forums-list table td{padding:14.75px 9.9px;padding-bottom:15.75px;}
-.category-forums-list table .forum-icon{padding-right:2.95px;width:1%;}.category-forums-list table .forum-icon .forum-icon-wrap{background-color:#555555;border:1px solid #3b3b3b;border-radius:3px;padding:3px 4px;}.category-forums-list table .forum-icon .forum-icon-wrap.forum-icon-new{background-color:#cf402e;border:1px solid #a53325;}
-.category-forums-list table .forum-icon .forum-icon-wrap.forum-icon-redirect{background-color:#9466c6;border:1px solid #7a43b6;}
-.category-forums-list table .forum-main h3{float:left;margin:0px;padding:0px;font-size:17.5px;font-weight:normal;line-height:20px;}.category-forums-list table .forum-main h3 a:link,.category-forums-list table .forum-main h3 a:visited{color:#333333;}
-.category-forums-list table .forum-main .dropdown{float:right;right:14px;}.category-forums-list table .forum-main .dropdown .dropdown-toggle{padding:4px 8px;opacity:0.6;filter:alpha(opacity=60);color:#333333;font-weight:bold;}.category-forums-list table .forum-main .dropdown .dropdown-toggle:hover,.category-forums-list table .forum-main .dropdown .dropdown-toggle:active,.category-forums-list table .forum-main .dropdown .dropdown-toggle:focus{opacity:1;filter:alpha(opacity=100);text-decoration:none;}
-.category-forums-list table .forum-main .dropdown.open .dropdown-toggle{background-color:#eeeeee;border-radius:3px 3px 0px 0px;opacity:1;filter:alpha(opacity=100);padding-bottom:6px;text-decoration:none;}
-.category-forums-list table .forum-main .dropdown .dropdown-menu{background:none;border:none;box-shadow:none;}.category-forums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow{border-radius:3px;-webkit-box-shadow:0px 0px 3px #999999;-moz-box-shadow:0px 0px 3px #999999;box-shadow:0px 0px 3px #999999;width:256px;position:relative;right:0px;top:-4px;}.category-forums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul{background-color:#fbfbfb;border-radius:3px;margin:0px;padding:0px;}.category-forums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li{margin:0px;padding:0px;list-style:none;}.category-forums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a{border-bottom:1px dotted #d5d5d5;display:block;opacity:0.7;filter:alpha(opacity=70);padding:6px 8px;color:#333333;text-decoration:none;}.category-forums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:hover,.category-forums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:active{opacity:1;filter:alpha(opacity=100);}
-.category-forums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li:last-child a{border-bottom:none;}
-.category-forums-list table .forum-main .forum-details{border-left:1px dotted #e0e0e0;float:right;margin:-10px 0px;margin-top:-8.1px;padding-left:14px;height:35px;width:220px;}.category-forums-list table .forum-main .forum-details .thread-name a:link,.category-forums-list table .forum-main .forum-details .thread-name a:active,.category-forums-list table .forum-main .forum-details .thread-name a:visited,.category-forums-list table .forum-main .forum-details .thread-name a:hover{margin-bottom:1px;color:#333333;font-size:11.9px;font-weight:bold;}
-.category-forums-list table .forum-main .forum-details .muted{font-size:10.5px;line-height:10.5px;}.category-forums-list table .forum-main .forum-details .muted .last-poster,.category-forums-list table .forum-main .forum-details .muted a:link,.category-forums-list table .forum-main .forum-details .muted a:active,.category-forums-list table .forum-main .forum-details .muted a:visited,.category-forums-list table .forum-main .forum-details .muted a:hover{color:#555555;}
-.category-forums-list table .forum-main .forum-details em{position:relative;top:7.5px;color:#999999;}.category-forums-list table .forum-main .forum-details em strong{color:#333333;font-weight:normal;}
-.category-forums-list table .forum-main .forum-meta-tooltip .tooltip-inner{max-width:400px;text-align:left;}.category-forums-list table .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats{color:#999999;font-size:10.5px;}.category-forums-list table .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats strong{color:#ffffff;}
-.category-forums-list table .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats span{margin-right:14px;}
-.category-forums-list table .forum-main .forum-meta-tooltip .tooltip-inner .forum-description{clear:both;margin:0px;margin-bottom:7px;padding:0px;color:#eeeeee;font-size:14px;}
-.category-forums-list.category-forums-important{border:1px solid #902d20;-webkit-box-shadow:0px 0px 0px 3px #cf402e;-moz-box-shadow:0px 0px 0px 3px #cf402e;box-shadow:0px 0px 0px 3px #cf402e;}
-.category-forums-list.category-forums-inverse{border:1px solid #333333;-webkit-box-shadow:0px 0px 0px 3px #555555;-moz-box-shadow:0px 0px 0px 3px #555555;box-shadow:0px 0px 0px 3px #555555;}
-.category-forums-list.category-forums-info{border:1px solid #27576b;-webkit-box-shadow:0px 0px 0px 3px #3c85a3;-moz-box-shadow:0px 0px 0px 3px #3c85a3;box-shadow:0px 0px 0px 3px #3c85a3;}
+.category-forums-list{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.category-forums-list .header{background-color:#fbfbfb;border:1px solid #d5d5d5;border-radius:2px 2px 0px 0px;margin:-1px;margin-bottom:0px;padding:3.966666666666667px 9.9px;}.category-forums-list .header h2{margin:0px;padding:0px;color:#333333;font-size:11.9px;font-weight:bold;line-height:20px;text-align:left;}.category-forums-list .header h2 small{margin-left:7px;color:#999999;font-size:11.9px;}
+.category-forums-list .forum{border-bottom:1px solid #d5d5d5;height:21px;overflow:visible;padding:14.75px 9.9px;}.category-forums-list .forum.last{border-bottom:none;}
+.category-forums-list .forum .forum-icon{float:left;}.category-forums-list .forum .forum-icon .forum-icon-wrap{background-color:#555555;border:1px solid #3b3b3b;border-radius:3px;padding:1px 4px;position:relative;bottom:2px;}.category-forums-list .forum .forum-icon .forum-icon-wrap.forum-icon-new{background-color:#cf402e;border:1px solid #a53325;}
+.category-forums-list .forum .forum-icon .forum-icon-wrap.forum-icon-redirect{background-color:#9466c6;border:1px solid #7a43b6;}
+.category-forums-list .forum .forum-main{margin-left:34px;}.category-forums-list .forum .forum-main h3{float:left;margin:0px;padding:0px;font-size:17.5px;font-weight:normal;line-height:20px;}.category-forums-list .forum .forum-main h3 a:link,.category-forums-list .forum .forum-main h3 a:visited{color:#333333;}
+.category-forums-list .forum .forum-main .dropdown{float:right;right:14px;}.category-forums-list .forum .forum-main .dropdown .subforum:link,.category-forums-list .forum .forum-main .dropdown .subforum:visited{color:#999999;font-weight:bold;}
+.category-forums-list .forum .forum-main .dropdown .subforum:hover,.category-forums-list .forum .forum-main .dropdown .subforum:active{color:#333333;}
+.category-forums-list .forum .forum-main .dropdown .dropdown-toggle{padding:4px 8px;opacity:0.6;filter:alpha(opacity=60);color:#333333;font-weight:bold;}.category-forums-list .forum .forum-main .dropdown .dropdown-toggle:hover,.category-forums-list .forum .forum-main .dropdown .dropdown-toggle:active,.category-forums-list .forum .forum-main .dropdown .dropdown-toggle:focus{opacity:1;filter:alpha(opacity=100);text-decoration:none;}
+.category-forums-list .forum .forum-main .dropdown.open .dropdown-toggle{background-color:#eeeeee;border-radius:3px 3px 0px 0px;opacity:1;filter:alpha(opacity=100);padding-bottom:6px;text-decoration:none;}
+.category-forums-list .forum .forum-main .dropdown .dropdown-menu{background:none;border:none;box-shadow:none;}.category-forums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow{border-radius:3px;-webkit-box-shadow:0px 0px 3px #999999;-moz-box-shadow:0px 0px 3px #999999;box-shadow:0px 0px 3px #999999;width:256px;position:relative;right:0px;top:-4px;}.category-forums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul{background-color:#fbfbfb;border-radius:3px;margin:0px;padding:0px;}.category-forums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li{margin:0px;padding:0px;list-style:none;}.category-forums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a{border-bottom:1px dotted #d5d5d5;display:block;opacity:0.7;filter:alpha(opacity=70);padding:6px 8px;color:#333333;text-decoration:none;}.category-forums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:hover,.category-forums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:active{opacity:1;filter:alpha(opacity=100);}
+.category-forums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li:last-child a{border-bottom:none;}
+.category-forums-list .forum .forum-main .forum-details{border-left:1px dotted #e0e0e0;float:right;margin:-10px 0px;margin-top:-8.1px;padding-left:14px;height:35px;width:220px;}.category-forums-list .forum .forum-main .forum-details .thread-name a:link,.category-forums-list .forum .forum-main .forum-details .thread-name a:active,.category-forums-list .forum .forum-main .forum-details .thread-name a:visited,.category-forums-list .forum .forum-main .forum-details .thread-name a:hover{margin-bottom:1px;color:#333333;font-size:11.9px;font-weight:bold;}
+.category-forums-list .forum .forum-main .forum-details .muted{font-size:10.5px;line-height:10.5px;}.category-forums-list .forum .forum-main .forum-details .muted .last-poster,.category-forums-list .forum .forum-main .forum-details .muted a:link,.category-forums-list .forum .forum-main .forum-details .muted a:active,.category-forums-list .forum .forum-main .forum-details .muted a:visited,.category-forums-list .forum .forum-main .forum-details .muted a:hover{color:#555555;}
+.category-forums-list .forum .forum-main .forum-details em{position:relative;top:7.5px;color:#999999;}.category-forums-list .forum .forum-main .forum-details em strong{color:#333333;font-weight:normal;}
+.category-forums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner{max-width:400px;text-align:left;}.category-forums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats{color:#999999;font-size:10.5px;}.category-forums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats strong{color:#ffffff;}
+.category-forums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats span{margin-right:14px;}
+.category-forums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-description{clear:both;margin:0px;margin-bottom:7px;padding:0px;color:#eeeeee;font-size:14px;}
+.category-forums-list.category-forums-important{border-color:#cf402e;-webkit-box-shadow:0px 0px 0px 3px #a53325;-moz-box-shadow:0px 0px 0px 3px #a53325;box-shadow:0px 0px 0px 3px #a53325;}
+.category-forums-list.category-forums-inverse{border-color:#333333;-webkit-box-shadow:0px 0px 0px 3px #1a1a1a;-moz-box-shadow:0px 0px 0px 3px #1a1a1a;box-shadow:0px 0px 0px 3px #1a1a1a;}
+.category-forums-list.category-forums-info{border-color:#3c85a3;-webkit-box-shadow:0px 0px 0px 3px #2e677e;-moz-box-shadow:0px 0px 0px 3px #2e677e;box-shadow:0px 0px 0px 3px #2e677e;}
 .profiles-list .user-cell{overflow:auto;}.profiles-list .user-cell .user-avatar{float:left;}.profiles-list .user-cell .user-avatar img{border-radius:3px;width:36px;height:36px;}
 .profiles-list .user-cell{overflow:auto;}.profiles-list .user-cell .user-avatar{float:left;}.profiles-list .user-cell .user-avatar img{border-radius:3px;width:36px;height:36px;}
 .profiles-list .user-cell .user-name:link,.profiles-list .user-cell .user-name:active,.profiles-list .user-cell .user-name:visited,.profiles-list .user-cell .user-name:hover{display:block;margin:0px;margin-left:43px;margin-top:9.1px;color:#333333;font-size:23.8px;}
 .profiles-list .user-cell .user-name:link,.profiles-list .user-cell .user-name:active,.profiles-list .user-cell .user-name:visited,.profiles-list .user-cell .user-name:hover{display:block;margin:0px;margin-left:43px;margin-top:9.1px;color:#333333;font-size:23.8px;}
 .user-profile .user-profile-header .header-avatar{border-radius:3px;float:left;width:80px;height:80px;margin-top:-8px;}
 .user-profile .user-profile-header .header-avatar{border-radius:3px;float:left;width:80px;height:80px;margin-top:-8px;}
@@ -1059,37 +1077,53 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .user-profile .content-list .media{overflow:auto;}.user-profile .content-list .media .media-object{border-radius:3px;width:52px;height:52px;}
 .user-profile .content-list .media{overflow:auto;}.user-profile .content-list .media .media-object{border-radius:3px;width:52px;height:52px;}
 .user-profile .content-list .media .media-body{margin-left:66px;}.user-profile .content-list .media .media-body .post-preview:link,.user-profile .content-list .media .media-body .post-preview:active,.user-profile .content-list .media .media-body .post-preview:visited,.user-profile .content-list .media .media-body .post-preview:hover{display:block;margin-top:7px;color:#333333;font-size:16.8px;text-decoration:none;}
 .user-profile .content-list .media .media-body{margin-left:66px;}.user-profile .content-list .media .media-body .post-preview:link,.user-profile .content-list .media .media-body .post-preview:active,.user-profile .content-list .media .media-body .post-preview:visited,.user-profile .content-list .media .media-body .post-preview:hover{display:block;margin-top:7px;color:#333333;font-size:16.8px;text-decoration:none;}
 .user-profile .content-list .media .media-body .media-footer{margin:0px;color:#999999;font-size:10.5px;font-weight:normal;}.user-profile .content-list .media .media-body .media-footer a{color:#555555;}
 .user-profile .content-list .media .media-body .media-footer{margin:0px;color:#999999;font-size:10.5px;font-weight:normal;}.user-profile .content-list .media .media-body .media-footer a{color:#555555;}
-.forum-subforums-list{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.forum-subforums-list table{margin:0px;}.forum-subforums-list table caption{background-color:#fbfbfb;border:1px solid #d5d5d5;border-radius:2px 2px 0px 0px;margin:-1px;padding:3.966666666666667px 9.9px;color:#333333;font-size:11.9px;font-weight:bold;text-align:left;}.forum-subforums-list table caption small{margin-left:7px;color:#999999;font-size:11.9px;}
-.forum-subforums-list table td{padding:14.75px 9.9px;padding-bottom:15.75px;}
-.forum-subforums-list table .forum-icon{padding-right:2.95px;width:1%;}.forum-subforums-list table .forum-icon .forum-icon-wrap{background-color:#555555;border:1px solid #3b3b3b;border-radius:3px;padding:3px 4px;}.forum-subforums-list table .forum-icon .forum-icon-wrap.forum-icon-new{background-color:#cf402e;border:1px solid #a53325;}
-.forum-subforums-list table .forum-icon .forum-icon-wrap.forum-icon-redirect{background-color:#9466c6;border:1px solid #7a43b6;}
-.forum-subforums-list table .forum-main h3{float:left;margin:0px;padding:0px;font-size:17.5px;font-weight:normal;line-height:20px;}.forum-subforums-list table .forum-main h3 a:link,.forum-subforums-list table .forum-main h3 a:visited{color:#333333;}
-.forum-subforums-list table .forum-main .dropdown{float:right;right:14px;}.forum-subforums-list table .forum-main .dropdown .dropdown-toggle{padding:4px 8px;opacity:0.6;filter:alpha(opacity=60);color:#333333;font-weight:bold;}.forum-subforums-list table .forum-main .dropdown .dropdown-toggle:hover,.forum-subforums-list table .forum-main .dropdown .dropdown-toggle:active,.forum-subforums-list table .forum-main .dropdown .dropdown-toggle:focus{opacity:1;filter:alpha(opacity=100);text-decoration:none;}
-.forum-subforums-list table .forum-main .dropdown.open .dropdown-toggle{background-color:#eeeeee;border-radius:3px 3px 0px 0px;opacity:1;filter:alpha(opacity=100);padding-bottom:6px;text-decoration:none;}
-.forum-subforums-list table .forum-main .dropdown .dropdown-menu{background:none;border:none;box-shadow:none;}.forum-subforums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow{border-radius:3px;-webkit-box-shadow:0px 0px 3px #999999;-moz-box-shadow:0px 0px 3px #999999;box-shadow:0px 0px 3px #999999;width:256px;position:relative;right:0px;top:-4px;}.forum-subforums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul{background-color:#fbfbfb;border-radius:3px;margin:0px;padding:0px;}.forum-subforums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li{margin:0px;padding:0px;list-style:none;}.forum-subforums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a{border-bottom:1px dotted #d5d5d5;display:block;opacity:0.7;filter:alpha(opacity=70);padding:6px 8px;color:#333333;text-decoration:none;}.forum-subforums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:hover,.forum-subforums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:active{opacity:1;filter:alpha(opacity=100);}
-.forum-subforums-list table .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li:last-child a{border-bottom:none;}
-.forum-subforums-list table .forum-main .forum-details{border-left:1px dotted #e0e0e0;float:right;margin:-10px 0px;margin-top:-8.1px;padding-left:14px;height:35px;width:220px;}.forum-subforums-list table .forum-main .forum-details .thread-name a:link,.forum-subforums-list table .forum-main .forum-details .thread-name a:active,.forum-subforums-list table .forum-main .forum-details .thread-name a:visited,.forum-subforums-list table .forum-main .forum-details .thread-name a:hover{margin-bottom:1px;color:#333333;font-size:11.9px;font-weight:bold;}
-.forum-subforums-list table .forum-main .forum-details .muted{font-size:10.5px;line-height:10.5px;}.forum-subforums-list table .forum-main .forum-details .muted .last-poster,.forum-subforums-list table .forum-main .forum-details .muted a:link,.forum-subforums-list table .forum-main .forum-details .muted a:active,.forum-subforums-list table .forum-main .forum-details .muted a:visited,.forum-subforums-list table .forum-main .forum-details .muted a:hover{color:#555555;}
-.forum-subforums-list table .forum-main .forum-details em{position:relative;top:7.5px;color:#999999;}.forum-subforums-list table .forum-main .forum-details em strong{color:#333333;font-weight:normal;}
-.forum-subforums-list table .forum-main .forum-meta-tooltip .tooltip-inner{max-width:400px;text-align:left;}.forum-subforums-list table .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats{color:#999999;font-size:10.5px;}.forum-subforums-list table .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats strong{color:#ffffff;}
-.forum-subforums-list table .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats span{margin-right:14px;}
-.forum-subforums-list table .forum-main .forum-meta-tooltip .tooltip-inner .forum-description{clear:both;margin:0px;margin-bottom:7px;padding:0px;color:#eeeeee;font-size:14px;}
-.forum-subforums-list.forum-subforums-important caption{background-color:#cf402e;border:1px solid #a53325;color:#ffffff;text-shadow:0px 1px 0px #672017;}.forum-subforums-list.forum-subforums-important caption small{color:#280c09;text-shadow:none;}
-.forum-subforums-list.forum-subforums-inverse caption{background-color:#333333;border:1px solid #1a1a1a;color:#eeeeee;text-shadow:0px 1px 0px #000000;}.forum-subforums-list.forum-subforums-inverse caption small{color:#b3b3b3;text-shadow:none;}
-.forum-subforums-list.forum-subforums-info caption{background-color:#3c85a3;border:1px solid #2e677e;color:#ffffff;text-shadow:0px 1px 0px #1a3946;}.forum-subforums-list.forum-subforums-info caption small{color:#1a3946;text-shadow:none;}
-.forum-threads-list{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.forum-threads-list table{margin:0px;}.forum-threads-list table th{background-color:#fbfbfb;border-bottom:1px solid #eeeeee;padding:2px 10px;color:#999999;font-size:11.9px;}
-.forum-threads-list table td{vertical-align:middle;}.forum-threads-list table td.threads-list-empty{padding:11px 19px;font-size:17.5px;text-align:center;}
-.forum-threads-list table td .thread-icon:link,.forum-threads-list table td .thread-icon:active,.forum-threads-list table td .thread-icon:visited,.forum-threads-list table td .thread-icon:hover{background-color:#555555;border:1px solid #2f2f2f;border-radius:3px;margin-right:7px;padding:3px 4px;}.forum-threads-list table td .thread-icon:link.thread-new,.forum-threads-list table td .thread-icon:active.thread-new,.forum-threads-list table td .thread-icon:visited.thread-new,.forum-threads-list table td .thread-icon:hover.thread-new{background-color:#cf402e;border:1px solid #902d20;}
-.forum-threads-list table td .thread-icon i{background-image:url("../img/glyphicons-halflings-white.png");}
-.forum-threads-list table td .thread-name{color:#333333;font-weight:bold;}
-.forum-threads-list table td .thread-details,.forum-threads-list table td .thread-last-reply{color:#999999;}.forum-threads-list table td .thread-details a:link,.forum-threads-list table td .thread-last-reply a:link,.forum-threads-list table td .thread-details a:visited,.forum-threads-list table td .thread-last-reply a:visited{color:#333333;}
-.forum-threads-list table td .thread-details{font-size:10.5px;}
-.forum-threads-list table td .thread-flags{float:right;margin:0px;opacity:0.8;filter:alpha(opacity=80);padding:0px;}.forum-threads-list table td .thread-flags li{margin-left:3px;float:left;}
-.forum-threads-list table td .thread-rating{background-color:#eeeeee;border-radius:3px;padding:4px;color:#999999;font-size:17.5px;font-weight:bold;text-align:center;}.forum-threads-list table td .thread-rating.thread-rating-negative,.forum-threads-list table td .thread-rating.thread-rating-positive{color:#ffffff;}
-.forum-threads-list table td .thread-rating.thread-rating-negative{background-color:#cf402e;}
-.forum-threads-list table td .thread-rating.thread-rating-positive{background-color:#46a546;}
-.forum-threads-list table th.check-cell,.forum-threads-list table td.check-cell{width:1%;text-align:center;vertical-align:middle;}.forum-threads-list table th.check-cell .checkbox,.forum-threads-list table td.check-cell .checkbox{margin:0px;padding:0px;}.forum-threads-list table th.check-cell .checkbox input,.forum-threads-list table td.check-cell .checkbox input{margin:0px;padding:0px;position:relative;top:3px;}
-.forum-threads-list table th.check-cell input{right:2px;}
+.forum-subforums-list{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.forum-subforums-list .header{background-color:#fbfbfb;border:1px solid #d5d5d5;border-radius:2px 2px 0px 0px;margin:-1px;margin-bottom:0px;padding:3.966666666666667px 9.9px;}.forum-subforums-list .header h2{margin:0px;padding:0px;color:#333333;font-size:11.9px;font-weight:bold;line-height:20px;text-align:left;}.forum-subforums-list .header h2 small{margin-left:7px;color:#999999;font-size:11.9px;}
+.forum-subforums-list .forum{border-bottom:1px solid #d5d5d5;height:21px;overflow:visible;padding:14.75px 9.9px;}.forum-subforums-list .forum.last{border-bottom:none;}
+.forum-subforums-list .forum .forum-icon{float:left;}.forum-subforums-list .forum .forum-icon .forum-icon-wrap{background-color:#555555;border:1px solid #3b3b3b;border-radius:3px;padding:1px 4px;position:relative;bottom:2px;}.forum-subforums-list .forum .forum-icon .forum-icon-wrap.forum-icon-new{background-color:#cf402e;border:1px solid #a53325;}
+.forum-subforums-list .forum .forum-icon .forum-icon-wrap.forum-icon-redirect{background-color:#9466c6;border:1px solid #7a43b6;}
+.forum-subforums-list .forum .forum-main{margin-left:34px;}.forum-subforums-list .forum .forum-main h3{float:left;margin:0px;padding:0px;font-size:17.5px;font-weight:normal;line-height:20px;}.forum-subforums-list .forum .forum-main h3 a:link,.forum-subforums-list .forum .forum-main h3 a:visited{color:#333333;}
+.forum-subforums-list .forum .forum-main .dropdown{float:right;right:14px;}.forum-subforums-list .forum .forum-main .dropdown .subforum:link,.forum-subforums-list .forum .forum-main .dropdown .subforum:visited{color:#999999;font-weight:bold;}
+.forum-subforums-list .forum .forum-main .dropdown .subforum:hover,.forum-subforums-list .forum .forum-main .dropdown .subforum:active{color:#333333;}
+.forum-subforums-list .forum .forum-main .dropdown .dropdown-toggle{padding:4px 8px;opacity:0.6;filter:alpha(opacity=60);color:#333333;font-weight:bold;}.forum-subforums-list .forum .forum-main .dropdown .dropdown-toggle:hover,.forum-subforums-list .forum .forum-main .dropdown .dropdown-toggle:active,.forum-subforums-list .forum .forum-main .dropdown .dropdown-toggle:focus{opacity:1;filter:alpha(opacity=100);text-decoration:none;}
+.forum-subforums-list .forum .forum-main .dropdown.open .dropdown-toggle{background-color:#eeeeee;border-radius:3px 3px 0px 0px;opacity:1;filter:alpha(opacity=100);padding-bottom:6px;text-decoration:none;}
+.forum-subforums-list .forum .forum-main .dropdown .dropdown-menu{background:none;border:none;box-shadow:none;}.forum-subforums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow{border-radius:3px;-webkit-box-shadow:0px 0px 3px #999999;-moz-box-shadow:0px 0px 3px #999999;box-shadow:0px 0px 3px #999999;width:256px;position:relative;right:0px;top:-4px;}.forum-subforums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul{background-color:#fbfbfb;border-radius:3px;margin:0px;padding:0px;}.forum-subforums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li{margin:0px;padding:0px;list-style:none;}.forum-subforums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a{border-bottom:1px dotted #d5d5d5;display:block;opacity:0.7;filter:alpha(opacity=70);padding:6px 8px;color:#333333;text-decoration:none;}.forum-subforums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:hover,.forum-subforums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li a:active{opacity:1;filter:alpha(opacity=100);}
+.forum-subforums-list .forum .forum-main .dropdown .dropdown-menu .dropdown-shadow ul li:last-child a{border-bottom:none;}
+.forum-subforums-list .forum .forum-main .forum-details{border-left:1px dotted #e0e0e0;float:right;margin:-10px 0px;margin-top:-8.1px;padding-left:14px;height:35px;width:220px;}.forum-subforums-list .forum .forum-main .forum-details .thread-name a:link,.forum-subforums-list .forum .forum-main .forum-details .thread-name a:active,.forum-subforums-list .forum .forum-main .forum-details .thread-name a:visited,.forum-subforums-list .forum .forum-main .forum-details .thread-name a:hover{margin-bottom:1px;color:#333333;font-size:11.9px;font-weight:bold;}
+.forum-subforums-list .forum .forum-main .forum-details .muted{font-size:10.5px;line-height:10.5px;}.forum-subforums-list .forum .forum-main .forum-details .muted .last-poster,.forum-subforums-list .forum .forum-main .forum-details .muted a:link,.forum-subforums-list .forum .forum-main .forum-details .muted a:active,.forum-subforums-list .forum .forum-main .forum-details .muted a:visited,.forum-subforums-list .forum .forum-main .forum-details .muted a:hover{color:#555555;}
+.forum-subforums-list .forum .forum-main .forum-details em{position:relative;top:7.5px;color:#999999;}.forum-subforums-list .forum .forum-main .forum-details em strong{color:#333333;font-weight:normal;}
+.forum-subforums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner{max-width:400px;text-align:left;}.forum-subforums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats{color:#999999;font-size:10.5px;}.forum-subforums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats strong{color:#ffffff;}
+.forum-subforums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-stats span{margin-right:14px;}
+.forum-subforums-list .forum .forum-main .forum-meta-tooltip .tooltip-inner .forum-description{clear:both;margin:0px;margin-bottom:7px;padding:0px;color:#eeeeee;font-size:14px;}
+.forum-subforums-list.forum-subforums-important .header{background-color:#cf402e;border:1px solid #a53325;}.forum-subforums-list.forum-subforums-important .header h2{color:#ffffff;text-shadow:0px 1px 0px #672017;}.forum-subforums-list.forum-subforums-important .header h2 small{color:#280c09;text-shadow:none;}
+.forum-subforums-list.forum-subforums-inverse .header{background-color:#333333;border:1px solid #1a1a1a;}.forum-subforums-list.forum-subforums-inverse .header h2{color:#eeeeee;text-shadow:0px 1px 0px #000000;}.forum-subforums-list.forum-subforums-inverse .header h2 small{color:#b3b3b3;text-shadow:none;}
+.forum-subforums-list.forum-subforums-info .header{background-color:#3c85a3;border:1px solid #2e677e;}.forum-subforums-list.forum-subforums-info .header h2{color:#ffffff;text-shadow:0px 1px 0px #1a3946;}.forum-subforums-list.forum-subforums-info .header h2 small{color:#1a3946;text-shadow:none;}
+.forum-threads-list{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.forum-threads-list .header{background-color:#fbfbfb;border:1px solid #d5d5d5;border-radius:2px 2px 0px 0px;margin:-1px;margin-bottom:0px;padding-bottom:1px;overflow:auto;color:#999999;font-weight:bold;font-size:11.9px;}.forum-threads-list .header .row-fluid>div{min-height:auto;padding:2px 10px;}
+.forum-threads-list .header .row-fluid .thread-replies{float:left;width:106px;}
+.forum-threads-list .header .row-fluid .thread-last{float:left;}
+.forum-threads-list .header .check-cell label{margin:0px;}
+.forum-threads-list .thread-row{border-bottom:1px solid #d5d5d5;height:38px;overflow:hidden;padding:9.9px 0px;}.forum-threads-list .thread-row .row-fluid>div{min-height:auto;padding:2px 10px;padding-bottom:0px;}
+.forum-threads-list .thread-row.thread-last{border-bottom:none;}.forum-threads-list .thread-row.thread-last>div{padding-bottom:1px;}
+.forum-threads-list .thread-row .thread-icon{background-color:#555555;border:1px solid #3b3b3b;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:block;float:left;margin:-2px 0px;margin-left:-1px;padding:1px 4px;}.forum-threads-list .thread-row .thread-icon:hover,.forum-threads-list .thread-row .thread-icon:active{opacity:1;filter:alpha(opacity=100);}
+.forum-threads-list .thread-row .thread-icon i{background-image:url("../img/glyphicons-halflings-white.png");}
+.forum-threads-list .thread-row.thread-new .thread-icon{background-color:#cf402e;border:1px solid #a53325;}
+.forum-threads-list .thread-row.thread-new .thread-name{color:#333333 !important;}
+.forum-threads-list .thread-row.threads-list-empty{height:auto;padding:11px 19px;font-size:17.5px;text-align:center;}
+.forum-threads-list .thread-row .thread-name{margin-left:10px;color:#5e5e5e;font-size:16px;font-weight:bold;}
+.forum-threads-list .thread-row .thread-details,.forum-threads-list .thread-row .thread-last-reply{color:#999999;line-height:14px;}.forum-threads-list .thread-row .thread-details a:link,.forum-threads-list .thread-row .thread-last-reply a:link,.forum-threads-list .thread-row .thread-details a:visited,.forum-threads-list .thread-row .thread-last-reply a:visited{color:#333333;}
+.forum-threads-list .thread-row .thread-details{margin-left:34px;font-size:10.5px;}
+.forum-threads-list .thread-row .thread-flags{float:right;margin:0px;position:relative;right:-30px;top:5px;padding:0px;}.forum-threads-list .thread-row .thread-flags li{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:block;float:left;margin-left:3px;padding:2px 5px;}.forum-threads-list .thread-row .thread-flags li.flag-reported{background-color:#f89406;}
+.forum-threads-list .thread-row .thread-flags li.flag-notreviewed{background-color:#7a43b6;}
+.forum-threads-list .thread-row .thread-flags li.flag-announcement{background-color:#049cdb;}
+.forum-threads-list .thread-row .thread-flags li.flag-sticky{background-color:#3c85a3;}
+.forum-threads-list .thread-row .thread-flags li.flag-deleted{background-color:#333333;}
+.forum-threads-list .thread-row .thread-flags li.flag-closed{background-color:#cf402e;}
+.forum-threads-list .thread-row .thread-flags i{background-image:url("../img/glyphicons-halflings-white.png");}
+.forum-threads-list .thread-row .thread-activity{border-left:1px dotted #e0e0e0;position:relative;bottom:3px;}.forum-threads-list .thread-row .thread-activity .thread-last-avatar{float:left;}.forum-threads-list .thread-row .thread-activity .thread-last-avatar img{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin:0px;margin-right:14px;width:40px;height:40px;}
+.forum-threads-list .thread-row .thread-activity .thread-replies{float:left;margin-top:-1px;margin-right:14px;color:#999999;font-size:11.9px;}.forum-threads-list .thread-row .thread-activity .thread-replies .lead{font-size:14px;font-weight:bold;line-height:20px;}
+.forum-threads-list .thread-row .thread-activity .thread-replies a:link,.forum-threads-list .thread-row .thread-activity .thread-replies a:active,.forum-threads-list .thread-row .thread-activity .thread-replies a:visited,.forum-threads-list .thread-row .thread-activity .thread-replies a:hover{color:#555555;}
+.forum-threads-list .thread-row .thread-activity .thread-last-reply{border-left:1px dotted #e0e0e0;padding-left:14px;}
+.forum-threads-list .thread-row .thread-activity .thread-select{background-color:#f2f2f2;display:block;float:right;margin:-12px -11px;margin-left:0px;width:29px;height:61px;}.forum-threads-list .thread-row .thread-activity .thread-select:hover,.forum-threads-list .thread-row .thread-activity .thread-select:focus,.forum-threads-list .thread-row .thread-activity .thread-select:active{background-color:#0088cc;}
+.forum-threads-list .thread-row .thread-activity .thread-select input{margin:0px;position:relative;right:2px;top:26px;}
 .forum-threads-list .threads-actions{background-color:#fbfbfb;border-top:1px solid #d5d5d5;border-radius:0px 0px 2px 2px;overflow:auto;padding:4px;color:#999999;font-size:11.9px;}.forum-threads-list .threads-actions form{margin-bottom:0px;}
 .forum-threads-list .threads-actions{background-color:#fbfbfb;border-top:1px solid #d5d5d5;border-radius:0px 0px 2px 2px;overflow:auto;padding:4px;color:#999999;font-size:11.9px;}.forum-threads-list .threads-actions form{margin-bottom:0px;}
 .forum-threads-extra{overflow:auto;}.forum-threads-extra.extra-top{margin-bottom:20px;}
 .forum-threads-extra{overflow:auto;}.forum-threads-extra.extra-top{margin-bottom:20px;}
 .forum-threads-extra .threads-signin-message{float:right;}.forum-threads-extra .threads-signin-message a:link,.forum-threads-extra .threads-signin-message a:visited{color:#333333;}
 .forum-threads-extra .threads-signin-message{float:right;}.forum-threads-extra .threads-signin-message a:link,.forum-threads-extra .threads-signin-message a:visited{color:#333333;}
@@ -1122,17 +1156,23 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link.post-hate:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link.post-hate:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link.post-hate:focus,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link.post-hate:disabled{color:#cf402e;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link.post-hate:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link.post-hate:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link.post-hate:focus,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link.post-hate:disabled{color:#cf402e;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link:disabled:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link:disabled:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link:disabled:focus{text-decoration:none;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link:disabled:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link:disabled:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating form .btn-link:disabled:focus{text-decoration:none;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions{border-left:1px dotted #e7e7e7;float:right;padding:7px 14px;color:#999999;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions span,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form{float:left;overflow:auto;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions{border-left:1px dotted #e7e7e7;float:right;padding:7px 14px;color:#999999;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions span,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form{float:left;overflow:auto;}
-.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form{margin:0px;padding:0px;}
+.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form{margin:0px;padding:0px;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn{float:right;margin:0px;margin-left:14px;opacity:1;filter:alpha(opacity=100);padding:0px;color:#999999;font-weight:normal;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn:focus{color:#cf402e;text-decoration:underline;}
+.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn.btn-report:disabled{color:#46a546;}
+.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn.btn-hide:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn.btn-hide:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form .btn.btn-hide:focus{color:#f89406;}
+.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions form:first-child .btn{margin-left:0px;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a{margin-left:14px;color:#999999;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a a:active{color:#333333;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a{margin-left:14px;color:#999999;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a a:active{color:#333333;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a:first-child{margin-left:0px;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a:first-child{margin-left:0px;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a.post-reply{color:#555555;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a.post-reply:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a.post-reply a:active{color:#049cdb;}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a.post-reply{color:#555555;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a.post-reply:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions a.post-reply a:active{color:#049cdb;}
-.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions .btn{float:right;margin:0px;margin-left:14px;opacity:1;filter:alpha(opacity=100);padding:0px;color:#999999;font-weight:normal;}.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions .btn:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions .btn:active,.thread-body .post-wrapper .post-body .post-content .post-footer .post-actions .btn:focus{color:#cf402e;text-decoration:underline;}
 .thread-body .post-wrapper .post-body.post-muted .user-avatar{width:50px;height:50px;opacity:0.75;filter:alpha(opacity=75);}
 .thread-body .post-wrapper .post-body.post-muted .user-avatar{width:50px;height:50px;opacity:0.75;filter:alpha(opacity=75);}
 .thread-body .post-wrapper .post-body.post-muted .post-content{margin-left:71px;min-height:0px;opacity:0.75;filter:alpha(opacity=75);padding:14px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-header{float:right;margin:0px;margin-top:-7px;margin-right:-14px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-header .post-header-compact{float:left;margin-right:14px;}
 .thread-body .post-wrapper .post-body.post-muted .post-content{margin-left:71px;min-height:0px;opacity:0.75;filter:alpha(opacity=75);padding:14px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-header{float:right;margin:0px;margin-top:-7px;margin-right:-14px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-header .post-header-compact{float:left;margin-right:14px;}
 .thread-body .post-wrapper .post-body.post-muted .post-content .post-message{color:#999999;font-size:17.5px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-message strong,.thread-body .post-wrapper .post-body.post-muted .post-content .post-message a{color:#333333;font-weight:normal;}
 .thread-body .post-wrapper .post-body.post-muted .post-content .post-message{color:#999999;font-size:17.5px;}.thread-body .post-wrapper .post-body.post-muted .post-content .post-message strong,.thread-body .post-wrapper .post-body.post-muted .post-content .post-message a{color:#333333;font-weight:normal;}
-.thread-body .post-checkpoints .post-checkpoint{text-align:center;margin-bottom:20px;}.thread-body .post-checkpoints .post-checkpoint hr{background-color:#999999;background-image:-webkit-gradient(linear, 0 0, 100% 100%, color-stop(0.25, rgba(255, 255, 255, 0.2)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.2)), color-stop(0.75, rgba(255, 255, 255, 0.2)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);-webkit-background-size:10px 10px;-moz-background-size:10px 10px;background-size:10px 10px;border:none;height:4px;margin-bottom:-12px;}
+.thread-body .post-checkpoints .post-checkpoint{text-align:center;margin-bottom:20px;}.thread-body .post-checkpoints .post-checkpoint.checkpoint-deleted{opacity:0.3;filter:alpha(opacity=30);}.thread-body .post-checkpoints .post-checkpoint.checkpoint-deleted:hover{opacity:0.6;filter:alpha(opacity=60);}
+.thread-body .post-checkpoints .post-checkpoint hr{background-color:#999999;background-image:-webkit-gradient(linear, 0 0, 100% 100%, color-stop(0.25, rgba(255, 255, 255, 0.2)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.2)), color-stop(0.75, rgba(255, 255, 255, 0.2)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);-webkit-background-size:10px 10px;-moz-background-size:10px 10px;background-size:10px 10px;border:none;height:4px;margin-bottom:-12px;}
 .thread-body .post-checkpoints .post-checkpoint span{background-color:#fbfbfb;padding:0px 14px;color:#999999;}.thread-body .post-checkpoints .post-checkpoint span a{color:#333333;}
 .thread-body .post-checkpoints .post-checkpoint span{background-color:#fbfbfb;padding:0px 14px;color:#999999;}.thread-body .post-checkpoints .post-checkpoint span a{color:#333333;}
 .thread-body .post-checkpoints .post-checkpoint span i{opacity:0.43;filter:alpha(opacity=43);}
 .thread-body .post-checkpoints .post-checkpoint span i{opacity:0.43;filter:alpha(opacity=43);}
+.thread-body .post-checkpoints .post-checkpoint span form{display:inline-block;margin:0px;margin-top:-3px;margin-left:7px;padding:0px;}.thread-body .post-checkpoints .post-checkpoint span form .btn{margin-top:-2px;padding:0px;font-weight:normal;}.thread-body .post-checkpoints .post-checkpoint span form .btn:active,.thread-body .post-checkpoints .post-checkpoint span form .btn:hover{text-decoration:underline;}.thread-body .post-checkpoints .post-checkpoint span form .btn:active.btn-show,.thread-body .post-checkpoints .post-checkpoint span form .btn:hover.btn-show,.thread-body .post-checkpoints .post-checkpoint span form .btn:active.btn-hide,.thread-body .post-checkpoints .post-checkpoint span form .btn:hover.btn-hide{color:#f89406;}
+.thread-body .post-checkpoints .post-checkpoint span form .btn:active.btn-delete,.thread-body .post-checkpoints .post-checkpoint span form .btn:hover.btn-delete{color:#cf402e;}
+.thread-body .post-checkpoints .post-checkpoint span form:first-of-type{margin-left:14px;}
 .thread-moderation{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:3px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;overflow:auto;padding:7px;}.thread-moderation form{margin:0px;}
 .thread-moderation{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:3px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;overflow:auto;padding:7px;}.thread-moderation form{margin:0px;}
 .thread-quick-reply{overflow:auto;margin-top:20px;}.thread-quick-reply .user-avatar{border-radius:3px;float:left;width:100px;height:100px;overflow:visible;}
 .thread-quick-reply{overflow:auto;margin-top:20px;}.thread-quick-reply .user-avatar{border-radius:3px;float:left;width:100px;height:100px;overflow:visible;}
 .thread-quick-reply .editor{margin-left:121px;position:relative;}.thread-quick-reply .editor:after,.thread-quick-reply .editor:before{right:100%;border:solid transparent;content:"";height:0;width:0;position:absolute;pointer-events:none;}
 .thread-quick-reply .editor{margin-left:121px;position:relative;}.thread-quick-reply .editor:after,.thread-quick-reply .editor:before{right:100%;border:solid transparent;content:"";height:0;width:0;position:absolute;pointer-events:none;}
@@ -1152,7 +1192,7 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .post-votes-list .vote-user .vote-icon{background-color:#999999;border-radius:3px;padding:2px 3px;position:relative;bottom:1.75px;font-size:14px;}.post-votes-list .vote-user .vote-icon i{background-image:url("../img/glyphicons-halflings-white.png");}
 .post-votes-list .vote-user .vote-icon{background-color:#999999;border-radius:3px;padding:2px 3px;position:relative;bottom:1.75px;font-size:14px;}.post-votes-list .vote-user .vote-icon i{background-image:url("../img/glyphicons-halflings-white.png");}
 .post-votes-list a.vote-user:hover,.post-votes-list a.vote-user:active{color:#333333;text-decoration:none;}
 .post-votes-list a.vote-user:hover,.post-votes-list a.vote-user:active{color:#333333;text-decoration:none;}
 .post-votes-list .post-likes .vote-icon{background-color:#46a546;}
 .post-votes-list .post-likes .vote-icon{background-color:#46a546;}
-.post-votes-list .post-hates .vote-icon{background-color:#cf402e;}
+.post-votes-list .post-dislikes .vote-icon{background-color:#cf402e;}
 .post-changelog table td{vertical-align:middle;}.post-changelog table td .change-added,.post-changelog table td .change-removed,.post-changelog table td .change-none{display:block;font-size:28px;font-weight:bold;text-align:right;}.post-changelog table td .change-added.change-small,.post-changelog table td .change-removed.change-small,.post-changelog table td .change-none.change-small{font-size:14px;}
 .post-changelog table td{vertical-align:middle;}.post-changelog table td .change-added,.post-changelog table td .change-removed,.post-changelog table td .change-none{display:block;font-size:28px;font-weight:bold;text-align:right;}.post-changelog table td .change-added.change-small,.post-changelog table td .change-removed.change-small,.post-changelog table td .change-none.change-small{font-size:14px;}
 .post-changelog table td .change-neutral{color:#555555;}
 .post-changelog table td .change-neutral{color:#555555;}
 .post-changelog table td .change-added{color:#46a546;}
 .post-changelog table td .change-added{color:#46a546;}
@@ -1167,6 +1207,23 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .post-diff .post-diff-details table td.added{background-color:#dff1df;color:#285d28;font-weight:bold;}.post-diff .post-diff-details table td.added.even{background-color:#cdeacd;}
 .post-diff .post-diff-details table td.added{background-color:#dff1df;color:#285d28;font-weight:bold;}.post-diff .post-diff-details table td.added.even{background-color:#cdeacd;}
 .post-diff .post-diff-details table td.removed{background-color:#faeae8;color:#7c261b;font-weight:bold;}.post-diff .post-diff-details table td.removed.even{background-color:#f5d7d4;}
 .post-diff .post-diff-details table td.removed{background-color:#faeae8;color:#7c261b;font-weight:bold;}.post-diff .post-diff-details table td.removed.even{background-color:#f5d7d4;}
 .post-diff .post-diff-details table td.stag{color:#555555;}
 .post-diff .post-diff-details table td.stag{color:#555555;}
+.report-view .report-wrapper{background-color:#eeeeee;background-image:-webkit-gradient(linear, 0 0, 100% 100%, color-stop(0.25, rgba(255, 255, 255, 0.2)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.2)), color-stop(0.75, rgba(255, 255, 255, 0.2)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);-webkit-background-size:10px 10px;-moz-background-size:10px 10px;background-size:10px 10px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;padding:8px;margin-bottom:8px;}.report-view .report-wrapper .post-body{margin-bottom:0px;}.report-view .report-wrapper .post-body .report-actions{border-left:none !important;float:left !important;padding:0px !important;}.report-view .report-wrapper .post-body .report-actions form:first-child .btn{-webkit-border-radius:0px 0px 0px 3px !important;-moz-border-radius:0px 0px 0px 3px !important;border-radius:0px 0px 0px 3px !important;}
+.report-view .report-wrapper .post-body .report-actions .btn{opacity:0.9 !important;filter:alpha(opacity=90) !important;margin:0px !important;padding:8px 12px !important;color:#ffffff !important;font-weight:bold !important;}.report-view .report-wrapper .post-body .report-actions .btn i{background-image:url("../img/glyphicons-halflings-white.png");position:relative;top:0px;}
+.report-view .report-wrapper .post-body .report-actions .btn:hover,.report-view .report-wrapper .post-body .report-actions .btn:active,.report-view .report-wrapper .post-body .report-actions .btn:focus{opacity:1 !important;filter:alpha(opacity=100) !important;text-decoration:none !important;}
+.report-view .report-wrapper .post-body .report-actions .btn.btn-resolve{background-color:#3e933e;text-shadow:0px 1px 0px #285d28;}
+.report-view .report-wrapper .post-body .report-actions .btn.btn-bogus{background-color:#333333;text-shadow:0px 1px 0px #000000;}
+.reports-list .thread-label{overflow:visible;}.reports-list .thread-label .report-label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;float:right;padding:3px 8px;position:relative;right:-32px;bottom:2px;color:#ffffff;font-weight:bold;}.reports-list .thread-label .report-label i{background-image:url("../img/glyphicons-halflings-white.png");}
+.reports-list .thread-label .report-label.report-open{background-color:#f89406;text-shadow:0px 1px 0px #945904;}
+.reports-list .thread-label .report-label.report-resolved{background-color:#46a546;text-shadow:0px 1px 0px #285d28;}
+.reports-list .thread-label .report-label.report-bogus{background-color:#555555;text-shadow:0px 1px 0px #222222;}
+.reports-list .thread-name .report-id{color:#999999 !important;}
+.reports-list .thread-name,.reports-list .thread-details{margin-left:0px !important;}
+.reports-list .thread-icon{display:none !important;float:none;}
+.search-resume .muted{color:#7b7b7b;}.search-resume .muted a{color:#333333;}
+.search-results .results-list .result{border-bottom:1px solid #eeeeee;margin-bottom:10px;padding-bottom:10px;}.search-results .results-list .result h3{margin:0px;line-height:20px;}.search-results .results-list .result h3 a:link,.search-results .results-list .result h3 a:visited{color:#555555;font-weight:normal;font-size:18.2px;text-decoration:underline;}
+.search-results .results-list .result h3 a:hover,.search-results .results-list .result h3 a:active{color:#333333;}
+.search-results .results-list .result .post-extra{margin:0px;color:#999999;font-size:11.200000000000001px;line-height:20px;}.search-results .results-list .result .post-extra a{color:#333333;}
+.search-results .results-list .result .post-preview{margin:0px;color:#333333;font-size:15.400000000000002px;line-height:20px;}.search-results .results-list .result .post-preview strong{color:#cf402e;}
 .index-rank-team ul li .label{background-color:#cf402e;color:#ffffff;text-shadow:0px 1px 0px #3d130e;}
 .index-rank-team ul li .label{background-color:#cf402e;color:#ffffff;text-shadow:0px 1px 0px #3d130e;}
 .post-label-team{background-color:#cf402e;}
 .post-label-team{background-color:#cf402e;}
 .index-rank-mvp ul li .label{background-color:#049cdb;color:#ffffff;text-shadow:0px 1px 0px #011f2c;}
 .index-rank-mvp ul li .label{background-color:#049cdb;color:#ffffff;text-shadow:0px 1px 0px #011f2c;}

+ 3 - 0
static/cranefly/css/cranefly.less

@@ -89,6 +89,9 @@
 @import "cranefly/thread.less";
 @import "cranefly/thread.less";
 @import "cranefly/karmas.less";
 @import "cranefly/karmas.less";
 @import "cranefly/changelog.less";
 @import "cranefly/changelog.less";
+@import "cranefly/report.less";
+@import "cranefly/reports.less";
+@import "cranefly/search.less";
 
 
 // Keep ranks last for easy overrides!
 // Keep ranks last for easy overrides!
 @import "ranks.less";
 @import "ranks.less";

+ 57 - 20
static/cranefly/css/cranefly/category.less

@@ -8,29 +8,53 @@
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   margin-bottom: @baseLineHeight;
   margin-bottom: @baseLineHeight;
 
 
-  table {
-    margin: 0px;
-    
-    tr:first-child {
-      td {
-        border-top: none;
+  .header {
+    background-color: @categoryHeader;
+    border: 1px solid @categoryBorder;
+    border-radius: @borderRadiusSmall @borderRadiusSmall 0px 0px;
+    margin: -1px;
+    margin-bottom: 0px;
+    padding: (@fontSizeSmall / 3) (@fontSizeSmall - 2px);
+
+    h2 {
+      margin: 0px;
+      padding: 0px;
+
+      color: @grayDark;
+      font-size: @fontSizeSmall;
+      font-weight: bold;
+      line-height: @baseLineHeight;
+      text-align: left;
+
+      small {
+        margin-left: @baseFontSize / 2;
+
+        color: @grayLight;
+        font-size: @fontSizeSmall;
       }
       }
     }
     }
-    
-    td {
-      padding: ((@fontSizeLarge / 2) + 6px) (@fontSizeSmall - 2px);
-      padding-bottom: (@fontSizeLarge / 2) + 7px;
+  }
+
+  .forum {
+    border-bottom: 1px solid @categoryBorder;
+    height: 21px;
+    overflow: visible;
+    padding: ((@fontSizeLarge / 2) + 6px) (@fontSizeSmall - 2px);
+
+    &.last {
+      border-bottom: none;
     }
     }
 
 
     .forum-icon {
     .forum-icon {
-      padding-right: (@fontSizeSmall / 2) - 3px;
-      width: 1%;
+      float: left;
 
 
       .forum-icon-wrap {
       .forum-icon-wrap {
         background-color: @itemOldColor;
         background-color: @itemOldColor;
         border: 1px solid darken(@itemOldColor, 10%);
         border: 1px solid darken(@itemOldColor, 10%);
         border-radius: @baseBorderRadius;
         border-radius: @baseBorderRadius;
-        padding: (@forumIconSize - 1px) @forumIconSize;
+        padding: ((@forumIconSize - 22px) / 2) ((@forumIconSize - 16px) / 2);
+        position: relative;
+        bottom: (@forumIconSize - @baseLineHeight) / 2;
 
 
         &.forum-icon-new {
         &.forum-icon-new {
           background-color: @itemNewColor;
           background-color: @itemNewColor;
@@ -43,8 +67,10 @@
         }
         }
       }
       }
     }
     }
-    
+
     .forum-main {
     .forum-main {
+      margin-left: @forumIconSize + 10px;
+
       h3 {
       h3 {
         float: left;
         float: left;
         margin: 0px;
         margin: 0px;
@@ -63,6 +89,17 @@
         float: right;
         float: right;
         right: @baseFontSize;
         right: @baseFontSize;
 
 
+        .subforum {
+          &:link, &:visited {
+            color: @grayLight;
+            font-weight: bold;
+          }
+
+          &:hover, &:active {
+            color: @textColor;
+          }
+        }
+
         .dropdown-toggle {
         .dropdown-toggle {
           padding: 4px 8px;
           padding: 4px 8px;
           .opacity(60);
           .opacity(60);
@@ -207,17 +244,17 @@
   }
   }
 
 
   &.category-forums-important {
   &.category-forums-important {
-    border: 1px solid darken(@red, 15%);
-    .box-shadow(0px 0px 0px 3px @red);
+    border-color: @red;
+    .box-shadow(0px 0px 0px 3px darken(@red, 10%));
   }
   }
 
 
   &.category-forums-inverse {
   &.category-forums-inverse {
-    border: 1px solid @grayDark;
-    .box-shadow(0px 0px 0px 3px @gray);
+    border-color: @grayDark;
+    .box-shadow(0px 0px 0px 3px darken(@grayDark, 10%));
   }
   }
 
 
   &.category-forums-info {
   &.category-forums-info {
-    border: 1px solid darken(@bluePale, 15%);
-    .box-shadow(0px 0px 0px 3px @bluePale);
+    border-color: @bluePale;
+    .box-shadow(0px 0px 0px 3px darken(@bluePale, 10%));
   }
   }
 }
 }

+ 252 - 115
static/cranefly/css/cranefly/forum.less

@@ -9,19 +9,22 @@
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   margin-bottom: @baseLineHeight;
   margin-bottom: @baseLineHeight;
 
 
-  table {
-    margin: 0px;
-
-    caption {
-      background-color: @categoryHeader;
-      border: 1px solid @categoryBorder;
-      border-radius: @borderRadiusSmall @borderRadiusSmall 0px 0px;
-      margin: -1px;
-      padding: (@fontSizeSmall / 3) (@fontSizeSmall - 2px);
+  .header {
+    background-color: @categoryHeader;
+    border: 1px solid @categoryBorder;
+    border-radius: @borderRadiusSmall @borderRadiusSmall 0px 0px;
+    margin: -1px;
+    margin-bottom: 0px;
+    padding: (@fontSizeSmall / 3) (@fontSizeSmall - 2px);
+
+    h2 {
+      margin: 0px;
+      padding: 0px;
 
 
       color: @grayDark;
       color: @grayDark;
       font-size: @fontSizeSmall;
       font-size: @fontSizeSmall;
       font-weight: bold;
       font-weight: bold;
+      line-height: @baseLineHeight;
       text-align: left;
       text-align: left;
 
 
       small {
       small {
@@ -31,21 +34,28 @@
         font-size: @fontSizeSmall;
         font-size: @fontSizeSmall;
       }
       }
     }
     }
-    
-    td {
-      padding: ((@fontSizeLarge / 2) + 6px) (@fontSizeSmall - 2px);
-      padding-bottom: (@fontSizeLarge / 2) + 7px;
+  }
+
+  .forum {
+    border-bottom: 1px solid @categoryBorder;
+    height: 21px;
+    overflow: visible;
+    padding: ((@fontSizeLarge / 2) + 6px) (@fontSizeSmall - 2px);
+
+    &.last {
+      border-bottom: none;
     }
     }
 
 
     .forum-icon {
     .forum-icon {
-      padding-right: (@fontSizeSmall / 2) - 3px;
-      width: 1%;
+      float: left;
 
 
       .forum-icon-wrap {
       .forum-icon-wrap {
         background-color: @itemOldColor;
         background-color: @itemOldColor;
         border: 1px solid darken(@itemOldColor, 10%);
         border: 1px solid darken(@itemOldColor, 10%);
         border-radius: @baseBorderRadius;
         border-radius: @baseBorderRadius;
-        padding: (@forumIconSize - 1px) @forumIconSize;
+        padding: ((@forumIconSize - 22px) / 2) ((@forumIconSize - 16px) / 2);
+        position: relative;
+        bottom: (@forumIconSize - @baseLineHeight) / 2;
 
 
         &.forum-icon-new {
         &.forum-icon-new {
           background-color: @itemNewColor;
           background-color: @itemNewColor;
@@ -58,8 +68,10 @@
         }
         }
       }
       }
     }
     }
-    
+
     .forum-main {
     .forum-main {
+      margin-left: 34px;
+
       h3 {
       h3 {
         float: left;
         float: left;
         margin: 0px;
         margin: 0px;
@@ -78,6 +90,17 @@
         float: right;
         float: right;
         right: @baseFontSize;
         right: @baseFontSize;
 
 
+        .subforum {
+          &:link, &:visited {
+            color: @grayLight;
+            font-weight: bold;
+          }
+
+          &:hover, &:active {
+            color: @textColor;
+          }
+        }
+
         .dropdown-toggle {
         .dropdown-toggle {
           padding: 4px 8px;
           padding: 4px 8px;
           .opacity(60);
           .opacity(60);
@@ -222,46 +245,52 @@
   }
   }
 
 
   &.forum-subforums-important {
   &.forum-subforums-important {
-    caption {
+    .header {
       background-color: @red;
       background-color: @red;
       border: 1px solid darken(@red, 10%);
       border: 1px solid darken(@red, 10%);
 
 
-      color: @white;
-      text-shadow: 0px 1px 0px darken(@red, 25%);
+      h2 {
+        color: @white;
+        text-shadow: 0px 1px 0px darken(@red, 25%);
 
 
-      small {
-        color: darken(@red, 40%);
-        text-shadow: none;
+        small {
+          color: darken(@red, 40%);
+          text-shadow: none;
+        }
       }
       }
     }
     }
   }
   }
 
 
   &.forum-subforums-inverse {
   &.forum-subforums-inverse {
-    caption {
+    .header {
       background-color: @grayDark;
       background-color: @grayDark;
       border: 1px solid darken(@grayDark, 10%);
       border: 1px solid darken(@grayDark, 10%);
 
 
-      color: @grayLighter;
-      text-shadow: 0px 1px 0px darken(@black, 25%);
+      h2 {
+        color: @grayLighter;
+        text-shadow: 0px 1px 0px darken(@black, 25%);
 
 
-      small {
-        color: lighten(@grayLight, 10%);
-        text-shadow: none;
+        small {
+          color: lighten(@grayLight, 10%);
+          text-shadow: none;
+        }
       }
       }
     }
     }
   }
   }
 
 
   &.forum-subforums-info {
   &.forum-subforums-info {
-    caption {
+    .header {
       background-color: @bluePale;
       background-color: @bluePale;
       border: 1px solid darken(@bluePale, 10%);
       border: 1px solid darken(@bluePale, 10%);
 
 
-      color: @white;
-      text-shadow: 0px 1px 0px darken(@bluePale, 25%);
+      h2 {
+        color: @white;
+        text-shadow: 0px 1px 0px darken(@bluePale, 25%);
 
 
-      small {
-        color: darken(@bluePale, 25%);
-        text-shadow: none;
+        small {
+          color: darken(@bluePale, 25%);
+          text-shadow: none;
+        }
       }
       }
     }
     }
   }
   }
@@ -275,123 +304,231 @@
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   margin-bottom: @baseLineHeight;
   margin-bottom: @baseLineHeight;
 
 
-  table {
-    margin: 0px;
+  .header {
+    background-color: @categoryHeader;
+    border: 1px solid @categoryBorder;
+    border-radius: @borderRadiusSmall @borderRadiusSmall 0px 0px;
+    margin: -1px;
+    margin-bottom: 0px;
+    padding-bottom: 1px;
+    overflow: auto;
+
+    color: @grayLight;
+    font-weight: bold;
+    font-size: @fontSizeSmall;
 
 
-    th {
-      background-color: @bodyBackground;
-      border-bottom: 1px solid @grayLighter;
-      padding: @paddingSmall;
+    .row-fluid {
+      &>div {
+        min-height: auto;
+        padding: @paddingSmall;
+      }
 
 
-      color: @grayLight;
-      font-size: @fontSizeSmall;
+      .thread-replies {
+        float: left;
+        width: 106px;
+      }
+
+      .thread-last {
+        float: left;
+      }
+    }
+
+    .check-cell {
+      label {
+        margin: 0px;
+      }
     }
     }
+  }
 
 
-    td {
-      vertical-align: middle;
+  .thread-row {
+    border-bottom: 1px solid @categoryBorder;
+    height: 38px;
+    overflow: hidden;
+    padding: (@fontSizeSmall - 2px) 0px;
+
+    .row-fluid {
+      &>div {
+        min-height: auto;
+        padding: @paddingSmall;
+        padding-bottom: 0px;
+      }
+    }
 
 
-      &.threads-list-empty {
-        padding: @paddingLarge;
+    &.thread-last {
+      border-bottom: none;
 
 
-        font-size: @fontSizeLarge;
-        text-align: center;
+      &>div {
+        padding-bottom: 1px;
       }
       }
+    }
 
 
-      .thread-icon {
-        &:link, &:active, &:visited, &:hover {
-          background-color: @itemOldColor;
-          border: 1px solid darken(@itemOldColor, 15%);
-          border-radius: @baseBorderRadius;
-          margin-right: @baseFontSize / 2;
-          padding: 3px 4px;
-
-          &.thread-new {
-            background-color: @itemNewColor;
-            border: 1px solid darken(@itemNewColor, 15%);
-          }
-        }
+    .thread-icon {
+      background-color: @itemOldColor;
+      border: 1px solid darken(@itemOldColor, 10%);
+      .border-radius(@baseBorderRadius);
+      display: block;
+      float: left;
+      margin: -2px 0px;
+      margin-left: -1px;
+      padding: 1px 4px;
+
+      &:hover, &:active {
+        .opacity(100);
+      }
 
 
-        i {
-          background-image: url("@{iconWhiteSpritePath}");
-        }
+      i {
+        background-image: url("@{iconWhiteSpritePath}");
+      }
+    }
+
+    &.thread-new {
+      .thread-icon {
+        background-color: @itemNewColor;
+        border: 1px solid darken(@itemNewColor, 10%);
       }
       }
 
 
       .thread-name {
       .thread-name {
-        color: @textColor;
-        font-weight: bold;
+        color: @textColor !important;
       }
       }
+    }
 
 
-      .thread-details, .thread-last-reply {
-        color: @grayLight;
+    &.threads-list-empty {
+      height: auto;
+      padding: @paddingLarge;
 
 
-        a:link, a:visited {
-          color: @textColor;
-        }
-      }
+      font-size: @fontSizeLarge;
+      text-align: center;
+    }
+
+    .thread-name {
+      margin-left: 10px;
+
+      color: lighten(@textColor, 17%);
+      font-size: @baseFontSize + 2px;
+      font-weight: bold;
+    }
 
 
-      .thread-details {
-        font-size: @fontSizeMini;
+    .thread-details, .thread-last-reply {
+      color: @grayLight;
+      line-height: @baseFontSize;
+
+      a:link, a:visited {
+        color: @textColor;
       }
       }
+    }
 
 
-      .thread-flags {
-        float: right;
-        margin: 0px;
-        .opacity(80);
-        padding: 0px;
+    .thread-details {
+      margin-left: 34px;
 
 
-        li {
-          margin-left: 3px;
-          float: left;
+      font-size: @fontSizeMini;
+    }
+
+    .thread-flags {
+      float: right;
+      margin: 0px;
+      position: relative;
+      right: -30px;
+      top: 5px;
+      padding: 0px;
+
+      li {
+        .border-radius(@baseBorderRadius);
+        display: block;
+        float: left;
+        margin-left: 3px;
+        padding: 2px 5px;
+
+        &.flag-reported {
+          background-color: @flagReported;
         }
         }
-      }
 
 
-      .thread-rating {
-        background-color: @grayLighter;
-        border-radius: @baseBorderRadius;
-        padding: 4px;
+        &.flag-notreviewed {
+          background-color: @flagReviewed;
+        }
 
 
-        color: @grayLight;
-        font-size: @fontSizeLarge;
-        font-weight: bold;
-        text-align: center;
+        &.flag-announcement {
+          background-color: @flagAnnouncement;
+        }
 
 
-        &.thread-rating-negative, &.thread-rating-positive {
-          color: @white;
+        &.flag-sticky {
+          background-color: @flagSticky;
         }
         }
 
 
-        &.thread-rating-negative {
-          background-color: @red;
+        &.flag-deleted {
+          background-color: @flagDeleted;
         }
         }
 
 
-        &.thread-rating-positive {
-          background-color: @green;
+        &.flag-closed {
+          background-color: @flagClosed;
         }
         }
       }
       }
+
+      i {
+        background-image: url("@{iconWhiteSpritePath}");
+      }
     }
     }
 
 
-    th, td {
-      &.check-cell {
-        width: 1%;
-        text-align: center;
-        vertical-align: middle;
+    .thread-activity {
+      border-left: 1px dotted darken(@categoryBackground, 12%);
+      position: relative;
+      bottom: 3px;
+
+      .thread-last-avatar {
+        float: left;
 
 
-        .checkbox {
+        img {
+          .border-radius(@baseBorderRadius);
           margin: 0px;
           margin: 0px;
-          padding: 0px;
+          margin-right: @baseFontSize;
+          width: 40px;
+          height: 40px;
+        }
+      }
 
 
-          input {
-            margin: 0px;
-            padding: 0px;
-            position: relative;
-            top: 3px;
-          }
+      .thread-replies {
+        float: left;
+        margin-top: -1px;
+        margin-right: @baseFontSize;
+
+        color: @grayLight;
+        font-size: @fontSizeSmall;
+
+        .lead {
+          font-size: @baseFontSize;
+          font-weight: bold;
+          line-height: @baseLineHeight;
+        }
+
+        a:link, a:active, a:visited, a:hover {
+          color: @gray;
         }
         }
       }
       }
-    }
 
 
-    th.check-cell {
-      input{
-        right: 2px;
+      .thread-last-reply {
+        border-left: 1px dotted darken(@categoryBackground, 12%);
+        padding-left: @baseFontSize;
+
+      }
+
+      .thread-select {
+        background-color: darken(@categoryBackground, 5%);
+        display: block;
+        float: right;
+        margin: -12px -11px;
+        margin-left: 0px;
+        width: 29px;
+        height: 61px;
+
+        &:hover, &:focus, &:active {
+          background-color: @linkColor;
+        }
+
+        input {
+          margin: 0px;
+          position: relative;
+          right: 2px;
+          top: 26px;
+        }
       }
       }
     }
     }
   }
   }

+ 107 - 111
static/cranefly/css/cranefly/forummap.less

@@ -8,19 +8,22 @@
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   margin-bottom: @baseLineHeight;
   margin-bottom: @baseLineHeight;
 
 
-  table {
-  	margin: 0px;
-
-  	caption {
-  	  background-color: @categoryHeader;
-      border: 1px solid @categoryBorder;
-      border-radius: @borderRadiusSmall @borderRadiusSmall 0px 0px;
-      margin: -1px;
-      padding: (@fontSizeSmall / 3) (@fontSizeSmall - 2px);
+  .header {
+    background-color: @categoryHeader;
+    border: 1px solid @categoryBorder;
+    border-radius: @borderRadiusSmall @borderRadiusSmall 0px 0px;
+    margin: -1px;
+    margin-bottom: 0px;
+    padding: (@fontSizeSmall / 3) (@fontSizeSmall - 2px);
+
+    h2 {
+      margin: 0px;
+      padding: 0px;
 
 
       color: @grayDark;
       color: @grayDark;
       font-size: @fontSizeSmall;
       font-size: @fontSizeSmall;
       font-weight: bold;
       font-weight: bold;
+      line-height: @baseLineHeight;
       text-align: left;
       text-align: left;
 
 
       small {
       small {
@@ -29,141 +32,134 @@
         color: @grayLight;
         color: @grayLight;
         font-size: @fontSizeSmall;
         font-size: @fontSizeSmall;
       }
       }
-  	}
-    
-    .forum-map-forum, .forum-map-subforum {
-      h3 {
-				margin: 0px;
-				padding: 0px;
-
-				font-size: @baseFontSize;
-				line-height: @baseLineHeight;
-
-				a:link, a:visited {
-				  color: @gray;
-				}
-
-	      a:active, a:hover {
-			    color: @textColor;
-			  }
-      }
+    }
+  }
 
 
-      .forum-details {
-        float: right;
-        margin-top: (@baseLineHeight * -1) + 1px;
+  .forum-map-forum, .forum-map-subforum {
+    border-bottom: 1px solid @categoryBorder;
+    overflow: auto;
+    padding: (@fontSizeSmall / 2) (@fontSizeSmall - 2px);
 
 
-        color: @grayLight;
-        font-size: @fontSizeSmall;
+    h3 {
+      margin: 0px;
+      padding: 0px;
 
 
-        strong, a {
-          color: @gray;
-          font-weight: normal
-        }
+      font-size: @baseFontSize;
+      line-height: @baseLineHeight;
 
 
-        a:hover, a:active {
-          color: @textColor;
-        }
+      a:link, a:visited {
+        color: @gray;
+      }
 
 
-        strong.stat-increment {
-          color: @green;
-        }
+      a:active, a:hover {
+        color: @textColor;
       }
       }
     }
     }
+  }
 
 
-    .forum-map-subforum {
-    	padding-left: @baseLineHeight * 0.75;
-
-    	span {
-    		&.tree-t, &.tree-l, &.tree-s, &.tree-i {
-    			display: inline-block;
-    			height: @baseLineHeight;
-    			width: @baseLineHeight * 0.5;
-    		}
-
-    		&.tree-t {
-    			border-left: 1px solid @grayLight;
-          margin-right: 2px;
-
-    			span {
-    				border-top: 1px solid @grayLight;
-    			  display: inline-block;
-    				height: 1px;
-    				width: 100%;
-    				margin-bottom: 3px;
-    			}
-    		}
-
-    		&.tree-l {
-          margin-right: 4px;
-
-    			span {
-    			  border-left: 1px solid @grayLight;
-    				border-bottom: 1px solid @grayLight;
-    			  display: inline-block;
-    				height: @baseLineHeight * 0.5;
-    				width: 100%;
-    				margin-bottom: 3px;
-    			}
-    		}
-
-    		&.tree-i {
-    			border-left: 1px solid @grayLight;
-    			position: relative;
-    			top: 5px;
-          margin-top: -5px;
-    			margin-right: 4px;
-    		}
-
-    		&.tree-s {
-          height: 1px;
-    			width: (@baseLineHeight * 0.5) + 2px;
-    			margin-right: 4px;
-    		}
-    	}
-    }
+  .forum-map-subforum {
+  	padding-left: @baseLineHeight * 0.75;
+
+  	span {
+  		&.tree-t, &.tree-l, &.tree-s, &.tree-i {
+  			display: inline-block;
+  			height: @baseLineHeight;
+  			width: @baseLineHeight * 0.5;
+  		}
+
+  		&.tree-t {
+  			border-left: 1px solid @grayLight;
+        margin-right: 2px;
+
+  			span {
+  				border-top: 1px solid @grayLight;
+  			  display: inline-block;
+  				height: 1px;
+  				width: 100%;
+  				margin-bottom: 3px;
+  			}
+  		}
+
+  		&.tree-l {
+        margin-right: 4px;
+
+  			span {
+  			  border-left: 1px solid @grayLight;
+  				border-bottom: 1px solid @grayLight;
+  			  display: inline-block;
+  				height: @baseLineHeight * 0.5;
+  				width: 100%;
+  				margin-bottom: 3px;
+  			}
+  		}
+
+  		&.tree-i {
+  			border-left: 1px solid @grayLight;
+  			position: relative;
+  			top: 5px;
+        margin-top: -5px;
+  			margin-right: 4px;
+  		}
+
+  		&.tree-s {
+        height: 1px;
+  			width: (@baseLineHeight * 0.5) + 2px;
+  			margin-right: 4px;
+  		}
+  	}
   }
   }
 
 
+  &>div:last-child {
+    border-bottom: none;
+  }
+  
   &.forum-map-category-important {
   &.forum-map-category-important {
-    caption {
+    .header {
       background-color: @red;
       background-color: @red;
       border: 1px solid darken(@red, 10%);
       border: 1px solid darken(@red, 10%);
 
 
-      color: @white;
-      text-shadow: 0px 1px 0px darken(@red, 25%);
+      h2 {
+        color: @white;
+        text-shadow: 0px 1px 0px darken(@red, 25%);
 
 
-      small {
-        color: darken(@red, 40%);
-        text-shadow: none;
+        small {
+          color: darken(@red, 40%);
+          text-shadow: none;
+        }
       }
       }
     }
     }
   }
   }
 
 
   &.forum-map-category-inverse {
   &.forum-map-category-inverse {
-    caption {
+    .header {
       background-color: @grayDark;
       background-color: @grayDark;
       border: 1px solid darken(@grayDark, 10%);
       border: 1px solid darken(@grayDark, 10%);
 
 
-      color: @grayLighter;
-      text-shadow: 0px 1px 0px darken(@black, 25%);
+      h2 {
+        color: @grayLighter;
+        text-shadow: 0px 1px 0px darken(@black, 25%);
 
 
-      small {
-        color: lighten(@grayLight, 10%);
-        text-shadow: none;
+        small {
+          color: lighten(@grayLight, 10%);
+          text-shadow: none;
+        }
       }
       }
     }
     }
   }
   }
 
 
   &.forum-map-category-info {
   &.forum-map-category-info {
-    caption {
+    .header {
       background-color: @bluePale;
       background-color: @bluePale;
       border: 1px solid darken(@bluePale, 10%);
       border: 1px solid darken(@bluePale, 10%);
 
 
-      color: @white;
-      text-shadow: 0px 1px 0px darken(@bluePale, 25%);
+      h2 {
+        color: @white;
+        text-shadow: 0px 1px 0px darken(@bluePale, 25%);
 
 
-      small {
-        color: darken(@bluePale, 25%);
-        text-shadow: none;
+        small {
+          color: darken(@bluePale, 25%);
+          text-shadow: none;
+        }
       }
       }
     }
     }
   }
   }

+ 23 - 0
static/cranefly/css/cranefly/header.less

@@ -46,6 +46,29 @@
     font-weight: normal;
     font-weight: normal;
   }
   }
 
 
+  &.header-search {
+    h1 {
+      float: left;
+
+      form {
+        float: right;
+        margin: 0px;
+
+        input {
+          margin-left: @baseFontSize * 1.5;
+          .box-shadow(0px 0px 0px 3px darken(@bodyBackground, 5%));
+
+          font-size: @fontSizeLarge;
+
+          &:focus, &:active {
+            border-color: lighten(@linkColor, 25%);
+            .box-shadow(0px 0px 0px 3px lighten(@linkColor, 45%));
+          }
+        }
+      }
+    }  
+  }
+
   .header-stats {
   .header-stats {
     overflow: visible;
     overflow: visible;
     margin-bottom: 0px;
     margin-bottom: 0px;

+ 100 - 62
static/cranefly/css/cranefly/index.less

@@ -16,20 +16,23 @@
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   .box-shadow(0px 0px 0px 3px @categoryShadow);
   margin-bottom: @baseLineHeight;
   margin-bottom: @baseLineHeight;
 
 
-  table {
-  	margin: 0px;
-
-  	caption {
-  	  background-color: @categoryHeader;
-      border: 1px solid @categoryBorder;
-      border-radius: @borderRadiusSmall @borderRadiusSmall 0px 0px;
-      margin: -1px;
-      padding: (@fontSizeSmall / 3) (@fontSizeSmall - 2px);
+  .header {
+    background-color: @categoryHeader;
+    border: 1px solid @categoryBorder;
+    border-radius: @borderRadiusSmall @borderRadiusSmall 0px 0px;
+    margin: -1px;
+    margin-bottom: 0px;
+    padding: (@fontSizeSmall / 3) (@fontSizeSmall - 2px);
+
+    h2 {
+      margin: 0px;
+      padding: 0px;
 
 
       color: @grayDark;
       color: @grayDark;
-  	  font-size: @fontSizeSmall;
-  	  font-weight: bold;
-  	  text-align: left;
+      font-size: @fontSizeSmall;
+      font-weight: bold;
+      line-height: @baseLineHeight;
+      text-align: left;
 
 
       small {
       small {
         margin-left: @baseFontSize / 2;
         margin-left: @baseFontSize / 2;
@@ -37,44 +40,53 @@
         color: @grayLight;
         color: @grayLight;
         font-size: @fontSizeSmall;
         font-size: @fontSizeSmall;
       }
       }
-  	}
-    
-    td {
-      padding: ((@fontSizeLarge / 2) + 6px) (@fontSizeSmall - 2px);
-      padding-bottom: (@fontSizeLarge / 2) + 7px;
+    }
+  }
+
+  .forum {
+    border-bottom: 1px solid @categoryBorder;
+    height: 21px;
+    overflow: visible;
+    padding: ((@fontSizeLarge / 2) + 6px) (@fontSizeSmall - 2px);
+
+    &.last {
+      border-bottom: none;
     }
     }
 
 
     .forum-icon {
     .forum-icon {
-      padding-right: (@fontSizeSmall / 2) - 3px;
-      width: 1%;
+      float: left;
 
 
       .forum-icon-wrap {
       .forum-icon-wrap {
-      	background-color: @itemOldColor;
-      	border: 1px solid darken(@itemOldColor, 10%);
-    		border-radius: @baseBorderRadius;
-    		padding: (@forumIconSize - 1px) @forumIconSize;
-
-    		&.forum-icon-new {
-  	      background-color: @itemNewColor;
-  	      border: 1px solid darken(@itemNewColor, 10%);
-    		}
-
-    		&.forum-icon-redirect {
-  	      background-color: @itemMovedColor;
-  	      border: 1px solid darken(@itemMovedColor, 10%);
-    		}
+        background-color: @itemOldColor;
+        border: 1px solid darken(@itemOldColor, 10%);
+        border-radius: @baseBorderRadius;
+        padding: ((@forumIconSize - 22px) / 2) ((@forumIconSize - 16px) / 2);
+        position: relative;
+        bottom: (@forumIconSize - @baseLineHeight) / 2;
+
+        &.forum-icon-new {
+          background-color: @itemNewColor;
+          border: 1px solid darken(@itemNewColor, 10%);
+        }
+
+        &.forum-icon-redirect {
+          background-color: @itemMovedColor;
+          border: 1px solid darken(@itemMovedColor, 10%);
+        }
       }
       }
     }
     }
-    
+
     .forum-main {
     .forum-main {
+      margin-left: @forumIconSize + 10px;
+
       h3 {
       h3 {
         float: left;
         float: left;
-    		margin: 0px;
-    		padding: 0px;
+        margin: 0px;
+        padding: 0px;
 
 
-    		font-size: @fontSizeLarge;
+        font-size: @fontSizeLarge;
         font-weight: normal;
         font-weight: normal;
-    		line-height: @baseLineHeight;
+        line-height: @baseLineHeight;
 
 
         a:link, a:visited {
         a:link, a:visited {
           color: @textColor;
           color: @textColor;
@@ -85,6 +97,17 @@
         float: right;
         float: right;
         right: @baseFontSize;
         right: @baseFontSize;
 
 
+        .subforum {
+          &:link, &:visited {
+            color: @grayLight;
+            font-weight: bold;
+          }
+
+          &:hover, &:active {
+            color: @textColor;
+          }
+        }
+
         .dropdown-toggle {
         .dropdown-toggle {
           padding: 4px 8px;
           padding: 4px 8px;
           .opacity(60);
           .opacity(60);
@@ -229,46 +252,53 @@
   }
   }
 
 
   &.index-category-important {
   &.index-category-important {
-    caption {
+    .header {
       background-color: @red;
       background-color: @red;
       border: 1px solid darken(@red, 10%);
       border: 1px solid darken(@red, 10%);
 
 
-      color: @white;
-      text-shadow: 0px 1px 0px darken(@red, 25%);
+      h2 {
+        color: @white;
+        text-shadow: 0px 1px 0px darken(@red, 25%);
 
 
-      small {
-        color: darken(@red, 40%);
-        text-shadow: none;
+        small {
+          color: darken(@red, 40%);
+          text-shadow: none;
+        }
       }
       }
     }
     }
   }
   }
 
 
   &.index-category-inverse {
   &.index-category-inverse {
-    caption {
+    .header {
       background-color: @grayDark;
       background-color: @grayDark;
       border: 1px solid darken(@grayDark, 10%);
       border: 1px solid darken(@grayDark, 10%);
 
 
-      color: @grayLighter;
-      text-shadow: 0px 1px 0px darken(@black, 25%);
+      h2 {
+        color: @grayLighter;
+        text-shadow: 0px 1px 0px darken(@black, 25%);
 
 
-      small {
-        color: lighten(@grayLight, 10%);
-        text-shadow: none;
+        small {
+          color: lighten(@grayLight, 10%);
+          text-shadow: none;
+        }
       }
       }
     }
     }
   }
   }
 
 
   &.index-category-info {
   &.index-category-info {
-    caption {
+    .header{
       background-color: @bluePale;
       background-color: @bluePale;
       border: 1px solid darken(@bluePale, 10%);
       border: 1px solid darken(@bluePale, 10%);
 
 
-      color: @white;
-      text-shadow: 0px 1px 0px darken(@bluePale, 25%);
+      h2 {
 
 
-      small {
-        color: darken(@bluePale, 25%);
-        text-shadow: none;
+        color: @white;
+        text-shadow: 0px 1px 0px darken(@bluePale, 25%);
+
+        small {
+          color: darken(@bluePale, 25%);
+          text-shadow: none;
+        }
       }
       }
     }
     }
   }
   }
@@ -302,6 +332,12 @@
     color: @grayLight;
     color: @grayLight;
     font-size: @fontSizeLarge;
     font-size: @fontSizeLarge;
     font-weight: bold;
     font-weight: bold;
+
+    a:link, a:active, a:visited, a:hover {
+      color: @grayLight;
+      font-size: @fontSizeLarge;
+      text-decoration: none;
+    }
   }
   }
 
 
   ul {
   ul {
@@ -326,13 +362,15 @@
         height: 28px;
         height: 28px;
       }
       }
 
 
-      a:link, a:active, a:visited, a:hover {
-        position: relative;
-        top: (@fontSizeLarge - @baseFontSize) / 2;
-        margin: 0px 4px;
+      .user-name {
+        &:link, &:active, &:visited, &:hover {
+          position: relative;
+          top: (@fontSizeLarge - @baseFontSize) / 2;
+          margin: 0px 4px;
 
 
-        color: @textColor;
-        font-size: @fontSizeLarge;
+          color: @textColor;
+          font-size: @fontSizeLarge;
+        }
       }
       }
 
 
       .label {
       .label {

+ 1 - 1
static/cranefly/css/cranefly/karmas.less

@@ -37,7 +37,7 @@
     }
     }
   }
   }
 
 
-  .post-hates {
+  .post-dislikes {
     .vote-icon {
     .vote-icon {
       background-color: @red;
       background-color: @red;
     }
     }

+ 72 - 88
static/cranefly/css/cranefly/markdown.less

@@ -1,7 +1,7 @@
 // Markdown output
 // Markdown output
 // -------------------------
 // -------------------------
 
 
-.markdown {
+.markdown, .markdown article {
   margin: 0px;
   margin: 0px;
   overflow: auto;
   overflow: auto;
 
 
@@ -11,22 +11,26 @@
 
 
   &>:last-child {
   &>:last-child {
     margin-bottom: 0px;
     margin-bottom: 0px;
+
+    img:last-child {
+      margin-bottom: 0px;
+    }
   }
   }
 
 
   h1 {
   h1 {
-    font-size: @baseFontSize * 2;
+    font-size: @baseFontSize * 1.7;
   }
   }
 
 
   h2 {
   h2 {
-    font-size: @baseFontSize * 1.7;
+    font-size: @baseFontSize * 1.5;
   }
   }
 
 
   h3 {
   h3 {
-    font-size: @baseFontSize * 1.5;
+    font-size: @baseFontSize * 1.2;
   }
   }
 
 
   h4 {
   h4 {
-    font-size: @baseFontSize * 1.2;
+    font-size: @baseFontSize;
   }
   }
 
 
   hr {
   hr {
@@ -36,38 +40,25 @@
   }
   }
 
 
   blockquote {
   blockquote {
-    background-color: darken(@postBackground, 1%);
-    border: 1px solid darken(@postBackground, 6%);
-    border-radius: @baseBorderRadius;
-    padding: @baseFontSize;
+    border-left-color: darken(@grayLighter, 5%);
+    padding: (@baseFontSize / 3) @baseFontSize;
 
 
-    &>h3:first-child {
-      margin: 0px;
-      margin-bottom: @baseLineHeight / 2;
-      padding: 0px;
+    header {
+      padding-bottom: (@baseLineHeight / 2);
 
 
-      font-size: @baseFontSize;
+      font-size: @baseFontSize * 1.1;
+      font-weight: bold;
       line-height: @baseLineHeight;
       line-height: @baseLineHeight;
     }
     }
 
 
-    &>:first-child {
-      margin-top: 0px;
-    }
-
-    &>:last-child {
-      margin-bottom: 0px;
-    }
-
     p {
     p {
-      margin: 0 0 @baseLineHeight / 2;
-      font-size: @fontSizeSmall;
+      margin: 0 0 (@baseLineHeight / 2);
+
+      font-size: @baseFontSize;
     }
     }
 
 
     blockquote {
     blockquote {
-      background-color: @postBackground;
-      border: 1px solid darken(@postBackground, 5%);
-      border-radius: @baseBorderRadius;
-      padding: @baseFontSize;
+      .opacity(85);
     }
     }
   }
   }
 
 
@@ -93,70 +84,11 @@
   }
   }
 
 
   img {
   img {
+    background-color: @white;
     border-radius: @baseBorderRadius;
     border-radius: @baseBorderRadius;
-    box-shadow: 0px 0px 4px @gray;
     margin: (@baseLineHeight / 2) 0px;
     margin: (@baseLineHeight / 2) 0px;
   }
   }
 
 
-  .md-img {
-    overflow: auto;
-
-    .md-img-span {
-      margin: (@baseLineHeight / 2) 0px;
-      float: none;
-
-      .md-img-wrap {
-        background-color: @grayLighter;
-        border: 1px solid @white;
-        border-radius: @baseBorderRadius + 1px;
-        box-shadow: 0px 0px 2px @grayLight;
-        margin: 3px;
-
-        .md-img-bg {
-          background-color: @white;
-          border-radius: @baseBorderRadius;
-          padding: (@baseLineHeight / 2);
-          
-          text-align: center;
-
-          img {
-            border-radius: @baseBorderRadius;
-            box-shadow: none;
-          }
-
-          .md-img-error {
-            background: url('../img/img_broken.jpg');
-            border-radius: @baseBorderRadius;
-            padding: (@baseLineHeight * 2.5) 0px;
-
-            span {
-              background-color: @grayDark;
-              border-radius: @borderRadiusLarge;
-              .opacity(80);
-              padding: (@baseFontSize / 2) @baseFontSize;
-              margin: 0px auto;
-
-              color: @white;
-              text-shadow: 0px 1px 0px @black;
-            }
-          }
-        }
-
-        .md-img-label {
-          display: block;
-          padding: (@baseFontSize / 2) @baseFontSize;
-
-          color: @grayDark;
-
-          &:hover, &:active {
-            color: @textColor;
-            text-decoration: none;
-          }
-        }
-      }
-    }
-  }
-
   // Blocks margins
   // Blocks margins
   pre, blockquote, iframe {
   pre, blockquote, iframe {
     margin-top: @baseLineHeight;
     margin-top: @baseLineHeight;
@@ -170,4 +102,56 @@
       margin-bottom: 0px;
       margin-bottom: 0px;
     }
     }
   }
   }
+
+  // Emoticons
+  .emoji {
+    background: none;
+    border-radius: 0px;
+    margin: 0px;
+    vertical-align: middle;
+  }
+
+  h1 {
+    .emoji {
+      width: (@baseFontSize * 1.7) + 4px;
+      height: (@baseFontSize * 1.7) + 4px;
+    }
+  }
+
+  h2 {
+    .emoji {
+      width: (@baseFontSize * 1.5) + 4px;
+      height: (@baseFontSize * 1.5) + 4px;
+    }
+  }
+
+  h3 {
+    .emoji {
+      width: (@baseFontSize * 1.2) + 4px;
+      height: (@baseFontSize * 1.2) + 4px;
+    }
+  }
+
+  h4 {
+    .emoji {
+      width: @baseFontSize + 4px;
+      height: @baseFontSize + 4px;
+    }
+  }
+
+  p {
+    .emoji {
+      width: @baseFontSize + 4px;
+      height: @baseFontSize + 4px;
+    }
+  }
+
+  blockquote {
+    p {
+      .emoji {
+        width: @fontSizeSmall + 2px;
+        height: @fontSizeSmall + 2px;
+      }
+    }
+  }
 }
 }

+ 5 - 1
static/cranefly/css/cranefly/messages.less

@@ -25,7 +25,7 @@
   a:active, a:hover {
   a:active, a:hover {
     .opacity(100);
     .opacity(100);
   }
   }
-
+  
   .alert-info {
   .alert-info {
     text-shadow: 0px 1px 0px darken(@infoBorder, 10%);
     text-shadow: 0px 1px 0px darken(@infoBorder, 10%);
   }
   }
@@ -34,6 +34,10 @@
     text-shadow: 0px 1px 0px darken(@successBorder, 10%);
     text-shadow: 0px 1px 0px darken(@successBorder, 10%);
   }
   }
 
 
+  .alert-warning {
+    text-shadow: 0px 1px 0px darken(@warningBorder, 10%);
+  }
+
   .alert-error {
   .alert-error {
     text-shadow: 0px 1px 0px darken(@errorBorder, 10%);    
     text-shadow: 0px 1px 0px darken(@errorBorder, 10%);    
   }
   }

+ 158 - 2
static/cranefly/css/cranefly/navbar.less

@@ -37,9 +37,14 @@
 
 
       color: @textColor;
       color: @textColor;
 
 
-      input {
+      &.search-disabled {
+        .opacity(60);
+      }
+
+      input, input:disabled {
         border: none;
         border: none;
         .box-shadow(none);
         .box-shadow(none);
+        background: none;
         margin: 0px;
         margin: 0px;
       }
       }
 
 
@@ -128,7 +133,7 @@
 
 
             img {
             img {
               border-radius: @baseBorderRadius;
               border-radius: @baseBorderRadius;
-              margin-right: 4px;
+              margin-right: 6px;
               width: 32px;
               width: 32px;
               height: 32px;
               height: 32px;
               position: relative;
               position: relative;
@@ -139,6 +144,157 @@
       }
       }
     }
     }
 
 
+    .navbar-compact {
+      display: none;
+      
+      li {
+        &.user-profile {
+          &>a {
+            &:link, &:visited {
+              .border-radius(0);
+              margin-top: 0px;
+              padding: @baseFontSize;
+              padding-top: 10px;
+              padding-bottom: 8px;
+
+              img {
+                margin-right: 0px;
+                margin-left: 6px;
+              }
+
+              .caret-border {
+                background-color: @bodyBackground;
+                border: 1px solid @navbarBorder;
+                .border-radius(2px);
+                margin-left: 8px;
+                padding: 0px 4px;
+
+                .caret {
+                  margin: 0px;
+                  padding: 0px;
+                  position: relative;
+                  top: 13px;
+                }
+              }
+            }
+
+            &:hover, &:active {
+              background-color: @bodyBackground;
+
+              .caret-border {
+                border-color: @grayLight;
+              }
+            }
+          }
+
+          &.open .dropdown-toggle {
+            &:link, &:visited, &:hover, &:focus {
+              background-color: @bodyBackground;
+
+              .caret-border {
+                background: @red;
+                border-color: @red;
+
+                .caret {
+                  border-top-color: @white;
+                }
+              }
+            }
+          }
+
+          .dropdown-menu {
+            border: none;
+            .border-radius(2px);
+            border-top: 4px solid @red;
+            .box-shadow(0px 3px 4px @grayLight);
+            margin: 0px;
+            margin-top: -8px;
+            margin-right: 1px;
+            padding: 4px 0px;
+            width: 270px;
+
+            &:before {
+              display: none;
+            }
+
+            &:after {
+              border-bottom: 6px solid @red;
+              margin-top: -3px;
+              margin-right: 11px;
+            }
+
+            &>li {
+              margin: 0px;
+              padding: 0px;
+
+              .label {
+                float: right;
+                margin: 0px;
+                margin-top: 2px;
+              }
+
+              a, .btn-link {
+                .border-radius(0);
+                clear: none;
+                display: block;
+                float: none;
+                margin: 0px;
+                padding: 6px 12px;
+
+                color: @textColor;
+                font-weight: normal;
+                text-align: left;
+                
+                i {
+                  .opacity(100);
+                }
+
+                &:link, &:active, &:visited, &:hover {
+                  .label {
+                    background-color: @red;
+                    float: right;
+
+                    color: @white;
+                    text-shadow: 0px 1px 0px darken(@red, 20%);
+                  }
+                }
+              }
+
+              a:link, a:visited {
+                .opacity(80);
+              }
+
+              a:hover {
+                background-color: @gray;
+                .opacity(100);
+
+                color: @white;
+              }
+
+              .btn-link {
+                background: none;
+                border: none;
+                width: 100%;
+
+                &:hover, &:active {
+                  background: @red;
+                  .opacity(100);
+
+                  color: @white;
+                  text-shadow: 0px 1px 0px darken(@red, 20%);
+                }
+
+                i {
+                  position: relative;
+                  top: 0px;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
     .navbar-user-nav {
     .navbar-user-nav {
       li {
       li {
         .btn {
         .btn {

+ 79 - 0
static/cranefly/css/cranefly/report.less

@@ -0,0 +1,79 @@
+// Report view
+// ------------------------
+
+.report-view {
+  .report-wrapper {
+    background-color: @grayLighter;
+    background-image: -webkit-gradient(linear, 0 0, 100% 100%,
+                color-stop(.25, rgba(255, 255, 255, .2)), color-stop(.25, transparent),
+                color-stop(.5, transparent), color-stop(.5, rgba(255, 255, 255, .2)),
+                color-stop(.75, rgba(255, 255, 255, .2)), color-stop(.75, transparent),
+                to(transparent));
+    background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, .2) 25%, transparent 25%,
+              transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%,
+              transparent 75%, transparent);
+    background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, .2) 25%, transparent 25%,
+              transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%,
+              transparent 75%, transparent);
+    background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, .2) 25%, transparent 25%,
+              transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%,
+              transparent 75%, transparent);
+    background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, .2) 25%, transparent 25%,
+              transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%,
+              transparent 75%, transparent);
+    background-image: linear-gradient(-45deg, rgba(255, 255, 255, .2) 25%, transparent 25%,
+              transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%,
+              transparent 75%, transparent);
+    -webkit-background-size: 10px 10px;
+    -moz-background-size: 10px 10px;
+    background-size: 10px 10px;
+    .border-radius(6px);
+    padding: 8px;
+    margin-bottom: 8px;
+
+    .post-body {
+      margin-bottom: 0px;
+
+      .report-actions {
+        border-left: none !important;
+        float: left !important;
+        padding: 0px !important;
+
+        form:first-child .btn {
+          .border-radius(0px 0px 0px @baseBorderRadius) !important;
+        }
+
+        .btn {
+          .opacity(90) !important;
+          margin: 0px !important;
+          padding: 8px 12px !important;
+
+          color: @white !important;
+          font-weight: bold !important;
+
+          i {
+            background-image: url("@{iconWhiteSpritePath}");
+            position: relative;
+            top: 0px;
+          }
+
+          &:hover, &:active, &:focus {
+            .opacity(100) !important;
+
+            text-decoration: none !important;
+          }
+
+          &.btn-resolve {
+            background-color: darken(@green, 5%);
+            text-shadow: 0px 1px 0px darken(@green, 20%);
+          }
+
+          &.btn-bogus {
+            background-color: @grayDark;
+            text-shadow: 0px 1px 0px darken(@grayDark, 20%);
+          }
+        }
+      }
+    }
+  }
+}

+ 54 - 0
static/cranefly/css/cranefly/reports.less

@@ -0,0 +1,54 @@
+// Reports list
+// ------------------------
+
+.reports-list {
+  .thread-label {
+    overflow: visible;
+
+    .report-label {
+      .border-radius(3px);
+      float: right;
+      padding: 3px 8px;
+      position: relative;
+      right: -32px;
+      bottom: 2px;
+
+      color: @white;
+      font-weight: bold;
+
+      i {
+        background-image: url("@{iconWhiteSpritePath}");
+      }
+
+      &.report-open {
+        background-color: @orange;
+        text-shadow: 0px 1px 0px darken(@orange, 20%);
+      }
+
+      &.report-resolved {
+        background-color: @green;
+        text-shadow: 0px 1px 0px darken(@green, 20%);
+      }
+
+      &.report-bogus {
+        background-color: @gray;
+        text-shadow: 0px 1px 0px darken(@gray, 20%);
+      }
+    }
+  }
+
+  .thread-name {
+    .report-id {
+      color: @grayLight !important;
+    }
+  }
+
+  .thread-name, .thread-details {
+    margin-left: 0px !important;
+  }
+
+  .thread-icon {
+    display: none !important;
+    float: none;
+  }
+}

+ 18 - 11
static/cranefly/css/cranefly/scaffolding.less

@@ -9,11 +9,11 @@ html, body {
   min-height: 100%;
   min-height: 100%;
   height: auto !important;
   height: auto !important;
   height: 100%;
   height: 100%;
-  margin: 0 auto ((@footerHeight + @baseLineHeight) * -1);
+  margin: 0 auto ((@footerHeight * -1) - @baseFontSize + 2px);
 
 
   .container-primary {
   .container-primary {
     padding-top: @baseLineHeight;
     padding-top: @baseLineHeight;
-    padding-bottom: @footerHeight + (@footerHeight * 0.5);
+    padding-bottom: @footerHeight + (@baseLineHeight * 1.5);
 
 
     .page-description {
     .page-description {
       margin-bottom: @baseLineHeight;
       margin-bottom: @baseLineHeight;
@@ -33,18 +33,25 @@ footer {
   border-top: 1px solid @footerBorder;
   border-top: 1px solid @footerBorder;
   height: @footerHeight;
   height: @footerHeight;
   padding: @paddingLarge;
   padding: @paddingLarge;
+  padding-bottom: 0px;
 
 
-  hr {
-    border-bottom: 1px solid @footerBorder;
-    margin: (@baseLineHeight / 2) 0px;
-  }
+  .container {
+    hr {
+      border-bottom: 1px solid @footerBorder;
+      margin: (@baseLineHeight / 2) 0px;
+    }
+
+    .credits {
+      p {
+        margin-bottom: 0px;
 
 
-  .credits {
-    color: @gray;
-    font-size: 90%;
+        color: @gray;
+        font-size: 90%;
 
 
-    a:link, a:active, a:visited, a:hover {
-      color: @gray;
+        a:link, a:active, a:visited, a:hover {
+          color: @gray;
+        }
+      }
     }
     }
   }
   }
 }
 }

+ 63 - 0
static/cranefly/css/cranefly/search.less

@@ -0,0 +1,63 @@
+// Search forum
+// ------------------------
+
+.search-resume {
+  .muted {
+    color: lighten(@gray, 15%);
+
+    a {
+      color: @textColor;
+    }
+  }
+}
+
+.search-results {
+  .results-list {
+    .result {
+      border-bottom: 1px solid darken(@bodyBackground, 5%);
+      margin-bottom: (@baseLineHeight / 2);
+      padding-bottom: (@baseLineHeight / 2);
+
+      h3 {
+        margin: 0px;
+
+        line-height: @baseLineHeight;
+
+        a:link, a:visited {
+          color: @gray;
+          font-weight: normal;
+          font-size: @baseFontSize * 1.3;
+          text-decoration: underline;
+        }
+
+        a:hover, a:active {
+          color: @textColor;
+        }
+      }
+
+      .post-extra {
+        margin: 0px;
+
+        color: @grayLight;
+        font-size: @baseFontSize * 0.8;
+        line-height: @baseLineHeight;
+
+        a {
+          color: @textColor;
+        }
+      }
+
+      .post-preview {
+        margin: 0px;
+
+        color: @textColor;
+        font-size: @baseFontSize * 1.1;
+        line-height: @baseLineHeight;
+
+        strong {
+          color: @red;
+        }
+      }
+    }
+  }
+}

+ 71 - 16
static/cranefly/css/cranefly/thread.less

@@ -268,6 +268,38 @@
             form {
             form {
               margin: 0px;
               margin: 0px;
               padding: 0px;
               padding: 0px;
+
+              .btn {
+                float: right;
+                margin: 0px;
+                margin-left: @baseFontSize;
+                .opacity(100);
+                padding: 0px;
+
+                color: @grayLight;
+                font-weight: normal;
+
+                &:hover, &:active, &:focus {
+                  color: @red;
+                  text-decoration: underline;
+                }
+
+                &.btn-report {
+                  &:disabled {
+                    color: @green;
+                  }
+                }
+
+                &.btn-hide {
+                  &:hover, &:active, &:focus {
+                    color: @orange;
+                  }
+                }
+              }
+
+              &:first-child .btn {
+                margin-left: 0px;
+              }
             }
             }
 
 
             a {
             a {
@@ -291,22 +323,6 @@
                 }
                 }
               }
               }
             }
             }
-
-            .btn {
-              float: right;
-              margin: 0px;
-              margin-left: @baseFontSize;
-              .opacity(100);
-              padding: 0px;
-
-              color: @grayLight;
-              font-weight: normal;
-
-              &:hover, &:active, &:focus {
-                color: @red;
-                text-decoration: underline;
-              }
-            }
           }
           }
         }
         }
       }
       }
@@ -356,6 +372,14 @@
 
 
       margin-bottom: @baseLineHeight;
       margin-bottom: @baseLineHeight;
 
 
+      &.checkpoint-deleted {
+        .opacity(30);
+
+        &:hover {
+          .opacity(60);
+        }
+      }
+
       hr {
       hr {
         background-color: @grayLight;
         background-color: @grayLight;
         background-image: -webkit-gradient(linear, 0 0, 100% 100%,
         background-image: -webkit-gradient(linear, 0 0, 100% 100%,
@@ -399,6 +423,37 @@
         i {
         i {
           .opacity(43);
           .opacity(43);
         }
         }
+
+        form {
+          display: inline-block;
+          margin: 0px;
+          margin-top: -3px;
+          margin-left: @baseFontSize / 2;
+          padding: 0px;
+
+          .btn {
+            margin-top: -2px;
+            padding: 0px;
+
+            font-weight: normal;
+
+            &:active, &:hover {
+              text-decoration: underline;
+
+              &.btn-show, &.btn-hide {
+                color: @orange;
+              }
+
+              &.btn-delete {
+                color: @red;
+              }
+            }
+          }
+        }
+
+        form:first-of-type {
+          margin-left: @baseFontSize;
+        }
       }
       }
     }
     }
   }
   }

+ 20 - 28
static/cranefly/css/cranefly/watchedthreads.less

@@ -2,37 +2,29 @@
 // -------------------------
 // -------------------------
 
 
 .watched-threads {
 .watched-threads {
-  table {
-    .watched-thread-flags {
-      overflow: auto;
-
-      form {
-        float: right;
-        margin: 0px;
-        padding: 0px;
+  .thread-last-reply {
+    border-left: none !important;
+    padding-left: 0px !important;
+  }
 
 
-        .btn {
-          float: right;
-          padding: 3px 5px;
-          padding-bottom: 0px;
-          margin-right: @baseFontSize + 2px;
-        }
-      }
-    }
-    
-    .thread-replies {
-      color: @grayLight;
-      text-align: right;
-    }
+  .thread-options {
+    float: right;
+    overflow: auto;
+    position: relative;
+    top: 8px;
 
 
-    .thread-forum {
-      a:link, a:visited {
-        color: @gray;
-        font-weight: bold;
-      }
+    form {
+      display: inline-block;
+      float: left;
+      margin: 0px;
+      padding: 0px;
+      overflow: auto;
 
 
-      a:active, a:hover {
-        color: @textColor;
+      .btn {
+        float: right;
+        padding: 3px 5px;
+        padding-bottom: 0px;
+        margin-left: @baseFontSize + 2px;
       }
       }
     }
     }
   }
   }

+ 12 - 3
static/cranefly/css/variables.less

@@ -86,6 +86,15 @@
 @itemNewColor:                      @red;
 @itemNewColor:                      @red;
 @itemMovedColor:                    lighten(@purple, 10%);
 @itemMovedColor:                    lighten(@purple, 10%);
 
 
+// Thread flags colors
+// -------------------------
+@flagReported:                      @orange;
+@flagReviewed:                      @purple;
+@flagAnnouncement:                  @blue;
+@flagSticky:                        @bluePale;
+@flagDeleted:                       @grayDark;
+@flagClosed:                        @red;
+
 // Board index forums list
 // Board index forums list
 // -------------------------
 // -------------------------
 @categoryBackground:                lighten(@bodyBackground, 5%);
 @categoryBackground:                lighten(@bodyBackground, 5%);
@@ -93,7 +102,7 @@
 @categoryHeader:                    @bodyBackground;
 @categoryHeader:                    @bodyBackground;
 @categoryShadow:                    darken(@bodyBackground, 5%);
 @categoryShadow:                    darken(@bodyBackground, 5%);
 
 
-@forumIconSize:                     4px;
+@forumIconSize:                     24px;
 
 
 // Posts
 // Posts
 // -------------------------
 // -------------------------
@@ -248,7 +257,7 @@
 
 
 // Footer
 // Footer
 // -------------------------
 // -------------------------
-@footerHeight:                        80px;
+@footerHeight:                        90px;
 @footerBackground:                    darken(@bodyBackground, 5%);
 @footerBackground:                    darken(@bodyBackground, 5%);
 @footerBorder:                        darken(@footerBackground, 8%);
 @footerBorder:                        darken(@footerBackground, 8%);
 
 
@@ -269,7 +278,7 @@
 // Form states and alerts
 // Form states and alerts
 // -------------------------
 // -------------------------
 @warningText:             #FFF;
 @warningText:             #FFF;
-@warningBackground:       #fcf8e3;
+@warningBackground:       desaturate(@orange, 10%);
 @warningBorder:           darken(spin(@warningBackground, -10), 3%);
 @warningBorder:           darken(spin(@warningBackground, -10), 3%);
 
 
 @errorText:               @red;
 @errorText:               @red;

BIN
static/cranefly/img/img_broken.jpg


+ 194 - 173
static/cranefly/js/cranefly.js

@@ -1,195 +1,216 @@
 $(function () {
 $(function () {
-	// Register tooltips
-	$('.tooltip-top').tooltip({placement: 'top', container: 'body'})
-	$('.tooltip-bottom').tooltip({placement: 'bottom', container: 'body'})
-	$('.tooltip-left').tooltip({placement: 'left', container: 'body'})
-	$('.tooltip-right').tooltip({placement: 'right', container: 'body'})
-	
-	// Register popovers
-	$('.popover-top').popover({placement: 'top'})
-	$('.popover-bottom').popover({placement: 'bottom'})
-	$('.popover-left').popover({placement: 'left'})
-	$('.popover-right').popover({placement: 'right'})
+  // Register tooltips
+  $('.tooltip-top').tooltip({placement: 'top', container: 'body'})
+  $('.tooltip-bottom').tooltip({placement: 'bottom', container: 'body'})
+  $('.tooltip-left').tooltip({placement: 'left', container: 'body'})
+  $('.tooltip-right').tooltip({placement: 'right', container: 'body'})
+  
+  // Register popovers
+  $('.popover-top').popover({placement: 'top'})
+  $('.popover-bottom').popover({placement: 'bottom'})
+  $('.popover-left').popover({placement: 'left'})
+  $('.popover-right').popover({placement: 'right'})
 
 
-	// Dont fire popovers on touch devices
-	$("[class^='tooltip-']").on('show', function (e) {
-	  if ('ontouchstart' in document.documentElement) {
-	  	e.preventDefault();
-	  }
-	});
-	
-	// Start all dropdowns
-	$('.dropdown-toggle').dropdown()
-	
-	// Dont hide clickable dropdowns
-	$('.dropdown-clickable').on('click', function (e) {
-	  e.stopPropagation()
-	});
-	
-	// Checkbox Group Master
-	$('input.checkbox-master').live('click', function(){
-		if($(this).is(':checked')){
-			$('input.checkbox-member').attr("checked" ,"checked");
-		}
-		else
-		{
-			$('input.checkbox-member').removeAttr('checked');
-		}
-	});
-	
-	// Checkbox Group Member
-	$('input.checkbox-member').live('click', function(){
-		if(!$(this).is(':checked')){
-			$('input.checkbox-master').removeAttr('checked');
-		}
-	});
-	
-	// Check Confirmation on links
-	$('a.confirm').live('click', function(){
-		var decision = confirm(jQuery.data(this, 'jsconfirm'));
-		return decision
-	});
-	
-	// Check Confirmation on forms
-	$('form.confirm').live('submit', function(){
-		data = $(this).data();
-		var decision = confirm(data.jsconfirm);
-		return decision
-	});
-	
-	// Show go back link?
-	if (document.referrer
-			&& document.referrer.indexOf(location.protocol + "//" + location.host) === 0
-			&& document.referrer != document.url) {
-		$('.go-back').show();
-	}
+  // Dont fire popovers on touch devices
+  $("[class^='tooltip-']").on('show', function (e) {
+    if ('ontouchstart' in document.documentElement) {
+      e.preventDefault();
+    }
+  });
+  
+  // Start all dropdowns
+  $('.dropdown-toggle').dropdown()
+  
+  // Dont hide clickable dropdowns
+  $('.dropdown-clickable').on('click', function (e) {
+    e.stopPropagation()
+  });
+  
+  // Checkbox Group Master
+  $('input.checkbox-master').live('click', function(){
+    if($(this).is(':checked')){
+      $('input.checkbox-member').attr("checked" ,"checked");
+    }
+    else
+    {
+      $('input.checkbox-member').removeAttr('checked');
+    }
+  });
+  
+  // Checkbox Group Member
+  $('input.checkbox-member').live('click', function(){
+    if(!$(this).is(':checked')){
+      $('input.checkbox-master').removeAttr('checked');
+    }
+  });
+  
+  // Check Confirmation on links
+  $('a.confirm').live('click', function(){
+    var decision = confirm(jQuery.data(this, 'jsconfirm'));
+    return decision
+  });
+  
+  // Check Confirmation on forms
+  $('form.confirm').live('submit', function(){
+    data = $(this).data();
+    var decision = confirm(data.jsconfirm);
+    return decision
+  });
+  
+  // Show go back link?
+  if (document.referrer
+      && document.referrer.indexOf(location.protocol + "//" + location.host) === 0
+      && document.referrer != document.url) {
+    $('.go-back').show();
+  }
 
 
-	// Go back one page
-	$('.go-back').on('click', function (e) {
-	    history.go(-1)
-	    return false;
-	})
+  // Go back one page
+  $('.go-back').on('click', function (e) {
+      history.go(-1)
+      return false;
+  })
 })
 })
 
 
 function EnhancePostsMD() {
 function EnhancePostsMD() {
-	$(function () {
-		// Add labels to images
-		$('.markdown.js-extra img').each(function() {
-	    $(this).addClass('img-rounded');
-	    $(this).wrap(function() { return '<div class="md-img" />'; });
-	    $(this).wrap(function() { return '<div class="span5 md-img-span" />'; });
-	    $(this).wrap(function() { return '<div class="md-img-wrap" />'; });
-	    $(this).after('<a href="' + $(this).attr('src') + '" class="md-img-label" target="_blank">' + $(this).attr('alt') + '</a>');
-	    $(this).wrap(function() { return '<div class="md-img-bg" />'; });
-		});
+  $(function () {
+    // Add labels to images
+    $('.markdown.js-extra img').not('.emoji').each(function() {
+      $(this).addClass('img-rounded');
+      if ($(this).attr('alt').length > 0 && $(this).attr('alt') != $(this).attr('src')) {
+        $(this).attr('title', $(this).attr('alt'));
+        $(this).tooltip({placement: 'top', container: 'body'});
+      }
+    });
 
 
-		// Handle prokened images
-	  $('.markdown.js-extra img').one('error', function() {
-	  	$(this).after('<div class="md-img-error"><span>' + l_img_broken_msg + '</span></div>');
-	  	$(this).hide();
-		});
-
-		// Automagically turn links into players
-		var players = new Array();
-		$('.markdown.js-extra a').each(function() {
-			match = link2player($.trim($(this).text()));
-			if (match && $.inArray(match, players) == -1) {
-				players.push(match);
-				$(this).replaceWith(match);
-				if (players.length == 10) {
-					return false;
-				}
-			}
-		});
-	});
+    // Automagically turn links into players
+    var players = new Array();
+    $('.markdown.js-extra').each(function() {
+      var post_players = 0;
+      $(this).find('a').each(function() {
+        match = link2player($.trim($(this).text()));
+        if (match && $.inArray(match, players) == -1 && players.length < 16 && post_players < 4) {
+          players.push(match);
+          post_players ++;
+          $(this).replaceWith(match);
+        }
+      });
+    });
+  });
 }
 }
 
 
 // Turn link to player
 // Turn link to player
 function link2player(link_href) {
 function link2player(link_href) {
-	// Youtube link
-	var re = /watch\?v=((\w|-)+)/;
-	if (re.test(link_href)) {
-		media_url = link_href.match(re);
-		return '<iframe width="480" height="360" src="http://www.youtube.com/embed/' + media_url[1] + '" frameborder="0" allowfullscreen></iframe>';
-	}
+  // Youtube link
+  var re = /watch\?v=((\w|-)+)/;
+  if (re.test(link_href)) {
+    media_url = link_href.match(re);
+    return '<iframe width="480" height="360" src="http://www.youtube.com/embed/' + media_url[1] + '" frameborder="0" allowfullscreen></iframe>';
+  }
 
 
-	// Youtube feature=embed
-	var re = /watch\?feature=player_embedded&v=((\w|-)+)/;
-	if (re.test(link_href)) {
-		media_url = link_href.match(re);
-		return '<iframe width="480" height="360" src="http://www.youtube.com/embed/' + media_url[1] + '" frameborder="0" allowfullscreen></iframe>';
-	}
+  // Youtube feature=embed
+  var re = /watch\?feature=player_embedded&v=((\w|-)+)/;
+  if (re.test(link_href)) {
+    media_url = link_href.match(re);
+    return '<iframe width="480" height="360" src="http://www.youtube.com/embed/' + media_url[1] + '" frameborder="0" allowfullscreen></iframe>';
+  }
 
 
-	// Youtube embed with start time
-	var re = /youtu.be\/((\w|-)+)\?t=([A-Za-z0-9]+)/;
-	if (re.test(link_href)) {
-		media_url = link_href.match(re);
-		media_minutes = media_url[2].match(/([0-9]+)m/);
-		media_seconds = media_url[2].match(/([0-9]+)s/);
-		media_url[2] = 0;
-		if (media_minutes) { media_url[2] += (media_minutes[1] - 0) * 60; }
-		if (media_seconds) { media_url[2] += (media_seconds[1] - 0); }
-		return '<iframe width="480" height="360" src="http://www.youtube.com/embed/' + media_url[1] + '?start=' + media_url[2] + '" frameborder="0" allowfullscreen></iframe>';
-	}
-	
-	// Youtube embed
-	var re = /youtu.be\/((\w|-)+)/;
-	if (re.test(link_href)) {
-		media_url = link_href.match(re);
-		return '<iframe width="480" height="360" src="http://www.youtube.com/embed/' + media_url[1] + '" frameborder="0" allowfullscreen></iframe>';
-	}
+  // Youtube embed with start time
+  var re = /youtu.be\/((\w|-)+)\?t=([A-Za-z0-9]+)/;
+  if (re.test(link_href)) {
+    media_url = link_href.match(re);
+    media_minutes = media_url[2].match(/([0-9]+)m/);
+    media_seconds = media_url[2].match(/([0-9]+)s/);
+    media_url[2] = 0;
+    if (media_minutes) { media_url[2] += (media_minutes[1] - 0) * 60; }
+    if (media_seconds) { media_url[2] += (media_seconds[1] - 0); }
+    return '<iframe width="480" height="360" src="http://www.youtube.com/embed/' + media_url[1] + '?start=' + media_url[2] + '" frameborder="0" allowfullscreen></iframe>';
+  }
+  
+  // Youtube embed
+  var re = /youtu.be\/((\w|-)+)/;
+  if (re.test(link_href)) {
+    media_url = link_href.match(re);
+    return '<iframe width="480" height="360" src="http://www.youtube.com/embed/' + media_url[1] + '" frameborder="0" allowfullscreen></iframe>';
+  }
 
 
-	// Vimeo link
-	var re = /vimeo.com\/([0-9]+)/;
-	if (re.test(link_href)) {
-		media_url = link_href.match(re);
-		return '<iframe src="http://player.vimeo.com/video/' + media_url[1] + '?color=CF402E" width="500" height="281" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>';
-	}
+  // Vimeo link
+  var re = /vimeo.com\/([0-9]+)/;
+  if (re.test(link_href)) {
+    media_url = link_href.match(re);
+    return '<iframe src="http://player.vimeo.com/video/' + media_url[1] + '?color=CF402E" width="500" height="281" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>';
+  }
 
 
-	// No link
-	return false;
+  // No link
+  return false;
 }
 }
 
 
 // Ajax: Post votes
 // Ajax: Post votes
 $(function() {
 $(function() {
-	$('.post-rating-actions').each(function() {
-		var action_parent = this;
-		var csrf_token = $(this).find('input[name="_csrf_token"]').val();
-		$(this).find('form').submit(function() {
-			var form = this;
-			$.post(this.action, {'_csrf_token': csrf_token}, "json").done(function(data, textStatus, jqXHR) {
-				// Reset stuff and set classess
-				$(action_parent).find('.post-score').removeClass('post-score-good post-score-bad');
-				if (data.score_total > 0) {
-					$(action_parent).find('.post-score-total').addClass('post-score-good');
-				} else if (data.score_total < 0) {
-					$(action_parent).find('.post-score-total').addClass('post-score-bad');
-				} 
-				if (data.score_upvotes > 0) {
-					$(action_parent).find('.post-score-upvotes').addClass('post-score-good');
-				}
-				if (data.score_downvotes > 0) {
-					$(action_parent).find('.post-score-downvotes').addClass('post-score-bad');
-				}
+  $('.post-rating-actions').each(function() {
+    var action_parent = this;
+    var csrf_token = $(this).find('input[name="_csrf_token"]').val();
+    $(this).find('form').submit(function() {
+      var form = this;
+      $.post(this.action, {'_csrf_token': csrf_token}, "json").done(function(data, textStatus, jqXHR) {
+        // Reset stuff and set classess
+        $(action_parent).find('.post-score').removeClass('post-score-good post-score-bad');
+        if (data.score_total > 0) {
+          $(action_parent).find('.post-score-total').addClass('post-score-good');
+        } else if (data.score_total < 0) {
+          $(action_parent).find('.post-score-total').addClass('post-score-bad');
+        } 
+        if (data.score_upvotes > 0) {
+          $(action_parent).find('.post-score-upvotes').addClass('post-score-good');
+        }
+        if (data.score_downvotes > 0) {
+          $(action_parent).find('.post-score-downvotes').addClass('post-score-bad');
+        }
 
 
-				// Set votes
-				$(action_parent).find('.post-score-total').text(data.score_total);
-				$(action_parent).find('.post-score-upvotes').text(data.score_upvotes);
-				$(action_parent).find('.post-score-downvotes').text(data.score_downvotes);
+        // Set votes
+        $(action_parent).find('.post-score-total').text(data.score_total);
+        $(action_parent).find('.post-score-upvotes').text(data.score_upvotes);
+        $(action_parent).find('.post-score-downvotes').text(data.score_downvotes);
 
 
-				// Disable and enable forms
-				if (data.user_vote == 1) {
-					$(action_parent).find('.form-upvote button').attr("disabled", "disabled");
-					$(action_parent).find('.form-downvote button').removeAttr("disabled");
-				} else {
-					$(action_parent).find('.form-upvote button').removeAttr("disabled");
-					$(action_parent).find('.form-downvote button').attr("disabled", "disabled");
-				}
-			}).fail(function() {
-				$(form).unbind();
-				$(form).trigger('submit');
-			});
-			return false;
-		});
-	});
+        // Disable and enable forms
+        if (data.user_vote == 1) {
+          $(action_parent).find('.form-upvote button').attr("disabled", "disabled");
+          $(action_parent).find('.form-downvote button').removeAttr("disabled");
+        } else {
+          $(action_parent).find('.form-upvote button').removeAttr("disabled");
+          $(action_parent).find('.form-downvote button').attr("disabled", "disabled");
+        }
+      }).fail(function() {
+        $(form).unbind();
+        $(form).trigger('submit');
+      });
+      return false;
+    });
+  });
 });
 });
+
+// Ajax: Post reports
+$(function() {
+  $('.form-report').each(function() {
+    var action_parent = this;
+    var csrf_token = $(this).find('input[name="_csrf_token"]').val();
+    var button = $(this).find('button');
+    $(this).submit(function() {
+      var form = this;
+      $.post(form.action, {'_csrf_token': csrf_token}, "json").done(function(data, textStatus, jqXHR) {        
+        $(button).text(l_post_reported);
+        $(button).tooltip('destroy');
+        $(button).attr("title", data.message);
+        $(button).tooltip({placement: 'top', container: 'body'});
+        $(button).tooltip("show");
+        $(button).attr("disabled", "disabled");
+        setTimeout(function() {
+          $(button).tooltip('hide');
+        }, 2500);
+      }).fail(function() {
+        $(form).unbind();
+        $(form).trigger('submit');
+      });
+      return false;
+    });
+  });
+});

+ 166 - 89
static/cranefly/js/editor.js

@@ -1,126 +1,203 @@
 // Basic editor functions
 // Basic editor functions
 function storeCaret(ftext) {    
 function storeCaret(ftext) {    
-    if (ftext.createTextRange) {
-        ftext.caretPos = document.selection.createRange().duplicate();
-    }
+  if (ftext.createTextRange) {
+    ftext.caretPos = document.selection.createRange().duplicate();
+  }
 }
 }
 
 
 function SelectionRange(start, end) {
 function SelectionRange(start, end) {
-    this.start = start;
-    this.end = end;
+  this.start = start;
+  this.end = end;
 }
 }
 
 
 function getSelection(textId) {
 function getSelection(textId) {
-    ctrl = document.getElementById(textId);
-    if (document.selection) {
-        ctrl.focus();
-        var range = document.selection.createRange();
-        var length = range.text.length;
-        range.moveStart('character', -ctrl.value.length);
-        return new SelectionRange(range.text.length - length, range.text.length);
-    } else if (ctrl.selectionStart || ctrl.selectionStart == '0') {
-        return new SelectionRange(ctrl.selectionStart, ctrl.selectionEnd);
-    }
+  ctrl = document.getElementById(textId);
+  if (document.selection) {
+    ctrl.focus();
+    var range = document.selection.createRange();
+    var length = range.text.length;
+    range.moveStart('character', -ctrl.value.length);
+    return new SelectionRange(range.text.length - length, range.text.length);
+  } else if (ctrl.selectionStart || ctrl.selectionStart == '0') {
+    return new SelectionRange(ctrl.selectionStart, ctrl.selectionEnd);
+  }
+}
+
+function getSelectionText(textId) {
+  var ctrl = document.getElementById(textId);
+  var text = ctrl.value;
+  myRange = getSelection(textId);
+  return $.trim(text.substring(myRange.start, myRange.end));
 }
 }
 
 
 function setSelection(textId, SelectionRange) {
 function setSelection(textId, SelectionRange) {
-    ctrl = document.getElementById(textId);
-    if (ctrl.setSelectionRange) {
-        ctrl.focus();
-        ctrl.setSelectionRange(SelectionRange.start, SelectionRange.end);
-    } else if (ctrl.createTextRange) {
-        var range = ctrl.createTextRange();
-        range.collapse(true);
-        range.moveStart('character', SelectionRange.start);
-        range.moveEnd('character', SelectionRange.end);
-        range.select();
-    }
+  ctrl = document.getElementById(textId);
+  if (ctrl.setSelectionRange) {
+    ctrl.focus();
+    ctrl.setSelectionRange(SelectionRange.start, SelectionRange.end);
+  } else if (ctrl.createTextRange) {
+    var range = ctrl.createTextRange();
+    range.collapse(true);
+    range.moveStart('character', SelectionRange.start);
+    range.moveEnd('character', SelectionRange.end);
+    range.select();
+  }
 }
 }
 
 
 function _makeWrap(textId, myRange, wrap_start, wrap_end) {
 function _makeWrap(textId, myRange, wrap_start, wrap_end) {
-    var ctrl = document.getElementById(textId);
-    var text = ctrl.value;
-    var startText = text.substring(0, myRange.start) + wrap_start;
-    var middleText = text.substring(myRange.start, myRange.end);
-    var endText = wrap_end + text.substring(myRange.end);
-    ctrl.value = startText + middleText + endText;
-    setSelection(textId, new SelectionRange(startText.length, startText.length + middleText.length));
+  var ctrl = document.getElementById(textId);
+  var text = ctrl.value;
+  var startText = text.substring(0, myRange.start) + wrap_start;
+  var middleText = text.substring(myRange.start, myRange.end);
+  var endText = wrap_end + text.substring(myRange.end);
+  ctrl.value = startText + middleText + endText;
+  setSelection(textId, new SelectionRange(startText.length, startText.length + middleText.length));
 }
 }
 
 
 function makeWrap(textId, wrap_start, wrap_end) {
 function makeWrap(textId, wrap_start, wrap_end) {
-    _makeWrap(textId, getSelection(textId), wrap_start, wrap_end);
+  _makeWrap(textId, getSelection(textId), wrap_start, wrap_end);
 }
 }
 
 
 function _makeReplace(textId, myRange, replacement) {
 function _makeReplace(textId, myRange, replacement) {
-    var ctrl = document.getElementById(textId);
-    var text = ctrl.value;
-    var startText = text.substring(0, myRange.start);
-    var middleText = text.substring(myRange.start, myRange.end);
-    var endText = text.substring(myRange.end);
-    ctrl.value = text.substring(0, myRange.start) + replacement + text.substring(myRange.end);
-    setSelection(textId, new SelectionRange(startText.length + middleText.length, startText.length + middleText.length));
+  var ctrl = document.getElementById(textId);
+  var text = ctrl.value;
+  var startText = text.substring(0, myRange.start);
+  var middleText = text.substring(myRange.start, myRange.end);
+  var endText = text.substring(myRange.end);
+  ctrl.value = text.substring(0, myRange.start) + replacement + text.substring(myRange.end);
+  setSelection(textId, new SelectionRange(startText.length + middleText.length, startText.length + middleText.length));
 }
 }
 
 
 function makeReplace(textId, replacement) {
 function makeReplace(textId, replacement) {
-    _makeReplace(textId, getSelection(textId), replacement);
+  _makeReplace(textId, getSelection(textId), replacement);
+}
+
+var url_pattern = new RegExp('^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$','i');
+function is_url(str) {
+  return url_pattern.test($.trim(str));
 }
 }
 
 
-// Nice editor functionality
+function extractor(query) {
+    var result = /([^\s]+)$/.exec(query);
+    if(result && result[1])
+        return result[1].trim();
+    return '';
+}
+
+// Small and nice editor functionality
 $(function() {
 $(function() {
   $('.editor-tools').fadeIn(600);
   $('.editor-tools').fadeIn(600);
-  
-  function get_textarea(obj) {
-      return $(obj).parent().parent().parent().parent().find('textarea');
-  }
-  
-  $('.editor-bold').click(function() {
-      ta = get_textarea(this).attr('id');
-      makeWrap(ta, '**', '**');
+  $('.editor').each(function() {
+    // Get textarea stuff
+    var textarea = $(this).find('textarea');
+    var textarea_id = $(textarea).attr('id');
+    
+    // Do we have emojis?
+    if (1==2 && ed_emojis.length > 1) {
+      var mode = 0;
+      var open = -1;
+
+      $(textarea).focusout(function() {
+        mode = 0;
+        open = -1;
+      });
+
+      $(textarea).keyup(function() {
+        text = $(textarea).val();
+        cursor = getSelection(textarea_id).start;
+        if (cursor > 0) {
+          // Read typed character and previous character
+          input = text.substring(cursor - 1, cursor);
+          if (cursor > 1) {
+            pre = text.substring(cursor - 2, cursor - 1);
+          } else {
+            pre = '';
+          }
+
+          // Act accordingly to current mode
+          if (mode == 0) {
+            if (input == ':' && !pre.match(/^[A-Za-z0-9]+$/i)) {
+              // Test passed, mode 1 engaged!
+              mode = 1;
+              open = cursor;
+            }
+          } else if (mode == 1) {
+            // Inside emoji mode, we are helping user enter emoji input
+            if (cursor > open && !input.match(/^(\+|\-|[_A-Za-z0-9])+$/i)) {
+              // Emoji fail
+              mode = 0;
+            }
+          }
+        }
+      });
+    }
+
+    // Handle buttons
+    $('.editor-bold').click(function() {
+      makeWrap(textarea_id, '**', '**');
       return false;
       return false;
-  });
-  
-  $('.editor-emphasis').click(function() {
-      ta = get_textarea(this).attr('id');
-      makeWrap(ta, '_', '_');
+    });
+    
+    $('.editor-emphasis').click(function() {
+      makeWrap(textarea_id, '*', '*');
       return false;
       return false;
-  });
-  
-  $('.editor-link').click(function() {
-      ta = get_textarea(this).attr('id');
-      var link_url = $.trim(prompt(ed_lang_enter_link_url));
-      if (link_url.length > 0) {
-          link_url = link_url.toLowerCase();
-          var pattern = /^(("[\w-+\s]+")|([\w-+]+(?:\.[\w-+]+)*)|("[\w-+\s]+")([\w-+]+(?:\.[\w-+]+)*))(@((?:[\w-+]+\.)*\w[\w-+]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][\d]\.|1[\d]{2}\.|[\d]{1,2}\.))((25[0-5]|2[0-4][\d]|1[\d]{2}|[\d]{1,2})\.){2}(25[0-5]|2[0-4][\d]|1[\d]{2}|[\d]{1,2})\]?$)/i;
-          if (!pattern.test(link_url)) {
-              if (link_url.indexOf("http://") != 0 && link_url.indexOf("https://") != 0 && link_url.indexOf("ftp://") != 0) {
-                  link_url = "http://" + link_url;
-              }
-          }
+    });
+    
+    $('.editor-link').click(function() {
+      var selection = $.trim(getSelectionText(textarea_id));
+      if (is_url(selection)) {
+        var link_url = $.trim(prompt(ed_lang_enter_link_url, selection));
+        selection = false;
+      } else {
+        var link_url = $.trim(prompt(ed_lang_enter_link_url));
+      }
+
+      if (is_url(link_url)) {
+        if (selection) {
+          var link_label = $.trim(prompt(ed_lang_enter_link_label, selection));
+        } else {
           var link_label = $.trim(prompt(ed_lang_enter_link_label));
           var link_label = $.trim(prompt(ed_lang_enter_link_label));
-          if (link_label.length > 0) {
-              makeReplace(ta, '[' + link_label + '](' + link_url + ')');
-          } else {
-              makeReplace(ta, '<' + link_url + '>');
-          }
+        }
+
+        if (link_label.length > 0) {
+          makeReplace(textarea_id, '[' + link_label + '](' + link_url + ')');
+        } else {
+          makeReplace(textarea_id, '<' + link_url + '>');
+        }
       }
       }
+
       return false;
       return false;
-  });
-  
-  $('.editor-image').click(function() {
-      ta = get_textarea(this).attr('id');
-      var image_url = $.trim(prompt(ed_lang_enter_image_url));
-      if (image_url.length > 0) {
+    });
+    
+    $('.editor-image').click(function() {
+      var selection = $.trim(getSelectionText(textarea_id));
+      if (is_url(selection)) {
+        var image_url = $.trim(prompt(ed_lang_enter_image_url, selection));
+        selection = false;
+      } else {
+        var image_url = $.trim(prompt(ed_lang_enter_image_url));
+      }
+
+      if (is_url(image_url)) {
+        if (selection) {
+          var image_label = $.trim(prompt(ed_lang_enter_image_label, selection));
+        } else {
           var image_label = $.trim(prompt(ed_lang_enter_image_label));
           var image_label = $.trim(prompt(ed_lang_enter_image_label));
-          if (image_label.length > 0) {
-              makeReplace(ta, '![' + image_label + '](' + image_url + ')');
-          }
+        }
+
+        if (image_label.length > 0) {
+          makeReplace(textarea_id, '![' + image_label + '](' + image_url + ')');
+        } else {
+          makeReplace(textarea_id, '!(' + image_url + ')');
+        }
       }
       }
+
       return false;
       return false;
-  });
-  
-  $('.editor-hr').click(function() {
-      ta = get_textarea(this).attr('id');
-      makeReplace(ta, '\r\n\r\n- - - - -\r\n\r\n');
+    });
+    
+    $('.editor-hr').click(function() {
+      makeReplace(textarea_id, '\r\n\r\n- - - - -\r\n\r\n');
       return false;
       return false;
+    });
   });
   });
-});
+});

+ 1 - 1
templates/_email/base.html

@@ -4,7 +4,7 @@
 {%- set style_well_big = "style=\"background-color: #F0F0F0; border-radius: 3px; margin: 24px 0px; padding: 12px 16px; font-size: 18px; text-align: center;\"" -%}
 {%- set style_well_big = "style=\"background-color: #F0F0F0; border-radius: 3px; margin: 24px 0px; padding: 12px 16px; font-size: 18px; text-align: center;\"" -%}
 {%- set style_button = "style=\"background-color: #08C; border-radius: 3px; color: #FFF; display: block; text-align: center; margin: 24px 0px; padding: 12px 0px; text-decoration: none; font-weight: bold; font-size: 18px;\"" -%}
 {%- set style_button = "style=\"background-color: #08C; border-radius: 3px; color: #FFF; display: block; text-align: center; margin: 24px 0px; padding: 12px 0px; text-decoration: none; font-weight: bold; font-size: 18px;\"" -%}
 <!DOCTYPE html>
 <!DOCTYPE html>
-<html lang="en">
+<html lang="{{ LANGUAGE_CODE }}">
   <head>
   <head>
     <meta charset="utf-8">
     <meta charset="utf-8">
   </head>
   </head>

+ 9 - 0
templates/_email/report_reply_notification.html

@@ -0,0 +1,9 @@
+{% extends "_email/base.html" %}
+
+{% block title %}{% trans %}New reply notification{% endtrans %}{% endblock %}
+
+{% block content %}
+<p {{ style_p|safe }}>{% trans username=user.username, author=author.username, thread=thread.name %}{{ username }}, you are receiving this message because {{ author }} has replied to report "{{ thread }}" that you are watching.{% endtrans %}</p>
+<p {{ style_p|safe }}>{% trans %}To go to this reply follow the link below:{% endtrans %}</p>
+<a href="{{ board_address }}{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" {{ style_link|safe }}>{{ board_address }}{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}</a>
+{% endblock %}

+ 10 - 0
templates/_email/report_reply_notification.txt

@@ -0,0 +1,10 @@
+{% extends "_email/base.txt" %}
+
+{% block title %}{% trans %}New reply notification{% endtrans %}{% endblock %}
+
+{% block content %}
+{% trans username=user.username, author=author.username, thread=thread.name %}{{ username }}, you are receiving this message because {{ author }} has replied to report "{{ thread }}" that you are watching.{% endtrans %}
+
+{% trans %}To go to this reply follow the link below:{% endtrans %}
+{{ board_address }}{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}
+{% endblock %}

+ 1 - 1
templates/admin/admin/list.html

@@ -112,7 +112,7 @@ Showing {{ shown }} of {{ total }} items
 
 
 {% block javascripts -%}
 {% block javascripts -%}
 {{ super() }}
 {{ super() }}
-{%- if action.prompt_select %}
+{%- if list_form %}
   <script type="text/javascript">
   <script type="text/javascript">
     $(function () {
     $(function () {
       $('#list_form').submit(function() {
       $('#list_form').submit(function() {

+ 5 - 5
templates/admin/banning/list.html → templates/admin/bans/list.html

@@ -5,13 +5,13 @@
 
 
 {% block table_row scoped %}
 {% block table_row scoped %}
   <td class="lead-cell">
   <td class="lead-cell">
-  	<strong>{{ item.ban }}</strong> <span class="label {% if item.type == 0 -%}label-inverse">{% trans %}Username and E-mail{% endtrans %}
-  	{%- elif item.type == 1 -%}label-important">{% trans %}Username{% endtrans %}
-  	{%- elif item.type == 2 -%}label-warning">{% trans %}E-mail{% endtrans %}
+  	<strong>{{ item.ban }}</strong> <span class="label {% if item.test == 0 -%}label-inverse">{% trans %}Username and E-mail{% endtrans %}
+  	{%- elif item.test == 1 -%}label-important">{% trans %}Username{% endtrans %}
+  	{%- elif item.test == 2 -%}label-warning">{% trans %}E-mail{% endtrans %}
   	{%- else -%}label-info">{% trans %}IP Address{% endtrans %}
   	{%- else -%}label-info">{% trans %}IP Address{% endtrans %}
   	{%- endif %}</span>
   	{%- endif %}</span>
   </td>
   </td>
   <td>
   <td>
-  	{% if item.expires %}{{ item.expires|date }}{% else %}<em>{% trans %}Permanent ban{% endtrans %}</em>{% endif %}
+  	{% if item.expires %}{{ item.expires|date }}{% else %}<em>{% trans %}Permanent{% endtrans %}</em>{% endif %}
   </td>
   </td>
-{% endblock%}
+{% endblock %}

+ 1 - 1
templates/admin/base.html

@@ -1,6 +1,6 @@
 {% from "admin/macros.html" import page_title -%}
 {% from "admin/macros.html" import page_title -%}
 <!DOCTYPE html>
 <!DOCTYPE html>
-<html lang="en">
+<html lang="{{ LANGUAGE_CODE }}">
   <head>
   <head>
     <meta charset="utf-8">
     <meta charset="utf-8">
     <title>{% block title %}{{ page_title() }}{% endblock %}</title>
     <title>{% block title %}{{ page_title() }}{% endblock %}</title>

+ 1 - 1
templates/admin/ranks/list.html

@@ -11,7 +11,7 @@
 
 
 {% block table_row scoped %}
 {% block table_row scoped %}
   <td class="lead-cell">
   <td class="lead-cell">
-  	<strong>{{ item.name }}</strong>{% if item.special %} <span class="label label-info">{% trans %}Special{% endtrans %}</span>{% endif %}{% if item.as_tab %} <span class="label label-inverse">{% trans %}Tab{% endtrans %}</span>{% endif %}{% if item.on_index %} <span class="label label-orange">{% trans %}On Index{% endtrans %}</span>{% endif %}
+  	<strong>{{ _(item.name) }}</strong>{% if item.special %} <span class="label label-info">{% trans %}Special{% endtrans %}</span>{% endif %}{% if item.as_tab %} <span class="label label-inverse">{% trans %}Tab{% endtrans %}</span>{% endif %}{% if item.on_index %} <span class="label label-orange">{% trans %}On Index{% endtrans %}</span>{% endif %}
   </td>
   </td>
   <td class="span2">
   <td class="span2">
   	{{ form_theme.field_widget(table_form['pos_' + item.pk|string], attrs={'form': 'table_form'}, width=2) }}
   	{{ form_theme.field_widget(table_form['pos_' + item.pk|string], attrs={'form': 'table_form'}, width=2) }}

+ 4 - 1
templates/admin/settings/settings.html

@@ -16,7 +16,7 @@
       <h4>{% trans %}Search Settings{% endtrans %}</h4>
       <h4>{% trans %}Search Settings{% endtrans %}</h4>
       <form action="{% url 'admin_settings_search' %}" class="form-inline" method="post">
       <form action="{% url 'admin_settings_search' %}" class="form-inline" method="post">
         <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
         <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-        {{ form_theme.input_text(search_form.fields.search_text, width=2, attrs={'placeholder': _('Search Settings...')}) }}
+        {{ form_theme.input_text(search_form.fields.search_text, width=2, attrs={'placeholder': lang_search_settings()}) }}
         <button type="submit" class="btn btn-primary"><i class="icon-search icon-white"></i></button>
         <button type="submit" class="btn btn-primary"><i class="icon-search icon-white"></i></button>
       </form>
       </form>
       <h4>{% trans%}Settings Groups{% endtrans %}</h4>{% for group in groups %}
       <h4>{% trans%}Settings Groups{% endtrans %}</h4>{% for group in groups %}
@@ -39,3 +39,6 @@
   {% endblock %}</div>
   {% endblock %}</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
+
+{# Language strings macros #}
+{% macro lang_search_settings() -%}{% trans %}Search Settings...{% endtrans %}{%- endmacro %}

+ 12 - 5
templates/cranefly/alerts.html

@@ -20,11 +20,11 @@
 <div class="container container-primary">
 <div class="container container-primary">
   {% if alerts %}
   {% if alerts %}
   <div class="user-alerts">
   <div class="user-alerts">
-    {% if alerts.today %}{{ alerts_list(_("Today Notifications"), alerts.today) }}{% endif %}
-    {% if alerts.yesterday %}{{ alerts_list(_("Yesterday Notifications"), alerts.yesterday) }}{% endif %}
-    {% if alerts.week %}{{ alerts_list(_("This Week"), alerts.week) }}{% endif %}
-    {% if alerts.month %}{{ alerts_list(_("This Month"), alerts.month) }}{% endif %}
-    {% if alerts.older %}{{ alerts_list(_("Older Notifications"), alerts.older) }}{% endif %}
+    {% if alerts.today %}{{ alerts_list(lang_today(), alerts.today) }}{% endif %}
+    {% if alerts.yesterday %}{{ alerts_list(lang_yesterday(), alerts.yesterday) }}{% endif %}
+    {% if alerts.week %}{{ alerts_list(lang_week(), alerts.week) }}{% endif %}
+    {% if alerts.month %}{{ alerts_list(lang_month(), alerts.month) }}{% endif %}
+    {% if alerts.older %}{{ alerts_list(lang_older(), alerts.older) }}{% endif %}
   </div>
   </div>
   {% else %}
   {% else %}
   <p class="lead">{% trans %}Looks like you don't have any notifications... yet.{% endtrans %}</p>
   <p class="lead">{% trans %}Looks like you don't have any notifications... yet.{% endtrans %}</p>
@@ -63,3 +63,10 @@ You have {{ alerts }} new alerts
     </tbody>
     </tbody>
   </table>
   </table>
 {% endmacro %}
 {% endmacro %}
+
+{# Language strings macros #}
+{% macro lang_today() -%}{% trans %}Today Notifications{% endtrans %}{%- endmacro %}
+{% macro lang_yesterday() -%}{% trans %}Yesterday Notifications{% endtrans %}{%- endmacro %}
+{% macro lang_week() -%}{% trans %}This Week{% endtrans %}{%- endmacro %}
+{% macro lang_month() -%}{% trans %}This Month{% endtrans %}{%- endmacro %}
+{% macro lang_older() -%}{% trans %}Older Notifications{% endtrans %}{%- endmacro %}

+ 5 - 1
templates/cranefly/base.html

@@ -1,6 +1,6 @@
 {% from "cranefly/macros.html" import page_title -%}
 {% from "cranefly/macros.html" import page_title -%}
 <!DOCTYPE html>
 <!DOCTYPE html>
-<html lang="en">
+<html lang="{{ LANGUAGE_CODE }}">
   <head>
   <head>
     <meta charset="utf-8">
     <meta charset="utf-8">
     <title>{% block title %}{{ page_title() }}{% endblock %}</title>
     <title>{% block title %}{{ page_title() }}{% endblock %}</title>
@@ -15,7 +15,11 @@
   	<script src="{{ STATIC_URL }}cranefly/js/bootstrap.min.js"></script>
   	<script src="{{ STATIC_URL }}cranefly/js/bootstrap.min.js"></script>
     <script type="text/javascript">
     <script type="text/javascript">
       var l_img_broken_msg = "{{ _('Image could not be loaded.') }}";
       var l_img_broken_msg = "{{ _('Image could not be loaded.') }}";
+      $(function() {
+        $('#fancy-user-nav').show();
+      });
     </script>
     </script>
   	<script src="{{ STATIC_URL }}cranefly/js/cranefly.js"></script>{% block javascripts %}{% endblock %}
   	<script src="{{ STATIC_URL }}cranefly/js/cranefly.js"></script>{% block javascripts %}{% endblock %}
+    {{ hook_append_extra|safe }}
   </body>
   </body>
 </html>
 </html>

+ 63 - 63
templates/cranefly/category.html

@@ -4,9 +4,7 @@
 {% block title %}{{ macros.page_title(title=category.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=category.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li class="active">{{ category.name }}
 <li class="active">{{ category.name }}
 {%- endblock %}
 {%- endblock %}
 
 
@@ -29,66 +27,68 @@
   {% endif %}
   {% endif %}
   {% if category.subforums %}
   {% if category.subforums %}
   <div id="subforums" class="category-forums-list{% if category.style %} category-forums-{{ category.style }}{% endif %}">
   <div id="subforums" class="category-forums-list{% if category.style %} category-forums-{{ category.style }}{% endif %}">
-    <table class="table">
-      <tbody>
-        {% for forum in category.subforums %}
-        <tr>
-          <td class="forum-icon"><span class="forum-icon-wrap{% if forum.type == 'redirect' %} forum-icon-redirect{% elif not forum.is_read %} forum-icon-new{% endif %}"><i class="icon-{% if forum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %} icon-white"></i></span></td>
-          <td id="forum-{{ forum.id }}" class="forum-main">
-            <h3 class="forum-title{% if not forum.is_read %} forum-title-new{% endif %}"><a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}">{{ forum.name }}</a></h3>
-            {% if forum.show_details %}
-            <div class="forum-details">
-              {% if forum.type != 'redirect' %}
-              {% if acl.forums.can_browse(forum) and (acl.threads.can_read_threads(forum) == 2 or (acl.threads.can_read_threads(forum) == 1 and forum.last_poster_id == user.pk)) %}
-              {% if forum.last_thread_id -%}
-              <div class="thread-name">
-                <a href="{% url 'thread_new' thread=forum.last_thread_id, slug=forum.last_thread_slug %}"{% if forum.last_thread_name|length > 36 %} class="tooltip-top" title="{{ forum.last_thread_name }}"{% endif %}>{{ forum.last_thread_name|short_string(36) }}</a>
-              </div>
-              <div class="muted">{% if forum.last_poster_id %}<a href="{% url 'user' user=forum.last_poster_id, username=forum.last_poster_slug %}" class="last-poster">{{ forum.last_poster_name }}</a>{% else %}<span class="last-poster">{{ forum.last_poster_name }}</span>{% endif %} - {{ forum.last_thread_date|reltimesince }}</div>
-              {%- else -%}
-              <em>{% trans %}This forum is empty{% endtrans %}</em>
-              {%- endif %}
-              {%- else -%}
-              <em>{% trans %}This forum is protected{% endtrans %}</em>
-              {%- endif %}
-              {%- else -%}
-              <div class="thread-name">
-                <a href="{% url 'redirect' slug=forum.slug, forum=forum.id %}">{{ forum.redirect_domain() }}</a>
-              </div>
-              <div class="muted">{% trans clicks=macros.wrap(forum.redirects|intcomma, 'span', 'class="last-poster"') %}{{ clicks }} clicks{% endtrans %}</div>
-              {%- endif %}
+    {% for forum in category.subforums %}
+    <div class="forum{% if loop.last %} last{% endif %}">
+      <div class="forum-icon">
+        <div class="forum-icon-wrap{% if forum.type == 'redirect' %} forum-icon-redirect{% elif not forum.is_read %} forum-icon-new{% endif %}"><i class="icon-{% if forum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %} icon-white"></i></div>
+      </div>
+      <div id="forum-{{ forum.id }}" class="forum-main">
+        <h3 class="forum-title{% if not forum.is_read %} forum-title-new{% endif %}"><a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}">{{ forum.name }}</a></h3>
+        {% if forum.show_details %}
+        <div class="forum-details">
+          {% if forum.type != 'redirect' %}
+          {% if acl.forums.can_browse(forum) and (acl.threads.can_read_threads(forum) == 2 or (acl.threads.can_read_threads(forum) == 1 and forum.last_poster_id == user.pk)) %}
+          {% if forum.last_thread_id -%}
+          <div class="thread-name">
+            <a href="{% url 'thread_new' thread=forum.last_thread_id, slug=forum.last_thread_slug %}"{% if forum.last_thread_name|length > 34 %} class="tooltip-top" title="{{ forum.last_thread_name }}"{% endif %}>{{ forum.last_thread_name|short_string(34) }}</a>
+          </div>
+          <div class="muted">{% if forum.last_poster_id %}<a href="{% url 'user' user=forum.last_poster_id, username=forum.last_poster_slug %}" class="last-poster">{{ forum.last_poster_name }}</a>{% else %}<span class="last-poster">{{ forum.last_poster_name }}</span>{% endif %} - {{ forum.last_thread_date|reltimesince }}</div>
+          {%- else -%}
+          <em>{% trans %}This forum is empty{% endtrans %}</em>
+          {%- endif %}
+          {%- else -%}
+          <em>{% trans %}This forum is protected{% endtrans %}</em>
+          {%- endif %}
+          {%- else -%}
+          <div class="thread-name">
+            <a href="{% url 'redirect' slug=forum.slug, forum=forum.id %}">{{ forum.redirect_domain() }}</a>
+          </div>
+          <div class="muted">{% trans count=forum.redirects, clicks=macros.wrap(forum.redirects|intcomma, 'span', 'class="last-poster"') %}{{ clicks }} click{% pluralize %}{{ clicks }} clicks{% endtrans %}</div>
+          {%- endif %}
+        </div>
+        {% endif %}
+        {% if forum.subforums %}
+        <div class="dropdown">
+          {% if forum.subforums|length > 1 %}
+          <a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}#subforums" class="dropdown-toggle" data-toggle="dropdown"><i class="icon-chevron-down"></i> {% trans %}Subforums{% endtrans %}</a>
+          <div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
+            <div class="dropdown-shadow">
+              <ul>
+                {% for subforum in forum.subforums %}
+                <li><a href="{{ subforum.type|url(slug=subforum.slug, forum=subforum.id) }}"><i class="icon-{% if subforum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %}"></i> {{ subforum.name }}</a></li>
+                {% endfor %}
+              </ul>
             </div>
             </div>
+          </div>
+          {% else %}
+          <a href="{{ forum.subforums[0].type|url(slug=forum.subforums[0].slug, forum=forum.subforums[0].id) }}" class="subforum tooltip-top" title="{% trans forum=forum.subforums[0].name %}Go to the {{ forum }} subforum{% endtrans %}">{{ forum.subforums[0].name|short_string(16) }}</a>
+          {% endif %}
+        </div>
+        {% endif%}
+        <div class="hide forum-meta">
+          {% if forum.description %}<p class="forum-description">{{ forum.description }}</p>{% endif %}
+          <div class="forum-stats">
+            {% if forum.type != 'redirect' %}
+            <span>{% trans %}Posts{% endtrans %}: <strong>{{ forum.posts|intcomma }}</strong></span>
+            {% trans %}Threads{% endtrans %}: <strong>{{ forum.threads|intcomma }}</strong>
+            {% else %}
+            {% trans %}Clicks{% endtrans %}: <strong>{{ forum.redirects|intcomma }}</strong>
             {% endif %}
             {% endif %}
-            {% if forum.subforums %}
-            <div class="dropdown">
-              <a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}#subforums" class="dropdown-toggle" data-toggle="dropdown"><i class="icon-chevron-down"></i> {% trans %}Subforums{% endtrans %}</a>
-              <div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
-                <div class="dropdown-shadow">
-                  <ul>
-                    {% for subforum in forum.subforums %}
-                    <li><a href="{{ subforum.type|url(slug=subforum.slug, forum=subforum.id) }}"><i class="icon-{% if subforum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %}"></i> {{ subforum.name }}</a></li>
-                    {% endfor %}
-                  </ul>
-                </div>
-              </div>
-            </div>
-            {% endif%}
-            <div class="hide forum-meta">
-              {% if forum.description %}<p class="forum-description">{{ forum.description }}</p>{% endif %}
-              <div class="forum-stats">
-                {% if forum.type != 'redirect' %}
-                <span>{% trans %}Posts{% endtrans %}: <strong>{{ forum.posts|intcomma }}</strong></span>
-                {% trans %}Threads{% endtrans %}: <strong>{{ forum.threads|intcomma }}</strong>
-                {% else %}
-                {% trans %}Clicks{% endtrans %}: <strong>{{ forum.redirects|intcomma }}</strong>
-                {% endif %}
-              </div>
-            </div>
-          </td>
-        </tr>
-        {% endfor %}
-      </tbody>
-    </table>
+          </div>
+        </div>
+      </div>
+    </div>
+    {% endfor %}
   </div>
   </div>
   {% else %}
   {% else %}
   <p class="lead">{% trans %}Looks like there are no forums to display in this category.{% endtrans %}</p>
   <p class="lead">{% trans %}Looks like there are no forums to display in this category.{% endtrans %}</p>
@@ -102,14 +102,14 @@
       function populateForumTooltip(target) {
       function populateForumTooltip(target) {
         return $('#forum-' + target + ' .forum-meta').html();
         return $('#forum-' + target + ' .forum-meta').html();
       };
       };
-      {% for category in forums_list %}{% for forum in category.subforums %}
+      {% for forum in category.subforums %}
         $('#forum-{{ forum.id }} .forum-title').tooltip({
         $('#forum-{{ forum.id }} .forum-title').tooltip({
           template: '<div class="tooltip forum-meta-tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
           template: '<div class="tooltip forum-meta-tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
           placement: 'right',
           placement: 'right',
           html: true,
           html: true,
           title: populateForumTooltip({{ forum.id }})
           title: populateForumTooltip({{ forum.id }})
         });
         });
-      {% endfor %}{% endfor %}
+      {% endfor %}
     });
     });
   </script>
   </script>
 {%- endblock %}
 {%- endblock %}

+ 5 - 4
templates/cranefly/editor.html

@@ -29,10 +29,11 @@
 {% macro js() %}
 {% macro js() %}
   <script type="text/javascript">
   <script type="text/javascript">
     $(function () {
     $(function () {
-        ed_lang_enter_link_url = "{% trans %}Enter link address{% endtrans %}";
-        ed_lang_enter_link_label = "{% trans %}Enter link label (optional){% endtrans %}";
-        ed_lang_enter_image_url = "{% trans %}Enter image address{% endtrans %}";
-        ed_lang_enter_image_label = "{% trans %}Enter image label{% endtrans %}";
+      ed_lang_enter_link_url = "{% trans %}Enter link address{% endtrans %}";
+      ed_lang_enter_link_label = "{% trans %}Enter link label (optional){% endtrans %}";
+      ed_lang_enter_image_url = "{% trans %}Enter image address{% endtrans %}";
+      ed_lang_enter_image_label = "{% trans %}Enter image label{% endtrans %}";
+      ed_emojis = ['{{ ("', '".join(emojis))|safe }}'];
     });
     });
   </script>
   </script>
   <script src="{{ STATIC_URL }}cranefly/js/editor.js"></script>
   <script src="{{ STATIC_URL }}cranefly/js/editor.js"></script>

+ 18 - 61
templates/cranefly/forum_map.html

@@ -15,12 +15,12 @@
   {% if forums %}
   {% if forums %}
   <div class="row">
   <div class="row">
     <div class="span6">
     <div class="span6">
-      {% for category in forums %}{% if loop.index is odd and category.subforums %}
+      {% for category in forums %}{% if loop.index0 is odd and category.subforums %}
       {{ draw_category(category) }}
       {{ draw_category(category) }}
       {% endif %}{% endfor %}
       {% endif %}{% endfor %}
     </div>
     </div>
     <div class="span6">
     <div class="span6">
-      {% for category in forums %}{% if loop.index is even and category.subforums %}
+      {% for category in forums %}{% if loop.index0 is even and category.subforums %}
       {{ draw_category(category) }}
       {{ draw_category(category) }}
       {% endif %}{% endfor %}
       {% endif %}{% endfor %}
     </div>
     </div>
@@ -34,20 +34,17 @@
 
 
 {% macro draw_category(category) %}
 {% macro draw_category(category) %}
 <div class="forum-map-category{% if category.style %} forum-map-category-{{ category.style }}{% endif %}">
 <div class="forum-map-category{% if category.style %} forum-map-category-{{ category.style }}{% endif %}">
-  <table class="table">
-    <caption>{{ category.name }}</caption>
-    <tbody>
-      {% for forum in category.subforums%}
-      {{ draw_forum(forum) }}
-      {% endfor %}
-    </tbody>
-  </table>
+  <div class="header">
+    <h2>{{ category.name }}</h2>
+  </div>
+  {% for forum in category.subforums%}
+  {{ draw_forum(forum) }}
+  {% endfor %}
 </div>
 </div>
 {% endmacro %}
 {% endmacro %}
 
 
 {% macro draw_forum(forum, depth=0, branch='', last=false) %}
 {% macro draw_forum(forum, depth=0, branch='', last=false) %}
-<tr>
-  <td class="{% if depth -%}
+  <div class="{% if depth -%}
     forum-map-subforum
     forum-map-subforum
     {%- else -%}
     {%- else -%}
     forum-map-forum
     forum-map-forum
@@ -57,62 +54,22 @@
     {%- else -%}
     {%- else -%}
     {{ draw_tree(branch ~ 't') }}
     {{ draw_tree(branch ~ 't') }}
     {%- endif %}{% endif %} <a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}">{{ forum.name }}</a></h3>
     {%- endif %}{% endif %} <a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}">{{ forum.name }}</a></h3>
-    <div class="forum-details">
-      {% if forum.type == 'redirect' %}
-      {{ redirect_stats(forum) }}
+  </div>
+  {% for subforum in forum.subforums %}
+    {% if depth %}
+      {% if last %}
+      {{ draw_forum(subforum, (depth + 1), (branch ~ 's'), loop.last) }}
       {% else %}
       {% else %}
-      {{ forum_stats(forum) }}
+      {{ draw_forum(subforum, (depth + 1), (branch ~ 'i'), loop.last) }}
       {% endif %}
       {% endif %}
-    </div>
-</td>
-</tr>
-{% for subforum in forum.subforums %}
-  {% if depth %}
-    {% if last %}
-    {{ draw_forum(subforum, (depth + 1), (branch ~ 's'), loop.last) }}
     {% else %}
     {% else %}
-    {{ draw_forum(subforum, (depth + 1), (branch ~ 'i'), loop.last) }}
+    {{ draw_forum(subforum, (depth + 1), '', loop.last) }}
     {% endif %}
     {% endif %}
-  {% else %}
-  {{ draw_forum(subforum, (depth + 1), '', loop.last) }}
-  {% endif %}
-{% endfor %}
+  {% endfor %}
 {% endmacro %}
 {% endmacro %}
 
 
 {% macro draw_tree(branch) %}
 {% macro draw_tree(branch) %}
 {% for item in branch %}
 {% for item in branch %}
 <span class="tree-{{ item }}"><span></span></span>
 <span class="tree-{{ item }}"><span></span></span>
 {% endfor %}
 {% endfor %}
-{% endmacro %}
-
-{% macro forum_stats(forum) -%}
-{% if forum.last_thread_id and not forum.attr('hidethread') -%}
-  {% trans count=forum.posts, posts=fancy_number(forum.posts, forum.posts_delta), thread=forum_thread(forum) -%}
-  {{ posts }} post - last in {{ thread }}
-  {%- pluralize -%}
-  {{ posts }} posts - last in {{ thread }}
-  {%- endtrans %}
-{%- else -%}
-  {% trans count=forum.posts, posts=fancy_number(forum.posts, forum.posts_delta) -%}
-  {{ posts }} post
-  {%- pluralize -%}
-  {{ posts }} posts
-  {%- endtrans %}
-{%- endif %}
-{%- endmacro %}
-
-{% macro forum_thread(forum) -%}
-<a href="{% url 'thread' thread=forum.last_thread_id, slug=forum.last_thread_slug %}">{{ forum.last_thread_name }}</a>
-{%- endmacro %}
-
-{% macro redirect_stats(forum) -%}
-{% trans count=forum.redirects, redirects=fancy_number(forum.redirects, forum.redirects_delta) -%}
-{{ redirects }} click
-{%- pluralize -%}
-{{ redirects }} clicks
-{%- endtrans %}
-{%- endmacro %}
-
-{% macro fancy_number(number, delta) -%}
-<strong{% if delta < number %} class="stat-increment"{% endif %}>{{ number|intcomma }}</strong>
-{%- endmacro %}
+{% endmacro %}

+ 92 - 66
templates/cranefly/index.html

@@ -8,91 +8,106 @@
 {%- endif %}{%- endblock %}
 {%- endif %}{%- endblock %}
       
       
 {% block content %}
 {% block content %}
+{{ hook_above_forum_home|safe }}
 <div class="row">
 <div class="row">
   <div class="span8">
   <div class="span8">
     <div class="index-forums-list">
     <div class="index-forums-list">
+      {{ hook_above_home_forums_list|safe }}
 
 
       {% for category in forums_list %}{% if category.subforums %}
       {% for category in forums_list %}{% if category.subforums %}
-      <div class="index-category{% if category.style %} index-category-{{ category.style }}{% endif %}">
-        <table class="table">
-          <caption>{{ category.name }}{% if category.description %} <small>{{ category.description }}</small>{% endif %}</caption>
-          <tbody>
-            {% for forum in category.subforums %}
-            <tr>
-              <td class="forum-icon"><span class="forum-icon-wrap{% if forum.type == 'redirect' %} forum-icon-redirect{% elif not forum.is_read %} forum-icon-new{% endif %}"><i class="icon-{% if forum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %} icon-white"></i></span></td>
-              <td id="forum-{{ forum.id }}" class="forum-main">
-                <h3 class="forum-title{% if not forum.is_read %} forum-title-new{% endif %}"><a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}">{{ forum.name }}</a></h3>
-                {% if forum.show_details %}
-                <div class="forum-details">
-                  {% if forum.type != 'redirect' %}
-                  {% if acl.forums.can_browse(forum) and (acl.threads.can_read_threads(forum) == 2 or (acl.threads.can_read_threads(forum) == 1 and forum.last_poster_id == user.pk)) %}
-                  {% if forum.last_thread_id -%}
-                  <div class="thread-name">
-                    <a href="{% url 'thread_new' thread=forum.last_thread_id, slug=forum.last_thread_slug %}"{% if forum.last_thread_name|length > 36 %} class="tooltip-top" title="{{ forum.last_thread_name }}"{% endif %}>{{ forum.last_thread_name|short_string(36) }}</a>
-                  </div>
-                  <div class="muted">{% if forum.last_poster_id %}<a href="{% url 'user' user=forum.last_poster_id, username=forum.last_poster_slug %}" class="last-poster">{{ forum.last_poster_name }}</a>{% else %}<span class="last-poster">{{ forum.last_poster_name }}</span>{% endif %} - {{ forum.last_thread_date|reltimesince }}</div>
-                  {%- else -%}
-                  <em>{% trans %}This forum is empty{% endtrans %}</em>
-                  {%- endif %}
-                  {%- else -%}
-                  <em>{% trans %}This forum is protected{% endtrans %}</em>
-                  {%- endif %}
-                  {%- else -%}
-                  <div class="thread-name">
-                    <a href="{% url 'redirect' slug=forum.slug, forum=forum.id %}">{{ forum.redirect_domain() }}</a>
-                  </div>
-                  <div class="muted">{% trans clicks=macros.wrap(forum.redirects|intcomma, 'span', 'class="last-poster"') %}{{ clicks }} clicks{% endtrans %}</div>
-                  {%- endif %}
+      <div id="{{ category.slug }}" class="index-category{% if category.style %} index-category-{{ category.style }}{% endif %}">
+        <div class="header">
+          <h2>{{ category.name }}{% if category.description %} <small>{{ category.description }}</small>{% endif %}</h2>
+        </div>
+        {% for forum in category.subforums %}
+        <div class="forum{% if loop.last %} last{% endif %}">
+          <div class="forum-icon">
+            <div class="forum-icon-wrap{% if forum.type == 'redirect' %} forum-icon-redirect{% elif not forum.is_read %} forum-icon-new{% endif %}"><i class="icon-{% if forum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %} icon-white"></i></div>
+          </div>
+          <div id="forum-{{ forum.id }}" class="forum-main">
+            <h3 class="forum-title{% if not forum.is_read %} forum-title-new{% endif %}"><a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}">{{ forum.name }}</a></h3>
+            {% if forum.show_details %}
+            <div class="forum-details">
+              {% if forum.type != 'redirect' %}
+              {% if acl.forums.can_browse(forum) and (acl.threads.can_read_threads(forum) == 2 or (acl.threads.can_read_threads(forum) == 1 and forum.last_poster_id == user.pk)) %}
+              {% if forum.last_thread_id -%}
+              <div class="thread-name">
+                <a href="{% url 'thread_new' thread=forum.last_thread_id, slug=forum.last_thread_slug %}"{% if forum.last_thread_name|length > 34 %} class="tooltip-top" title="{{ forum.last_thread_name }}"{% endif %}>{{ forum.last_thread_name|short_string(34) }}</a>
+              </div>
+              <div class="muted">{% if forum.last_poster_id %}<a href="{% url 'user' user=forum.last_poster_id, username=forum.last_poster_slug %}" class="last-poster">{{ forum.last_poster_name }}</a>{% else %}<span class="last-poster">{{ forum.last_poster_name }}</span>{% endif %} - {{ forum.last_thread_date|reltimesince }}</div>
+              {%- else -%}
+              <em>{% trans %}This forum is empty{% endtrans %}</em>
+              {%- endif %}
+              {%- else -%}
+              <em>{% trans %}This forum is protected{% endtrans %}</em>
+              {%- endif %}
+              {%- else -%}
+              <div class="thread-name">
+                <a href="{% url 'redirect' slug=forum.slug, forum=forum.id %}">{{ forum.redirect_domain() }}</a>
+              </div>
+              <div class="muted">{% trans count=forum.redirects, clicks=macros.wrap(forum.redirects|intcomma, 'span', 'class="last-poster"') %}{{ clicks }} click{% pluralize %}{{ clicks }} clicks{% endtrans %}</div>
+              {%- endif %}
+            </div>
+            {% endif %}
+            {% if forum.subforums %}
+            <div class="dropdown">
+              {% if forum.subforums|length > 1 %}
+              <a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}#subforums" class="dropdown-toggle" data-toggle="dropdown"><i class="icon-chevron-down"></i> {% trans %}Subforums{% endtrans %}</a>
+              <div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
+                <div class="dropdown-shadow">
+                  <ul>
+                    {% for subforum in forum.subforums %}
+                    <li><a href="{{ subforum.type|url(slug=subforum.slug, forum=subforum.id) }}"><i class="icon-{% if subforum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %}"></i> {{ subforum.name }}</a></li>
+                    {% endfor %}
+                  </ul>
                 </div>
                 </div>
+              </div>
+              {% else %}
+              <a href="{{ forum.subforums[0].type|url(slug=forum.subforums[0].slug, forum=forum.subforums[0].id) }}" class="subforum tooltip-top" title="{% trans forum=forum.subforums[0].name %}Go to the {{ forum }} subforum{% endtrans %}">{{ forum.subforums[0].name|short_string(16) }}</a>
+              {% endif %}
+            </div>
+            {% endif%}
+            <div class="hide forum-meta">
+              {% if forum.description %}<p class="forum-description">{{ forum.description }}</p>{% endif %}
+              <div class="forum-stats">
+                {% if forum.type != 'redirect' %}
+                <span>{% trans %}Posts{% endtrans %}: <strong>{{ forum.posts|intcomma }}</strong></span>
+                {% trans %}Threads{% endtrans %}: <strong>{{ forum.threads|intcomma }}</strong>
+                {% else %}
+                {% trans %}Clicks{% endtrans %}: <strong>{{ forum.redirects|intcomma }}</strong>
                 {% endif %}
                 {% endif %}
-                {% if forum.subforums %}
-                <div class="dropdown">
-                  <a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}#subforums" class="dropdown-toggle" data-toggle="dropdown"><i class="icon-chevron-down"></i> {% trans %}Subforums{% endtrans %}</a>
-                  <div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
-                    <div class="dropdown-shadow">
-                      <ul>
-                        {% for subforum in forum.subforums %}
-                        <li><a href="{{ subforum.type|url(slug=subforum.slug, forum=subforum.id) }}"><i class="icon-{% if subforum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %}"></i> {{ subforum.name }}</a></li>
-                        {% endfor %}
-                      </ul>
-                    </div>
-                  </div>
-                </div>
-                {% endif%}
-                <div class="hide forum-meta">
-                  {% if forum.description %}<p class="forum-description">{{ forum.description }}</p>{% endif %}
-                  <div class="forum-stats">
-                    {% if forum.type != 'redirect' %}
-                    <span>{% trans %}Posts{% endtrans %}: <strong>{{ forum.posts|intcomma }}</strong></span>
-                    {% trans %}Threads{% endtrans %}: <strong>{{ forum.threads|intcomma }}</strong>
-                    {% else %}
-                    {% trans %}Clicks{% endtrans %}: <strong>{{ forum.redirects|intcomma }}</strong>
-                    {% endif %}
-                  </div>
-                </div>
-              </td>
-            </tr>
-            {% endfor %}
-          </tbody>
-        </table>
+              </div>
+            </div>
+          </div>
+        </div>
+        {% endfor %}
       </div>
       </div>
       {% endif %}{% endfor %}
       {% endif %}{% endfor %}
 
 
+      {{ hook_below_home_forums_list|safe }}
     </div>
     </div>
   </div>
   </div>
   <div class="span4 index-sidebar">
   <div class="span4 index-sidebar">
 
 
+    {{ hook_above_home_sidepanel|safe }}
+
     {% if ranks_online %}
     {% if ranks_online %}
     <div class="index-ranks-list">
     <div class="index-ranks-list">
       {% for rank in ranks_online %}{% if rank.online %}
       {% for rank in ranks_online %}{% if rank.online %}
       <div class="inder-rank{% if rank.style %} index-rank-{{ rank.style }}{% endif %}">
       <div class="inder-rank{% if rank.style %} index-rank-{{ rank.style }}{% endif %}">
-        <h3>{% trans rank_name=_(rank.name) %}{{ rank_name }} Online{% endtrans %}</h3>
+        <h3>{% if rank.slug %}<a href="{% url 'users' slug=rank.slug %}">{% endif %}{% trans rank_name=_(rank.name) %}{{ rank_name }} Online{% endtrans %}{% if rank.slug %}</a>{% endif %}</h3>
         <ul class="unstyled">
         <ul class="unstyled">
           {% for online in rank.online %}
           {% for online in rank.online %}
           <li>
           <li>
             <img src="{{ online.get_avatar(24) }}" alt="" class="avatar-small">
             <img src="{{ online.get_avatar(24) }}" alt="" class="avatar-small">
-            <a href="{% url 'user' username=online.username_slug, user=online.pk %}">{{ online.username }}</a>
-            {% if rank.title or online.title %}<span class="label">{% if online.title %}{{ online.title }}{% else %}{{ _(rank.title) }}{% endif %}</span>{% endif %}
+            <a href="{% url 'user' username=online.username_slug, user=online.pk %}" class="user-name">{{ online.username }}</a>
+            {% if rank.title or online.title %}
+            {% if rank.slug -%}
+            <a href="{% url 'users' slug=rank.slug %}" class="label">{% if online.title %}{{ online.title }}{% else %}{{ _(rank.title) }}{% endif %}</a>
+            {%- else -%}
+            <span class="label">{% if online.title %}{{ online.title }}{% else %}{{ _(rank.title) }}{% endif %}</span>
+            {% endif %}
+            {% endif %}
           </li>
           </li>
           {% endfor %}
           {% endfor %}
         </ul>
         </ul>
@@ -101,13 +116,15 @@
     </div>
     </div>
     {% endif %}
     {% endif %}
 
 
+    {{ hook_after_home_sidepanel_ranks_online|safe }}
+
     {% if popular_threads %}
     {% if popular_threads %}
     <div class="index-popular-threads">
     <div class="index-popular-threads">
-      <h3>{% trans %}Popular Threads{% endtrans %}</h3>
+      <h4>{% trans %}Popular Threads{% endtrans %}</h4>
       <ul class="unstyled">
       <ul class="unstyled">
         {% for thread in popular_threads %}
         {% for thread in popular_threads %}
         <li>
         <li>
-          <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="index-popular-thread">{{ thread.name|short_string(38) }}</a>
+          <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="index-popular-thread{% if thread.name|length > 42 %} tooltip-top{% endif %}"{% if thread.name|length > 42 %} title="{{ thread.name }}"{% endif %}>{{ thread.name|short_string(42) }}</a>
           <div class="muted"><a href="{% url 'forum' forum=thread.forum_id, slug=thread.forum_slug %}">{{ thread.forum_name }}</a> - {{ thread.last|reltimesince }}</div>
           <div class="muted"><a href="{% url 'forum' forum=thread.forum_id, slug=thread.forum_slug %}">{{ thread.forum_name }}</a> - {{ thread.last|reltimesince }}</div>
         </li>
         </li>
         {% endfor %}
         {% endfor %}
@@ -115,6 +132,8 @@
     </div>
     </div>
     {% endif %}
     {% endif %}
 
 
+    {{ hook_after_home_sidepanel_popular_threads|safe }}
+
     <div class="index-stats">
     <div class="index-stats">
       <ul class="unstyled">
       <ul class="unstyled">
         <li>
         <li>
@@ -126,12 +145,16 @@
         <li>
         <li>
           <span class="tooltip-top" title="{% trans %}Members{% endtrans %}"><i class="icon-user"></i> {{ monitor.users|int|intcomma }}</span>
           <span class="tooltip-top" title="{% trans %}Members{% endtrans %}"><i class="icon-user"></i> {{ monitor.users|int|intcomma }}</span>
         </li>
         </li>
+        {% if settings.online_counting != 'no' %}
         <li>
         <li>
-          <span class="tooltip-top" title="{% trans %}Online{% endtrans %}"><i class="icon-map-marker"></i> {{ users_online|int|intcomma }}</span>
+          <span class="tooltip-top" title="{% trans %}Online{% endtrans %}"><i class="icon-map-marker"></i> {{ users_online.members|int|intcomma }} <span class="muted">{{ users_online.all|int|intcomma }}</span></span>
         </li>
         </li>
+        {% endif %}
       </ul>
       </ul>
     </div>
     </div>
 
 
+    {{ hook_after_home_sidepanel_forum_stats|safe }}
+
     {% if user.is_authenticated() %}
     {% if user.is_authenticated() %}
     <form action="{% url 'read_all' %}" method="post" class="index-forums-read-all">
     <form action="{% url 'read_all' %}" method="post" class="index-forums-read-all">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
@@ -139,8 +162,11 @@
     </form>
     </form>
     {% endif %}
     {% endif %}
 
 
+    {{ hook_below_home_sidepanel|safe }}
+
   </div>
   </div>
 </div>
 </div>
+{{ hook_below_forum_home|safe }}
 {% endblock %}
 {% endblock %}
 
 
 {% block javascripts -%}{{ super() }}
 {% block javascripts -%}{{ super() }}

+ 72 - 42
templates/cranefly/layout.html

@@ -3,66 +3,91 @@
 
 
 {% block body %}
 {% block body %}
 <div id="wrap">
 <div id="wrap">
-
-  {#{% if acl.special.can_use_mcp() %}
-  <div class="navbar navbar-inverse navbar-modbar navbar-static-top">
-    <div class="navbar-inner">
-      <div class="container">
-        <ul class="nav">
-          <li><a href="#">Frontlines</a></li>
-          <li><a href="#">Reports <span class="label label-important">5</span></a></li>
-          <li><a href="#">Warnings</a></li>
-        </ul>
-        <form class="navbar-form pull-right">
-          <div class="navbar-search-form">
-            <input type="text" class="span2" placeholder="{% trans %}Enter IP Address or Username...{% endtrans %}">
-            <button type="submit" class="btn btn-link"><i class="icon-search"></i></button>
-          </div>
-        </form>
-      </div>
-    </div>
-  </div>
-  {% endif %}#}
-
   <div class="navbar navbar-header navbar-static-top">
   <div class="navbar navbar-header navbar-static-top">
     <div class="navbar-inner">
     <div class="navbar-inner">
       <div class="container">
       <div class="container">
         <a href="{% url 'index' %}" class="brand">{% if settings.board_header %}{{ settings.board_header }}{% else %}{{ settings.board_name }}{% endif %}</a>
         <a href="{% url 'index' %}" class="brand">{% if settings.board_header %}{{ settings.board_header }}{% else %}{{ settings.board_name }}{% endif %}</a>
-        {% if not user.is_crawler() %}
-        <form class="navbar-form pull-left">
-          <div class="navbar-search-form">
-            <input type="text" class="span2" placeholder="{% trans %}Search community...{% endtrans %}">
-            <button type="submit" class="btn btn-link"><i class="icon-search"></i></button>
+        {% if acl.search.can_search() and not user.is_crawler() %}
+        <form action="{% url 'search' %}" method="post" class="navbar-form pull-left">
+          <div class="navbar-search-form{% if disable_search %} search-disabled{% endif %}">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="text" name="search_query"{% if disable_search %} disabled="disabled"{% endif %} class="span2" placeholder="{% trans %}Search forums...{% endtrans %}"{% if search_phrase is defined %} value="{{ search_query }}"{% endif %}>
+            <button type="submit"{% if disable_search %} disabled="disabled"{% endif %} class="btn btn-link"><i class="icon-search"></i></button>
           </div>
           </div>
         </form>
         </form>
         {% endif %}
         {% endif %}
         <ul class="nav navbar-blocks pull-left">
         <ul class="nav navbar-blocks pull-left">
           <li><a href="{% url 'index' %}" title="{% trans %}Forum Home{% endtrans %}" class="tooltip-bottom"><i class="icon-th-list"></i></a></li>
           <li><a href="{% url 'index' %}" title="{% trans %}Forum Home{% endtrans %}" class="tooltip-bottom"><i class="icon-th-list"></i></a></li>
+          {{ hook_primary_menu_prepend|safe }}
           <li><a href="{% url 'popular_threads' %}" title="{% trans %}Popular Threads{% endtrans %}" class="hot tooltip-bottom"><i class="icon-fire"></i></a></li>
           <li><a href="{% url 'popular_threads' %}" title="{% trans %}Popular Threads{% endtrans %}" class="hot tooltip-bottom"><i class="icon-fire"></i></a></li>
           <li><a href="{% url 'new_threads' %}" title="{% trans %}New Threads{% endtrans %}" class="fresh tooltip-bottom"><i class="icon-leaf"></i></a></li>{% if not user.crawler %}
           <li><a href="{% url 'new_threads' %}" title="{% trans %}New Threads{% endtrans %}" class="fresh tooltip-bottom"><i class="icon-leaf"></i></a></li>{% if not user.crawler %}
-          {% if not user.is_crawler() %}
-          <li><a href="#" title="{% trans %}Search Community{% endtrans %}" class="tooltip-bottom"><i class="icon-search"></i></a></li>{% endif %}
+          {% if acl.search.can_search() and not user.is_crawler() %}
+          <li><a href="{% url 'search' %}" title="{% trans %}Search Forums{% endtrans %}" class="tooltip-bottom"><i class="icon-search"></i></a></li>{% endif %}
           {% endif %}
           {% endif %}
           <li><a href="{% url 'users' %}" title="{% trans %}Browse Users{% endtrans %}" class="tooltip-bottom"><i class="icon-user"></i></a></li>
           <li><a href="{% url 'users' %}" title="{% trans %}Browse Users{% endtrans %}" class="tooltip-bottom"><i class="icon-user"></i></a></li>
           {% if settings.tos_url or settings.tos_content %}<li><a href="{% if settings.tos_url %}{{ settings.tos_url }}{% else %}{% url 'tos' %}{% endif %}" title="{% if settings.tos_title %}{{ settings.tos_title }}{% else %}{% trans %}Forum Terms of Service{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-certificate"></i></a></li>{% endif %}
           {% if settings.tos_url or settings.tos_content %}<li><a href="{% if settings.tos_url %}{{ settings.tos_url }}{% else %}{% url 'tos' %}{% endif %}" title="{% if settings.tos_title %}{{ settings.tos_title }}{% else %}{% trans %}Forum Terms of Service{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-certificate"></i></a></li>{% endif %}
+          {{ hook_primary_menu_append|safe }}
         </ul>
         </ul>
         {% if not user.is_crawler() %}
         {% if not user.is_crawler() %}
         {% if user.is_authenticated() %}
         {% if user.is_authenticated() %}
-        <ul class="nav navbar-blocks pull-right">
-          <li class="user-profile"><a href="{% url 'user' user=user.id, username=user.username_slug %}" title="{% trans %}Go to your profile{% endtrans %}" class="tooltip-bottom"><div><img src="{{ user.get_avatar(28) }}" alt=""> {{ user.username }}</div></a></li>
-          <li><a href="{% url 'alerts' %}" title="{% if user.alerts %}{% trans %}You have new notifications!{% endtrans %}{% else %}{% trans %}Your Notifications{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-asterisk"></i>{% if user.alerts %}<span class="label label-important">{{ user.alerts }}</span>{% endif %}</a></li>
-          {% if settings.enable_private_threads and acl.private_threads.can_participate() %}
-          <li><a href="{% url 'private_threads' %}" title="{% if user.unread_pds %}{% trans %}There are unread Private Threads!{% endtrans %}{% else %}{% trans %}Your Private Threads{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-inbox"></i>{% if user.unread_pds %}<span class="label label-important">{{ user.unread_pds }}</span>{% endif %}</a></li>
+        <ul id="fancy-user-nav" class="nav navbar-blocks navbar-compact pull-right">
+          {{ hook_user_menu_important_prepend|safe }}
+          {% if acl.reports.can_handle() and monitor.reported_posts %}
+          <li><a href="{% url 'reports' %}" title="{% trans %}There are unresolved reports!{% endtrans %}" class="tooltip-bottom"><i class="icon-fire"></i><span class="label label-important">{{ monitor.reported_posts }}</span></a></li>
           {% endif %}
           {% endif %}
-          <li><a href="{% url 'newsfeed' %}" title="{% trans %}Your News Feed{% endtrans %}" class="tooltip-bottom"><i class="icon-signal"></i></a></li>
-          <li><a href="{% url 'watched_threads' %}" title="{% trans %}Threads you are watching{% endtrans %}" class="tooltip-bottom"><i class="icon-bookmark"></i></a></li>
-          <li><a href="{% url 'usercp' %}" title="{% trans %}Edit your profile options{% endtrans %}" class="tooltip-bottom"><i class="icon-cog"></i></a></li>
-          <li><form action="{% url 'sign_out' %}" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><button type="submit" title="{% trans %}Sign Out and browse as guest{% endtrans %}" class="btn btn-link danger tooltip-bottom"><i class="icon-off"></i></button></form></li>
+          {% if user.alerts %}
+          <li><a href="{% url 'alerts' %}" title="{% trans %}You have new notifications!{% endtrans %}" class="tooltip-bottom"><i class="icon-asterisk"></i><span class="label label-important">{{ user.alerts }}</span></a></li>
+          {% endif %}
+          {% if settings.enable_private_threads and acl.private_threads.can_participate() and user.unread_pds%}
+          <li><a href="{% url 'private_threads' %}" title="{% trans %}There are unread Private Threads!{% endtrans %}" class="tooltip-bottom"><i class="icon-inbox"></i><span class="label label-important">{{ user.unread_pds }}</span></a></li>
+          {% endif %}
+          {{ hook_user_menu_important_append|safe }}
+          <li class="user-profile dropdown">
+            <a href="{% url 'user' user=user.id, username=user.username_slug %}" class="dropdown-toggle" data-toggle="dropdown"><div>{{ user.username }} <img src="{{ user.get_avatar(28) }}" alt=""><span class="caret-border"><b class="caret"></b></span></div></a>
+            <ul class="dropdown-menu">
+              <li><a href="{% url 'user' user=user.id, username=user.username_slug %}"><i class="icon-user"></i> {% trans %}Your profile{% endtrans %}</a></li>
+              <li><a href="{% url 'usercp' %}"><i class="icon-cog"></i> {% trans %}Change options{% endtrans %}</a></li>
+              <li role="presentation" class="divider"></li>
+              {% if acl.reports.can_handle() %}
+              <li><a href="{% url 'reports' %}">{% if monitor.reported_posts %}<span class="label">{{ monitor.reported_posts }}</span>{% endif %}<i class="icon-fire"></i> {% trans %}Reported Posts{% endtrans %}</a></li>
+              {% endif %}
+              <li><a href="{% url 'alerts' %}">{% if user.alerts %}<span class="label">{{ user.alerts }}</span>{% endif %}<i class="icon-asterisk"></i> {% trans %}Notifications{% endtrans %}</a></li>
+              {{ hook_user_menu_dropdown_prepend|safe }}
+              {% if settings.enable_private_threads and acl.private_threads.can_participate() %}
+              <li><a href="{% url 'private_threads' %}">{% if user.unread_pds %}<span class="label">{{ user.unread_pds }}</span>{% endif %}<i class="icon-inbox"></i> {% trans %}Private Threads{% endtrans %}</a></li>
+              {% endif %}
+              <li><a href="{% url 'newsfeed' %}"><i class="icon-signal"></i> {% trans %}News Feed{% endtrans %}</a></li>
+              <li><a href="{% url 'watched_threads' %}"><i class="icon-bookmark"></i> {% trans %}Watched Threads{% endtrans %}</a></li>
+              {{ hook_user_menu_dropdown_prepend|safe }}
+              <li role="presentation" class="divider"></li>
+              <li><form action="{% url 'sign_out' %}" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><button type="submit" class="btn btn-link danger"><i class="icon-off"></i> {% trans %}Sign out{% endtrans %}</button></form></li>
+            </ul>
+          </li>
         </ul>
         </ul>
+        <noscript>
+          <ul class="nav navbar-blocks pull-right">
+            {{ hook_user_menu_prepend|safe }}
+            <li><a href="{% url 'alerts' %}" title="{% if user.alerts %}{% trans %}You have new notifications!{% endtrans %}{% else %}{% trans %}Your Notifications{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-asterisk"></i>{% if user.alerts %}<span class="label label-important">{{ user.alerts }}</span>{% endif %}</a></li>
+            {% if acl.reports.can_handle() %}
+            <li><a href="{% url 'reports' %}" title="{% if monitor.reported_posts %}{% trans %}There are unresolved reports!{% endtrans %}{% else %}{% trans %}Reports{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-fire"></i>{% if monitor.reported_posts %}<span class="label label-important">{{ monitor.reported_posts }}</span>{% endif %}</a></li>
+            {% endif %}
+            {% if settings.enable_private_threads and acl.private_threads.can_participate() %}
+            <li><a href="{% url 'private_threads' %}" title="{% if user.unread_pds %}{% trans %}There are unread Private Threads!{% endtrans %}{% else %}{% trans %}Your Private Threads{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-inbox"></i>{% if user.unread_pds %}<span class="label label-important">{{ user.unread_pds }}</span>{% endif %}</a></li>
+            {% endif %}
+            <li><a href="{% url 'newsfeed' %}" title="{% trans %}Your News Feed{% endtrans %}" class="tooltip-bottom"><i class="icon-signal"></i></a></li>
+            <li><a href="{% url 'watched_threads' %}" title="{% trans %}Threads you are watching{% endtrans %}" class="tooltip-bottom"><i class="icon-bookmark"></i></a></li>
+            <li><a href="{% url 'usercp' %}" title="{% trans %}Edit your profile options{% endtrans %}" class="tooltip-bottom"><i class="icon-cog"></i></a></li>
+            {{ hook_user_menu_append|safe }}
+            <li class="user-profile"><a href="{% url 'user' user=user.id, username=user.username_slug %}" title="{% trans %}Go to your profile{% endtrans %}" class="tooltip-bottom"><div><img src="{{ user.get_avatar(28) }}" alt=""> {{ user.username }}</div></a></li>
+            <li><form action="{% url 'sign_out' %}" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><button type="submit" title="{% trans %}Sign Out and browse as guest{% endtrans %}" class="btn btn-link danger tooltip-bottom"><i class="icon-off"></i></button></form></li>
+          </ul>
+        </noscript>
         {% else %}
         {% else %}
         <ul class="nav navbar-user-nav pull-right">
         <ul class="nav navbar-user-nav pull-right">
+          {{ hook_guest_menu_prepend|safe }}
           <li><a href="{% url 'sign_in' %}" title="{% trans %}Sign In to Your Account{% endtrans %}" class="tooltip-bottom btn btn-danger"><i class="icon-check"></i> {% trans %}Sign In{% endtrans %}</a></li>{% if settings.account_activation != 'block' %}
           <li><a href="{% url 'sign_in' %}" title="{% trans %}Sign In to Your Account{% endtrans %}" class="tooltip-bottom btn btn-danger"><i class="icon-check"></i> {% trans %}Sign In{% endtrans %}</a></li>{% if settings.account_activation != 'block' %}
           <li><a href="{% url 'register' %}" title="{% trans %}Register new account{% endtrans %}" class="tooltip-bottom btn btn-inverse"><i class="icon-edit"></i> {% trans %}Register{% endtrans %}</a></li>{% endif %}
           <li><a href="{% url 'register' %}" title="{% trans %}Register new account{% endtrans %}" class="tooltip-bottom btn btn-inverse"><i class="icon-edit"></i> {% trans %}Register{% endtrans %}</a></li>{% endif %}
+          {{ hook_guest_menu_append|safe }}
         </ul>
         </ul>
         {% endif %}
         {% endif %}
         {% endif %}
         {% endif %}
@@ -85,14 +110,19 @@
   <div class="container">
   <div class="container">
     <ul class="breadcrumb">
     <ul class="breadcrumb">
       {% block breadcrumb %}<li class="first"><a href="{% url 'index' %}">{{ settings.board_name }}</a>{% endblock %}</li>
       {% block breadcrumb %}<li class="first"><a href="{% url 'index' %}">{{ settings.board_name }}</a>{% endblock %}</li>
+      {{ hook_foot_menu_prepend|safe }}
       <li class="pull-right"><i class="icon-move"></i> <a href="{% url 'forum_map' %}">{% trans %}Forum Map{% endtrans %}</a></li>
       <li class="pull-right"><i class="icon-move"></i> <a href="{% url 'forum_map' %}">{% trans %}Forum Map{% endtrans %}</a></li>
+      {{ hook_foot_menu_append|safe }}
     </ul>
     </ul>
     <hr>
     <hr>
     <div class="credits">
     <div class="credits">
-      {% if settings.board_credits %}
-      <p>{{ settings.board_credits|safe }}</p>
-      {% endif %}
-      <p class="software"><a href="http://misago-project.org">This community is powered by Misago forum software by Rafał Pitoń</a></p>
+      <p>
+        {% if settings.board_credits %}
+        {{ settings.board_credits|safe }}<br>
+        {% endif %}
+        <a href="http://misago-project.org">This community is powered by Misago forum software by Rafał Pitoń</a>
+      </p>
+      {{ hook_html_credits_side|safe }}
     </div>
     </div>
   </div>
   </div>
-</footer>{% endblock %}
+</footer>{% endblock %}

+ 32 - 0
templates/cranefly/macros.html

@@ -45,6 +45,38 @@
 itemprop="breadcrumb"
 itemprop="breadcrumb"
 {%- endmacro %}
 {%- endmacro %}
 
 
+{% macro parents_list(forums) -%}
+{% for forum in forums %}
+<li><a href="{% if loop.first %}{% url 'index' %}#{{ forum.slug }}{% else %}{{ forum.type|url(forum=forum.pk, slug=forum.slug) }}{% endif %}">{{ forum.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+{% endfor %}
+{%- endmacro %}
+
 {% macro wrap(item, wrap, extra='') -%}
 {% macro wrap(item, wrap, extra='') -%}
 <{{ wrap }}{% if extra %} {{ extra|safe }}{% endif %}>{{ item }}</{{ wrap }}>
 <{{ wrap }}{% if extra %} {{ extra|safe }}{% endif %}>{{ item }}</{{ wrap }}>
+{%- endmacro %}
+
+{% macro thread_flags(thread) -%}
+<ul class="unstyled thread-flags">
+  {% if thread.replies_reported and ((forum is defined and acl.threads.can_mod_posts(forum)) or acl.threads.can_mod_posts(thread.forum)) %}
+  <li class="flag-reported"><i class="icon-warning-sign tooltip-top" title="{% trans %}This thread has reported replies{% endtrans %}"></i></li>
+  {% endif %}
+  {% if thread.replies_moderated %}
+  <li class="flag-notreviewed"><i class="icon-question-sign tooltip-top" title="{% trans %}This thread has unreviewed replies{% endtrans %}"></i></li>
+  {% endif %}
+  {% if thread.weight == 2 %}
+  <li class="flag-announcement"><i class="icon-star tooltip-top" title="{% trans %}This thread is an annoucement{% endtrans %}"></i></li>
+  {% endif %}
+  {% if thread.weight == 1 %}
+  <li class="flag-sticky"><i class="icon-star-empty tooltip-top" title="{% trans %}This thread is sticky{% endtrans %}"></i></li>
+  {% endif %}
+  {% if thread.moderated  %}
+  <li class="flag-notreviewed"><i class="icon-eye-close tooltip-top" title="{% trans %}This thread awaits review{% endtrans %}"></i></li>
+  {% endif %}
+  {% if thread.deleted %}
+  <li class="flag-deleted"><i class="icon-trash tooltip-top" title="{% trans %}This thread is deleted{% endtrans %}"></i></li>
+  {% endif %}
+  {% if thread.closed %}
+  <li class="flag-closed"><i class="icon-lock tooltip-top" title="{% trans %}This thread is closed{% endtrans %}"></i></li>
+  {% endif %}
+</ul>
 {%- endmacro %}
 {%- endmacro %}

+ 48 - 50
templates/cranefly/new_threads.html

@@ -14,56 +14,54 @@
 <div class="container container-primary">
 <div class="container container-primary">
   {% if threads %}
   {% if threads %}
   <div class="forum-threads-list">
   <div class="forum-threads-list">
-    <table class="table">
-      <thead>
-        <tr>
-          <th>{% trans %}Thread{% endtrans %}</th>
-          <th class="span1">{% trans %}Rating{% endtrans %}</th>
-          <th class="span5">{% trans %}Activity{% endtrans %}</th>
-          {% if user.is_authenticated() and list_form %}
-          <th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>
+    <div class="header">
+      <div class="row-fluid">
+        <div class="span7">{% trans %}Thread{% endtrans %}</div>
+        <div class="span5 thread-activity">
+          <div class="thread-replies">{% trans %}Activity{% endtrans %}</div>
+        </div>
+      </div>
+    </div>
+    {% for thread in threads %}
+    <div class="thread-row{% if not thread.is_read %} thread-new{% endif %}{% if loop.last %} thread-last{% endif %}">
+      <div class="row-fluid">
+        <div class="span7">
+          {% if thread.is_read %}
+          <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-last tooltip-top" title="{% trans %}Click to see last post{% endtrans %}"><i class="icon-asterisk"></i></a>
+          {% else %}
+          <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-new tooltip-top" title="{% trans %}Click to see first unread post{% endtrans %}"><i class="icon-fire"></i></a>
           {% endif %}
           {% endif %}
-        </tr>
-      </thead>
-      <tbody>
-        {% for thread in threads %}
-        <tr>
-          <td>
-            <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon{% if not thread.is_read %} thread-new{% endif %} tooltip-top" title="{% if not thread.is_read -%}
-            {% trans %}Click to see first unread post{% endtrans %}
-            {%- else -%}
-            {% trans %}Click to see last post{% endtrans %}
-            {%- endif %}"><i class="icon-comment"></i></a>
-            <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
-            <span class="thread-details">
-              {% trans user=thread_starter(thread), forum=thread_forum(thread), start=thread.start|reltimesince|low %}by {{ user }} in {{ forum }} {{ start }}{% endtrans %}
-            </span>
-            <ul class="unstyled thread-flags">
-              {% if thread.weight == 2 %}
-              <li><i class="icon-star tooltip-top" title="{% trans %}This thread is an annoucement{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.weight == 1 %}
-              <li><i class="icon-star-empty tooltip-top" title="{% trans %}This thread is sticky{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.closed %}
-              <li><i class="icon-lock tooltip-top" title="{% trans %}This thread is closed{% endtrans %}"></i></li>
-              {% endif %}
-            </ul>
-          </td>
-          <td>
-            <div class="thread-rating{% if (thread.upvotes-thread.downvotes) > 0 %} thread-rating-positive{% elif (thread.upvotes-thread.downvotes) < 0 %} thread-rating-negative{% endif %}">
-              {% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}{{ (thread.upvotes-thread.downvotes)|abs|intcomma }}
-            </div>
-          </td>
-          <td>
-            <div class="thread-last-reply">
-              {{ replies(thread.replies) }} - {% trans user=thread_reply(thread), last=thread.last|reltimesince|low %}last by {{ user }} {{ last }}{% endtrans %}
-            </div>
-          </td>
-        </tr>
-        {% endfor %}
-      </tbody>
-    </table>
+
+          <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
+
+          {{ macros.thread_flags(thread) }}
+          
+          <div class="thread-details">
+            {% trans user=thread_starter(thread), forum=thread_forum(thread), start=thread.start|reltimesince|low %}by {{ user }} in {{ forum }} {{ start }}{% endtrans %}
+          </div>
+
+        </div>
+        <div class="span5 thread-activity">
+
+          {% if settings.avatars_on_threads_list %}
+          <div class="thread-last-avatar">
+            {% if thread.last_poster_id %}
+            <a href="{% url 'user' user=thread.last_poster.pk, username=thread.last_poster.username_slug %}"><img src="{{ thread.last_poster.get_avatar(40) }}" alt=""></a>
+            {% else %}
+            <img src="{{ macros.avatar_guest(40) }}" alt="" class="user-avatar">
+            {% endif %}
+          </div>
+          {% endif %}
+
+          <div class="thread-replies">
+            <strong class="lead">{{ thread_reply(thread) }}, {{ thread.last|reldate|low }}</strong><br>
+            {{ replies(thread.replies) }}, <span{% if (thread.upvotes-thread.downvotes) > 0 %} class="text-success"{% elif (thread.upvotes-thread.downvotes) < 0 %} class="text-error"{% endif %}><strong>{% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}</strong>{% trans rating=(thread.upvotes-thread.downvotes)|abs|intcomma %}{{ rating }} thread rating{% endtrans %}</span>
+          </div>
+          
+        </div>
+      </div>
+    </div>
+    {% endfor %}
   </div>
   </div>
   {{ pager() }}
   {{ pager() }}
   {% else %}
   {% else %}
@@ -74,7 +72,7 @@
 
 
 
 
 {% macro replies(thread_replies) -%}
 {% macro replies(thread_replies) -%}
-{% trans count=thread_replies, replies=('<strong>' ~ (thread_replies|intcomma) ~ '</strong>')|safe -%}
+{% trans count=thread_replies, replies=thread_replies|intcomma -%}
 {{ replies }} reply
 {{ replies }} reply
 {%- pluralize -%}
 {%- pluralize -%}
 {{ replies }} replies
 {{ replies }} replies

+ 49 - 50
templates/cranefly/popular_threads.html

@@ -14,57 +14,56 @@
 <div class="container container-primary">
 <div class="container container-primary">
   {% if threads %}
   {% if threads %}
   <div class="forum-threads-list">
   <div class="forum-threads-list">
-    <table class="table">
-      <thead>
-        <tr>
-          <th>{% trans %}Thread{% endtrans %}</th>
-          <th class="span1">{% trans %}Rating{% endtrans %}</th>
-          <th class="span5">{% trans %}Activity{% endtrans %}</th>
-          {% if user.is_authenticated() and list_form %}
-          <th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>
+    <div class="header">
+      <div class="row-fluid">
+        <div class="span7">{% trans %}Thread{% endtrans %}</div>
+        <div class="span5 thread-activity">
+          <div class="thread-replies">{% trans %}Activity{% endtrans %}</div>
+        </div>
+      </div>
+    </div>
+    {% for thread in threads %}
+    <div class="thread-row{% if not thread.is_read %} thread-new{% endif %}{% if loop.last %} thread-last{% endif %}">
+      <div class="row-fluid">
+        <div class="span7">
+          {% if thread.is_read %}
+          <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-last tooltip-top" title="{% trans %}Click to see last post{% endtrans %}"><i class="icon-asterisk"></i></a>
+          {% else %}
+          <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-new tooltip-top" title="{% trans %}Click to see first unread post{% endtrans %}"><i class="icon-fire"></i></a>
           {% endif %}
           {% endif %}
-        </tr>
-      </thead>
-      <tbody>
-        {% for thread in threads %}
-        <tr>
-          <td>
-            <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon{% if not thread.is_read %} thread-new{% endif %} tooltip-top" title="{% if not thread.is_read -%}
-            {% trans %}Click to see first unread post{% endtrans %}
-            {%- else -%}
-            {% trans %}Click to see last post{% endtrans %}
-            {%- endif %}"><i class="icon-comment"></i></a>
-            <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
-            <span class="thread-details">
-              {% trans user=thread_starter(thread), forum=thread_forum(thread), start=thread.start|reltimesince|low %}by {{ user }} in {{ forum }} {{ start }}{% endtrans %}
-            </span>
-            <ul class="unstyled thread-flags">
-              {% if thread.weight == 2 %}
-              <li><i class="icon-star tooltip-top" title="{% trans %}This thread is an annoucement{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.weight == 1 %}
-              <li><i class="icon-star-empty tooltip-top" title="{% trans %}This thread is sticky{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.closed %}
-              <li><i class="icon-lock tooltip-top" title="{% trans %}This thread is closed{% endtrans %}"></i></li>
-              {% endif %}
-            </ul>
-          </td>
-          <td>
-            <div class="thread-rating{% if (thread.upvotes-thread.downvotes) > 0 %} thread-rating-positive{% elif (thread.upvotes-thread.downvotes) < 0 %} thread-rating-negative{% endif %}">
-              {% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}{{ (thread.upvotes-thread.downvotes)|abs|intcomma }}
-            </div>
-          </td>
-          <td>
-            <div class="thread-last-reply">
-              {{ replies(thread.replies) }} - {% trans user=thread_reply(thread), last=thread.last|reltimesince|low %}last by {{ user }} {{ last }}{% endtrans %}
-            </div>
-          </td>
-        </tr>
-        {% endfor %}
-      </tbody>
-    </table>
+
+          {{ macros.thread_flags(thread) }}
+
+          <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
+          
+          <div class="thread-details">
+            {% trans user=thread_starter(thread), forum=thread_forum(thread), start=thread.start|reltimesince|low %}by {{ user }} in {{ forum }} {{ start }}{% endtrans %}
+          </div>
+
+        </div>
+        <div class="span5 thread-activity">
+
+          {% if settings.avatars_on_threads_list %}
+          <div class="thread-last-avatar">
+            {% if thread.last_poster_id %}
+            <a href="{% url 'user' user=thread.last_poster.pk, username=thread.last_poster.username_slug %}"><img src="{{ thread.last_poster.get_avatar(40) }}" alt=""></a>
+            {% else %}
+            <img src="{{ macros.avatar_guest(40) }}" alt="" class="user-avatar">
+            {% endif %}
+          </div>
+          {% endif %}
+
+          <div class="thread-replies">
+            <strong class="lead">{{ thread_reply(thread) }}, {{ thread.last|reldate|low }}</strong><br>
+            {{ replies(thread.replies) }}, <span{% if (thread.upvotes-thread.downvotes) > 0 %} class="text-success"{% elif (thread.upvotes-thread.downvotes) < 0 %} class="text-error"{% endif %}><strong>{% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}</strong>{% trans rating=(thread.upvotes-thread.downvotes)|abs|intcomma %}{{ rating }} thread rating{% endtrans %}</span>
+          </div>
+
+        </div>
+      </div>
+    </div>
+    {% endfor %}
   </div>
   </div>
+  {{ pager() }}
   {% else %}
   {% else %}
   <p class="lead">{% trans %}Looks like there are no popular threads... yet!{% endtrans %}</p>
   <p class="lead">{% trans %}Looks like there are no popular threads... yet!{% endtrans %}</p>
   {% endif %}
   {% endif %}
@@ -73,7 +72,7 @@
 
 
 
 
 {% macro replies(thread_replies) -%}
 {% macro replies(thread_replies) -%}
-{% trans count=thread_replies, replies=('<strong>' ~ (thread_replies|intcomma) ~ '</strong>')|safe -%}
+{% trans count=thread_replies, replies=thread_replies|intcomma -%}
 {{ replies }} reply
 {{ replies }} reply
 {%- pluralize -%}
 {%- pluralize -%}
 {{ replies }} replies
 {{ replies }} replies

+ 1 - 1
templates/cranefly/private_threads/changelog.html

@@ -58,7 +58,7 @@
               {% trans chars=edit.change|abs %}Removed one character from post.{% pluralize %}Removed {{ chars }} characters from post.{% endtrans %}
               {% trans chars=edit.change|abs %}Removed one character from post.{% pluralize %}Removed {{ chars }} characters from post.{% endtrans %}
               {%- else -%}
               {%- else -%}
               {% trans %}No change in message's length.{% endtrans %}
               {% trans %}No change in message's length.{% endtrans %}
-              {%- endif %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Changed thread name from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Renamed thread from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}</a>
+              {%- endif %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Changed thread name from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}</a>
               <span class="change-details">
               <span class="change-details">
                 {% trans user=edit_user(edit), date=edit.date|reldate|low %}By {{ user }} {{ date }}{% endtrans %}
                 {% trans user=edit_user(edit), date=edit.date|reldate|low %}By {{ user }} {{ date }}{% endtrans %}
               </span>
               </span>

+ 65 - 76
templates/cranefly/private_threads/list.html

@@ -9,11 +9,15 @@
 {%- endblock %}
 {%- endblock %}
 
 
 {% block container %}
 {% block container %}
-<div class="page-header header-primary">
+<div class="page-header header-primary header-search">
   <div class="container">
   <div class="container">
     {{ messages_list(messages) }}
     {{ messages_list(messages) }}
     <ul class="breadcrumb" {{ macros.itemprop_bread() }}>
     <ul class="breadcrumb" {{ macros.itemprop_bread() }}>
-      {{ self.breadcrumb() }}</li>
+      {{ self.breadcrumb() }} <form action="{% url 'private_threads_search' %}" class="form-inline" method="post">
+       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+       <input maxlength="255" type="text" name="search_query" class="span4">
+       <button type="submit" class="btn btn-primary">{% trans %}Search{% endtrans %}</button>
+    </form></li>
     </ul>
     </ul>
     <h1>{% trans %}Private Threads{% endtrans %}</h1>
     <h1>{% trans %}Private Threads{% endtrans %}</h1>
   </div>
   </div>
@@ -35,86 +39,71 @@
   </div>
   </div>
 
 
   <div class="forum-threads-list">
   <div class="forum-threads-list">
-    <table class="table">
-      <thead>
-        <tr>
-          <th>{% trans %}Thread{% endtrans %}</th>
-          <th class="span1">{% trans %}Rating{% endtrans %}</th>
-          <th class="span5">{% trans %}Activity{% endtrans %}</th>
+    <div class="header">
+      <div class="row-fluid">
+        <div class="span7">{% trans %}Thread{% endtrans %}</div>
+        <div class="span5 thread-activity">
+          <div class="thread-replies">{% trans %}Activity{% endtrans %}</div>
           {% if list_form %}
           {% if list_form %}
-          <th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>
+          <div class="pull-right check-cell">
+            <label class="checkbox"><input type="checkbox" class="checkbox-master"></label>
+          </div>
           {% endif %}
           {% endif %}
-        </tr>
-      </thead>
-      <tbody>
-        {% for thread in threads %}
-        <tr>
-          <td>
-            <a href="{% url 'private_thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon{% if not thread.is_read %} thread-new{% endif %} tooltip-top" title="{% if not thread.is_read -%}
-            {% trans %}Click to see first unread post{% endtrans %}
-            {%- else -%}
-            {% trans %}Click to see last post{% endtrans %}
-            {%- endif %}"><i class="icon-comment"></i></a>
-            <a href="{% url 'private_thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
-            <span class="thread-details">
-              {% trans user=thread_starter(thread), start=thread.start|reltimesince|low %}by {{ user }} {{ start }}{% endtrans %}
-            </span>
-            <ul class="unstyled thread-flags">
-              {% if thread.replies_reported %}
-              <li><i class="icon-warning-sign tooltip-top" title="{% trans %}This thread has reported replies{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.replies_moderated %}
-              <li><i class="icon-question-sign tooltip-top" title="{% trans %}This thread has unreviewed replies{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.weight == 2 %}
-              <li><i class="icon-star tooltip-top" title="{% trans %}This thread is an annoucement{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.weight == 1 %}
-              <li><i class="icon-star-empty tooltip-top" title="{% trans %}This thread is sticky{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.moderated  %}
-              <li><i class="icon-eye-close tooltip-top" title="{% trans %}This thread awaits review{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.deleted %}
-              <li><i class="icon-trash tooltip-top" title="{% trans %}This thread is deleted{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.closed %}
-              <li><i class="icon-lock tooltip-top" title="{% trans %}This thread is closed{% endtrans %}"></i></li>
-              {% endif %}
-            </ul>
-          </td>
-          <td>
-            <div class="thread-rating{% if (thread.upvotes-thread.downvotes) > 0 %} thread-rating-positive{% elif (thread.upvotes-thread.downvotes) < 0 %} thread-rating-negative{% endif %}">
-              {% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}{{ (thread.upvotes-thread.downvotes)|abs|intcomma }}
-            </div>
-          </td>
-          <td>
-            <div class="thread-last-reply">
-              {{ replies(thread.replies) }} - {% trans user=thread_reply(thread), last=thread.last|reltimesince|low %}last by {{ user }} {{ last }}{% endtrans %}
-            </div>
-          </td>
-          {% if list_form %}
-          <td class="check-cell">{% if thread.forum_id == forum.pk %}<label class="checkbox"><input form="threads_form" name="{{ list_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ thread.pk }}"{% if list_form['list_items']['has_value'] and ('' ~ thread.pk) in list_form['list_items']['value'] %} checked="checked"{% endif %}></label>{% else %}&nbsp;{% endif %}</td>
+        </div>
+      </div>
+    </div>
+    {% for thread in threads %}
+    <div class="thread-row{% if not thread.is_read %} thread-new{% endif %}{% if loop.last %} thread-last{% endif %}">
+      <div class="row-fluid">
+        <div class="span7">
+          {% if thread.is_read %}
+          <a href="{% url 'private_thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-last tooltip-top" title="{% trans %}Click to see last post{% endtrans %}"><i class="icon-asterisk"></i></a>
+          {% else %}
+          <a href="{% url 'private_thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-new tooltip-top" title="{% trans %}Click to see first unread post{% endtrans %}"><i class="icon-fire"></i></a>
           {% endif %}
           {% endif %}
-        </tr>
-        {% else %}
-        <tr>
-          <td colspan="4" class="threads-list-empty">
-            {% if tab == 'all' %}
-            {% trans %}You are not participating in any private discussions.{% endtrans %}
-            {% elif tab == 'new' %}
-            {% trans %}There are no unread private threads.{% endtrans %}
+
+          <a href="{% url 'private_thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
+
+          {{ macros.thread_flags(thread) }}
+
+          <div class="thread-details">
+            {% trans user=thread_starter(thread), start=thread.start|reldate|low %}by {{ user }}, {{ start }}{% endtrans %}
+          </div>
+
+        </div>
+        <div class="span5 thread-activity">
+
+          {% if settings.avatars_on_threads_list %}
+          <div class="thread-last-avatar">
+            {% if thread.last_poster_id %}
+            <a href="{% url 'user' user=thread.last_poster.pk, username=thread.last_poster.username_slug %}"><img src="{{ thread.last_poster.get_avatar(40) }}" alt=""></a>
             {% else %}
             {% else %}
-            {% trans %}You have started no private threads.{% endtrans %}
+            <img src="{{ macros.avatar_guest(40) }}" alt="" class="user-avatar">
             {% endif %}
             {% endif %}
-          </td>
-        </tr>
-        {% endfor %}
-      </tbody>
-    </table>
+          </div>
+          {% endif %}
+
+          <div class="thread-replies">
+            <strong class="lead">{{ thread_reply(thread) }}, {{ thread.last|reldate|low }}</strong><br>
+            {{ replies(thread.replies) }}
+          </div>
+
+          {% if list_form %}
+          <label class="thread-select checkbox"><input form="threads_form" name="{{ list_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ thread.pk }}"{% if list_form['list_items']['has_value'] and ('' ~ thread.pk) in list_form['list_items']['value'] %} checked="checked"{% endif %}></label>
+          {% endif %}
+
+        </div>
+      </div>
+    </div>
+    {% else %}
+    <div class="thread-row threads-list-empty">
+      {% trans %}You are not participating in any private discussions.{% endtrans %}
+    </div>
+    {% endfor %}
+    
     {% if list_form %}
     {% if list_form %}
     <div class="threads-actions">
     <div class="threads-actions">
-      <form id="threads_form" class="form-inline pull-right" action="{% url 'private_threads' %}" method="POST">
+      <form id="threads_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
         <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
         <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
         {{ form_theme.input_select(list_form['list_action'],width=3) }}
         {{ form_theme.input_select(list_form['list_action'],width=3) }}
         <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
         <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
@@ -135,7 +124,7 @@
 
 
 
 
 {% macro replies(thread_replies) -%}
 {% macro replies(thread_replies) -%}
-{% trans count=thread_replies, replies=('<strong>' ~ (thread_replies|intcomma) ~ '</strong>')|safe -%}
+{% trans count=thread_replies, replies=thread_replies|intcomma -%}
 {{ replies }} reply
 {{ replies }} reply
 {%- pluralize -%}
 {%- pluralize -%}
 {{ replies }} replies
 {{ replies }} replies

+ 5 - 3
templates/cranefly/private_threads/posting.html

@@ -11,7 +11,7 @@
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'private_threads' %}">{% trans %}Private Threads{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'private_threads' %}">{% trans %}Private Threads{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% if thread %}<li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>{% endif %}
+{% if thread %}<li><a href="{% url 'private_thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>{% endif %}
 <li class="active">{{ get_title() }}
 <li class="active">{{ get_title() }}
 {%- endblock %}
 {%- endblock %}
 
 
@@ -49,7 +49,9 @@
           {% if preview %}
           {% if preview %}
           <div class="form-preview">
           <div class="form-preview">
             <div class="markdown js-extra">
             <div class="markdown js-extra">
-              {{ preview|markdown_final|safe }}
+              <article>
+                {{ preview|markdown_final|safe }}
+              </article>
             </div>
             </div>
           </div>
           </div>
           {% endif %}
           {% endif %}
@@ -64,7 +66,7 @@
             {% endif %}
             {% endif %}
             {% if 'thread_name' in form.fields or (action == 'new_thread' and 'invite_users' in form.fields) %}
             {% if 'thread_name' in form.fields or (action == 'new_thread' and 'invite_users' in form.fields) %}
             <hr>
             <hr>
-            <h4>Message Body</h4>
+            <h4>{% trans %}Message Body{% endtrans %}</h4>
             {% endif %}
             {% endif %}
             {{ editor.editor(form.fields.post, get_button(), rows=8, extra=get_extra()) }}
             {{ editor.editor(form.fields.post, get_button(), rows=8, extra=get_extra()) }}
             {% if 'edit_reason' in form.fields or (action == 'new_reply' and 'invite_users' in form.fields) %}
             {% if 'edit_reason' in form.fields or (action == 'new_reply' and 'invite_users' in form.fields) %}

+ 96 - 59
templates/cranefly/private_threads/thread.html

@@ -32,7 +32,7 @@
         <form class="leave-form" action="{% url 'private_thread_remove_user' thread=thread.pk, slug=thread.slug %}" method="post">
         <form class="leave-form" action="{% url 'private_thread_remove_user' thread=thread.pk, slug=thread.slug %}" method="post">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="user" value="{{ user.pk }}">
           <input type="hidden" name="user" value="{{ user.pk }}">
-          <button type="submit" class="btn"><i class="icon-remove"></i> Leave Thread</button>
+          <button type="submit" class="btn"><i class="icon-remove"></i> {% trans %}Leave Thread{% endtrans %}</button>
         </form>
         </form>
       </li>
       </li>
     </ul>
     </ul>
@@ -51,7 +51,7 @@
 
 
       <div class="thread-buttons">
       <div class="thread-buttons">
         {{ pager() }} 
         {{ pager() }} 
-        {% if acl.threads.can_reply(forum, thread) and participants|length > 1 %}
+        {% if acl.threads.can_reply(forum, thread) and (acl.private_threads.is_mod() or participants|length > 1) %}
         <a href="{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug %}" class="btn btn-inverse pull-right"><i class="icon-pencil"></i> {% trans %}Reply{% endtrans %}</a>
         <a href="{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug %}" class="btn btn-inverse pull-right"><i class="icon-pencil"></i> {% trans %}Reply{% endtrans %}</a>
         {% endif %}
         {% endif %}
         {% if watcher %}
         {% if watcher %}
@@ -83,7 +83,7 @@
             {% if post.user_id %}
             {% if post.user_id %}
             <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(50) }}" alt="" class="user-avatar"></a>
             <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(50) }}" alt="" class="user-avatar"></a>
             {% else %}
             {% else %}
-            <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar"></a>
+            <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar">
             {% endif %}
             {% endif %}
             <div class="post-content">
             <div class="post-content">
               <div class="post-header">
               <div class="post-header">
@@ -94,26 +94,18 @@
                   <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
                   <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
                   {% endif %}
                   {% endif %}
                   <span class="separator">&ndash;</span>
                   <span class="separator">&ndash;</span>
-                  <a href="{% if pagination['page'] > 1 -%}
-                  {% url 'private_thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-                  {%- else -%}
-                  {% url 'private_thread' thread=thread.pk, slug=thread.slug %}
-                  {%- endif %}#post-{{ post.pk }}" class="post-date">{{ post.date|reltimesince }}</a>
+                  <a href="{% url 'private_thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
                   {% if post.edits %}
                   {% if post.edits %}
                   <span class="separator">&ndash;</span>
                   <span class="separator">&ndash;</span>
                   {% if acl.threads.can_see_changelog(user, forum, post) %}
                   {% if acl.threads.can_see_changelog(user, forum, post) %}
-                  <a href="{% url 'changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
+                  <a href="{% url 'private_thread_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
                   {% else %}
                   {% else %}
                   <span class="post-changelog">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</span>
                   <span class="post-changelog">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</span>
                   {% endif %}
                   {% endif %}
                   {% endif %}
                   {% endif %}
                 </div>
                 </div>
 
 
-                <a href="{% if pagination['page'] > 1 -%}
-                {% url 'private_thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-                {%- else -%}
-                {% url 'private_thread' thread=thread.pk, slug=thread.slug %}
-                {%- endif %}#post-{{ post.pk }}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+                <a href="{% url 'private_thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
 
 
                 {% if not post.is_read %}
                 {% if not post.is_read %}
                 <div class="post-extra">
                 <div class="post-extra">
@@ -125,29 +117,21 @@
 
 
               </div>
               </div>
               <div class="post-message">
               <div class="post-message">
-                {% trans user=edit_user(post), date=post.edit_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
+                {% trans user=edit_user(post), date=post.current_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
               </div>
               </div>
             </dv>
             </dv>
           </div>
           </div>
           {% elif post.ignored %}
           {% elif post.ignored %}
           <div class="post-body post-muted">
           <div class="post-body post-muted">
-            <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar"></a>
+            <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar">
             <div class="post-arrow"></div>
             <div class="post-arrow"></div>
             <div class="post-content">
             <div class="post-content">
               <div class="post-header">
               <div class="post-header">
                 <div class="post-header-compact">
                 <div class="post-header-compact">
-                  <a href="{% if pagination['page'] > 1 -%}
-                  {% url 'private_thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-                  {%- else -%}
-                  {% url 'private_thread' thread=thread.pk, slug=thread.slug %}
-                  {%- endif %}#post-{{ post.pk }}" class="post-date">{{ post.date|reltimesince }}</a>
+                  <a href="{% url 'private_thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
                 </div>
                 </div>
 
 
-                <a href="{% if pagination['page'] > 1 -%}
-                {% url 'private_thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-                {%- else -%}
-                {% url 'private_thread' thread=thread.pk, slug=thread.slug %}
-                {%- endif %}#post-{{ post.pk }}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+                <a href="{% url 'private_thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
 
 
                 {% if not post.is_read %}
                 {% if not post.is_read %}
                 <div class="post-extra">
                 <div class="post-extra">
@@ -168,7 +152,7 @@
             {% if post.user_id %}
             {% if post.user_id %}
             <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(100) }}" alt="" class="user-avatar"></a>
             <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(100) }}" alt="" class="user-avatar"></a>
             {% else %}
             {% else %}
-            <img src="{{ macros.avatar_guest(100) }}" alt="" class="user-avatar"></a>
+            <img src="{{ macros.avatar_guest(100) }}" alt="" class="user-avatar">
             {% endif %}
             {% endif %}
             <div class="post-arrow"></div>
             <div class="post-arrow"></div>
             <div class="post-content">
             <div class="post-content">
@@ -222,7 +206,7 @@
                   </span>
                   </span>
                   {% endif %}
                   {% endif %}
 
 
-                  {% if post.reported %}
+                  {% if acl.threads.can_mod_posts(forum) and post.reported %}
                   <span class="label label-important">
                   <span class="label label-important">
                     {% trans %}Reported{% endtrans %}
                     {% trans %}Reported{% endtrans %}
                   </span>
                   </span>
@@ -237,7 +221,9 @@
               </div>
               </div>
               <div class="post-message">
               <div class="post-message">
                 <div class="markdown js-extra">
                 <div class="markdown js-extra">
-                  {{ post.post_preparsed|markdown_final|safe }}
+                  <article>
+                    {{ post.post_preparsed|markdown_final|safe }}
+                  </article>
                 </div>
                 </div>
                 {% if post.user.signature %}
                 {% if post.user.signature %}
                 <div class="post-signature">
                 <div class="post-signature">
@@ -252,48 +238,65 @@
                   {% if acl.users.can_see_users_trails() -%}
                   {% if acl.users.can_see_users_trails() -%}
                   <a href="{% url 'private_post_info' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-trail">{% trans %}Info{% endtrans %}</a>
                   <a href="{% url 'private_post_info' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-trail">{% trans %}Info{% endtrans %}</a>
                   {% endif %}
                   {% endif %}
+                  {% if post.reported and acl.reports.can_handle() and acl.threads.can_mod_posts(forum) %}
+                  <a href="{% url 'private_post_report_show' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans %}Show report{% endtrans %}</a>
+                  {% endif %}
+                  {% if acl.reports.can_report() %}
+                  <form action="{% url 'private_post_report' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline form-report" method="post" autocomplete="off">
+                    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                    <button type="submit" class="btn btn-link btn-report tooltip-top" title="{% trans %}Bring this post to moderator attention.{% endtrans %}">{% trans %}Report{% endtrans %}</button>
+                  </form>
+                  {% endif %}
                   {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk %}
                   {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk %}
                   <a href="{% url 'private_thread_edit' thread=thread.pk, slug=thread.slug %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
                   <a href="{% url 'private_thread_edit' thread=thread.pk, slug=thread.slug %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
                   {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
                   {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
                   <a href="{% url 'private_post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
                   <a href="{% url 'private_post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
                   {%- endif %}
                   {%- endif %}
-                  {% if acl.threads.can_reply(forum, thread) %}<a href="{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug, quote=post.pk %}" class="post-reply">{% trans %}Reply{% endtrans %}</a>{% endif %}
+                  {% if acl.threads.can_reply(forum, thread) and (acl.private_threads.is_mod() or participants|length > 1) %}
+                  <a href="{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug, quote=post.pk %}" class="post-reply">{% trans %}Reply{% endtrans %}</a>
+                  {% endif %}
                 </div>
                 </div>
                 {% if post.pk == thread.start_post_id %}
                 {% if post.pk == thread.start_post_id %}
                 <div class="post-actions">
                 <div class="post-actions">
-                  {% if acl.threads.can_delete_thread(user, forum, thread, post) == 2 %}
-                  <form action="{% url 'private_thread_delete' thread=thread.pk, slug=thread.slug %}" class="form-inline prompt-delete-thread" method="post">
+                  {% if acl.threads.can_delete_thread(user, forum, thread, post) %}
+                  {% if post.deleted %}
+                  <form action="{% url 'private_thread_show' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post">
                     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-                    <span>{% trans %}Delete thread:{% endtrans %}</span>
-                    <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this thread for good{% endtrans %}">{% trans %}Hard{% endtrans %}</button>
+                    <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Make this thread visible to other users{% endtrans %}">{% trans %}Restore{% endtrans %}</button>
                   </form>
                   </form>
+                  {% else %}
+                  <form action="{% url 'private_thread_hide' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post">
+                    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                    <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Hide this thread from other users{% endtrans %}">{% trans %}Hide{% endtrans %}</button>
+                  </form>
+                  {% endif %}
                   {% endif %}
                   {% endif %}
-                  {% if not post.deleted and acl.threads.can_delete_thread(user, forum, thread, post) %}
-                  <form action="{% url 'private_thread_hide' thread=thread.pk, slug=thread.slug %}" class="form-inline prompt-delete-thread" method="post">
+                  {% if acl.threads.can_delete_thread(user, forum, thread, post) == 2 %}
+                  <form action="{% url 'private_thread_delete' thread=thread.pk, slug=thread.slug %}" class="form-inline prompt-delete-thread" method="post">
                     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-                    {% if acl.threads.can_delete_thread(user, forum, thread, post) != 2 %}
-                    <span>{% trans %}Delete thread:{% endtrans %}</span>
-                    {% endif %}
-                    <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Hide this thread from other users{% endtrans %}">{% trans %}Soft{% endtrans %}</button>
+                    <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this thread for good{% endtrans %}">{% trans %}Delete{% endtrans %}</button>
                   </form>
                   </form>
                   {% endif %}
                   {% endif %}
                 </div>
                 </div>
                 {% elif post.pk != thread.start_post_id and acl.threads.can_delete_post(user, forum, thread, post) %}
                 {% elif post.pk != thread.start_post_id and acl.threads.can_delete_post(user, forum, thread, post) %}
                 <div class="post-actions">
                 <div class="post-actions">
-                  {% if acl.threads.can_delete_post(user, forum, thread, post) == 2 -%}
-                  <form action="{% url 'private_post_delete' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline prompt-delete-post" method="post">
+                  {% if acl.threads.can_delete_post(user, forum, thread, post) %}
+                  {% if post.deleted %}
+                  <form action="{% url 'private_post_show' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline" method="post">
+                    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                    <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Make this reply visible to other users{% endtrans %}">{% trans %}Restore{% endtrans %}</button>
+                  </form>
+                  {% else %}
+                  <form action="{% url 'private_post_hide' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline" method="post">
                     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-                    <span>{% trans %}Delete reply:{% endtrans %}</span>
-                    <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this reply for good{% endtrans %}">{% trans %}Hard{% endtrans %}</button>
+                    <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Hide this reply from other users{% endtrans %}">{% trans %}Hide{% endtrans %}</button>
                   </form>
                   </form>
                   {% endif %}
                   {% endif %}
-                  {% if not post.deleted and acl.threads.can_delete_post(user, forum, thread, post) %}
-                  <form action="{% url 'private_post_hide' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline prompt-delete-post" method="post">
+                  {% endif %}
+                  {% if acl.threads.can_delete_post(user, forum, thread, post) == 2 -%}
+                  <form action="{% url 'private_post_delete' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline prompt-delete-post" method="post">
                     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                     <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-                    {% if acl.threads.can_delete_post(user, forum, thread, post) != 2 %}
-                    <span>{% trans %}Delete reply:{% endtrans %}</span>
-                    {% endif %}
-                    <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Hide this reply from other users{% endtrans %}">{% trans %}Soft{% endtrans %}</button>
+                    <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this reply for good{% endtrans %}">{% trans %}Delete{% endtrans %}</button>
                   </form>
                   </form>
                   {% endif %}
                   {% endif %}
                 </div>
                 </div>
@@ -304,9 +307,9 @@
           {% endif %}
           {% endif %}
         </div>
         </div>
 
 
-        {% if post.checkpoint_set.all() %}
+        {% if post.checkpoints_visible %}
         <div class="post-checkpoints">
         <div class="post-checkpoints">
-          {% for checkpoint in post.checkpoint_set.all() %}
+          {% for checkpoint in post.checkpoints_visible %}
           <div class="post-checkpoint">
           <div class="post-checkpoint">
             <hr>
             <hr>
             <span>
             <span>
@@ -329,6 +332,30 @@
               {%- elif checkpoint.action == 'left' -%}
               {%- elif checkpoint.action == 'left' -%}
               <i class="icon-remove-sign"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} left thread {{ date }}{% endtrans %}
               <i class="icon-remove-sign"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} left thread {{ date }}{% endtrans %}
               {%- endif -%}
               {%- endif -%}
+              {% if user.is_authenticated() %}
+              {% if acl.threads.can_delete_checkpoint(forum) %}
+              {% if checkpoint.deleted %}
+              <form action="{% url 'private_post_checkpoint_show' slug=thread.slug, thread=thread.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="hidden" name="retreat" value="{{ request_path }}#post-{{ post.pk }}">
+                <button type="submit" class="btn btn-link btn-show">{% trans %}Restore{% endtrans %}</button>
+              </form>
+              {% else %}
+              <form action="{% url 'private_post_checkpoint_hide' slug=thread.slug, thread=thread.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="hidden" name="retreat" value="{{ request_path }}#post-{{ post.pk }}">
+                <button type="submit" class="btn btn-link btn-hide">{% trans %}Hide{% endtrans %}</button>
+              </form>
+              {% endif %}
+              {% endif %}
+              {% if acl.threads.can_delete_checkpoint(forum) == 2 %}
+              <form action="{% url 'private_post_checkpoint_delete' slug=thread.slug, thread=thread.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline prompt-delete-checkpoint">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="hidden" name="retreat" value="{{ request_path }}#post-{{ post.pk }}">
+                <button type="submit" class="btn btn-link btn-delete">{% trans %}Delete{% endtrans %}</button>
+              </form>
+              {% endif %}
+              {% endif %}
             </span>
             </span>
           </div>
           </div>
           {% endfor %}
           {% endfor %}
@@ -340,7 +367,7 @@
       {% if thread_form or posts_form %}
       {% if thread_form or posts_form %}
       <div class="thread-moderation">
       <div class="thread-moderation">
         {% if thread_form%}
         {% if thread_form%}
-        <form id="thread_form" class="form-inline pull-left" action="{% url 'private_thread' slug=thread.slug, thread=thread.id, page=pagination['page'] %}" method="POST">
+        <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="origin" value="thread_form">
           <input type="hidden" name="origin" value="thread_form">
           {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
           {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
@@ -348,7 +375,7 @@
         </form>
         </form>
         {% endif %}
         {% endif %}
         {% if posts_form%}
         {% if posts_form%}
-        <form id="posts_form" class="form-inline pull-right" action="{% url 'private_thread' slug=thread.slug, thread=thread.id, page=pagination['page'] %}" method="POST">
+        <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="origin" value="posts_form">
           <input type="hidden" name="origin" value="posts_form">
           {{ form_theme.input_select(posts_form['list_action'],width=3) }}
           {{ form_theme.input_select(posts_form['list_action'],width=3) }}
@@ -360,14 +387,14 @@
 
 
       <div class="thread-buttons">
       <div class="thread-buttons">
         {{ pager(false) }}
         {{ pager(false) }}
-        {% if acl.threads.can_reply(forum, thread) and participants|length > 1 %}
+        {% if acl.threads.can_reply(forum, thread) and (acl.private_threads.is_mod() or participants|length > 1) %}
         <a href="{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug %}" class="btn btn-inverse pull-right"><i class="icon-pencil"></i> {% trans %}Reply{% endtrans %}</a>
         <a href="{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug %}" class="btn btn-inverse pull-right"><i class="icon-pencil"></i> {% trans %}Reply{% endtrans %}</a>
         {% else %}
         {% else %}
-        <p class="lead thread-signin-message">{% trans %}This thread has no participants.{% endtrans %}</p>
+        <p class="lead thread-signin-message">{% trans %}This thread has too few participants.{% endtrans %}</p>
         {% endif %}
         {% endif %}
       </div>
       </div>
 
 
-      {% if acl.threads.can_reply(forum, thread) and participants|length > 1 %}
+      {% if acl.threads.can_reply(forum, thread) and (acl.private_threads.is_mod() or participants|length > 1) %}
       <div class="thread-quick-reply">
       <div class="thread-quick-reply">
         <form action="{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug %}" method="post">
         <form action="{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug %}" method="post">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
@@ -402,7 +429,7 @@
           {% endfor %}
           {% endfor %}
         </ul>
         </ul>
 
 
-        {% if participants|length < 2%}
+        {% if participants|length < 2 %}
         <p class="no-participants">{% trans %}This thread has too few participants. Invite other users to open it for new replies.{% endtrans %}</p>
         <p class="no-participants">{% trans %}This thread has too few participants. Invite other users to open it for new replies.{% endtrans %}</p>
         {% endif %}
         {% endif %}
 
 
@@ -411,7 +438,7 @@
           <form class="form-inline" action="{% url 'private_thread_invite_user' thread=thread.pk, slug=thread.slug %}" method="post">
           <form class="form-inline" action="{% url 'private_thread_invite_user' thread=thread.pk, slug=thread.slug %}" method="post">
             <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
             <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
             <input type="hidden" name="retreat" value="{{ request_path }}">
             <input type="hidden" name="retreat" value="{{ request_path }}">
-            {{ form_theme.input_text(invite_form.fields.username, width="2", attrs={'placeholder':_("User to invite...")}) }}
+            {{ form_theme.input_text(invite_form.fields.username, width="2", attrs={'placeholder':lang_user_to_invite()}) }}
             <button class="btn" type="submit"><i class="icon-plus"></i></button>
             <button class="btn" type="submit"><i class="icon-plus"></i></button>
           </form>
           </form>
         </div>
         </div>
@@ -429,6 +456,7 @@
 {% block javascripts -%}{{ super() }}
 {% block javascripts -%}{{ super() }}
   <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
   <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
   <script type="text/javascript">
   <script type="text/javascript">
+    var l_post_reported = "{{ _('Reported!') }}";
     hljs.tabReplace = '    ';
     hljs.tabReplace = '    ';
     hljs.initHighlightingOnLoad();
     hljs.initHighlightingOnLoad();
     EnhancePostsMD();
     EnhancePostsMD();
@@ -479,6 +507,10 @@
           var decision = confirm("{% trans %}Are you sure you want to delete this post?{% endtrans %}");
           var decision = confirm("{% trans %}Are you sure you want to delete this post?{% endtrans %}");
           return decision;
           return decision;
       });
       });
+      $('.prompt-delete-checkpoint').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this checkpoint?{% endtrans %}");
+          return decision;
+      });
     });
     });
   </script>
   </script>
   {% if acl.threads.can_reply(forum, thread) %}
   {% if acl.threads.can_reply(forum, thread) %}
@@ -505,6 +537,7 @@
     {% if extra %}
     {% if extra %}
     {% if not is_read %}<li><a href="{% url 'private_thread_new' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first unread{% endtrans %}"><i class="icon-star"></i> {% trans %}First Unread{% endtrans %}</a></li>{% endif %}
     {% if not is_read %}<li><a href="{% url 'private_thread_new' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first unread{% endtrans %}"><i class="icon-star"></i> {% trans %}First Unread{% endtrans %}</a></li>{% endif %}
     {% endif %}
     {% endif %}
+    {% if thread.replies_reported > 0 and acl.threads.can_mod_posts(thread) %}<li><a href="{% url 'private_thread_reported' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first reported post{% endtrans %}"><i class="icon-fire"></i> {% trans %}First Reported{% endtrans %}</a></li>{% endif %}
   </ul>
   </ul>
 </div>
 </div>
 {% endmacro %}
 {% endmacro %}
@@ -540,3 +573,7 @@
 {% macro editor_extra() %}
 {% macro editor_extra() %}
   <button id="editor-preview" name="preview" type="submit" class="btn pull-right">{% trans %}Full Editor{% endtrans %}</button>
   <button id="editor-preview" name="preview" type="submit" class="btn pull-right">{% trans %}Full Editor{% endtrans %}</button>
 {% endmacro %}
 {% endmacro %}
+
+
+{# Language strings macros #}
+{% macro lang_user_to_invite() -%}{% trans %}User to invite...{% endtrans %}{%- endmacro %}

+ 5 - 2
templates/cranefly/profiles/details.html

@@ -2,7 +2,7 @@
 {% import "_forms.html" as form_theme with context %}
 {% import "_forms.html" as form_theme with context %}
 {% import "cranefly/macros.html" as macros with context %}
 {% import "cranefly/macros.html" as macros with context %}
 
 
-{% block title %}{{ macros.page_title(_('Member Details'), profile.username) }}{% endblock %}
+{% block title %}{{ macros.page_title(lang_member_details(), profile.username) }}{% endblock %}
 
 
 {% block tab %}
 {% block tab %}
 <div class="user-details">
 <div class="user-details">
@@ -238,4 +238,7 @@
   </div>
   </div>
 </div>
 </div>
 {% endif %}
 {% endif %}
-{% endblock %}
+{% endblock %}
+
+{# Language strings macros #}
+{% macro lang_member_details() -%}{% trans %}Member Details{% endtrans %}{%- endmacro %}

+ 5 - 2
templates/cranefly/profiles/list.html

@@ -23,7 +23,7 @@
       <li class="pull-right">
       <li class="pull-right">
         <form action="{% url 'users' %}" class="form-inline" method="post">
         <form action="{% url 'users' %}" class="form-inline" method="post">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-          {{ form_theme.field_widget(search_form.username, width=2, attrs={'placeholder': _('Find user...')}) }}
+          {{ form_theme.field_widget(search_form.username, width=2, attrs={'placeholder': lang_find_user()}) }}
           <button type="submit" class="btn btn-icon"><i class="icon-search"></i></button>
           <button type="submit" class="btn btn-icon"><i class="icon-search"></i></button>
         </form>
         </form>
       </li>
       </li>
@@ -108,4 +108,7 @@
 </div>
 </div>
 {% endif %}
 {% endif %}
 {% endif %}
 {% endif %}
-{%- endmacro %}
+{%- endmacro %}
+
+{# Language strings macros #}
+{% macro lang_find_user() -%}{% trans %}Find User...{% endtrans %}{%- endmacro %}

+ 7 - 7
templates/cranefly/profiles/profile.html

@@ -44,26 +44,26 @@
             {% endfor %}
             {% endfor %}
             {% if user.is_authenticated() and user.pk != profile.pk %}
             {% if user.is_authenticated() and user.pk != profile.pk %}
             <li class="pull-right">
             <li class="pull-right">
-              <form class="form-inline" action="{% if follows %}{% url 'unfollow_user' user=profile.id %}{% else %}{% url 'follow_user' user=profile.id %}{% endif %}" method="post">
+              <form class="form-inline" action="{% if ignores %}{% url 'unignore_user' user=profile.id %}{% else %}{% url 'ignore_user' user=profile.id %}{% endif %}" method="post">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                 <input type="hidden" name="fallback" value="{{ fallback }}">
                 <input type="hidden" name="fallback" value="{{ fallback }}">
-                <button type="submit" class="btn{% if follows %} btn-success{% endif %}">
-                  <i class="icon-heart"></i> {% if follows %}{% trans %}Following{% endtrans %}{% else %}{% trans %}Follow{% endtrans %}{% endif %}
+                <button type="submit" class="btn tooltip-top{% if ignores %} btn-inverse{% endif %}" title="{% if ignores %}{% trans user=profile.username %}Remove {{ user }} from ignored{% endtrans %}{% else %}{% trans user=profile.username %}Add {{ user }} to ignored{% endtrans %}{% endif %}">
+                  <i class="icon-ban-circle"></i>
                 </button>
                 </button>
               </form>
               </form>
             </li>
             </li>
             <li class="pull-right">
             <li class="pull-right">
-              <form class="form-inline" action="{% if ignores %}{% url 'unignore_user' user=profile.id %}{% else %}{% url 'ignore_user' user=profile.id %}{% endif %}" method="post">
+              <form class="form-inline" action="{% if follows %}{% url 'unfollow_user' user=profile.id %}{% else %}{% url 'follow_user' user=profile.id %}{% endif %}" method="post">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                 <input type="hidden" name="fallback" value="{{ fallback }}">
                 <input type="hidden" name="fallback" value="{{ fallback }}">
-                <button type="submit" class="btn{% if ignores %} btn-inverse{% endif %}">
-                  <i class="icon-ban-circle"></i> {% if ignores %}{% trans %}Ignoring{% endtrans %}{% else %}{% trans %}Ignore{% endtrans %}{% endif %}
+                <button type="submit" class="btn tooltip-top{% if follows %} btn-success{% endif %}" title="{% if follows %}{% trans user=profile.username %}Stop following {{ user }}{% endtrans %}{% else %}{% trans user=profile.username %}Start following {{ user }}{% endtrans %}{% endif %}">
+                  <i class="icon-heart"></i>
                 </button>
                 </button>
               </form>
               </form>
             </li>
             </li>
             {% if acl.private_threads.can_start() %}
             {% if acl.private_threads.can_start() %}
             <li class="pull-right">
             <li class="pull-right">
-              <a href="{% url 'private_thread_start_with' username=profile.username_slug, user=profile.pk %}" class="btn"><i class="icon-envelope"></i> {% trans %}Message{% endtrans %}</a>
+              <a href="{% url 'private_thread_start_with' username=profile.username_slug, user=profile.pk %}" class="btn tooltip-top" title="{% trans user=profile.username %}Start private thread with {{ user }}{% endtrans %}"><i class="icon-envelope"></i></a>
             </li>
             </li>
             {% endif %}
             {% endif %}
             {% endif %}
             {% endif %}

+ 81 - 0
templates/cranefly/reports/changelog.html

@@ -0,0 +1,81 @@
+{% extends "cranefly/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=(_("Post #%(post)s Changelog") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'report' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %} <small>{{ thread.name }}</small></h1>
+    <ul class="unstyled header-stats">
+      <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
+      <li><i class="icon-user"></i> {% if post.user %}<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}">{{ post.user.username }}</a>{% else %}{{ post.user_name }}{% endif %}</li>
+      <li><i class="icon-pencil"></i> {% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</li>
+      {% if post.protected %}<li><i class="icon-lock"></i> {% trans %}Protected{% endtrans %}</li>{% endif %}
+    </ul>
+  </div>
+</div>
+
+<div class="container container-primary">
+  <div class="post-changelog">
+    {% if edits %}
+    <table class="table table-striped">
+      <thead>
+        <tr>
+          <th style="width: 1%;">&nbsp;</th>
+          <th>{% trans %}Change Log{% endtrans %}</th>
+        </tr>
+      </thead>
+      <tbody>
+        {% for edit in edits %}
+        <tr>
+          <td>
+            <span class="change-{% if edit.change > 0 %}added{% elif edit.change < 0 %}removed{% else %}none{% endif %}{% if not edit.reason %} change-small{% endif %}">
+              {% if edit.change > 0 %}+{% endif %}{{ edit.change }}
+            </span>
+          </td>
+          <td>
+            <a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=edit.pk %}" class="change-no">#{{ loop.revindex }}</a>
+            {% if edit.reason %}
+            <div class="change-reason">
+              <a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=edit.pk %}">{{ edit.reason }}</a>
+            </div>{% endif %}
+            <div class="change-description">
+              <a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=edit.pk %}">
+              {% if edit.change != 0 %}{% if edit.change > 0 -%}
+              {% trans chars=edit.change %}Added one character to post.{% pluralize %}Added {{ chars }} characters to post.{% endtrans %}
+              {%- elif edit.change < 0 -%}
+              {% trans chars=edit.change|abs %}Removed one character from post.{% pluralize %}Removed {{ chars }} characters from post.{% endtrans %}
+              {%- else -%}
+              {% trans %}No change in message's length.{% endtrans %}
+              {%- endif %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Changed thread name from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}</a>
+              <span class="change-details">
+                {% trans user=edit_user(edit), date=edit.date|reldate|low %}By {{ user }} {{ date }}{% endtrans %}
+              </span>
+            </div>
+          </td>
+        </tr>
+        {% endfor %}
+      </tbody>
+    </table>
+    {% else %}
+    <p class="lead">{% trans %}This post was never edited.{% endtrans %}</p>
+    {% endif %}
+  </div>
+</div>
+{% endblock %}
+
+
+{% macro edit_user(edit) -%}
+{% if edit.user_id %}<a href="{% url 'user' user=edit.user_id, username=edit.user_slug %}">{{ edit.user_name }}</a>{% else %}{{ edit.user_name }}{% endif %}
+{%- endmacro %}

+ 95 - 0
templates/cranefly/reports/changelog_diff.html

@@ -0,0 +1,95 @@
+{% extends "cranefly/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=(_("Post #%(post)s Changelog") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'report' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'report_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{% trans date=change.date|reltimesince|low %}Edit from {{ date }}{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans date=change.date|reltimesince|low %}Edit from {{ date }}{% endtrans %} <small>{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}</small></h1>
+    <ul class="unstyled header-stats pull-left">
+      <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
+      <li><i class="icon-user"></i> {% if change.user_id %}<a href="{% url 'user' user=change.user_id, username=change.user_slug %}">{{ change.user_name }}</a>{% else %}{{ change.user_name }}{% endif %}</li>
+      {% if acl.users.can_see_users_trails() %}
+      <li><i class="icon-globe"></i> {{ change.ip }}</li>
+      <li><i class="icon-qrcode"></i> {{ change.agent }}</li>
+      {% endif %}
+      {% if change.change != 0 %}<li><i class="icon-{% if change.change > 0 %}plus{% elif change.change < 0 %}minus{% endif %}"></i> {% if change.change > 0 -%}
+      {% trans chars=change.change %}Added one character{% pluralize %}Added {{ chars }} characters{% endtrans %}
+      {%- elif change.change < 0 -%}
+      {% trans chars=change.change|abs %}Removed one character{% pluralize %}Removed {{ chars }} characters{% endtrans %}
+      {%- endif %}</li>{% endif %}
+    </ul>
+  </div>
+</div>
+
+<div class="container container-primary">
+  <div class="post-diff">
+    {% if message %}
+    <div class="messages-list">
+      {{ macros.draw_message(message) }}
+    </div>
+    {% endif %}
+
+    {% if change.reason %}
+    <p class="lead">{{ change.reason }}</p>
+    {% endif %}
+
+    {% if acl.threads.can_edit_reply(user, forum, thread, post) or prev or next %}
+    <div class="diff-extra">
+      {{ pager() }}
+      {% if user.is_authenticated() and acl.threads.can_make_revert(forum, thread) %}
+      <form class="form-inline pull-right" action="{% url 'report_changelog_revert' thread=thread.pk, slug=thread.slug, post=post.pk, change=change.pk %}" method="post">
+        <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+        <button type="submit" class="btn btn-danger">{% trans %}Revert this edit{% endtrans %}</button></li>
+      </form>
+      {%- endif %}
+    </div>
+    {% endif %}
+
+    <div class="post-diff-details">
+      <table>
+        <tbody>
+          {% for line in diff %}{% if line[0] != "?" %}
+          <tr>
+            <td class="line"><a href="#{{ l }}">{{ l }}.</a></td>
+            <td class="{% if line[0] == '+' %}added{% elif line[0] == '-' %}removed{% else %}stag{% endif %}{% if l is even %} even{% endif %}">{% if line[2:] %}{{ line[2:] }}{% else %}&nbsp;{% endif %}</td>
+          </tr>
+          {% set l = l + 1 %}
+          {% endif %}{% endfor %}
+        </tbody>
+      </table>
+    </div>
+
+    {% if prev or next %}
+    <div class="diff-extra">
+      {{ pager() }}
+    </div>
+    {% endif %}
+
+  </div>
+</div>
+{% endblock %}
+
+
+{% macro pager() %}
+{% if prev or prev %}
+<div class="pagination pull-left">
+  <ul>
+    {% if prev %}<li><a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=prev.pk %}"><i class="icon-chevron-left"></i> {{ prev.date|reldate }}</a></li>{% endif %}
+    {% if next %}<li><a href="{% url 'report_changelog_diff' thread=thread.pk, slug=thread.slug, post=post.pk, change=next.pk %}">{{ next.date|reldate }} <i class="icon-chevron-right"></i></a></li>{% endif %}
+  </ul>
+</div>
+{% endif %}
+{% endmacro %}

+ 35 - 0
templates/cranefly/reports/details.html

@@ -0,0 +1,35 @@
+{% extends "cranefly/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=(_("Post #%(post)s Info") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'report' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{% trans post=post.pk %}Post #{{ post }} Info{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans post=post.pk %}Post #{{ post }} Info{% endtrans %} <small>{{ thread.name }}</small></h1>
+    <ul class="unstyled header-stats">
+      <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
+      <li><i class="icon-user"></i> {% if post.user %}<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}">{{ post.user.username }}</a>{% else %}{{ post.user_name }}{% endif %}</li>
+      <li><i class="icon-pencil"></i> {% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</li>
+      {% if post.protected %}<li><i class="icon-lock"></i> {% trans %}Protected{% endtrans %}</li>{% endif %}
+    </ul>
+  </div>
+</div>
+
+<div class="container container-primary">
+  <h2>{% trans %}IP Address{% endtrans %}</h2>
+  <p class="lead">{{ post.ip }}</p>
+  <h2>{% trans %}UserAgent{% endtrans %}</h2>
+  <p class="lead">{{ post.agent }}</p>
+</div>
+{% endblock %}

+ 187 - 0
templates/cranefly/reports/list.html

@@ -0,0 +1,187 @@
+{% extends "cranefly/layout.html" %}
+{% import "_forms.html" as form_theme with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_('Reported Posts'),page=pagination['page']) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{% trans %}Reported Posts{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary header-search">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb" {{ macros.itemprop_bread() }}>
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans %}Reported Posts{% endtrans %} <form action="{% url 'reports_search' %}" class="form-inline" method="post">
+       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+       <input maxlength="255" type="text" name="search_query" class="span4">
+       <button type="submit" class="btn btn-primary">{% trans %}Search{% endtrans %}</button>
+    </form></h1>
+  </div>
+</div>
+
+<div class="container container-primary">
+
+  {% if message %}
+  <div class="messages-list">
+    {{ macros.draw_message(message) }}
+  </div>
+  {% endif %}
+
+  <div class="forum-threads-extra extra-top">
+    {{ pager() }}
+  </div>
+
+  <div class="forum-threads-list reports-list">
+    <div class="header">
+      <div class="row-fluid">
+        <div class="span6 offset2">{% trans %}Report{% endtrans %}</div>
+        <div class="span4 thread-activity">
+          <div class="thread-replies">{% trans %}Activity{% endtrans %}</div>
+          {% if list_form %}
+          <div class="pull-right check-cell">
+            <label class="checkbox"><input type="checkbox" class="checkbox-master"></label>
+          </div>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+    {% for thread in threads %}
+    <div class="thread-row{% if not thread.is_read %} thread-new{% endif %}{% if loop.last %} thread-last{% endif %}">
+      <div class="row-fluid">
+        <div class="span2 thread-label">
+          {% if thread.weight == 2 %}
+          <span class="report-label report-open"><i class="icon-time"></i> {% trans %}Open{% endtrans %}</span>
+          {% elif thread.weight == 1 %}
+          <span class="report-label report-resolved"><i class="icon-ok"></i> {% trans %}Resolved{% endtrans %}</span>
+          {% else %}
+          <span class="report-label report-bogus"><i class="icon-remove"></i> {% trans %}Bogus{% endtrans %}</span>
+          {% endif %}
+        </div>
+        <div class="span6">
+
+          {% if thread.deleted %}
+          <ul class="unstyled thread-flags">
+            <li class="flag-deleted"><i class="icon-trash tooltip-top" title="{% trans %}This thread is deleted{% endtrans %}"></i></li>
+          </ul>
+          {% endif %}
+          
+          <a href="{% url 'report' thread=thread.pk, slug=thread.slug %}" class="thread-name"><span class="report-id">#{{ thread.pk }}</span> {{ thread.name }}</a>
+          
+          {% if thread.is_read %}
+          <a href="{% url 'report_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-last tooltip-top" title="{% trans %}Click to see last comment{% endtrans %}"><i class="icon-asterisk"></i></a>
+          {% else %}
+          <a href="{% url 'report_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-new tooltip-top" title="{% trans %}Click to see first unread comment{% endtrans %}"><i class="icon-fire"></i></a>
+          {% endif %}
+
+          <div class="thread-details">
+            {% if thread.report_forum %}
+            {% trans user=thread_starter(thread), start=thread.start|reldate|low, forum=report_forum(thread) %}Post reported by {{ user }} {{ start }} in forum {{ forum }}{% endtrans %}
+            {% else %}
+            {% trans user=thread_starter(thread), start=thread.start|reldate|low %}Deleted post reported by {{ user }} {{ start }}{% endtrans %}
+            {% endif %}
+          </div>
+
+        </div>
+        <div class="span4 thread-activity">
+
+          <div class="thread-replies">
+            <strong class="lead">{{ thread_reply(thread) }}, {{ thread.last|reldate|low }}</strong><br>
+            {{ replies(thread.replies) }}
+          </div>
+
+          {% if list_form %}
+          <label class="thread-select checkbox"><input form="threads_form" name="{{ list_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ thread.pk }}"{% if list_form['list_items']['has_value'] and ('' ~ thread.pk) in list_form['list_items']['value'] %} checked="checked"{% endif %}></label>
+          {% endif %}
+        </div>
+      </div>
+    </div>
+    {% else %}
+    <div class="thread-row threads-list-empty">
+      {% trans %}There are no reports currently. Whef!{% endtrans %}
+    </div>
+    {% endfor %}
+
+    {% if list_form %}
+    <div class="threads-actions">
+      <form id="threads_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
+        <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+        {{ form_theme.input_select(list_form['list_action'],width=3) }}
+        <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
+      </form>
+    </div>
+    {% endif %}
+  </div>
+
+  <div class="forum-threads-extra">
+    {{ pager() }}
+  </div>
+
+</div>
+{% endblock %}
+
+{% macro replies(thread_replies) -%}
+{% trans count=thread_replies, replies=thread_replies|intcomma -%}
+{{ replies }} reply
+{%- pluralize -%}
+{{ replies }} replies
+{%- endtrans %}
+{%- endmacro %}
+
+{% macro thread_starter(thread) -%}
+{% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="user-link">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}
+{%- endmacro %}
+
+{% macro thread_reply(thread) -%}
+{% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="user-link">{{ thread.last_poster_name }}</a>{% else %}{{ thread.last_poster_name }}{% endif %}
+{%- endmacro %}
+
+{% macro report_forum(thread) -%}
+<a href="{{ thread.report_forum.url }}" class="forum-link">{{ thread.report_forum }}</a>
+{%- endmacro %}
+
+{% macro pager() %}
+{% if pagination['total'] > 0 %}
+<div class="pagination pull-left">
+  <ul>
+    <li class="count">{{ macros.pager_label(pagination) }}</li>
+    {%- if pagination['prev'] > 1 %}<li><a href="{% url 'forum' slug=forum.slug, forum=forum.id %}" class="tooltip-top" title="{% trans %}First Page{% endtrans %}"><i class="icon-chevron-left"></i> {% trans %}First{% endtrans %}</a></li>{% endif -%}
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['prev'] %}{% else %}{% url 'forum' slug=forum.slug, forum=forum.id %}{% endif %}" class="tooltip-top" title="{% trans %}Newest Threads{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['next'] %}" class="tooltip-top" title="{% trans %}Older Threads{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+  </ul>
+</div>
+{% endif %}
+{% endmacro %}
+
+{% block javascripts -%}{{ super() }}
+  <script type="text/javascript">
+    $(function () {
+      function populateForumTooltip(target) {
+        return $('#forum-' + target + ' .forum-meta').html();
+      };
+      {% for subforum in forum.subforums %}
+        $('#forum-{{ subforum.id }} .forum-title').tooltip({
+          template: '<div class="tooltip forum-meta-tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+          placement: 'right',
+          html: true,
+          title: populateForumTooltip({{ subforum.id }})
+        });
+      {% endfor %}
+      {%- if list_form %}
+      $('#threads_form').submit(function() {
+        if ($('.thread-select[]:checked').length == 0) {
+          alert("{% trans %}You have to select at least one thread.{% endtrans %}");
+          return false;
+        }
+        if ($('#id_list_action').val() == 'hard') {
+          var decision = confirm("{% trans %}Are you sure you want to delete selected reports? This action is not reversible!{% endtrans %}");
+          return decision;
+        }
+        return true;
+      });{% endif %}
+    });
+  </script>
+{%- endblock %}

+ 177 - 0
templates/cranefly/reports/posting.html

@@ -0,0 +1,177 @@
+{% extends "cranefly/layout.html" %}
+{% import "_forms.html" as form_theme with context %}
+{% import "cranefly/editor.html" as editor with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_(get_title()), parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'report' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{{ get_title() }}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{{ get_title() }} <small>{{ thread.name }}</small></h1>
+    <ul class="unstyled header-stats">
+      <li><i class="icon-tag"></i> {% if thread.weight == 2 -%}
+        {% trans %}Open{% endtrans %}
+        {%- elif thread.weight == 1 -%}
+        Resolved
+        {%- else -%}
+        Bogus
+        {%- endif %}</li>
+      {{ get_info() }}
+    </ul>
+  </div>
+</div>
+<div class="container container-primary">
+  <div class="row">
+    <div class="span8 offset2">
+      <div class="posting">
+        <div class="form-container">
+
+          <div class="form-header">
+            <h1>{{ get_title() }}</h1>
+          </div>
+
+          {% if message %}
+          <div class="messages-list">
+            {{ macros.draw_message(message) }}
+          </div>
+          {% endif %}
+
+          {% if preview %}
+          <div class="form-preview">
+            <div class="markdown js-extra">
+              <article>
+                {{ preview|markdown_final|safe }}
+              </article>
+            </div>
+          </div>
+          {% endif %}
+
+          <form action="{{ get_action() }}" method="post">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            {% if 'thread_name' in form.fields %}
+            {{ form_theme.row_widget(form.fields.thread_name, width=8) }}
+            <hr>
+            <h4>{% trans %}Message Body{% endtrans %}</h4>
+            {% endif %}
+            {{ editor.editor(form.fields.post, get_button(), rows=8, extra=get_extra()) }}
+            {% if intersect(form.fields, ('edit_reason', 'thread_weight', 'close_thread')) %}
+            <hr>
+            {% if 'edit_reason' in form.fields %}
+            {{ form_theme.row_widget(form.fields.edit_reason, width=8) }}
+            {% endif %}
+
+            {% if 'thread_weight' in form.fields %}
+            <div class="control-group">
+              <label class="control-label">{% trans %}Set Report Status{% endtrans %}:</label>
+              <div class="controls">
+                {{ form_theme.input_radio_select(form.fields.thread_weight, width=8) }}
+              </div>
+            </div>
+            {% endif %}
+
+            <div class="form-actions">
+              <button type="submit" class="btn btn-primary">{{ get_button() }}</button>
+              <button id="editor-preview" name="preview" type="submit" class="btn">{% trans %}Preview{% endtrans %}</button>
+            </div>
+            {% endif %}
+          </form>
+
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+{% endblock %}
+
+{% block stylesheets %}{{ super() }}
+<link href="{{ STATIC_URL }}cranefly/highlight/styles/monokai.css" rel="stylesheet">
+{% endblock %}
+
+{% block javascripts %}{{ super() }}
+  <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
+  <script type="text/javascript">
+    hljs.tabReplace = '    ';
+    hljs.initHighlightingOnLoad();
+    EnhancePostsMD();
+  </script>
+  {{ editor.js() }}
+{% endblock %}
+
+
+{% macro get_action() -%}
+{% if action == 'new_thread' -%}
+{% url 'report_start' forum=forum.pk, slug=forum.slug %}
+{%- elif action == 'edit_thread' -%}
+{% url 'report_edit' thread=thread.pk, slug=thread.slug %}
+{%- elif action in 'new_reply' -%}
+{%- if quote -%}
+{% url 'report_reply' thread=thread.pk, slug=thread.slug, quote=quote.pk %}
+{%- else -%}
+{% url 'report_reply' thread=thread.pk, slug=thread.slug %}
+{%- endif -%}
+{%- elif action == 'edit_reply' -%}
+{% url 'post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_title() -%}
+{%- if action == 'edit_thread' -%}
+{% trans %}Edit Report{% endtrans %}
+{%- elif action == 'new_reply' -%}
+{% trans %}Post New Comment{% endtrans %}
+{%- elif action == 'edit_reply' -%}
+{% trans %}Edit Comment{% endtrans %}
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_info() -%}
+{% if action == 'edit_reply' -%}
+    <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
+    <li><i class="icon-user"></i> {% if post.user %}<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}">{{ post.user.username }}</a>{% else %}{{ post.user_name }}{% endif %}</li>
+    <li><i class="icon-pencil"></i> {% if post.edits > 0 -%}
+      {% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}
+    {%- else -%}
+      {% trans %}First edit{% endtrans %}
+    {%- endif %}</li>
+{%- else -%}
+    {% if action == 'edit_thread' %}
+    <li><i class="icon-time"></i> <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=thread.start_post_id %}">{{ thread.start|reltimesince }}</a></li>
+    {% else %}
+    <li><i class="icon-time"></i> <a href="{% url 'report_new' thread=thread.pk, slug=thread.slug %}">{{ thread.last|reltimesince }}</a></li>
+    {% endif %}
+    <li><i class="icon-user"></i> {% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}</li>
+    <li><i class="icon-comment"></i> {% if thread.replies > 0 -%}
+      {% trans count=thread.replies, replies=thread.replies|intcomma %}One comment{% pluralize %}{{ replies }} comments{% endtrans %}
+    {%- else -%}
+      {% trans %}No comments{% endtrans %}
+    {%- endif %}</li>
+{%- endif %}
+    {% if thread.closed %}<li><i class="icon-lock"></i> {% trans %}Locked{% endtrans %}</li>{% endif %}
+{%- endmacro %}
+
+
+{% macro get_button() -%}
+{% if action == 'new_reply' -%}
+{% trans %}Post Comment{% endtrans %}
+{%- else -%}
+{% trans %}Save Changes{% endtrans %}
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_extra() %}
+  <button id="editor-preview" name="preview" type="submit" class="btn pull-right">{% trans %}Preview{% endtrans %}</button>
+{% endmacro %}

+ 415 - 0
templates/cranefly/reports/thread.html

@@ -0,0 +1,415 @@
+{% extends "cranefly/layout.html" %}
+{% import "_forms.html" as form_theme with context %}
+{% import "cranefly/editor.html" as editor with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=thread.name,parent=forum.name,page=pagination['page']) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'reports' %}">{% trans %}Reported Posts{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li class="active">{{ thread.name }}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb" {{ macros.itemprop_bread() }}>
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{{ thread.name }}</h1>
+    <ul class="unstyled header-stats">
+      <li><i class="icon-tag"></i> {% if thread.weight == 2 -%}
+        {% trans %}Open{% endtrans %}
+        {%- elif thread.weight == 1 -%}
+        Resolved
+        {%- else -%}
+        Bogus
+        {%- endif %}</li>
+      <li><i class="icon-time"></i> {{ thread.last|reltimesince }}</li>
+      <li><i class="icon-user"></i> {% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}</li>
+      <li><i class="icon-comment"></i> {% if thread.replies > 0 -%}
+        {% trans count=thread.replies, replies=thread.replies|intcomma %}One comment{% pluralize %}{{ replies }} comments{% endtrans %}
+      {%- else -%}
+        {% trans %}No comments{% endtrans %}
+      {%- endif %}</li>
+    </ul>
+  </div>
+</div>
+
+<div class="container container-primary report-view">
+  {% if message %}
+  <div class="messages-list">
+    {{ macros.draw_message(message) }}
+  </div>
+  {% endif %}
+
+  <div class="thread-buttons">
+    {{ pager() }}
+    <a href="{% url 'report_reply' thread=thread.pk, slug=thread.slug %}" class="btn btn-inverse pull-right"><i class="icon-pencil"></i> {% trans %}Comment{% endtrans %}</a>
+    {% if watcher %}
+    <form action="{% url 'report_unwatch' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn btn-success tooltip-top" title="{% trans %}Remove thread from watched list{% endtrans %}"><i class="icon-bookmark"></i></button></form>
+    {% if watcher.email %}
+    <form action="{% url 'report_unwatch_email' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn btn-success tooltip-top" title="{% trans %}Don't e-mail me anymore if anyone replies to this thread{% endtrans %}"><i class="icon-envelope"></i></button></form>
+    {% else %}
+    <form action="{% url 'report_watch_email' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}E-mail me if anyone replies{% endtrans %}"><i class="icon-envelope"></i></button></form>
+    {% endif %}
+    {% else %}
+    <form action="{% url 'report_watch' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}Add thread to watched list{% endtrans %}"><i class="icon-bookmark"></i></button></form>
+    <form action="{% url 'report_watch_email' thread=thread.pk, slug=thread.slug %}" class="form-inline pull-right" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}Add thread to watched list and e-mail me if anyone replies{% endtrans %}"><i class="icon-envelope"></i></button></form>
+    {% endif %}
+  </div>
+
+  <div class="thread-body">
+    {% if thread.start_post_id != posts[0].pk %}
+    {% set posts = (posts|list) %}
+    {% do posts.insert(0, thread.start_post) %}
+    {% endif %}
+    {% for post in posts %}
+    <div id="post-{{ post.pk }}" class="post-wrapper{% if post.pk == thread.start_post_id %} report-wrapper{% endif %}">
+      {% if post.message %}
+      <div class="messages-list">
+        {{ macros.draw_message(post.message) }}
+      </div>
+      {% endif %}
+      <div class="post-body">
+        {% if post.user_id %}
+        <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(100) }}" alt="" class="user-avatar"></a>
+        {% else %}
+        <img src="{{ macros.avatar_guest(100) }}" alt="" class="user-avatar">
+        {% endif %}
+        <div class="post-arrow"></div>
+        <div class="post-content">
+          <div class="post-header">
+            {% if post.user_id %}
+            <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="post-author">{{ post.user.username }}</a>{% if post.user.get_title() %} {{ user_label(post.user) }}{% endif %}
+            {% else %}
+            <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
+            {% endif %}
+            <span class="separator">&ndash;</span>
+            <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
+            {% if post.edits %}
+            <span class="separator">&ndash;</span>
+            {% if acl.threads.can_see_changelog(user, forum, post) %}
+            <a href="{% url 'report_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
+            {% else %}
+            <span class="post-changelog">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</span>
+            {% endif %}
+            {% endif %}
+
+            {% if posts_form and thread.start_post_id != post.pk %}
+            <label class="checkbox post-checkbox"><input form="posts_form" name="{{ posts_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ post.pk }}"{% if posts_form['list_items']['has_value'] and ('' ~ post.pk) in posts_form['list_items']['value'] %} checked="checked"{% endif %}></label>
+            {% endif %}
+
+            {% if posts_form and thread.start_post_id == post.pk %}
+            <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#1</i></a>
+            {% else %}
+            <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index0 }}</a>
+            {% endif %}
+
+            <div class="post-extra">
+              {% if post.protected and acl.threads.can_protect(forum) %}
+              <span class="label label-info">
+                {% trans %}Protected{% endtrans %}
+              </span>
+              {% endif %}
+
+              {% if post.deleted %}
+              <span class="label label-inverse">
+                {% trans %}Deleted{% endtrans %}
+              </span>
+              {% endif %}
+
+              {% if not post.is_read %}
+              <span class="label label-warning">
+                {% trans %}New{% endtrans %}
+              </span>
+              {% endif %}
+            </div>
+          </div>
+          <div class="post-message">
+            <div class="markdown js-extra">
+              <article>
+                {{ post.post_preparsed|markdown_final|safe }}
+              </article>
+            </div>
+          </div>
+          <div class="post-footer">{% filter trim %}
+            {% if post.pk == thread.start_post_id %}
+            <div class="post-actions report-actions">
+              {% if thread.weight == 2 %}
+              <form action="{{ request_path }}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <input type="hidden" name="origin" value="thread_form">
+                <input type="hidden" name="thread_action" value="sticky">
+                <button type="submit" class="btn btn-link btn-resolve tooltip-top" title="{% trans %}Set this report as resolved{% endtrans %}"><i class="icon-ok"></i> {% trans %}Resolved{% endtrans %}</button>
+              </form>
+              <form action="{{ request_path }}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <input type="hidden" name="origin" value="thread_form">
+                <input type="hidden" name="thread_action" value="normal">
+                <button type="submit" class="btn btn-link btn-bogus tooltip-top" title="{% trans %}Set this report as bogus{% endtrans %}"><i class="icon-remove"></i> {% trans %}Bogus{% endtrans %}</button>
+              </form>
+              {% elif thread.weight != 1 %}
+              <form action="{{ request_path }}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <input type="hidden" name="origin" value="thread_form">
+                <input type="hidden" name="thread_action" value="normal">
+                <button type="submit" class="btn btn-link btn-resolve tooltip-top" title="{% trans %}Set this report as bogus{% endtrans %}"><i class="icon-ok"></i> {% trans %}Resolved{% endtrans %}</button>
+              </form>
+              {% elif thread.weight != 0 %}
+              <form action="{{ request_path }}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <input type="hidden" name="origin" value="thread_form">
+                <input type="hidden" name="thread_action" value="sticky">
+                <button type="submit" class="btn btn-link btn-bogus tooltip-top" title="{% trans %}Set this report as resolved{% endtrans %}"><i class="icon-remove"></i> {% trans %}Bogus{% endtrans %}</button>
+              </form>
+              {% endif %}
+            </div>
+            {% endif %}
+            <div class="post-actions">
+              {% if acl.users.can_see_users_trails() -%}
+              <a href="{% url 'report_post_info' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-trail">{% trans %}Info{% endtrans %}</a>
+              {% endif %}
+              {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk %}
+              <a href="{% url 'report_edit' thread=thread.pk, slug=thread.slug %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
+              {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
+              <a href="{% url 'report_post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
+              {%- endif %}
+              <a href="{% url 'report_reply' thread=thread.pk, slug=thread.slug, quote=post.pk %}" class="post-reply">{% trans %}Reply{% endtrans %}</a>
+            </div>
+            {% if post.pk == thread.start_post_id %}
+            <div class="post-actions">
+              {% if acl.threads.can_delete_thread(user, forum, thread, post) %}
+              {% if post.deleted %}
+              <form action="{% url 'report_show' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Make this thread visible to other users{% endtrans %}">{% trans %}Restore{% endtrans %}</button>
+              </form>
+              {% else %}
+              <form action="{% url 'report_hide' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Hide this thread from other users{% endtrans %}">{% trans %}Hide{% endtrans %}</button>
+              </form>
+              {% endif %}
+              {% endif %}
+              {% if acl.threads.can_delete_thread(user, forum, thread, post) == 2 %}
+              <form action="{% url 'report_delete' thread=thread.pk, slug=thread.slug %}" class="form-inline prompt-delete-thread" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this thread for good{% endtrans %}">{% trans %}Delete{% endtrans %}</button>
+              </form>
+              {% endif %}
+            </div>
+            {% elif post.pk != thread.start_post_id and acl.threads.can_delete_post(user, forum, thread, post) %}
+            <div class="post-actions">
+              {% if acl.threads.can_delete_post(user, forum, thread, post) %}
+              {% if post.deleted %}
+              <form action="{% url 'report_post_show' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Make this reply visible to other users{% endtrans %}">{% trans %}Restore{% endtrans %}</button>
+              </form>
+              {% else %}
+              <form action="{% url 'report_post_hide' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Hide this reply from other users{% endtrans %}">{% trans %}Hide{% endtrans %}</button>
+              </form>
+              {% endif %}
+              {% endif %}
+              {% if acl.threads.can_delete_post(user, forum, thread, post) == 2 -%}
+              <form action="{% url 'report_post_delete' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline prompt-delete-post" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this reply for good{% endtrans %}">{% trans %}Delete{% endtrans %}</button>
+              </form>
+              {% endif %}
+            </div>
+            {% endif %}
+          {% endfilter %}</div>
+        </div>
+      </div>
+    </div>
+
+    {% if post.checkpoints_visible %}
+    <div class="post-checkpoints">
+      {% for checkpoint in post.checkpoints_visible %}
+      <div class="post-checkpoint{% if checkpoint.deleted %} checkpoint-deleted{% endif %}">
+        <hr>
+        <span>
+          {%- if checkpoint.action == 'limit' -%}
+          <i class="icon-lock"></i> {% trans  %}This thread has reached its post limit and has been closed.{% endtrans %}
+          {%- elif checkpoint.action == 'resolved' -%}
+          <i class="icon-ok"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} has set this report as resolved {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'bogus' -%}
+          <i class="icon-remove"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} has set this report as bogus {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'reported' -%}
+          <i class="icon-fire"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} has also reported this post {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'deleted' -%}
+          <i class="icon-trash"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} deleted this thread {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'undeleted' -%}
+          <i class="icon-trash"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} restored this thread {{ date }}{% endtrans %}
+          {%- endif -%}
+          {% if acl.threads.can_delete_checkpoint(forum) %}
+          {% if checkpoint.deleted %}
+          <form action="{% url 'report_post_checkpoint_show' slug=thread.slug, thread=thread.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="hidden" name="retreat" value="{{ request_path }}#post-{{ post.pk }}">
+            <button type="submit" class="btn btn-link btn-show">{% trans %}Restore{% endtrans %}</button>
+          </form>
+          {% else %}
+          <form action="{% url 'report_post_checkpoint_hide' slug=thread.slug, thread=thread.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="hidden" name="retreat" value="{{ request_path }}#post-{{ post.pk }}">
+            <button type="submit" class="btn btn-link btn-hide">{% trans %}Hide{% endtrans %}</button>
+          </form>
+          {% endif %}
+          {% endif %}
+          {% if acl.threads.can_delete_checkpoint(forum) == 2 %}
+          <form action="{% url 'report_post_checkpoint_delete' slug=thread.slug, thread=thread.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline prompt-delete-checkpoint">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="hidden" name="retreat" value="{{ request_path }}#post-{{ post.pk }}">
+            <button type="submit" class="btn btn-link btn-delete">{% trans %}Delete{% endtrans %}</button>
+          </form>
+          {% endif %}
+        </span>
+      </div>
+      {% endfor %}
+    </div>
+    {% endif %}
+    {% endfor %}
+  </div>
+
+  {% if thread_form or posts_form %}
+  <div class="thread-moderation">
+    <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
+      <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      <input type="hidden" name="origin" value="thread_form">
+      {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
+      <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
+    </form>
+    {% if posts_form%}
+    <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
+      <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      <input type="hidden" name="origin" value="posts_form">
+      {{ form_theme.input_select(posts_form['list_action'],width=3) }}
+      <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
+    </form>
+    {% endif %}
+  </div>
+  {% endif %}
+
+  <div class="thread-buttons">
+    {{ pager(false) }}
+    <a href="{% url 'report_reply' thread=thread.pk, slug=thread.slug %}" class="btn btn-inverse pull-right"><i class="icon-pencil"></i> {% trans %}Comment{% endtrans %}</a>
+  </div>
+
+  <div class="thread-quick-reply">
+    <form action="{% url 'report_reply' thread=thread.pk, slug=thread.slug %}" method="post">
+      <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      <input type="hidden" name="quick_reply" value="1">
+      <img src="{{ user.get_avatar(100) }}" alt="{% trans %}Your Avatar{% endtrans %}" class="user-avatar">
+      {{ editor.editor(quick_reply.post, _('Post Comment'), extra=editor_extra()) }}
+    </form>
+  </div>
+
+</div>
+{% endblock %}
+
+{% block stylesheets %}{{ super() }}
+<link href="{{ STATIC_URL }}cranefly/highlight/styles/monokai.css" rel="stylesheet">
+{% endblock %}
+
+{% block javascripts -%}{{ super() }}
+  <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
+  <script type="text/javascript">
+    hljs.tabReplace = '    ';
+    hljs.initHighlightingOnLoad();
+    EnhancePostsMD();
+    $(function () {
+      $('#thread_form').submit(function() {
+        if ($('#id_thread_action').val() == 'hard') {
+          var decision = confirm("{% trans %}Are you sure you want to delete this report? This action is not reversible!{% endtrans %}");
+          return decision;
+        }
+        return true;
+      });
+      $('#posts_form').submit(function() {
+        if ($('.post-checkbox[]:checked').length == 0) {
+          alert("{% trans %}You have to select at least one comment.{% endtrans %}");
+          return false;
+        }
+        if ($('#id_list_action').val() == 'hard') {
+          var decision = confirm("{% trans %}Are you sure you want to delete selected comments? This action is not reversible!{% endtrans %}");
+          return decision;
+        }
+        return true;
+      });
+      $('.prompt-delete-thread').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this report?{% endtrans %}");
+          return decision;
+      });
+      $('.prompt-delete-post').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this comment?{% endtrans %}");
+          return decision;
+      });
+      $('.prompt-delete-checkpoint').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this checkpoint?{% endtrans %}");
+          return decision;
+      });
+    });
+  </script>
+  {{ editor.js() }}
+{%- endblock %}
+
+
+{% macro user_label(user) -%}
+<{% if user.rank and user.rank.as_tab %}a href="{% url 'users' slug=user.rank.slug %}"{% else %}span{% endif %} class="label post-author-label{% if user.rank and user.rank.style %} post-label-{{ user.rank.style }}{% endif %}">{{ user.get_title() }}</{% if user.rank and user.rank.as_tab%}a{% else %}span{% endif %}>
+{%- endmacro %}
+
+
+{% macro pager(extra=true) %}
+<div class="pagination pull-left">
+  <ul>
+    {% if pagination['total'] > 0 %}
+    <li class="count">{{ macros.pager_label(pagination) }}</li>
+    {%- if pagination['prev'] > 1 %}<li><a href="{% url 'report' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first page{% endtrans %}"><i class="icon-chevron-left"></i> {% trans %}First{% endtrans %}</a></li>{% endif -%}
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{% url 'report' slug=thread.slug, thread=thread.id, page=pagination['prev'] %}{% else %}{% url 'report' slug=thread.slug, thread=thread.id %}{% endif %}" class="tooltip-top" title="{% trans %}Older Posts{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{% url 'report' slug=thread.slug, thread=thread.id, page=pagination['next'] %}" class="tooltip-top" title="{% trans %}Newest Posts{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 and pagination['next'] < pagination['total'] %}<li><a href="{% url 'report' slug=thread.slug, thread=thread.id, page=pagination['total'] %}" class="tooltip-top" title="{% trans %}Go to last page{% endtrans %}">{% trans %}Last{% endtrans %} <i class="icon-chevron-right"></i></a></li>{% endif -%}
+    {% endif %}
+    {% if extra %}
+    {% if not is_read %}<li><a href="{% url 'report_new' slug=thread.slug, thread=thread.id %}" class="tooltip-top" title="{% trans %}Go to first unread{% endtrans %}"><i class="icon-star"></i> {% trans %}First Unread{% endtrans %}</a></li>{% endif %}
+    {% endif %}
+  </ul>
+</div>
+{% endmacro %}
+
+
+{% macro checkpoint_user(checkpoint) -%}
+{%- if checkpoint.user_id -%}
+<a href="{{ 'user'|url(user=checkpoint.user_id, username=checkpoint.user_slug) }}">{{ checkpoint.user_name }}</a>
+{%- else -%}
+<strong>{{ checkpoint.user_name }}</strong>
+{%- endif -%}
+{%- endmacro %}
+
+
+{% macro checkpoint_forum(checkpoint) -%}
+{%- if checkpoint.old_forum_id -%}
+<a href="{% url 'forum' forum=checkpoint.old_forum_id, slug=checkpoint.old_forum_slug %}">{{ checkpoint.old_forum_name }}</a>
+{%- else -%}
+<strong>{{ checkpoint.old_forum_name }}</strong>
+{%- endif -%}
+{%- endmacro %}
+
+
+{% macro edit_user(post) -%}
+{%- if post.edit_user_id -%}
+<a href="{{ 'user'|url(user=post.edit_user_id, username=post.edit_user_slug) }}">{{ post.edit_user_name }}</a>
+{%- else -%}
+<strong>{{ post.edit_user_name }}</strong>
+{%- endif -%}
+{%- endmacro %}
+
+
+{% macro editor_extra() %}
+  <button id="editor-preview" name="preview" type="submit" class="btn pull-right">{% trans %}Full Editor{% endtrans %}</button>
+{% endmacro %}

+ 6 - 0
templates/cranefly/search/error.html

@@ -0,0 +1,6 @@
+{% extends "cranefly/search/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block action %}
+<p class="lead">{{ message }}</p>
+{% endblock %}

+ 17 - 0
templates/cranefly/search/home.html

@@ -0,0 +1,17 @@
+{% extends "cranefly/search/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block action %}
+{% if search_result %}
+<div class="search-resume">
+  <p class="lead muted">{% trans search_query=style_query(search_result.search_query) %}Your search for "{{ search_query }}" is still available.{% endtrans %}</p>
+  <p class="lead muted">{% trans %}To discard it and start new search, enter phrases you want to find in text field above and press search button.{% endtrans %}</p>
+</div>
+{% else %}
+<p class="lead">{% trans %}To search forums, enter phrases you want to find in text field above and press search button.{% endtrans %}</p>
+{% endif %}
+{% endblock %}
+
+{% macro style_query(query) -%}
+<a href="{{ ('%s_results'|format(search_route))|url }}">{{ query }}</a>
+{%- endmacro %}

+ 21 - 0
templates/cranefly/search/layout.html

@@ -0,0 +1,21 @@
+{% extends "cranefly/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+{% import "_forms.html" as forms with context %}
+
+{% block title %}{{ macros.page_title(title=_('Search Community')) }}{% endblock %}
+
+{% block container %}
+<div class="page-header header-primary header-search">
+  <div class="container">
+    <h1>{% trans %}Search{% endtrans %} <form action="{{ search_route|url() }}" class="form-inline" method="post">
+       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+       {{ forms.input_text(form.fields.search_query, width=6) }}
+       <button type="submit" class="btn btn-primary">{% trans %}Search{% endtrans %}</button>
+    </form></h1>
+  </div>
+</div>
+
+<div class="container container-primary">
+  {% block action %}{% endblock %}
+</div>
+{% endblock %}

+ 48 - 0
templates/cranefly/search/results.html

@@ -0,0 +1,48 @@
+{% extends "cranefly/search/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_("Results"),parent=_('Search Community')) }}{% endblock %}
+
+{% block action %}
+  <div class="search-results">
+    {% if results %}
+    <h2>{% trans results=results|length|intcomma %}Search has returned one result{% pluralize %}Search has returned {{ results }} results:{% endtrans %}</h2>
+    <div class="results-list">
+      {% for result in results %}
+      <div class="result">
+        <h3><a href="{{ result.forum.thread_link('find')|url(thread=result.thread_id, slug=result.thread.slug, post=result.pk) }}">{{ result.thread.name }}</a></h3>
+        <p class="post-extra">{% trans forum=forum(result.forum), user=username(result), date=result.date|reltimesince|low %}In {{ forum }} by {{ user }} {{ date }}{% endtrans %}</p>
+        <p class="post-preview">{{ result.post_clean|highlight(search_query, 320)|safe }}</p>
+      </div>
+      {% endfor %}
+    </div>
+    {{ pager() }}
+    {% else %}
+    <p class="lead">{% trans %}Looks like your search has expired. Please try searching again.{% endtrans %}</p>
+    {% endif %}
+  </div>
+{% endblock %}
+
+{% macro forum(forum) -%}
+<a href="{% url 'forum' forum=forum.pk, slug=forum.slug %}" class="forum-link">{{ forum.name }}</a>
+{%- endmacro %}
+
+{% macro username(post) -%}
+{% if post.user_id -%}
+<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="user-link">{{ post.user.username }}</a>
+{%- else -%}
+{{ post.user_name }}
+{%- endif %}
+{%- endmacro %}
+
+{% macro pager() -%}
+{% if items_total > 0 and pagination['total'] > 1 %}
+<div class="pagination">
+  <ul>
+    <li class="count">{{ macros.pager_label(pagination) }}</li>
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{{ ('%s_results'|format(search_route))|url(page=pagination['prev']) }}{% else %}{{ ('%s_results'|format(search_route))|url() }}{% endif %}" class="tooltip-top" title="{% trans %}Newer Posts{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{{ ('%s_results'|format(search_route))|url(page=pagination['next']) }}" class="tooltip-top" title="{% trans %}Older Posts{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+  </ul>
+</div>
+{% endif %}
+{%- endmacro %}

+ 2 - 4
templates/cranefly/threads/changelog.html

@@ -4,9 +4,7 @@
 {% block title %}{{ macros.page_title(title=(_("Post #%(post)s Changelog") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=(_("Post #%(post)s Changelog") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li class="active">{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}
 <li class="active">{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}
 {%- endblock %}
 {%- endblock %}
@@ -60,7 +58,7 @@
               {% trans chars=edit.change|abs %}Removed one character from post.{% pluralize %}Removed {{ chars }} characters from post.{% endtrans %}
               {% trans chars=edit.change|abs %}Removed one character from post.{% pluralize %}Removed {{ chars }} characters from post.{% endtrans %}
               {%- else -%}
               {%- else -%}
               {% trans %}No change in message's length.{% endtrans %}
               {% trans %}No change in message's length.{% endtrans %}
-              {%- endif %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Changed thread name from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Renamed thread from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}</a>
+              {%- endif %}{% endif %}{% if edit.thread_name_old %} {% trans old=edit.thread_name_old, new=edit.thread_name_new %}Changed thread name from "{{ old }}" to "{{ new }}".{% endtrans %}{% endif %}</a>
               <span class="change-details">
               <span class="change-details">
                 {% trans user=edit_user(edit), date=edit.date|reldate|low %}By {{ user }} {{ date }}{% endtrans %}
                 {% trans user=edit_user(edit), date=edit.date|reldate|low %}By {{ user }} {{ date }}{% endtrans %}
               </span>
               </span>

+ 1 - 3
templates/cranefly/threads/changelog_diff.html

@@ -4,9 +4,7 @@
 {% block title %}{{ macros.page_title(title=(_("Post #%(post)s Changelog") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=(_("Post #%(post)s Changelog") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'thread_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'thread_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans post=post.pk %}Post #{{ post }} Changelog{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li class="active">{% trans date=change.date|reltimesince|low %}Edit from {{ date }}{% endtrans %}
 <li class="active">{% trans date=change.date|reltimesince|low %}Edit from {{ date }}{% endtrans %}

+ 1 - 3
templates/cranefly/threads/details.html

@@ -4,9 +4,7 @@
 {% block title %}{{ macros.page_title(title=(_("Post #%(post)s Info") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=(_("Post #%(post)s Info") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li class="active">{% trans post=post.pk %}Post #{{ post }} Info{% endtrans %}
 <li class="active">{% trans post=post.pk %}Post #{{ post }} Info{% endtrans %}
 {%- endblock %}
 {%- endblock %}

+ 5 - 7
templates/cranefly/threads/karmas.html

@@ -4,9 +4,7 @@
 {% block title %}{{ macros.page_title(title=(_("Post #%(post)s Votes") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=(_("Post #%(post)s Votes") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li class="active">{% trans post=post.pk %}Post #{{ post }} Votes{% endtrans %}
 <li class="active">{% trans post=post.pk %}Post #{{ post }} Votes{% endtrans %}
 {%- endblock %}
 {%- endblock %}
@@ -61,11 +59,11 @@
 
 
     <hr>
     <hr>
 
 
-    <div class="post-hates">
+    <div class="post-dislikes">
       <h2>{% trans count=downvotes|length, votes=downvotes|length|intcomma -%}
       <h2>{% trans count=downvotes|length, votes=downvotes|length|intcomma -%}
-        One hate
+        One dislike
         {%- pluralize -%}
         {%- pluralize -%}
-        {{ votes }} hates
+        {{ votes }} dislikes
         {%- endtrans %}</h2>
         {%- endtrans %}</h2>
       {% if downvotes %}
       {% if downvotes %}
       <table class="table table-striped">
       <table class="table table-striped">
@@ -86,7 +84,7 @@
         </tbody>
         </tbody>
       </table>
       </table>
       {% else %}
       {% else %}
-      <p class="lead">{% trans %}Nobody hated this post.{% endtrans %}</p>
+      <p class="lead">{% trans %}Nobody disliked this post.{% endtrans %}</p>
       {% endif %}
       {% endif %}
     </div>
     </div>
   </div>
   </div>

+ 128 - 136
templates/cranefly/threads/list.html

@@ -5,9 +5,7 @@
 {% block title %}{{ macros.page_title(title=forum.name,page=pagination['page']) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=forum.name,page=pagination['page']) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li class="active">{{ forum.name }}
 <li class="active">{{ forum.name }}
 {%- endblock %}
 {%- endblock %}
 
 
@@ -32,67 +30,71 @@
 
 
   {% if forum.subforums %}
   {% if forum.subforums %}
   <div id="subforums" class="forum-subforums-list{% if forum.style %} forum-subforums-{{ forum.style }}{% endif %}">
   <div id="subforums" class="forum-subforums-list{% if forum.style %} forum-subforums-{{ forum.style }}{% endif %}">
-    <table class="table">
-      <caption>{% trans %}Child forums{% endtrans %}</caption>
-      <tbody>
-        {% for subforum in forum.subforums %}
-        <tr>
-          <td class="forum-icon"><span class="forum-icon-wrap{% if subforum.type == 'redirect' %} forum-icon-redirect{% elif not subforum.is_read %} forum-icon-new{% endif %}"><i class="icon-{% if subforum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %} icon-white"></i></span></td>
-          <td id="forum-{{ subforum.id }}" class="forum-main">
-            <h3 class="forum-title{% if not subforum.is_read %} forum-title-new{% endif %}"><a href="{{ subforum.type|url(slug=subforum.slug, forum=subforum.id) }}">{{ subforum.name }}</a></h3>
-            {% if subforum.show_details %}
-            <div class="forum-details">
-              {% if subforum.type != 'redirect' %}
-              {% if acl.forums.can_browse(subforum) and (acl.threads.can_read_threads(subforum) == 2 or (acl.threads.can_read_threads(subforum) == 1 and subforum.last_poster_id == user.pk)) %}
-              {% if subforum.last_thread_id -%}
-              <div class="thread-name">
-                <a href="{% url 'thread_new' thread=subforum.last_thread_id, slug=subforum.last_thread_slug %}"{% if subforum.last_thread_name|length > 36 %} class="tooltip-top" title="{{ subforum.last_thread_name }}"{% endif %}>{{ subforum.last_thread_name|short_string(36) }}</a>
-              </div>
-              <div class="muted">{% if subforum.last_poster_id %}<a href="{% url 'user' user=subforum.last_poster_id, username=subforum.last_poster_slug %}" class="last-poster">{{ subforum.last_poster_name }}</a>{% else %}<span class="last-poster">{{ subforum.last_poster_name }}</span>{% endif %} - {{ subforum.last_thread_date|reltimesince }}</div>
-              {%- else -%}
-              <em>{% trans %}This forum is empty{% endtrans %}</em>
-              {%- endif %}
-              {%- else -%}
-              <em>{% trans %}This forum is protected{% endtrans %}</em>
-              {%- endif %}
-              {%- else -%}
-              <div class="thread-name">
-                <a href="{% url 'redirect' slug=forum.slug, forum=forum.id %}">{{ forum.redirect_domain() }}</a>
-              </div>
-              <div class="muted">{% trans clicks=macros.wrap(forum.redirects|intcomma, 'span', 'class="last-poster"') %}{{ clicks }} clicks{% endtrans %}</div>
-              {%- endif %}
+    <div class="header">
+      <h2>{% trans %}Child forums{% endtrans %}</h2>
+    </div>
+    {% for subforum in forum.subforums %}
+    <div class="forum{% if loop.last %} last{% endif %}">
+      <div class="forum-icon">
+        <div class="forum-icon-wrap{% if subforum.type == 'redirect' %} forum-icon-redirect{% elif not subforum.is_read %} forum-icon-new{% endif %}"><i class="icon-{% if subforum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %} icon-white"></i></div>
+      </div>
+      <div id="forum-{{ subforum.id }}" class="forum-main">
+        <h3 class="forum-title{% if not subforum.is_read %} forum-title-new{% endif %}"><a href="{{ subforum.type|url(slug=subforum.slug, forum=subforum.id) }}">{{ subforum.name }}</a></h3>
+        {% if subforum.show_details %}
+        <div class="forum-details">
+          {% if subforum.type != 'redirect' %}
+          {% if acl.forums.can_browse(subforum) and (acl.threads.can_read_threads(subforum) == 2 or (acl.threads.can_read_threads(subforum) == 1 and subforum.last_poster_id == user.pk)) %}
+          {% if subforum.last_thread_id -%}
+          <div class="thread-name">
+            <a href="{% url 'thread_new' thread=subforum.last_thread_id, slug=subforum.last_thread_slug %}"{% if subforum.last_thread_name|length > 34 %} class="tooltip-top" title="{{ subforum.last_thread_name }}"{% endif %}>{{ subforum.last_thread_name|short_string(34) }}</a>
+          </div>
+          <div class="muted">{% if subforum.last_poster_id %}<a href="{% url 'user' user=subforum.last_poster_id, username=subforum.last_poster_slug %}" class="last-poster">{{ subforum.last_poster_name }}</a>{% else %}<span class="last-poster">{{ subforum.last_poster_name }}</span>{% endif %} - {{ subforum.last_thread_date|reltimesince }}</div>
+          {%- else -%}
+          <em>{% trans %}This forum is empty{% endtrans %}</em>
+          {%- endif %}
+          {%- else -%}
+          <em>{% trans %}This forum is protected{% endtrans %}</em>
+          {%- endif %}
+          {%- else -%}
+          <div class="thread-name">
+            <a href="{% url 'redirect' slug=subforum.slug, forum=subforum.id %}">{{ subforum.redirect_domain() }}</a>
+          </div>
+          <div class="muted">{% trans count=subforum.redirects, clicks=macros.wrap(subforum.redirects|intcomma, 'span', 'class="last-poster"') %}{{ clicks }} click{% pluralize %}{{ clicks }} clicks{% endtrans %}</div>
+          {%- endif %}
+        </div>
+        {% endif %}
+        {% if subforum.subforums %}
+        <div class="dropdown">
+          {% if subforum.subforums|length > 1 %}
+          <a href="{{ subforum.type|url(slug=subforum.slug, forum=subforum.id) }}#subforums" class="dropdown-toggle" data-toggle="dropdown"><i class="icon-chevron-down"></i> {% trans %}Subforums{% endtrans %}</a>
+          <div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
+            <div class="dropdown-shadow">
+              <ul>
+                {% for subsubforum in subforum.subforums %}
+                <li><a href="{{ subsubforum.type|url(slug=subsubforum.slug, forum=subsubforum.id) }}"><i class="icon-{% if subsubforum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %}"></i> {{ subsubforum.name }}</a></li>
+                {% endfor %}
+              </ul>
             </div>
             </div>
+          </div>
+          {% else %}
+          <a href="{{ subforum.subforums[0].type|url(slug=subforum.subforums[0].slug, forum=subforum.subforums[0].id) }}" class="subforum tooltip-top" title="{% trans subforum=subforum.subforums[0].name %}Go to the {{ subforum }} subforum{% endtrans %}">{{ subforum.subforums[0].name|short_string(16) }}</a>
+          {% endif %}
+        </div>
+        {% endif%}
+        <div class="hide forum-meta">
+          {% if subforum.description %}<p class="forum-description">{{ subforum.description }}</p>{% endif %}
+          <div class="forum-stats">
+            {% if subforum.type != 'redirect' %}
+            <span>{% trans %}Posts{% endtrans %}: <strong>{{ subforum.posts|intcomma }}</strong></span>
+            {% trans %}Threads{% endtrans %}: <strong>{{ subforum.threads|intcomma }}</strong>
+            {% else %}
+            {% trans %}Clicks{% endtrans %}: <strong>{{ subforum.redirects|intcomma }}</strong>
             {% endif %}
             {% endif %}
-            {% if subforum.subforums %}
-            <div class="dropdown">
-              <a href="{{ subforum.type|url(slug=subforum.slug, forum=subforum.id) }}#subforums" class="dropdown-toggle" data-toggle="dropdown"><i class="icon-chevron-down"></i> {% trans %}Subforums{% endtrans %}</a>
-              <div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
-                <div class="dropdown-shadow">
-                  <ul>
-                    {% for subsubforum in subforum.subforums %}
-                    <li><a href="{{ subsubforum.type|url(slug=subsubforum.slug, forum=subsubforum.id) }}"><i class="icon-{% if subsubforum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %}"></i> {{ subsubforum.name }}</a></li>
-                    {% endfor %}
-                  </ul>
-                </div>
-              </div>
-            </div>
-            {% endif%}
-            <div class="hide forum-meta">
-              {% if subforum.description %}<p class="forum-description">{{ subforum.description }}</p>{% endif %}
-              <div class="forum-stats">
-                {% if subforum.type != 'redirect' %}
-                <span>{% trans %}Posts{% endtrans %}: <strong>{{ subforum.posts|intcomma }}</strong></span>
-                {% trans %}Threads{% endtrans %}: <strong>{{ subforum.threads|intcomma }}</strong>
-                {% else %}
-                {% trans %}Clicks{% endtrans %}: <strong>{{ subforum.redirects|intcomma }}</strong>
-                {% endif %}
-              </div>
-            </div>
-          </td>
-        </tr>
-        {% endfor %}
-      </tbody>
-    </table>
+          </div>
+        </div>
+      </div>
+    </div>
+    {% endfor %}
   </div>
   </div>
   {% endif %}
   {% endif %}
 
 
@@ -110,80 +112,70 @@
   </div>
   </div>
 
 
   <div class="forum-threads-list">
   <div class="forum-threads-list">
-    <table class="table">
-      <thead>
-        <tr>
-          <th>{% trans %}Thread{% endtrans %}</th>
-          <th class="span1">{% trans %}Rating{% endtrans %}</th>
-          <th class="span5">{% trans %}Activity{% endtrans %}</th>
+    <div class="header">
+      <div class="row-fluid">
+        <div class="span7">{% trans %}Thread{% endtrans %}</div>
+        <div class="span5 thread-activity">
+          <div class="thread-replies">{% trans %}Activity{% endtrans %}</div>
           {% if user.is_authenticated() and list_form %}
           {% if user.is_authenticated() and list_form %}
-          <th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>
+          <div class="pull-right check-cell">
+            <label class="checkbox"><input type="checkbox" class="checkbox-master"></label>
+          </div>
           {% endif %}
           {% endif %}
-        </tr>
-      </thead>
-      <tbody>
-        {% for thread in threads %}
-        <tr>
-          <td>
-            <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon{% if not thread.is_read %} thread-new{% endif %} tooltip-top" title="{% if not thread.is_read -%}
-            {% trans %}Click to see first unread post{% endtrans %}
-            {%- else -%}
-            {% trans %}Click to see last post{% endtrans %}
-            {%- endif %}"><i class="icon-comment"></i></a>
-            <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
-            <span class="thread-details">
-              {% trans user=thread_starter(thread), start=thread.start|reltimesince|low %}by {{ user }} {{ start }}{% endtrans %}
-            </span>
-            <ul class="unstyled thread-flags">
-              {% if thread.replies_reported %}
-              <li><i class="icon-warning-sign tooltip-top" title="{% trans %}This thread has reported replies{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.replies_moderated %}
-              <li><i class="icon-question-sign tooltip-top" title="{% trans %}This thread has unreviewed replies{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.weight == 2 %}
-              <li><i class="icon-star tooltip-top" title="{% trans %}This thread is an annoucement{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.weight == 1 %}
-              <li><i class="icon-star-empty tooltip-top" title="{% trans %}This thread is sticky{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.moderated  %}
-              <li><i class="icon-eye-close tooltip-top" title="{% trans %}This thread awaits review{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.deleted %}
-              <li><i class="icon-trash tooltip-top" title="{% trans %}This thread is deleted{% endtrans %}"></i></li>
-              {% endif %}
-              {% if thread.closed %}
-              <li><i class="icon-lock tooltip-top" title="{% trans %}This thread is closed{% endtrans %}"></i></li>
-              {% endif %}
-            </ul>
-          </td>
-          <td>
-            <div class="thread-rating{% if (thread.upvotes-thread.downvotes) > 0 %} thread-rating-positive{% elif (thread.upvotes-thread.downvotes) < 0 %} thread-rating-negative{% endif %}">
-              {% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}{{ (thread.upvotes-thread.downvotes)|abs|intcomma }}
-            </div>
-          </td>
-          <td>
-            <div class="thread-last-reply">
-              {{ replies(thread.replies) }} - {% trans user=thread_reply(thread), last=thread.last|reltimesince|low %}last by {{ user }} {{ last }}{% endtrans %}
-            </div>
-          </td>
+        </div>
+      </div>
+    </div>
+    {% for thread in threads %}
+    <div class="thread-row{% if not thread.is_read %} thread-new{% endif %}{% if loop.last %} thread-last{% endif %}">
+      <div class="row-fluid">
+        <div class="span7">
+          {% if thread.is_read %}
+          <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-last tooltip-top" title="{% trans %}Click to see last post{% endtrans %}"><i class="icon-asterisk"></i></a>
+          {% else %}
+          <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon thread-icon-new tooltip-top" title="{% trans %}Click to see first unread post{% endtrans %}"><i class="icon-fire"></i></a>
+          {% endif %}
+
+          {{ macros.thread_flags(thread) }}
+          
+          <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
+          
+          <div class="thread-details">
+            {% trans user=thread_starter(thread), start=thread.start|reldate|low %}by {{ user }}, {{ start }}{% endtrans %}
+          </div>
+
+        </div>
+        <div class="span5 thread-activity">
+
+          {% if settings.avatars_on_threads_list %}
+          <div class="thread-last-avatar">
+            {% if thread.last_poster_id %}
+            <a href="{% url 'user' user=thread.last_poster.pk, username=thread.last_poster.username_slug %}"><img src="{{ thread.last_poster.get_avatar(40) }}" alt=""></a>
+            {% else %}
+            <img src="{{ macros.avatar_guest(40) }}" alt="" class="user-avatar">
+            {% endif %}
+          </div>
+          {% endif %}
+
+          <div class="thread-replies">
+            <strong class="lead">{{ thread_reply(thread) }}, {{ thread.last|reldate|low }}</strong><br>
+            {{ replies(thread.replies) }}, <span{% if (thread.upvotes-thread.downvotes) > 0 %} class="text-success"{% elif (thread.upvotes-thread.downvotes) < 0 %} class="text-error"{% endif %}><strong>{% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}</strong>{% trans rating=(thread.upvotes-thread.downvotes)|abs|intcomma %}{{ rating }} thread rating{% endtrans %}</span>
+          </div>
+
           {% if user.is_authenticated() and list_form %}
           {% if user.is_authenticated() and list_form %}
-          <td class="check-cell">{% if thread.forum_id == forum.pk %}<label class="checkbox"><input form="threads_form" name="{{ list_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ thread.pk }}"{% if list_form['list_items']['has_value'] and ('' ~ thread.pk) in list_form['list_items']['value'] %} checked="checked"{% endif %}></label>{% else %}&nbsp;{% endif %}</td>
+          <label class="thread-select checkbox"><input form="threads_form" name="{{ list_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ thread.pk }}"{% if list_form['list_items']['has_value'] and ('' ~ thread.pk) in list_form['list_items']['value'] %} checked="checked"{% endif %}></label>
           {% endif %}
           {% endif %}
-        </tr>
-        {% else %}
-        <tr>
-          <td colspan="4" class="threads-list-empty">
-            {% trans %}There are no threads in this forum.{% endtrans %}
-          </td>
-        </tr>
-        {% endfor %}
-      </tbody>
-    </table>
+        </div>
+      </div>
+    </div>
+    {% else %}
+    <div class="thread-row threads-list-empty">
+      {% trans %}There are no threads in this forum.{% endtrans %}
+    </div>
+    {% endfor %}
+
     {% if user.is_authenticated() and list_form %}
     {% if user.is_authenticated() and list_form %}
     <div class="threads-actions">
     <div class="threads-actions">
-      <form id="threads_form" class="form-inline pull-right" action="{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['page'] %}" method="POST">
+      <form id="threads_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
         <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
         <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
         {{ form_theme.input_select(list_form['list_action'],width=3) }}
         {{ form_theme.input_select(list_form['list_action'],width=3) }}
         <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
         <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
@@ -238,7 +230,7 @@
 {%- endmacro %}
 {%- endmacro %}
 
 
 {% macro replies(thread_replies) -%}
 {% macro replies(thread_replies) -%}
-{% trans count=thread_replies, replies=('<strong>' ~ (thread_replies|intcomma) ~ '</strong>')|safe -%}
+{% trans count=thread_replies, replies=thread_replies|intcomma -%}
 {{ replies }} reply
 {{ replies }} reply
 {%- pluralize -%}
 {%- pluralize -%}
 {{ replies }} replies
 {{ replies }} replies
@@ -272,17 +264,17 @@
       function populateForumTooltip(target) {
       function populateForumTooltip(target) {
         return $('#forum-' + target + ' .forum-meta').html();
         return $('#forum-' + target + ' .forum-meta').html();
       };
       };
-      {% for category in forums_list %}{% for forum in category.subforums %}
-        $('#forum-{{ forum.id }} .forum-title').tooltip({
+      {% for subforum in forum.subforums %}
+        $('#forum-{{ subforum.id }} .forum-title').tooltip({
           template: '<div class="tooltip forum-meta-tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
           template: '<div class="tooltip forum-meta-tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
           placement: 'right',
           placement: 'right',
           html: true,
           html: true,
-          title: populateForumTooltip({{ forum.id }})
+          title: populateForumTooltip({{ subforum.id }})
         });
         });
-      {% endfor %}{% endfor %}
+      {% endfor %}
       {%- if user.is_authenticated() and list_form %}
       {%- if user.is_authenticated() and list_form %}
       $('#threads_form').submit(function() {
       $('#threads_form').submit(function() {
-        if ($('.check-cell[]:checked').length == 0) {
+        if ($('.thread-select[]:checked').length == 0) {
           alert("{% trans %}You have to select at least one thread.{% endtrans %}");
           alert("{% trans %}You have to select at least one thread.{% endtrans %}");
           return false;
           return false;
         }
         }

+ 7 - 4
templates/cranefly/threads/merge.html

@@ -5,9 +5,7 @@
 {% block title %}{{ macros.page_title(title=_("Merge Threads"),parent=forum.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=_("Merge Threads"),parent=forum.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{{ forum.type|url(forum=forum.pk, slug=forum.slug) }}">{{ forum.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{{ forum.type|url(forum=forum.pk, slug=forum.slug) }}">{{ forum.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li class="active">{% trans %}Merge Threads{% endtrans %}
 <li class="active">{% trans %}Merge Threads{% endtrans %}
 {%- endblock %}
 {%- endblock %}
@@ -32,9 +30,14 @@
           <h1>{% trans %}Merge Threads{% endtrans %}</h1>
           <h1>{% trans %}Merge Threads{% endtrans %}</h1>
         </div>
         </div>
 
 
-        {% if message %}
+        {% if message or warning %}
         <div class="messages-list">
         <div class="messages-list">
+          {% if message %}
           {{ macros.draw_message(message) }}
           {{ macros.draw_message(message) }}
+          {% endif %}
+          {% if warning %}
+          {{ macros.draw_message(warning) }}
+          {% endif %}
         </div>
         </div>
         {% endif %}
         {% endif %}
 
 

+ 1 - 3
templates/cranefly/threads/move_posts.html

@@ -5,9 +5,7 @@
 {% block title %}{{ macros.page_title(title=_("Move Posts"),parent=thread.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=_("Move Posts"),parent=thread.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li class="active">{% trans %}Move Posts{% endtrans %}
 <li class="active">{% trans %}Move Posts{% endtrans %}
 {%- endblock %}
 {%- endblock %}

+ 1 - 3
templates/cranefly/threads/move_thread.html

@@ -5,9 +5,7 @@
 {% block title %}{{ macros.page_title(title=_("Move Threads"),parent=forum.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=_("Move Threads"),parent=forum.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li class="active">{% trans %}Move Thread{% endtrans %}
 <li class="active">{% trans %}Move Thread{% endtrans %}
 {%- endblock %}
 {%- endblock %}

+ 1 - 3
templates/cranefly/threads/move_threads.html

@@ -5,9 +5,7 @@
 {% block title %}{{ macros.page_title(title=_("Move Threads"),parent=forum.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=_("Move Threads"),parent=forum.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{{ forum.type|url(forum=forum.pk, slug=forum.slug) }}">{{ forum.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{{ forum.type|url(forum=forum.pk, slug=forum.slug) }}">{{ forum.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li class="active">{% trans %}Move Threads{% endtrans %}
 <li class="active">{% trans %}Move Threads{% endtrans %}
 {%- endblock %}
 {%- endblock %}

+ 5 - 5
templates/cranefly/threads/posting.html

@@ -10,9 +10,7 @@
 {%- endif %}{% endblock %}
 {%- endif %}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{{ forum.type|url(forum=forum.pk, slug=forum.slug) }}">{{ forum.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{{ forum.type|url(forum=forum.pk, slug=forum.slug) }}">{{ forum.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% if thread %}<li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>{% endif %}
 {% if thread %}<li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>{% endif %}
 <li class="active">{{ get_title() }}
 <li class="active">{{ get_title() }}
@@ -52,7 +50,9 @@
           {% if preview %}
           {% if preview %}
           <div class="form-preview">
           <div class="form-preview">
             <div class="markdown js-extra">
             <div class="markdown js-extra">
-              {{ preview|markdown_final|safe }}
+              <article>
+                {{ preview|markdown_final|safe }}
+              </article>
             </div>
             </div>
           </div>
           </div>
           {% endif %}
           {% endif %}
@@ -62,7 +62,7 @@
             {% if 'thread_name' in form.fields %}
             {% if 'thread_name' in form.fields %}
             {{ form_theme.row_widget(form.fields.thread_name, width=8) }}
             {{ form_theme.row_widget(form.fields.thread_name, width=8) }}
             <hr>
             <hr>
-            <h4>Message Body</h4>
+            <h4>{% trans %}Message Body{% endtrans %}</h4>
             {% endif %}
             {% endif %}
             {{ editor.editor(form.fields.post, get_button(), rows=8, extra=get_extra()) }}
             {{ editor.editor(form.fields.post, get_button(), rows=8, extra=get_extra()) }}
             {% if intersect(form.fields, ('edit_reason', 'thread_weight', 'close_thread')) %}
             {% if intersect(form.fields, ('edit_reason', 'thread_weight', 'close_thread')) %}

+ 1 - 3
templates/cranefly/threads/split.html

@@ -5,9 +5,7 @@
 {% block title %}{{ macros.page_title(title=_("Split Thread"),parent=thread.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=_("Split Thread"),parent=thread.name) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
 <li class="active">{% trans %}Split Thread{% endtrans %}
 <li class="active">{% trans %}Split Thread{% endtrans %}
 {%- endblock %}
 {%- endblock %}

+ 99 - 68
templates/cranefly/threads/thread.html

@@ -6,9 +6,7 @@
 {% block title %}{{ macros.page_title(title=thread.name,parent=forum.name,page=pagination['page']) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=thread.name,parent=forum.name,page=pagination['page']) }}{% endblock %}
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% for parent in parents %}
-<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
-{% endfor %}
+{{ macros.parents_list(parents) }}
 <li class="active">{{ thread.name }}
 <li class="active">{{ thread.name }}
 {%- endblock %}
 {%- endblock %}
 
 
@@ -77,7 +75,7 @@
         {% if post.user_id %}
         {% if post.user_id %}
         <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(50) }}" alt="" class="user-avatar"></a>
         <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(50) }}" alt="" class="user-avatar"></a>
         {% else %}
         {% else %}
-        <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar"></a>
+        <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar">
         {% endif %}
         {% endif %}
         <div class="post-content">
         <div class="post-content">
           <div class="post-header">
           <div class="post-header">
@@ -88,26 +86,18 @@
               <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
               <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
               {% endif %}
               {% endif %}
               <span class="separator">&ndash;</span>
               <span class="separator">&ndash;</span>
-              <a href="{% if pagination['page'] > 1 -%}
-              {% url 'thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-              {%- else -%}
-              {% url 'thread' thread=thread.pk, slug=thread.slug %}
-              {%- endif %}#post-{{ post.pk }}" class="post-date">{{ post.date|reltimesince }}</a>
+              <a href="{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
               {% if post.edits %}
               {% if post.edits %}
               <span class="separator">&ndash;</span>
               <span class="separator">&ndash;</span>
               {% if acl.threads.can_see_changelog(user, forum, post) %}
               {% if acl.threads.can_see_changelog(user, forum, post) %}
-              <a href="{% url 'changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
+              <a href="{% url 'thread_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
               {% else %}
               {% else %}
               <span class="post-changelog">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</span>
               <span class="post-changelog">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</span>
               {% endif %}
               {% endif %}
               {% endif %}
               {% endif %}
             </div>
             </div>
 
 
-            <a href="{% if pagination['page'] > 1 -%}
-            {% url 'thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-            {%- else -%}
-            {% url 'thread' thread=thread.pk, slug=thread.slug %}
-            {%- endif %}#post-{{ post.pk }}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+            <a href="{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
 
 
             {% if not post.is_read %}
             {% if not post.is_read %}
             <div class="post-extra">
             <div class="post-extra">
@@ -119,29 +109,21 @@
 
 
           </div>
           </div>
           <div class="post-message">
           <div class="post-message">
-            {% trans user=edit_user(post), date=post.edit_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
+            {% trans user=edit_user(post), date=post.current_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
           </div>
           </div>
         </dv>
         </dv>
       </div>
       </div>
       {% elif post.ignored %}
       {% elif post.ignored %}
       <div class="post-body post-muted">
       <div class="post-body post-muted">
-        <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar"></a>
+        <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar">
         <div class="post-arrow"></div>
         <div class="post-arrow"></div>
         <div class="post-content">
         <div class="post-content">
           <div class="post-header">
           <div class="post-header">
             <div class="post-header-compact">
             <div class="post-header-compact">
-              <a href="{% if pagination['page'] > 1 -%}
-              {% url 'thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-              {%- else -%}
-              {% url 'thread' thread=thread.pk, slug=thread.slug %}
-              {%- endif %}#post-{{ post.pk }}" class="post-date">{{ post.date|reltimesince }}</a>
+              <a href="{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
             </div>
             </div>
 
 
-            <a href="{% if pagination['page'] > 1 -%}
-            {% url 'thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-            {%- else -%}
-            {% url 'thread' thread=thread.pk, slug=thread.slug %}
-            {%- endif %}#post-{{ post.pk }}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+            <a href="{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
 
 
             {% if not post.is_read %}
             {% if not post.is_read %}
             <div class="post-extra">
             <div class="post-extra">
@@ -162,7 +144,7 @@
         {% if post.user_id %}
         {% if post.user_id %}
         <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(100) }}" alt="" class="user-avatar"></a>
         <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(100) }}" alt="" class="user-avatar"></a>
         {% else %}
         {% else %}
-        <img src="{{ macros.avatar_guest(100) }}" alt="" class="user-avatar"></a>
+        <img src="{{ macros.avatar_guest(100) }}" alt="" class="user-avatar">
         {% endif %}
         {% endif %}
         <div class="post-arrow"></div>
         <div class="post-arrow"></div>
         <div class="post-content">
         <div class="post-content">
@@ -173,11 +155,7 @@
             <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
             <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
             {% endif %}
             {% endif %}
             <span class="separator">&ndash;</span>
             <span class="separator">&ndash;</span>
-            <a href="{% if pagination['page'] > 1 -%}
-            {% url 'thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-            {%- else -%}
-            {% url 'thread' thread=thread.pk, slug=thread.slug %}
-            {%- endif %}#post-{{ post.pk }}" class="post-date">{{ post.date|reltimesince }}</a>
+            <a href="{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
             {% if post.edits %}
             {% if post.edits %}
             <span class="separator">&ndash;</span>
             <span class="separator">&ndash;</span>
             {% if acl.threads.can_see_changelog(user, forum, post) %}
             {% if acl.threads.can_see_changelog(user, forum, post) %}
@@ -191,11 +169,7 @@
             <label class="checkbox post-checkbox"><input form="posts_form" name="{{ posts_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ post.pk }}"{% if posts_form['list_items']['has_value'] and ('' ~ post.pk) in posts_form['list_items']['value'] %} checked="checked"{% endif %}></label>
             <label class="checkbox post-checkbox"><input form="posts_form" name="{{ posts_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ post.pk }}"{% if posts_form['list_items']['has_value'] and ('' ~ post.pk) in posts_form['list_items']['value'] %} checked="checked"{% endif %}></label>
             {% endif %}
             {% endif %}
 
 
-            <a href="{% if pagination['page'] > 1 -%}
-            {% url 'thread' thread=thread.pk, slug=thread.slug, page=pagination['page'] %}
-            {%- else -%}
-            {% url 'thread' thread=thread.pk, slug=thread.slug %}
-            {%- endif %}#post-{{ post.pk }}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+            <a href="{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
 
 
             <div class="post-extra">
             <div class="post-extra">
               {% if post.protected and acl.threads.can_protect(forum) %}
               {% if post.protected and acl.threads.can_protect(forum) %}
@@ -216,7 +190,7 @@
               </span>
               </span>
               {% endif %}
               {% endif %}
 
 
-              {% if post.reported %}
+              {% if acl.threads.can_mod_posts(forum) and post.reported %}
               <span class="label label-important">
               <span class="label label-important">
                 {% trans %}Reported{% endtrans %}
                 {% trans %}Reported{% endtrans %}
               </span>
               </span>
@@ -231,7 +205,9 @@
           </div>
           </div>
           <div class="post-message">
           <div class="post-message">
             <div class="markdown js-extra">
             <div class="markdown js-extra">
-              {{ post.post_preparsed|markdown_final|safe }}
+              <article>
+                {{ post.post_preparsed|markdown_final|safe }}
+              </article>
             </div>
             </div>
             {% if post.user.signature %}
             {% if post.user.signature %}
             <div class="post-signature">
             <div class="post-signature">
@@ -266,10 +242,10 @@
                 {% if user.is_authenticated() and user.pk != post.user_id and acl.threads.can_downvote_posts(forum) %}
                 {% if user.is_authenticated() and user.pk != post.user_id and acl.threads.can_downvote_posts(forum) %}
                 <form action="{% url 'post_downvote' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline form-downvote" method="post">
                 <form action="{% url 'post_downvote' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline form-downvote" method="post">
                   <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                   <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-                  <button type="submit" class="btn btn-link post-hate"{% if post.karma_vote and post.karma_vote.score < 0 %} disabled="disabled"{% endif %}>{% trans %}Hate{% endtrans %}</button>
+                  <button type="submit" class="btn btn-link post-hate"{% if post.karma_vote and post.karma_vote.score < 0 %} disabled="disabled"{% endif %}>{% trans %}Dislike{% endtrans %}</button>
                 </form>
                 </form>
                 {% elif acl.threads.can_see_post_score(forum) == 2 %}
                 {% elif acl.threads.can_see_post_score(forum) == 2 %}
-                <span class="post-{% if post.downvotes %}hate{% else %}neutral{% endif %}">{% trans %}Hates{% endtrans %}</span>
+                <span class="post-{% if post.downvotes %}hate{% else %}neutral{% endif %}">{% trans %}Dislikes{% endtrans %}</span>
                 {% endif %}
                 {% endif %}
               </div>
               </div>
               {% if acl.threads.can_see_post_votes(forum) %}
               {% if acl.threads.can_see_post_votes(forum) %}
@@ -281,10 +257,19 @@
             {% endif %}
             {% endif %}
 
 
             {% if user.is_authenticated() %}
             {% if user.is_authenticated() %}
-            <div class="post-actions">              
+            <div class="post-actions">
               {% if acl.users.can_see_users_trails() -%}
               {% if acl.users.can_see_users_trails() -%}
               <a href="{% url 'post_info' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-trail">{% trans %}Info{% endtrans %}</a>
               <a href="{% url 'post_info' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-trail">{% trans %}Info{% endtrans %}</a>
               {% endif %}
               {% endif %}
+              {% if post.reported and acl.reports.can_handle() and acl.threads.can_mod_posts(forum) %}
+              <a href="{% url 'post_report_show' thread=thread.pk, slug=thread.slug, post=post.pk %}">{% trans %}Show report{% endtrans %}</a>
+              {% endif %}
+              {% if acl.reports.can_report() %}
+              <form action="{% url 'post_report' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline form-report" method="post" autocomplete="off">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-report tooltip-top" title="{% trans %}Bring this post to moderator attention.{% endtrans %}">{% trans %}Report{% endtrans %}</button>
+              </form>
+              {% endif %}
               {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk %}
               {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk %}
               <a href="{% url 'thread_edit' thread=thread.pk, slug=thread.slug %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
               <a href="{% url 'thread_edit' thread=thread.pk, slug=thread.slug %}" class="post-edit">{% trans %}Edit{% endtrans %}</a>
               {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
               {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
@@ -294,39 +279,45 @@
             </div>
             </div>
             {% if post.pk == thread.start_post_id %}
             {% if post.pk == thread.start_post_id %}
             <div class="post-actions">
             <div class="post-actions">
-              {% if acl.threads.can_delete_thread(user, forum, thread, post) == 2 %}
-              <form action="{% url 'thread_delete' thread=thread.pk, slug=thread.slug %}" class="form-inline prompt-delete-thread" method="post">
+              {% if acl.threads.can_delete_thread(user, forum, thread, post) %}
+              {% if post.deleted %}
+              <form action="{% url 'thread_show' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-                <span>{% trans %}Delete thread:{% endtrans %}</span>
-                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this thread for good{% endtrans %}">{% trans %}Hard{% endtrans %}</button>
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Make this thread visible to other users{% endtrans %}">{% trans %}Restore{% endtrans %}</button>
               </form>
               </form>
+              {% else %}
+              <form action="{% url 'thread_hide' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Hide this thread from other users{% endtrans %}">{% trans %}Hide{% endtrans %}</button>
+              </form>
+              {% endif %}
               {% endif %}
               {% endif %}
-              {% if not post.deleted and acl.threads.can_delete_thread(user, forum, thread, post) %}
-              <form action="{% url 'thread_hide' thread=thread.pk, slug=thread.slug %}" class="form-inline prompt-delete-thread" method="post">
+              {% if acl.threads.can_delete_thread(user, forum, thread, post) == 2 %}
+              <form action="{% url 'thread_delete' thread=thread.pk, slug=thread.slug %}" class="form-inline prompt-delete-thread" method="post">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-                {% if acl.threads.can_delete_thread(user, forum, thread, post) != 2 %}
-                <span>{% trans %}Delete thread:{% endtrans %}</span>
-                {% endif %}
-                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Hide this thread from other users{% endtrans %}">{% trans %}Soft{% endtrans %}</button>
+                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this thread for good{% endtrans %}">{% trans %}Delete{% endtrans %}</button>
               </form>
               </form>
               {% endif %}
               {% endif %}
             </div>
             </div>
             {% elif post.pk != thread.start_post_id and acl.threads.can_delete_post(user, forum, thread, post) %}
             {% elif post.pk != thread.start_post_id and acl.threads.can_delete_post(user, forum, thread, post) %}
             <div class="post-actions">
             <div class="post-actions">
-              {% if acl.threads.can_delete_post(user, forum, thread, post) == 2 -%}
-              <form action="{% url 'post_delete' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline prompt-delete-post" method="post">
+              {% if acl.threads.can_delete_post(user, forum, thread, post) %}
+              {% if post.deleted %}
+              <form action="{% url 'post_show' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline" method="post">
+                <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Make this reply visible to other users{% endtrans %}">{% trans %}Restore{% endtrans %}</button>
+              </form>
+              {% else %}
+              <form action="{% url 'post_hide' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline" method="post">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-                <span>{% trans %}Delete reply:{% endtrans %}</span>
-                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this reply for good{% endtrans %}">{% trans %}Hard{% endtrans %}</button>
+                <button type="submit" class="btn btn-link btn-hide tooltip-top" title="{% trans %}Hide this reply from other users{% endtrans %}">{% trans %}Hide{% endtrans %}</button>
               </form>
               </form>
               {% endif %}
               {% endif %}
-              {% if not post.deleted and acl.threads.can_delete_post(user, forum, thread, post) %}
-              <form action="{% url 'post_hide' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline prompt-delete-post" method="post">
+              {% endif %}
+              {% if acl.threads.can_delete_post(user, forum, thread, post) == 2 -%}
+              <form action="{% url 'post_delete' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="form-inline prompt-delete-post" method="post">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
                 <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-                {% if acl.threads.can_delete_post(user, forum, thread, post) != 2 %}
-                <span>{% trans %}Delete reply:{% endtrans %}</span>
-                {% endif %}
-                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Hide this reply from other users{% endtrans %}">{% trans %}Soft{% endtrans %}</button>
+                <button type="submit" class="btn btn-link tooltip-top" title="{% trans %}Delete this reply for good{% endtrans %}">{% trans %}Delete{% endtrans %}</button>
               </form>
               </form>
               {% endif %}
               {% endif %}
             </div>
             </div>
@@ -338,10 +329,10 @@
       {% endif %}
       {% endif %}
     </div>
     </div>
 
 
-    {% if post.checkpoint_set.all() %}
+    {% if post.checkpoints_visible %}
     <div class="post-checkpoints">
     <div class="post-checkpoints">
-      {% for checkpoint in post.checkpoint_set.all() %}
-      <div class="post-checkpoint">
+      {% for checkpoint in post.checkpoints_visible %}
+      <div class="post-checkpoint{% if checkpoint.deleted %} checkpoint-deleted{% endif %}">
         <hr>
         <hr>
         <span>
         <span>
           {%- if checkpoint.action == 'limit' -%}
           {%- if checkpoint.action == 'limit' -%}
@@ -356,7 +347,33 @@
           <i class="icon-trash"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} deleted this thread {{ date }}{% endtrans %}
           <i class="icon-trash"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} deleted this thread {{ date }}{% endtrans %}
           {%- elif checkpoint.action == 'undeleted' -%}
           {%- elif checkpoint.action == 'undeleted' -%}
           <i class="icon-trash"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} restored this thread {{ date }}{% endtrans %}
           <i class="icon-trash"></i> {% trans user=checkpoint_user(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} restored this thread {{ date }}{% endtrans %}
+          {%- elif checkpoint.action == 'moved' and acl.forums.can_see(checkpoint.old_forum_id) -%}
+          <i class="icon-arrow-right"></i> {% trans user=checkpoint_user(checkpoint), forum=checkpoint_forum(checkpoint), date=checkpoint.date|reltimesince|low %}{{ user }} moved this thread from {{ forum }} {{ date }}{% endtrans %}
           {%- endif -%}
           {%- endif -%}
+          {% if user.is_authenticated() %}
+          {% if acl.threads.can_delete_checkpoint(forum) %}
+          {% if checkpoint.deleted %}
+          <form action="{% url 'post_checkpoint_show' slug=thread.slug, thread=thread.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="hidden" name="retreat" value="{{ request_path }}#post-{{ post.pk }}">
+            <button type="submit" class="btn btn-link btn-show">{% trans %}Restore{% endtrans %}</button>
+          </form>
+          {% else %}
+          <form action="{% url 'post_checkpoint_hide' slug=thread.slug, thread=thread.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="hidden" name="retreat" value="{{ request_path }}#post-{{ post.pk }}">
+            <button type="submit" class="btn btn-link btn-hide">{% trans %}Hide{% endtrans %}</button>
+          </form>
+          {% endif %}
+          {% endif %}
+          {% if acl.threads.can_delete_checkpoint(forum) == 2 %}
+          <form action="{% url 'post_checkpoint_delete' slug=thread.slug, thread=thread.pk, checkpoint=checkpoint.pk %}" method="post" class="form-inline prompt-delete-checkpoint">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            <input type="hidden" name="retreat" value="{{ request_path }}#post-{{ post.pk }}">
+            <button type="submit" class="btn btn-link btn-delete">{% trans %}Delete{% endtrans %}</button>
+          </form>
+          {% endif %}
+          {% endif %}
         </span>
         </span>
       </div>
       </div>
       {% endfor %}
       {% endfor %}
@@ -368,7 +385,7 @@
   {% if user.is_authenticated() and (thread_form or posts_form) %}
   {% if user.is_authenticated() and (thread_form or posts_form) %}
   <div class="thread-moderation">
   <div class="thread-moderation">
     {% if thread_form%}
     {% if thread_form%}
-    <form id="thread_form" class="form-inline pull-left" action="{% url 'thread' slug=thread.slug, thread=thread.id, page=pagination['page'] %}" method="POST">
+    <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="origin" value="thread_form">
       <input type="hidden" name="origin" value="thread_form">
       {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
       {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
@@ -376,7 +393,7 @@
     </form>
     </form>
     {% endif %}
     {% endif %}
     {% if posts_form%}
     {% if posts_form%}
-    <form id="posts_form" class="form-inline pull-right" action="{% url 'thread' slug=thread.slug, thread=thread.id, page=pagination['page'] %}" method="POST">
+    <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="origin" value="posts_form">
       <input type="hidden" name="origin" value="posts_form">
       {{ form_theme.input_select(posts_form['list_action'],width=3) }}
       {{ form_theme.input_select(posts_form['list_action'],width=3) }}
@@ -416,6 +433,7 @@
 {% block javascripts -%}{{ super() }}
 {% block javascripts -%}{{ super() }}
   <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
   <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
   <script type="text/javascript">
   <script type="text/javascript">
+    var l_post_reported = "{{ _('Reported!') }}";
     hljs.tabReplace = '    ';
     hljs.tabReplace = '    ';
     hljs.initHighlightingOnLoad();
     hljs.initHighlightingOnLoad();
     EnhancePostsMD();
     EnhancePostsMD();
@@ -455,6 +473,10 @@
           var decision = confirm("{% trans %}Are you sure you want to delete this post?{% endtrans %}");
           var decision = confirm("{% trans %}Are you sure you want to delete this post?{% endtrans %}");
           return decision;
           return decision;
       });
       });
+      $('.prompt-delete-checkpoint').submit(function() {
+          var decision = confirm("{% trans %}Are you sure you want to delete this checkpoint?{% endtrans %}");
+          return decision;
+      });
     });
     });
     {% endif %}
     {% endif %}
   </script>
   </script>
@@ -498,6 +520,15 @@
 {%- endmacro %}
 {%- endmacro %}
 
 
 
 
+{% macro checkpoint_forum(checkpoint) -%}
+{%- if checkpoint.old_forum_id -%}
+<a href="{% url 'forum' forum=checkpoint.old_forum_id, slug=checkpoint.old_forum_slug %}">{{ checkpoint.old_forum_name }}</a>
+{%- else -%}
+<strong>{{ checkpoint.old_forum_name }}</strong>
+{%- endif -%}
+{%- endmacro %}
+
+
 {% macro edit_user(post) -%}
 {% macro edit_user(post) -%}
 {%- if post.edit_user_id -%}
 {%- if post.edit_user_id -%}
 <a href="{{ 'user'|url(user=post.edit_user_id, username=post.edit_user_slug) }}">{{ post.edit_user_name }}</a>
 <a href="{{ 'user'|url(user=post.edit_user_id, username=post.edit_user_slug) }}">{{ post.edit_user_name }}</a>

+ 67 - 13
templates/cranefly/watched.html

@@ -24,9 +24,69 @@
   {% endif %}
   {% endif %}
 
 
   {% if threads %}
   {% if threads %}
+  {{ pager() }}
   <div class="forum-threads-list watched-threads">
   <div class="forum-threads-list watched-threads">
-    {{ pager() }}
-    <table class="table">
+    <div class="header">
+      <div class="row-fluid">
+        <div class="span7">{% trans %}Thread{% endtrans %}</div>
+        <div class="span5 thread-activity">
+          <div class="thread-replies">{% trans %}Activity{% endtrans %}</div>
+        </div>
+      </div>
+    </div>
+    {% for thread in threads %}
+    <div id="watch-{{ loop.index }}" class="thread-row{% if not thread.is_read %} thread-new{% endif %}{% if loop.last %} thread-last{% endif %}">
+      <div class="row-fluid">
+        <div class="span7">
+          {% if thread.is_read %}
+          <a href="{{ thread_url(thread, 'new') }}" class="thread-icon thread-icon-last tooltip-top" title="{% trans %}Click to see last post{% endtrans %}"><i class="icon-asterisk"></i></a>
+          {% else %}
+          <a href="{{ thread_url(thread, 'new') }}" class="thread-icon thread-icon-new tooltip-top" title="{% trans %}Click to see first unread post{% endtrans %}"><i class="icon-fire"></i></a>
+          {% endif %}
+
+          {{ macros.thread_flags(thread) }}
+
+          <a href="{{ thread_url(thread) }}" class="thread-name">{{ thread.name }}</a>
+
+          <div class="thread-details">
+            {% trans user=thread_starter(thread), forum=thread_forum(thread), start=thread.start|reltimesince|low %}by {{ user }} in {{ forum }} {{ start }}{% endtrans %}
+          </div>
+
+        </div>
+        <div class="span5 thread-activity">
+          {% if settings.avatars_on_threads_list %}
+          <div class="thread-last-avatar">
+            {% if thread.last_poster_id %}
+            <a href="{% url 'user' user=thread.last_poster.pk, username=thread.last_poster.username_slug %}"><img src="{{ thread.last_poster.get_avatar(40) }}" alt=""></a>
+            {% else %}
+            <img src="{{ macros.avatar_guest(40) }}" alt="" class="user-avatar">
+            {% endif %}
+          </div>
+          {% endif %}
+
+          <div class="thread-replies">
+            <strong class="lead">{{ thread_reply(thread) }}, {{ thread.last|reldate|low }}</strong><br>
+            {{ replies(thread.replies) }}, <span{% if (thread.upvotes-thread.downvotes) > 0 %} class="text-success"{% elif (thread.upvotes-thread.downvotes) < 0 %} class="text-error"{% endif %}><strong>{% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}</strong>{% trans rating=(thread.upvotes-thread.downvotes)|abs|intcomma %}{{ rating }} thread rating{% endtrans %}</span>
+          </div>
+
+          <div class="thread-options">
+            <form action="{% if thread.send_email %}{{ thread_url(thread, 'unwatch_email') }}{% else %}{{ thread_url(thread, 'watch_email') }}{% endif %}" method="post">
+              <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+              <input type="hidden" name="retreat" value="{{ request_path }}#watch-{{ loop.index }}">
+              <button type="submit" class="btn btn-{% if thread.send_email %}success{% else %}inverse{% endif %} tooltip-top" title="{% if thread.send_email %}{% trans %}Don't notify with e-mail{% endtrans %}{% else %}{% trans %}Notify with e-mail{% endtrans %}{% endif %}"><i class="icon-envelope"></i></button>
+            </form>
+
+            <form action="{{ thread_url(thread, 'unwatch') }}" method="post">
+              <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+              <input type="hidden" name="retreat" value="{{ delete_retreat(loop) }}">
+              <button type="submit" class="btn btn-danger tooltip-top" title="{% trans %}Unwatch{% endtrans %}"><i class="icon-remove"></i></button>
+            </form>
+          </div>
+        </div>
+      </div>
+    </div>
+    {% endfor %}
+    {#<table class="table">
       <thead>
       <thead>
         <tr>
         <tr>
           <th>{% trans %}Thread{% endtrans %}</th>
           <th>{% trans %}Thread{% endtrans %}</th>
@@ -68,9 +128,9 @@
         </tr>
         </tr>
         {% endfor %}
         {% endfor %}
       </tbody>
       </tbody>
-    </table>
-    {{ pager() }}
+    </table>#}
   </div>
   </div>
+  {{ pager() }}
   {% else %}
   {% else %}
   <p class="lead">{% if new -%}
   <p class="lead">{% if new -%}
     {% trans %}There are no unread threads that you are watching.{% endtrans %}
     {% trans %}There are no unread threads that you are watching.{% endtrans %}
@@ -104,15 +164,9 @@ thread
 {% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="user-link">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}
 {% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="user-link">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}
 {%- endmacro %}
 {%- endmacro %}
 
 
-{% macro thread_forum(thread) -%}{% filter trim %}
-{% if thread.forum_id == private_threads.pk %}
-<a href="{% url 'private_threads' %}" class="forum-link">{% trans %}Private Threads{% endtrans %}</a>
-{% elif thread.forum_id == reports.pk %}
-TODO!
-{% else %}
-<a href="{% url 'forum' forum=thread.forum_id, slug=thread.forum.slug %}" class="forum-link">{{ thread.forum.name }}</a>
-{% endif%}
-{% endfilter %}{%- endmacro %}
+{% macro thread_forum(thread) -%}
+<a href="{{ thread.forum.url }}" class="forum-link">{{ thread.forum }}</a>
+{%- endmacro %}
 
 
 {% macro thread_reply(thread) -%}
 {% macro thread_reply(thread) -%}
 {% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="user-link">{{ thread.last_poster_name }}</a>{% else %}{{ thread.last_poster_name }}{% endif %}
 {% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="user-link">{{ thread.last_poster_name }}</a>{% else %}{{ thread.last_poster_name }}{% endif %}

+ 3 - 0
templates/search/indexes/misago/post_text.txt

@@ -0,0 +1,3 @@
+{{ object.thread.name }}
+{{ object.user.username }}
+{{ object.post_clean }}