Rafał Pitoń 11 лет назад
Родитель
Сommit
8220079e25
41 измененных файлов с 2143 добавлено и 2143 удалено
  1. 207 207
      .gitignore
  2. 6 6
      .travis.yml
  3. 14 14
      cron.txt
  4. 9 9
      deployment/urls.py
  5. 28 28
      deployment/wsgi.py
  6. 87 87
      heartbeat.py
  7. 10 10
      manage.py
  8. 280 280
      misago/admin.py
  9. 19 19
      misago/apps/admin/index.py
  10. 84 84
      misago/apps/admin/sections/forums.py
  11. 71 71
      misago/apps/admin/sections/overview.py
  12. 68 68
      misago/apps/admin/sections/perms.py
  13. 19 19
      misago/apps/admin/sections/system.py
  14. 158 158
      misago/apps/admin/sections/users.py
  15. 57 57
      misago/apps/alerts.py
  16. 22 22
      misago/apps/category.py
  17. 52 52
      misago/apps/errors.py
  18. 22 22
      misago/apps/newsfeed.py
  19. 29 29
      misago/apps/newthreads.py
  20. 29 29
      misago/apps/popularthreads.py
  21. 39 39
      misago/apps/privatethreads/list.py
  22. 45 45
      misago/apps/privatethreads/mixins.py
  23. 10 10
      misago/apps/profiles/details/views.py
  24. 23 23
      misago/apps/profiles/followers/views.py
  25. 24 24
      misago/apps/profiles/follows/views.py
  26. 40 40
      misago/apps/profiles/posts/views.py
  27. 40 40
      misago/apps/profiles/threads/views.py
  28. 72 72
      misago/apps/threads/list.py
  29. 65 65
      misago/apps/threadtype/base.py
  30. 73 73
      misago/apps/threadtype/details.py
  31. 84 84
      misago/apps/threadtype/posting/newthread.py
  32. 10 10
      misago/apps/tos.py
  33. 124 124
      misago/auth.py
  34. 51 51
      misago/context_processors.py
  35. 41 41
      misago/cookiejar.py
  36. 11 11
      misago/management/commands/countreports.py
  37. 44 44
      misago/management/commands/pruneforums.py
  38. 10 10
      misago/management/commands/rebuildacls.py
  39. 11 11
      misago/management/commands/syncusermonitor.py
  40. 38 38
      misago/management/commands/updateranking.py
  41. 17 17
      misago/management/commands/updatethreadranking.py

+ 207 - 207
.gitignore

@@ -1,207 +1,207 @@
-
-#################
-## Eclipse
-#################
-
-*.pydevproject
-.project
-.metadata
-bin/**
-tmp/**
-tmp/**/*
-*.tmp
-*.bak
-*.swp
-*~.nib
-local.properties
-.classpath
-.settings/
-.loadpath
-
-# External tool builders
-.externalToolBuilders/
-
-# Locally stored "Eclipse launch configurations"
-*.launch
-
-# CDT-specific
-.cproject
-
-# PDT-specific
-.buildpath
-
-
-#################
-## Visual Studio
-#################
-
-## Ignore Visual Studio temporary files, build results, and
-## files generated by popular Visual Studio add-ons.
-
-# User-specific files
-*.suo
-*.user
-*.sln.docstates
-
-# Build results
-**/[Dd]ebug/
-**/[Rr]elease/
-*_i.c
-*_p.c
-*.ilk
-*.meta
-*.obj
-*.pch
-*.pdb
-*.pgc
-*.pgd
-*.rsp
-*.sbr
-*.tlb
-*.tli
-*.tlh
-*.tmp
-*.vspscc
-.builds
-**/*.dotCover
-
-## TODO: If you have NuGet Package Restore enabled, uncomment this
-#**/packages/ 
-
-# Visual C++ cache files
-ipch/
-*.aps
-*.ncb
-*.opensdf
-*.sdf
-
-# Visual Studio profiler
-*.psess
-*.vsp
-
-# ReSharper is a .NET coding add-in
-_ReSharper*
-
-# Installshield output folder 
-[Ee]xpress
-
-# DocProject is a documentation generator add-in
-DocProject/buildhelp/
-DocProject/Help/*.HxT
-DocProject/Help/*.HxC
-DocProject/Help/*.hhc
-DocProject/Help/*.hhk
-DocProject/Help/*.hhp
-DocProject/Help/Html2
-DocProject/Help/html
-
-# Click-Once directory
-publish
-
-# Others
-[Bb]in
-[Oo]bj
-sql
-TestResults
-*.Cache
-ClientBin
-stylecop.*
-~$*
-*.dbmdl
-Generated_Code #added for RIA/Silverlight projects
-
-# Backup & report files from converting an old project file to a newer
-# Visual Studio version. Backup files are not needed, because we have git ;-)
-_UpgradeReport_Files/
-Backup*/
-UpgradeLog*.XML
-
-
-
-############
-## Windows
-############
-
-# Windows image file caches
-Thumbs.db 
-
-# Folder config file
-Desktop.ini
-Github for Windows Log.lnk
-
-
-#############
-## Python
-#############
-
-*.py[co]
-
-# Packages
-*.egg
-*.egg-info
-dist
-build
-eggs
-parts
-bin
-var
-sdist
-develop-eggs
-.installed.cfg
-
-# Installer logs
-pip-log.txt
-
-# Unit test / coverage reports
-.coverage
-.tox
-
-#Mr Developer
-.mr.developer.cfg
-
-# Mac crap
-.DS_Store
-
-
-##################
-## 3rd Party Libs
-##################
-
-
-coffin/**
-debug_toolbar/**
-dev/**
-django/**
-django_jinja/**
-floppyforms/**
-haystack/**
-jinja2/**
-markdown/**
-mptt/**
-pytz/**
-recaptcha/**
-south/**
-whoosh/**
-custom/**
-searchindex/**
-static/avatars/protoss
-static/avatars/terran
-static/avatars/zerg
-static/avatars/_thumbs
-static/avatars/custom
-static/avatars/custom.gif
-unidecode/**
-yaml/**
-dev-manage.py
-path.py
-!templates/debug_toolbar/panels/acl.html
-static/emojis/**
-templates/debug_toolbar/**
-
-
-############
-## Vagrant
-############
-
-.vagrant
-database.db
+
+#################
+## Eclipse
+#################
+
+*.pydevproject
+.project
+.metadata
+bin/**
+tmp/**
+tmp/**/*
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# PDT-specific
+.buildpath
+
+
+#################
+## Visual Studio
+#################
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+**/[Dd]ebug/
+**/[Rr]elease/
+*_i.c
+*_p.c
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.vspscc
+.builds
+**/*.dotCover
+
+## TODO: If you have NuGet Package Restore enabled, uncomment this
+#**/packages/ 
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+
+# Visual Studio profiler
+*.psess
+*.vsp
+
+# ReSharper is a .NET coding add-in
+_ReSharper*
+
+# Installshield output folder 
+[Ee]xpress
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish
+
+# Others
+[Bb]in
+[Oo]bj
+sql
+TestResults
+*.Cache
+ClientBin
+stylecop.*
+~$*
+*.dbmdl
+Generated_Code #added for RIA/Silverlight projects
+
+# Backup & report files from converting an old project file to a newer
+# Visual Studio version. Backup files are not needed, because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+
+
+
+############
+## Windows
+############
+
+# Windows image file caches
+Thumbs.db 
+
+# Folder config file
+Desktop.ini
+Github for Windows Log.lnk
+
+
+#############
+## Python
+#############
+
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Mr Developer
+.mr.developer.cfg
+
+# Mac crap
+.DS_Store
+
+
+##################
+## 3rd Party Libs
+##################
+
+
+coffin/**
+debug_toolbar/**
+dev/**
+django/**
+django_jinja/**
+floppyforms/**
+haystack/**
+jinja2/**
+markdown/**
+mptt/**
+pytz/**
+recaptcha/**
+south/**
+whoosh/**
+custom/**
+searchindex/**
+static/avatars/protoss
+static/avatars/terran
+static/avatars/zerg
+static/avatars/_thumbs
+static/avatars/custom
+static/avatars/custom.gif
+unidecode/**
+yaml/**
+dev-manage.py
+path.py
+!templates/debug_toolbar/panels/acl.html
+static/emojis/**
+templates/debug_toolbar/**
+
+
+############
+## Vagrant
+############
+
+.vagrant
+database.db

+ 6 - 6
.travis.yml

@@ -1,7 +1,7 @@
-language: python
-python:
-  - "2.7"
-# command to install dependencies
-install: "pip install -r requirements.txt --use-mirrors"
-# command to run tests
+language: python
+python:
+  - "2.7"
+# command to install dependencies
+install: "pip install -r requirements.txt --use-mirrors"
+# command to run tests
 script: python manage.py test

+ 14 - 14
cron.txt

@@ -1,14 +1,14 @@
-0 3 * * * python $HOME/misago/manage.py clearalerts
-0 3 * * * python $HOME/misago/manage.py clearattempts
-0 */4 * * * python $HOME/misago/manage.py clearsessions
-20 3 * * * python $HOME/misago/manage.py cleartokens
-15 3 * * * python $HOME/misago/manage.py cleartracker
-0 0 * * 0 python $HOME/misago/manage.py forcepdssync
-0 3 * * * python $HOME/misago/manage.py pruneforums
-5 3 * * * python $HOME/misago/manage.py syncdeltas
-10 3 * * * python $HOME/misago/manage.py updateranking
-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
+0 3 * * * python $HOME/misago/manage.py clearalerts
+0 3 * * * python $HOME/misago/manage.py clearattempts
+0 */4 * * * python $HOME/misago/manage.py clearsessions
+20 3 * * * python $HOME/misago/manage.py cleartokens
+15 3 * * * python $HOME/misago/manage.py cleartracker
+0 0 * * 0 python $HOME/misago/manage.py forcepdssync
+0 3 * * * python $HOME/misago/manage.py pruneforums
+5 3 * * * python $HOME/misago/manage.py syncdeltas
+10 3 * * * python $HOME/misago/manage.py updateranking
+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

+ 9 - 9
deployment/urls.py

@@ -1,9 +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')),
-#)
+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')),
+#)

+ 28 - 28
deployment/wsgi.py

@@ -1,28 +1,28 @@
-"""
-WSGI config for Misago project.
-
-This module contains the WSGI application used by Django's development server
-and any production WSGI deployments. It should expose a module-level variable
-named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
-this application via the ``WSGI_APPLICATION`` setting.
-
-Usually you will have the standard Django WSGI application here, but it also
-might make sense to replace the whole Django WSGI application with a custom one
-that later delegates to the Django one. For example, you could introduce WSGI
-middleware here, or combine a Django application with an application of another
-framework.
-
-"""
-import os
-
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "deployment.settings")
-
-# This application object is used by any WSGI server configured to use this
-# file. This includes Django's development server, if the WSGI_APPLICATION
-# setting points here.
-from django.core.wsgi import get_wsgi_application
-application = get_wsgi_application()
-
-# Apply WSGI middleware here.
-# from helloworld.wsgi import HelloWorldApplication
-# application = HelloWorldApplication(application)
+"""
+WSGI config for Misago project.
+
+This module contains the WSGI application used by Django's development server
+and any production WSGI deployments. It should expose a module-level variable
+named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
+this application via the ``WSGI_APPLICATION`` setting.
+
+Usually you will have the standard Django WSGI application here, but it also
+might make sense to replace the whole Django WSGI application with a custom one
+that later delegates to the Django one. For example, you could introduce WSGI
+middleware here, or combine a Django application with an application of another
+framework.
+
+"""
+import os
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "deployment.settings")
+
+# This application object is used by any WSGI server configured to use this
+# file. This includes Django's development server, if the WSGI_APPLICATION
+# setting points here.
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()
+
+# Apply WSGI middleware here.
+# from helloworld.wsgi import HelloWorldApplication
+# application = HelloWorldApplication(application)

+ 87 - 87
heartbeat.py

@@ -1,87 +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()
+#!/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()

+ 10 - 10
manage.py

@@ -1,10 +1,10 @@
-#!/usr/bin/env python
-import os
-import sys
-
-if __name__ == "__main__":
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "deployment.settings")
-
-    from django.core.management import execute_from_command_line
-
-    execute_from_command_line(sys.argv)
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "deployment.settings")
+
+    from django.core.management import execute_from_command_line
+
+    execute_from_command_line(sys.argv)

+ 280 - 280
misago/admin.py

@@ -1,280 +1,280 @@
-from django.conf import settings
-from django.conf.urls import patterns, include, url
-from django.core.urlresolvers import resolve
-from django.utils.importlib import import_module
-
-"""
-Clean admin path if it was defined, or leave variable empty if ACP is turned off.
-"""
-ADMIN_PATH = ''
-if settings.ADMIN_PATH:
-    ADMIN_PATH = settings.ADMIN_PATH
-    while ADMIN_PATH[:1] == '/':
-        ADMIN_PATH = ADMIN_PATH[1:]
-    while ADMIN_PATH[-1:] == '/':
-        ADMIN_PATH = ADMIN_PATH[:-1]
-    ADMIN_PATH += '/'
-
-
-"""
-Admin lists sorter for admin sections and actions
-"""
-class SortList(object):
-    def __init__(self, unsorted):
-        self.unsorted = unsorted
-
-    def sort(self):
-        # Sort and return sorted list
-        order = []
-        cache = {}
-        for item in self.unsorted:
-            if item.after:
-                try:
-                    cache[item.after].append(item.id)
-                except KeyError:
-                    cache[item.after] = []
-                    cache[item.after].append(item.id)
-            else:
-                order.append(item.id)
-        while cache:
-            for item in cache.keys():
-                try:
-                    target_index = order.index(item)
-                    for new_item in cache[item]:
-                        target_index += 1
-                        order.insert(target_index, new_item)
-                    del cache[item]
-                except ValueError:
-                    pass
-        sorted = []
-        for item in order:
-            for object in self.unsorted:
-                if item == object.id:
-                    sorted.append(object)
-                    break
-        return sorted
-
-
-"""
-Admin site section
-"""
-class AdminSiteItem(object):
-    def __init__(self, id, name, icon, target=None, link=None, help=None, after=None):
-        self.id = id
-        self.name = name
-        self.help = help
-        self.after = after
-        self.icon = icon
-        self.target = target
-        self.link = link
-        self.sorted = False
-
-
-"""
-Admin site action
-"""
-class AdminAction(AdminSiteItem):
-    def __init__(self, section=None, actions=[], model=None, messages={}, urlpatterns=None, **kwargs):
-        self.actions = actions
-        self.section = section
-        self.model = model
-        self.messages = messages
-        self.urlpatterns = urlpatterns
-        super(AdminAction, self).__init__(**kwargs)
-
-    def get_action_attr(self, id, attr):
-        for action in self.actions:
-            if action['id'] == id:
-                return action[attr]
-        return None
-
-    def is_active(self, full_path, section=None):
-        if section:
-            action_path = '/%s%s/%s/' % (ADMIN_PATH, section, self.id)
-        else:
-            action_path = '/%s%s/' % (ADMIN_PATH, self.id)
-        # Paths overlap = active action
-        return len(action_path) <= full_path and full_path[:len(action_path)] == action_path
-
-
-"""
-Admin site section
-"""
-class AdminSection(AdminSiteItem):
-    def __init__(self, section=None, **kwargs):
-        self.actions = []
-        self.last = None
-        super(AdminSection, self).__init__(**kwargs)
-
-    def get_links(self):
-        links = []
-        first_action = True
-        for action in self.actions:
-            if first_action:
-                links += patterns('', url('^', include(action.urlpatterns)))
-                first_action = False
-            else:
-                links += patterns('', url(('^%s/' % action.id), include(action.urlpatterns)))
-        return links
-
-    def is_active(self, full_path):
-        action_path = '/%s%s/' % (ADMIN_PATH, self.id)
-        # Paths overlap = active action
-        return len(action_path) <= full_path and full_path[:len(action_path)] == action_path
-
-
-"""
-Admin site class that knows ACP structure
-"""
-class AdminSite(object):
-    actions_index = {}
-    links = []
-    sections = []
-    sections_index = {}
-
-    def discover(self):
-        """
-        Build admin site structure
-        """
-        if self.links:
-            return self.links
-
-        # Found actions
-        actions = []
-
-        # Orphan actions that have no section yet
-        late_actions = []
-
-        # Load default admin site
-        from misago.apps.admin.sections import ADMIN_SECTIONS
-        for section in ADMIN_SECTIONS:
-            self.sections.append(section)
-            self.sections_index[section.id] = section
-
-            # Loop section actions
-            section_actions = import_module('misago.apps.admin.sections.%s' % section.id)
-            for action in section_actions.ADMIN_ACTIONS:
-                self.actions_index[action.id] = action
-                if not action.after:
-                     action.after = self.sections_index[section.id].last
-                actions.append(action)
-                self.sections_index[section.id].last = action.after
-
-        # Iterate over installed applications
-        for app_name in settings.INSTALLED_APPS:
-            try:
-                app = import_module(app_name + '.admin')
-
-                # Attempt to import sections
-                try:
-                    for section in app.ADMIN_SECTIONS:
-                        self.sections.append(section)
-                        self.sections_index[section.id] = section
-                except AttributeError:
-                    pass
-
-                # Attempt to import actions
-                try:
-                    for action in app.ADMIN_ACTIONS:
-                        self.actions_index[action.id] = action
-                        if action.section in self.sections_index:
-                            if not action.after:
-                                 action.after = self.sections_index[action.section].last
-                            actions.append(action)
-                            self.sections_index[action.section].last = action.after
-                        else:
-                            late_actions.append(action)
-                except AttributeError:
-                    pass
-            except ImportError:
-                pass
-
-        # So actions and late actions
-        actions += late_actions
-
-        # Sorth sections and actions
-        sort_sections = SortList(self.sections)
-        sort_actions = SortList(actions)
-        self.sections = sort_sections.sort()
-        actions = sort_actions.sort()
-
-        # Put actions in sections
-        for action in actions:
-            self.sections_index[action.section].actions.append(action)
-
-        # Return ready admin routing
-        first_section = True
-        for section in self.sections:
-            if first_section:
-                self.links += patterns('', url('^', include(section.get_links())))
-                first_section = False
-            else:
-                self.links += patterns('', url(('^%s/' % section.id), include(section.get_links())))
-        
-        return self.links
-
-    def get_action(self, action):
-        """
-        Get admin action
-        """
-        return self.actions_index.get(action)
-
-    def get_admin_index(self):
-        """
-        Return admin index link - first action of first section
-        """
-        return self.sections[0].actions[0].link
-
-    def get_admin_navigation(self, request):
-        """
-        Find and return current admin navigation
-        """
-        sections = []
-        actions = []
-        active_section = False
-        active_action = False
-
-        # Loop sections, build list of sections and find active section
-        for section in self.sections:
-            is_active = section.is_active(request.path)
-            sections.append({
-                             'is_active': is_active,
-                             'name': section.name,
-                             'icon': section.icon,
-                             'link': section.actions[0].link
-                             })
-            if is_active:
-                active_section = section
-
-        # If no section was found to be active, default to first one
-        if not active_section:
-            active_section = self.sections[0]
-            sections[0]['is_active'] = True
-
-        # Loop active section actions
-        for action in active_section.actions:
-            is_active = action.is_active(request.path, active_section.id if active_section != self.sections[0] else None)
-            actions.append({
-                             'is_active': is_active,
-                             'name': action.name,
-                             'icon': action.icon,
-                             'help': action.help,
-                             'link': action.link
-                             })
-            if is_active:
-                active_action = action
-
-        # If no action was found to be active, default to first one
-        if not active_action:
-            active_action = active_section.actions[0]
-            actions[0]['is_active'] = True
-
-        # Return admin navigation for this location
-        return {
-                'sections': sections,
-                'actions': actions,
-                'admin_index': self.get_admin_index(),
-                }
-
-
-site = AdminSite();
+from django.conf import settings
+from django.conf.urls import patterns, include, url
+from django.core.urlresolvers import resolve
+from django.utils.importlib import import_module
+
+"""
+Clean admin path if it was defined, or leave variable empty if ACP is turned off.
+"""
+ADMIN_PATH = ''
+if settings.ADMIN_PATH:
+    ADMIN_PATH = settings.ADMIN_PATH
+    while ADMIN_PATH[:1] == '/':
+        ADMIN_PATH = ADMIN_PATH[1:]
+    while ADMIN_PATH[-1:] == '/':
+        ADMIN_PATH = ADMIN_PATH[:-1]
+    ADMIN_PATH += '/'
+
+
+"""
+Admin lists sorter for admin sections and actions
+"""
+class SortList(object):
+    def __init__(self, unsorted):
+        self.unsorted = unsorted
+
+    def sort(self):
+        # Sort and return sorted list
+        order = []
+        cache = {}
+        for item in self.unsorted:
+            if item.after:
+                try:
+                    cache[item.after].append(item.id)
+                except KeyError:
+                    cache[item.after] = []
+                    cache[item.after].append(item.id)
+            else:
+                order.append(item.id)
+        while cache:
+            for item in cache.keys():
+                try:
+                    target_index = order.index(item)
+                    for new_item in cache[item]:
+                        target_index += 1
+                        order.insert(target_index, new_item)
+                    del cache[item]
+                except ValueError:
+                    pass
+        sorted = []
+        for item in order:
+            for object in self.unsorted:
+                if item == object.id:
+                    sorted.append(object)
+                    break
+        return sorted
+
+
+"""
+Admin site section
+"""
+class AdminSiteItem(object):
+    def __init__(self, id, name, icon, target=None, link=None, help=None, after=None):
+        self.id = id
+        self.name = name
+        self.help = help
+        self.after = after
+        self.icon = icon
+        self.target = target
+        self.link = link
+        self.sorted = False
+
+
+"""
+Admin site action
+"""
+class AdminAction(AdminSiteItem):
+    def __init__(self, section=None, actions=[], model=None, messages={}, urlpatterns=None, **kwargs):
+        self.actions = actions
+        self.section = section
+        self.model = model
+        self.messages = messages
+        self.urlpatterns = urlpatterns
+        super(AdminAction, self).__init__(**kwargs)
+
+    def get_action_attr(self, id, attr):
+        for action in self.actions:
+            if action['id'] == id:
+                return action[attr]
+        return None
+
+    def is_active(self, full_path, section=None):
+        if section:
+            action_path = '/%s%s/%s/' % (ADMIN_PATH, section, self.id)
+        else:
+            action_path = '/%s%s/' % (ADMIN_PATH, self.id)
+        # Paths overlap = active action
+        return len(action_path) <= full_path and full_path[:len(action_path)] == action_path
+
+
+"""
+Admin site section
+"""
+class AdminSection(AdminSiteItem):
+    def __init__(self, section=None, **kwargs):
+        self.actions = []
+        self.last = None
+        super(AdminSection, self).__init__(**kwargs)
+
+    def get_links(self):
+        links = []
+        first_action = True
+        for action in self.actions:
+            if first_action:
+                links += patterns('', url('^', include(action.urlpatterns)))
+                first_action = False
+            else:
+                links += patterns('', url(('^%s/' % action.id), include(action.urlpatterns)))
+        return links
+
+    def is_active(self, full_path):
+        action_path = '/%s%s/' % (ADMIN_PATH, self.id)
+        # Paths overlap = active action
+        return len(action_path) <= full_path and full_path[:len(action_path)] == action_path
+
+
+"""
+Admin site class that knows ACP structure
+"""
+class AdminSite(object):
+    actions_index = {}
+    links = []
+    sections = []
+    sections_index = {}
+
+    def discover(self):
+        """
+        Build admin site structure
+        """
+        if self.links:
+            return self.links
+
+        # Found actions
+        actions = []
+
+        # Orphan actions that have no section yet
+        late_actions = []
+
+        # Load default admin site
+        from misago.apps.admin.sections import ADMIN_SECTIONS
+        for section in ADMIN_SECTIONS:
+            self.sections.append(section)
+            self.sections_index[section.id] = section
+
+            # Loop section actions
+            section_actions = import_module('misago.apps.admin.sections.%s' % section.id)
+            for action in section_actions.ADMIN_ACTIONS:
+                self.actions_index[action.id] = action
+                if not action.after:
+                     action.after = self.sections_index[section.id].last
+                actions.append(action)
+                self.sections_index[section.id].last = action.after
+
+        # Iterate over installed applications
+        for app_name in settings.INSTALLED_APPS:
+            try:
+                app = import_module(app_name + '.admin')
+
+                # Attempt to import sections
+                try:
+                    for section in app.ADMIN_SECTIONS:
+                        self.sections.append(section)
+                        self.sections_index[section.id] = section
+                except AttributeError:
+                    pass
+
+                # Attempt to import actions
+                try:
+                    for action in app.ADMIN_ACTIONS:
+                        self.actions_index[action.id] = action
+                        if action.section in self.sections_index:
+                            if not action.after:
+                                 action.after = self.sections_index[action.section].last
+                            actions.append(action)
+                            self.sections_index[action.section].last = action.after
+                        else:
+                            late_actions.append(action)
+                except AttributeError:
+                    pass
+            except ImportError:
+                pass
+
+        # So actions and late actions
+        actions += late_actions
+
+        # Sorth sections and actions
+        sort_sections = SortList(self.sections)
+        sort_actions = SortList(actions)
+        self.sections = sort_sections.sort()
+        actions = sort_actions.sort()
+
+        # Put actions in sections
+        for action in actions:
+            self.sections_index[action.section].actions.append(action)
+
+        # Return ready admin routing
+        first_section = True
+        for section in self.sections:
+            if first_section:
+                self.links += patterns('', url('^', include(section.get_links())))
+                first_section = False
+            else:
+                self.links += patterns('', url(('^%s/' % section.id), include(section.get_links())))
+        
+        return self.links
+
+    def get_action(self, action):
+        """
+        Get admin action
+        """
+        return self.actions_index.get(action)
+
+    def get_admin_index(self):
+        """
+        Return admin index link - first action of first section
+        """
+        return self.sections[0].actions[0].link
+
+    def get_admin_navigation(self, request):
+        """
+        Find and return current admin navigation
+        """
+        sections = []
+        actions = []
+        active_section = False
+        active_action = False
+
+        # Loop sections, build list of sections and find active section
+        for section in self.sections:
+            is_active = section.is_active(request.path)
+            sections.append({
+                             'is_active': is_active,
+                             'name': section.name,
+                             'icon': section.icon,
+                             'link': section.actions[0].link
+                             })
+            if is_active:
+                active_section = section
+
+        # If no section was found to be active, default to first one
+        if not active_section:
+            active_section = self.sections[0]
+            sections[0]['is_active'] = True
+
+        # Loop active section actions
+        for action in active_section.actions:
+            is_active = action.is_active(request.path, active_section.id if active_section != self.sections[0] else None)
+            actions.append({
+                             'is_active': is_active,
+                             'name': action.name,
+                             'icon': action.icon,
+                             'help': action.help,
+                             'link': action.link
+                             })
+            if is_active:
+                active_action = action
+
+        # If no action was found to be active, default to first one
+        if not active_action:
+            active_action = active_section.actions[0]
+            actions[0]['is_active'] = True
+
+        # Return admin navigation for this location
+        return {
+                'sections': sections,
+                'actions': actions,
+                'admin_index': self.get_admin_index(),
+                }
+
+
+site = AdminSite();

+ 19 - 19
misago/apps/admin/index.py

@@ -1,19 +1,19 @@
-from django.template import RequestContext
-from misago.models import Session
-from misago.monitor import monitor
-from misago.shortcuts import render_to_response
-
-def index(request):
-    return render_to_response('index.html',
-                              {
-                               'users': monitor['users'],
-                               'users_inactive': monitor['users_inactive'],
-                               'threads': monitor['threads'],
-                               'posts': monitor['posts'],
-                               'admins': Session.objects.filter(user__isnull=False).filter(admin=1).order_by('user__username_slug').select_related('user'),
-                              },
-                              context_instance=RequestContext(request));
-
-
-def todo(request, *args, **kwargs):
-    return render_to_response('todo.html', context_instance=RequestContext(request));
+from django.template import RequestContext
+from misago.models import Session
+from misago.monitor import monitor
+from misago.shortcuts import render_to_response
+
+def index(request):
+    return render_to_response('index.html',
+                              {
+                               'users': monitor['users'],
+                               'users_inactive': monitor['users_inactive'],
+                               'threads': monitor['threads'],
+                               'posts': monitor['posts'],
+                               'admins': Session.objects.filter(user__isnull=False).filter(admin=1).order_by('user__username_slug').select_related('user'),
+                              },
+                              context_instance=RequestContext(request));
+
+
+def todo(request, *args, **kwargs):
+    return render_to_response('todo.html', context_instance=RequestContext(request));

+ 84 - 84
misago/apps/admin/sections/forums.py

@@ -1,84 +1,84 @@
-from django.conf.urls import patterns, include, url
-from django.utils.translation import ugettext_lazy as _
-from misago.admin import AdminAction
-from misago.models import Forum
-
-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"),
-                          'link': 'admin_forums'
-                          },
-                         {
-                          'id': 'new',
-                          'name': _("New Node"),
-                          'help': _("Create new forums tree node"),
-                          'link': 'admin_forums_new'
-                          },
-                         ],
-                link='admin_forums',
-                urlpatterns=patterns('misago.apps.admin.forums.views',
-                        url(r'^$', 'List', name='admin_forums'),
-                        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'^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'^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',
-                link='admin_forums_labels',
-                urlpatterns=patterns('misago.apps.admin.index',
-                        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',
-                link='admin_forums_badwords',
-                urlpatterns=patterns('misago.apps.admin.index',
-                        url(r'^$', 'todo', name='admin_forums_badwords'),
-                    ),
-                ),
-    AdminAction(
-                section='forums',
-                id='tests',
-                name=_("Tests"),
-                help=_("Tests that new messages have to pass"),
-                icon='filter',
-                link='admin_forums_tests',
-                urlpatterns=patterns('misago.apps.admin.index',
-                        url(r'^$', 'todo', name='admin_forums_tests'),
-                    ),
-                ),
-    AdminAction(
-                section='forums',
-                id='attachments',
-                name=_("Attachments"),
-                help=_("Manage allowed attachment types."),
-                icon='download-alt',
-                link='admin_forums_attachments',
-                urlpatterns=patterns('misago.apps.admin.index',
-                        url(r'^$', 'todo', name='admin_forums_attachments'),
-                    ),
-                ),
-)
+from django.conf.urls import patterns, include, url
+from django.utils.translation import ugettext_lazy as _
+from misago.admin import AdminAction
+from misago.models import Forum
+
+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"),
+                          'link': 'admin_forums'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("New Node"),
+                          'help': _("Create new forums tree node"),
+                          'link': 'admin_forums_new'
+                          },
+                         ],
+                link='admin_forums',
+                urlpatterns=patterns('misago.apps.admin.forums.views',
+                        url(r'^$', 'List', name='admin_forums'),
+                        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'^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'^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',
+                link='admin_forums_labels',
+                urlpatterns=patterns('misago.apps.admin.index',
+                        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',
+                link='admin_forums_badwords',
+                urlpatterns=patterns('misago.apps.admin.index',
+                        url(r'^$', 'todo', name='admin_forums_badwords'),
+                    ),
+                ),
+    AdminAction(
+                section='forums',
+                id='tests',
+                name=_("Tests"),
+                help=_("Tests that new messages have to pass"),
+                icon='filter',
+                link='admin_forums_tests',
+                urlpatterns=patterns('misago.apps.admin.index',
+                        url(r'^$', 'todo', name='admin_forums_tests'),
+                    ),
+                ),
+    AdminAction(
+                section='forums',
+                id='attachments',
+                name=_("Attachments"),
+                help=_("Manage allowed attachment types."),
+                icon='download-alt',
+                link='admin_forums_attachments',
+                urlpatterns=patterns('misago.apps.admin.index',
+                        url(r'^$', 'todo', name='admin_forums_attachments'),
+                    ),
+                ),
+)

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

@@ -1,71 +1,71 @@
-from django.conf.urls import patterns, include, url
-from django.utils.translation import ugettext_lazy as _
-from misago.admin import AdminAction
-from misago.models import Session, User
-
-ADMIN_ACTIONS = (
-    AdminAction(
-                section='overview',
-                id='index',
-                name=_("Home"),
-                help=_("Your forums right now"),
-                icon='home',
-                link='admin_home',
-                urlpatterns=patterns('misago.apps.admin.index',
-                        url(r'^$', 'index', name='admin_home'),
-                    ),
-                ),
-    AdminAction(
-                section='overview',
-                id='stats',
-                name=_("Stats"),
-                help=_("Create Statistics Reports"),
-                icon='signal',
-                link='admin_stats',
-                urlpatterns=patterns('misago.apps.admin.stats.views',
-                        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'),
-                    ),
-                ),
-    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"),
-                          'link': 'admin_online'
-                          },
-                         ],
-                link='admin_online',
-                urlpatterns=patterns('misago.apps.admin.online.views',
-                        url(r'^$', 'List', name='admin_online'),
-                        url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'List', name='admin_online'),
-                    ),
-                ),
-    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"),
-                          'link': 'admin_team'
-                          },
-                         ],
-                link='admin_team',
-                urlpatterns=patterns('misago.apps.admin.team',
-                        url(r'^$', 'List', name='admin_team'),
-                    ),
-                ),
-)
+from django.conf.urls import patterns, include, url
+from django.utils.translation import ugettext_lazy as _
+from misago.admin import AdminAction
+from misago.models import Session, User
+
+ADMIN_ACTIONS = (
+    AdminAction(
+                section='overview',
+                id='index',
+                name=_("Home"),
+                help=_("Your forums right now"),
+                icon='home',
+                link='admin_home',
+                urlpatterns=patterns('misago.apps.admin.index',
+                        url(r'^$', 'index', name='admin_home'),
+                    ),
+                ),
+    AdminAction(
+                section='overview',
+                id='stats',
+                name=_("Stats"),
+                help=_("Create Statistics Reports"),
+                icon='signal',
+                link='admin_stats',
+                urlpatterns=patterns('misago.apps.admin.stats.views',
+                        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'),
+                    ),
+                ),
+    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"),
+                          'link': 'admin_online'
+                          },
+                         ],
+                link='admin_online',
+                urlpatterns=patterns('misago.apps.admin.online.views',
+                        url(r'^$', 'List', name='admin_online'),
+                        url(r'^(?P<page>[1-9]([0-9]+)?)/$', 'List', name='admin_online'),
+                    ),
+                ),
+    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"),
+                          'link': 'admin_team'
+                          },
+                         ],
+                link='admin_team',
+                urlpatterns=patterns('misago.apps.admin.team',
+                        url(r'^$', 'List', name='admin_team'),
+                    ),
+                ),
+)

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

@@ -1,68 +1,68 @@
-from django.conf.urls import patterns, include, url
-from django.utils.translation import ugettext_lazy as _
-from misago.admin import AdminAction
-from misago.models import ForumRole, Role
-
-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"),
-                          'link': 'admin_roles'
-                          },
-                         {
-                          'id': 'new',
-                          'name': _("Add Role"),
-                          'help': _("Create new role"),
-                          'link': 'admin_roles_new'
-                          },
-                         ],
-                link='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"),
-                          'link': 'admin_roles_forums'
-                          },
-                         {
-                          'id': 'new',
-                          'name': _("Add Role"),
-                          'help': _("Create new role"),
-                          'link': 'admin_roles_forums_new'
-                          },
-                         ],
-                link='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'),
-                     ),
-                ),
-)
+from django.conf.urls import patterns, include, url
+from django.utils.translation import ugettext_lazy as _
+from misago.admin import AdminAction
+from misago.models import ForumRole, Role
+
+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"),
+                          'link': 'admin_roles'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Add Role"),
+                          'help': _("Create new role"),
+                          'link': 'admin_roles_new'
+                          },
+                         ],
+                link='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"),
+                          'link': 'admin_roles_forums'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Add Role"),
+                          'help': _("Create new role"),
+                          'link': 'admin_roles_forums_new'
+                          },
+                         ],
+                link='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'),
+                     ),
+                ),
+)

+ 19 - 19
misago/apps/admin/sections/system.py

@@ -1,19 +1,19 @@
-from django.conf.urls import patterns, include, url
-from django.utils.translation import ugettext_lazy as _
-from misago.admin import AdminAction
-
-ADMIN_ACTIONS = (
-    AdminAction(
-                section='system',
-                id='settings',
-                name=_("Settings"),
-                help=_("Change your forum configuration"),
-                icon='wrench',
-                link='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')
-                     ),
-                ),
-)
+from django.conf.urls import patterns, include, url
+from django.utils.translation import ugettext_lazy as _
+from misago.admin import AdminAction
+
+ADMIN_ACTIONS = (
+    AdminAction(
+                section='system',
+                id='settings',
+                name=_("Settings"),
+                help=_("Change your forum configuration"),
+                icon='wrench',
+                link='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')
+                     ),
+                ),
+)

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

@@ -1,158 +1,158 @@
-from django.conf.urls import patterns, include, url
-from django.utils.translation import ugettext_lazy as _
-from misago.admin import AdminAction
-from misago.models import Ban, Newsletter, PruningPolicy, Rank, User
-
-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"),
-                          'link': 'admin_users'
-                          },
-                         {
-                          'id': 'new',
-                          'name': _("Add User"),
-                          'help': _("Create new user account"),
-                          'link': 'admin_users_new'
-                          },
-                         ],
-                link='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"),
-                          'link': 'admin_ranks'
-                          },
-                         {
-                          'id': 'new',
-                          'name': _("Add Rank"),
-                          'help': _("Create new rank"),
-                          'link': 'admin_ranks_new'
-                          },
-                         ],
-                link='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"),
-                          'link': 'admin_bans'
-                          },
-                         {
-                          'id': 'new',
-                          'name': _("Set Ban"),
-                          'help': _("Set new Ban"),
-                          'link': 'admin_bans_new'
-                          },
-                         ],
-                link='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"),
-                          'link': 'admin_prune_users'
-                          },
-                         {
-                          'id': 'new',
-                          'name': _("Set New Policy"),
-                          'help': _("Set new pruning policy"),
-                          'link': 'admin_prune_users_new'
-                          },
-                         ],
-                link='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"),
-                          'link': 'admin_newsletters'
-                          },
-                         {
-                          'id': 'new',
-                          'name': _("New Newsletter"),
-                          'help': _("Create new Newsletter"),
-                          'link': 'admin_newsletters_new'
-                          },
-                         ],
-                link='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'),
-                     ),
-                ),
-)
+from django.conf.urls import patterns, include, url
+from django.utils.translation import ugettext_lazy as _
+from misago.admin import AdminAction
+from misago.models import Ban, Newsletter, PruningPolicy, Rank, User
+
+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"),
+                          'link': 'admin_users'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Add User"),
+                          'help': _("Create new user account"),
+                          'link': 'admin_users_new'
+                          },
+                         ],
+                link='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"),
+                          'link': 'admin_ranks'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Add Rank"),
+                          'help': _("Create new rank"),
+                          'link': 'admin_ranks_new'
+                          },
+                         ],
+                link='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"),
+                          'link': 'admin_bans'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Set Ban"),
+                          'help': _("Set new Ban"),
+                          'link': 'admin_bans_new'
+                          },
+                         ],
+                link='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"),
+                          'link': 'admin_prune_users'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Set New Policy"),
+                          'help': _("Set new pruning policy"),
+                          'link': 'admin_prune_users_new'
+                          },
+                         ],
+                link='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"),
+                          'link': 'admin_newsletters'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("New Newsletter"),
+                          'help': _("Create new Newsletter"),
+                          'link': 'admin_newsletters_new'
+                          },
+                         ],
+                link='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'),
+                     ),
+                ),
+)

+ 57 - 57
misago/apps/alerts.py

@@ -1,57 +1,57 @@
-from copy import deepcopy
-from datetime import timedelta
-from django.template import RequestContext
-from django.utils import timezone
-from django.utils.timezone import localtime
-from django.utils.translation import ugettext as _
-from misago.decorators import block_guest
-from misago.shortcuts import render_to_response
-
-@block_guest
-def alerts(request):
-    now = localtime(timezone.now())
-    yesterday = now - timedelta(days=1)
-    alerts = {}
-    if not request.user.alerts_date:
-        request.user.alerts_date = request.user.join_date
-
-    for alert in request.user.alert_set.order_by('-id'):
-        alert.new = alert.date > request.user.alerts_date
-        alert_date = localtime(deepcopy(alert.date))
-        diff = now - alert_date
-        if now.date() == alert_date.date():
-            try:
-                alerts['today'].append(alert)
-            except KeyError:
-                alerts['today'] = [alert]
-        elif yesterday.date() == alert_date.date():
-            try:
-                alerts['yesterday'].append(alert)
-            except KeyError:
-                alerts['yesterday'] = [alert]
-        elif diff.days <= 7:
-            try:
-                alerts['week'].append(alert)
-            except KeyError:
-                alerts['week'] = [alert]
-        elif diff.days <= 30:
-            try:
-                alerts['month'].append(alert)
-            except KeyError:
-                alerts['month'] = [alert]
-        else:
-            try:
-                alerts['older'].append(alert)
-            except KeyError:
-                alerts['older'] = [alert]
-
-    new_alerts = request.user.alerts
-    request.user.alerts = 0
-    request.user.alerts_date = now
-    request.user.save(force_update=True)
-    return render_to_response('alerts.html',
-                              {
-                              'new_alerts': new_alerts,
-                              'alerts': alerts,
-                              },
-                              context_instance=RequestContext(request))
+from copy import deepcopy
+from datetime import timedelta
+from django.template import RequestContext
+from django.utils import timezone
+from django.utils.timezone import localtime
+from django.utils.translation import ugettext as _
+from misago.decorators import block_guest
+from misago.shortcuts import render_to_response
+
+@block_guest
+def alerts(request):
+    now = localtime(timezone.now())
+    yesterday = now - timedelta(days=1)
+    alerts = {}
+    if not request.user.alerts_date:
+        request.user.alerts_date = request.user.join_date
+
+    for alert in request.user.alert_set.order_by('-id'):
+        alert.new = alert.date > request.user.alerts_date
+        alert_date = localtime(deepcopy(alert.date))
+        diff = now - alert_date
+        if now.date() == alert_date.date():
+            try:
+                alerts['today'].append(alert)
+            except KeyError:
+                alerts['today'] = [alert]
+        elif yesterday.date() == alert_date.date():
+            try:
+                alerts['yesterday'].append(alert)
+            except KeyError:
+                alerts['yesterday'] = [alert]
+        elif diff.days <= 7:
+            try:
+                alerts['week'].append(alert)
+            except KeyError:
+                alerts['week'] = [alert]
+        elif diff.days <= 30:
+            try:
+                alerts['month'].append(alert)
+            except KeyError:
+                alerts['month'] = [alert]
+        else:
+            try:
+                alerts['older'].append(alert)
+            except KeyError:
+                alerts['older'] = [alert]
+
+    new_alerts = request.user.alerts
+    request.user.alerts = 0
+    request.user.alerts_date = now
+    request.user.save(force_update=True)
+    return render_to_response('alerts.html',
+                              {
+                              'new_alerts': new_alerts,
+                              'alerts': alerts,
+                              },
+                              context_instance=RequestContext(request))

+ 22 - 22
misago/apps/category.py

@@ -1,23 +1,23 @@
-from django.template import RequestContext
-from misago.apps.errors import error403, error404
-from misago.models import Forum
-from misago.readstrackers import ForumsTracker
-from misago.shortcuts import render_to_response
-
-def category(request, forum, slug):
-    if not request.acl.forums.can_see(forum):
-        return error404(request)
-    try:
-        forum = Forum.objects.get(pk=forum, type='category')
-        if not request.acl.forums.can_browse(forum):
-            return error403(request, _("You don't have permission to browse this category."))
-    except Forum.DoesNotExist:
-        return error404(request)
-
-    forum.subforums = Forum.objects.treelist(request.acl.forums, forum, tracker=ForumsTracker(request.user))
-    return render_to_response('category.html',
-                              {
-                              'category': forum,
-                              'parents': Forum.objects.forum_parents(forum.pk),
-                              },
+from django.template import RequestContext
+from misago.apps.errors import error403, error404
+from misago.models import Forum
+from misago.readstrackers import ForumsTracker
+from misago.shortcuts import render_to_response
+
+def category(request, forum, slug):
+    if not request.acl.forums.can_see(forum):
+        return error404(request)
+    try:
+        forum = Forum.objects.get(pk=forum, type='category')
+        if not request.acl.forums.can_browse(forum):
+            return error403(request, _("You don't have permission to browse this category."))
+    except Forum.DoesNotExist:
+        return error404(request)
+
+    forum.subforums = Forum.objects.treelist(request.acl.forums, forum, tracker=ForumsTracker(request.user))
+    return render_to_response('category.html',
+                              {
+                              'category': forum,
+                              'parents': Forum.objects.forum_parents(forum.pk),
+                              },
                               context_instance=RequestContext(request));

+ 52 - 52
misago/apps/errors.py

@@ -1,53 +1,53 @@
-from django.template import RequestContext
-from django.utils.translation import ugettext as _
-from misago.shortcuts import render_to_response, json_response
-
-def error_not_implemented(request, *args, **kwargs):
-    """Generic "NOT IMPLEMENTED!" Error"""
-    raise NotImplementedError("This action is not implemented!")
-
-
-def error_view(request, error, message=None):
-    if message:
-        message = unicode(message)
-    if request.is_ajax():
-        if not message:
-            if error == 404:
-                message = _("Requested page could not be loaded.")
-            if error == 403:
-                message = _("You don't have permission to see requested page.")
-        return json_response(request, status=error, message=message)
-    response = render_to_response(('error%s.html' % error),
-                                  {
-                                  'message': message,
-                                  'hide_signin': True,
-                                  'exception_response': True,
-                                  },
-                                  context_instance=RequestContext(request));
-    response.status_code = error
-    return response
-
-
-def error403(request, message=None):
-    return error_view(request, 403, message)
-
-
-def error404(request, message=None):
-    return error_view(request, 404, message)
-
-
-def error_banned(request, user=None, ban=None):
-    if not ban:
-        ban = request.ban
-    if request.is_ajax():
-        return json_response(request, status=403, message=_("You are banned."))
-    response = render_to_response('error403_banned.html',
-                                  {
-                                  'banned_user': user,
-                                  'ban': ban,
-                                  'hide_signin': True,
-                                  'exception_response': True,
-                                  },
-                                  context_instance=RequestContext(request));
-    response.status_code = 403
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago.shortcuts import render_to_response, json_response
+
+def error_not_implemented(request, *args, **kwargs):
+    """Generic "NOT IMPLEMENTED!" Error"""
+    raise NotImplementedError("This action is not implemented!")
+
+
+def error_view(request, error, message=None):
+    if message:
+        message = unicode(message)
+    if request.is_ajax():
+        if not message:
+            if error == 404:
+                message = _("Requested page could not be loaded.")
+            if error == 403:
+                message = _("You don't have permission to see requested page.")
+        return json_response(request, status=error, message=message)
+    response = render_to_response(('error%s.html' % error),
+                                  {
+                                  'message': message,
+                                  'hide_signin': True,
+                                  'exception_response': True,
+                                  },
+                                  context_instance=RequestContext(request));
+    response.status_code = error
+    return response
+
+
+def error403(request, message=None):
+    return error_view(request, 403, message)
+
+
+def error404(request, message=None):
+    return error_view(request, 404, message)
+
+
+def error_banned(request, user=None, ban=None):
+    if not ban:
+        ban = request.ban
+    if request.is_ajax():
+        return json_response(request, status=403, message=_("You are banned."))
+    response = render_to_response('error403_banned.html',
+                                  {
+                                  'banned_user': user,
+                                  'ban': ban,
+                                  'hide_signin': True,
+                                  'exception_response': True,
+                                  },
+                                  context_instance=RequestContext(request));
+    response.status_code = 403
     return response

+ 22 - 22
misago/apps/newsfeed.py

@@ -1,23 +1,23 @@
-from django.template import RequestContext
-from misago.decorators import block_guest
-from misago.models import Forum, Post
-from misago.shortcuts import render_to_response
-
-@block_guest
-def newsfeed(request):
-    follows = []
-    for user in request.user.follows.iterator():
-        follows.append(user.pk)
-    queryset = []
-    if follows:
-        queryset = Post.objects.filter(forum_id__in=Forum.objects.readable_forums(request.acl))
-        queryset = queryset.filter(deleted=False).filter(moderated=False)
-        queryset = queryset.filter(user_id__in=follows)
-        queryset = queryset.prefetch_related('thread', 'forum', 'user').order_by('-id')
-        queryset = queryset[:18]
-    return render_to_response('newsfeed.html',
-                              {
-                              'follows': follows,
-                              'posts': queryset,
-                              },
+from django.template import RequestContext
+from misago.decorators import block_guest
+from misago.models import Forum, Post
+from misago.shortcuts import render_to_response
+
+@block_guest
+def newsfeed(request):
+    follows = []
+    for user in request.user.follows.iterator():
+        follows.append(user.pk)
+    queryset = []
+    if follows:
+        queryset = Post.objects.filter(forum_id__in=Forum.objects.readable_forums(request.acl))
+        queryset = queryset.filter(deleted=False).filter(moderated=False)
+        queryset = queryset.filter(user_id__in=follows)
+        queryset = queryset.prefetch_related('thread', 'forum', 'user').order_by('-id')
+        queryset = queryset[:18]
+    return render_to_response('newsfeed.html',
+                              {
+                              'follows': follows,
+                              'posts': queryset,
+                              },
                               context_instance=RequestContext(request))

+ 29 - 29
misago/apps/newthreads.py

@@ -1,30 +1,30 @@
-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.utils import timezone
-from misago.conf import settings
-from misago.models import Forum, Thread
-from misago.shortcuts import render_to_response
-from misago.utils.pagination import make_pagination
-
-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)))
-    items_total = queryset.count();
-    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']];
-    if settings.avatars_on_threads_list:
-        queryset = queryset.prefetch_related('start_poster', 'last_poster')
-
-    return render_to_response('new_threads.html',
-                              {
-                              'items_total': items_total,
-                              'threads': Thread.objects.with_reads(queryset, request.user),
-                              'pagination': pagination,
-                              },
+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.utils import timezone
+from misago.conf import settings
+from misago.models import Forum, Thread
+from misago.shortcuts import render_to_response
+from misago.utils.pagination import make_pagination
+
+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)))
+    items_total = queryset.count();
+    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']];
+    if settings.avatars_on_threads_list:
+        queryset = queryset.prefetch_related('start_poster', 'last_poster')
+
+    return render_to_response('new_threads.html',
+                              {
+                              'items_total': items_total,
+                              'threads': Thread.objects.with_reads(queryset, request.user),
+                              'pagination': pagination,
+                              },
                               context_instance=RequestContext(request));

+ 29 - 29
misago/apps/popularthreads.py

@@ -1,30 +1,30 @@
-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.utils import timezone
-from misago.conf import settings
-from misago.models import Forum, Thread
-from misago.shortcuts import render_to_response
-from misago.utils.pagination import make_pagination
-
-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)
-    items_total = queryset.count();
-    try:
-        pagination = make_pagination(page, items_total, 30)
-    except Http404:
-        return redirect(reverse('popular_threads'))
-
-    queryset = queryset.order_by('-score', '-last').prefetch_related('forum')[pagination['start']:pagination['stop']];
-    if settings.avatars_on_threads_list:
-        queryset = queryset.prefetch_related('start_poster', 'last_poster')
-
-    return render_to_response('popular_threads.html',
-                              {
-                              'items_total': items_total,
-                              'threads': Thread.objects.with_reads(queryset, request.user),
-                              'pagination': pagination,
-                              },
+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.utils import timezone
+from misago.conf import settings
+from misago.models import Forum, Thread
+from misago.shortcuts import render_to_response
+from misago.utils.pagination import make_pagination
+
+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)
+    items_total = queryset.count();
+    try:
+        pagination = make_pagination(page, items_total, 30)
+    except Http404:
+        return redirect(reverse('popular_threads'))
+
+    queryset = queryset.order_by('-score', '-last').prefetch_related('forum')[pagination['start']:pagination['stop']];
+    if settings.avatars_on_threads_list:
+        queryset = queryset.prefetch_related('start_poster', 'last_poster')
+
+    return render_to_response('popular_threads.html',
+                              {
+                              'items_total': items_total,
+                              'threads': Thread.objects.with_reads(queryset, request.user),
+                              'pagination': pagination,
+                              },
                               context_instance=RequestContext(request));

+ 39 - 39
misago/apps/privatethreads/list.py

@@ -1,39 +1,39 @@
-from itertools import chain
-from django.http import Http404
-from django.utils.translation import ugettext as _
-from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
-from misago.conf import settings
-from misago.models import Forum, Thread
-from misago.readstrackers import ThreadsTracker
-from misago.utils.pagination import make_pagination
-from misago.apps.privatethreads.mixins import TypeMixin
-
-class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
-    def fetch_forum(self):
-        self.forum = Forum.objects.get(special='private_threads')
-
-    def threads_queryset(self):
-        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):
-        qs_threads = self.threads_queryset()
-
-        # Add in first and last poster
-        if settings.avatars_on_threads_list:
-            qs_threads = qs_threads.prefetch_related('start_poster', 'last_poster')
-
-        self.count = qs_threads.count()
-        try:
-            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, settings.threads_per_page)
-        except Http404:
-            return self.threads_list_redirect()
-
-        tracker_forum = ThreadsTracker(self.request, self.forum)
-        for thread in qs_threads[self.pagination['start']:self.pagination['stop']]:
-            thread.is_read = tracker_forum.is_read(thread)
-            self.threads.append(thread)
+from itertools import chain
+from django.http import Http404
+from django.utils.translation import ugettext as _
+from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
+from misago.conf import settings
+from misago.models import Forum, Thread
+from misago.readstrackers import ThreadsTracker
+from misago.utils.pagination import make_pagination
+from misago.apps.privatethreads.mixins import TypeMixin
+
+class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
+    def fetch_forum(self):
+        self.forum = Forum.objects.get(special='private_threads')
+
+    def threads_queryset(self):
+        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):
+        qs_threads = self.threads_queryset()
+
+        # Add in first and last poster
+        if settings.avatars_on_threads_list:
+            qs_threads = qs_threads.prefetch_related('start_poster', 'last_poster')
+
+        self.count = qs_threads.count()
+        try:
+            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, settings.threads_per_page)
+        except Http404:
+            return self.threads_list_redirect()
+
+        tracker_forum = ThreadsTracker(self.request, self.forum)
+        for thread in qs_threads[self.pagination['start']:self.pagination['stop']]:
+            thread.is_read = tracker_forum.is_read(thread)
+            self.threads.append(thread)

+ 45 - 45
misago/apps/privatethreads/mixins.py

@@ -1,45 +1,45 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.conf import settings
-from misago.acl.exceptions import ACLError404
-
-class TypeMixin(object):
-    type_prefix = 'private_thread'
-
-    def type_available(self):
-        return settings.enable_private_threads
-
-    def check_permissions(self):
-        try:
-            if self.thread.pk:
-                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()
-        except AttributeError:
-            pass
-
-    def invite_users(self, users):
-        for user in users:
-            if not user in self.thread.participants.all():
-                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})
-                if self.action == 'new_reply':
-                    self.thread.set_checkpoint(self.request, 'invited', user)
-
-    def force_stats_sync(self):
-        self.thread.participants.exclude(id=self.request.user.id).update(sync_pds=True)
-                
-    def whitelist_mentions(self):
-        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):
-        return redirect(reverse('private_threads'))
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago.conf import settings
+from misago.acl.exceptions import ACLError404
+
+class TypeMixin(object):
+    type_prefix = 'private_thread'
+
+    def type_available(self):
+        return settings.enable_private_threads
+
+    def check_permissions(self):
+        try:
+            if self.thread.pk:
+                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()
+        except AttributeError:
+            pass
+
+    def invite_users(self, users):
+        for user in users:
+            if not user in self.thread.participants.all():
+                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})
+                if self.action == 'new_reply':
+                    self.thread.set_checkpoint(self.request, 'invited', user)
+
+    def force_stats_sync(self):
+        self.thread.participants.exclude(id=self.request.user.id).update(sync_pds=True)
+                
+    def whitelist_mentions(self):
+        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):
+        return redirect(reverse('private_threads'))

+ 10 - 10
misago/apps/profiles/details/views.py

@@ -1,10 +1,10 @@
-from misago.shortcuts import render_to_response
-from misago.apps.profiles.decorators import profile_view
-from misago.apps.profiles.template import RequestContext
-
-@profile_view('user_details')
-def details(request, user):
-    return render_to_response('profiles/details.html',
-                              context_instance=RequestContext(request, {
-                                'profile': user,
-                                'tab': 'details',}));
+from misago.shortcuts import render_to_response
+from misago.apps.profiles.decorators import profile_view
+from misago.apps.profiles.template import RequestContext
+
+@profile_view('user_details')
+def details(request, user):
+    return render_to_response('profiles/details.html',
+                              context_instance=RequestContext(request, {
+                                'profile': user,
+                                'tab': 'details',}));

+ 23 - 23
misago/apps/profiles/followers/views.py

@@ -1,24 +1,24 @@
-from django.core.urlresolvers import reverse
-from django.http import Http404
-from django.shortcuts import redirect
-from misago.shortcuts import render_to_response
-from misago.utils.pagination import make_pagination
-from misago.apps.profiles.decorators import profile_view
-from misago.apps.profiles.template import RequestContext
-
-@profile_view('user_followers')
-def followers(request, user, page=0):
-    queryset = user.follows_set.order_by('username_slug')
-    count = queryset.count()
-    try:
-        pagination = make_pagination(page, count, 24)
-    except Http404:
-        return redirect(reverse('user_followers', kwargs={'user': user.id, 'username': user.username_slug}))
-    
-    return render_to_response('profiles/followers.html',
-                              context_instance=RequestContext(request, {
-                                  'profile': user,
-                                  'tab': 'followers',
-                                  'items_total': count,
-                                  'items': queryset[pagination['start']:pagination['stop']],
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
+from misago.shortcuts import render_to_response
+from misago.utils.pagination import make_pagination
+from misago.apps.profiles.decorators import profile_view
+from misago.apps.profiles.template import RequestContext
+
+@profile_view('user_followers')
+def followers(request, user, page=0):
+    queryset = user.follows_set.order_by('username_slug')
+    count = queryset.count()
+    try:
+        pagination = make_pagination(page, count, 24)
+    except Http404:
+        return redirect(reverse('user_followers', kwargs={'user': user.id, 'username': user.username_slug}))
+    
+    return render_to_response('profiles/followers.html',
+                              context_instance=RequestContext(request, {
+                                  'profile': user,
+                                  'tab': 'followers',
+                                  'items_total': count,
+                                  'items': queryset[pagination['start']:pagination['stop']],
                                   'pagination': pagination,}));

+ 24 - 24
misago/apps/profiles/follows/views.py

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

+ 40 - 40
misago/apps/profiles/posts/views.py

@@ -1,40 +1,40 @@
-from datetime import timedelta
-from django.core.cache import cache
-from django.core.urlresolvers import reverse
-from django.http import Http404
-from django.shortcuts import redirect
-from django.utils import timezone
-from misago.apps.profiles.decorators import profile_view
-from misago.apps.profiles.template import RequestContext
-from misago.models import Forum
-from misago.shortcuts import render_to_response
-from misago.utils.pagination import make_pagination
-
-@profile_view('user_posts')
-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)
-    count = queryset.count()
-    try:
-        pagination = make_pagination(page, count, 12)
-    except Http404:
-        return redirect(reverse('user_posts', kwargs={'user': user.id, 'username': user.username_slug}))
-    
-    cache_key = 'user_profile_posts_graph_%s' % user.pk
-    graph = cache.get(cache_key, 'nada')
-    if graph == 'nada':
-        if user.posts:
-            graph = user.timeline(queryset.filter(date__gte=timezone.now()-timedelta(days=100)))
-        else:
-            graph = [0 for x in range(100)]
-        cache.set(cache_key, graph, 14400)
-
-    return render_to_response('profiles/posts.html',
-                              context_instance=RequestContext(request, {
-                                  'profile': user,
-                                  'tab': 'posts',
-                                  'graph_max': max(graph),
-                                  'graph': (str(i) for i in graph),
-                                  'items_total': count,
-                                  'items': queryset.select_related('thread', 'forum').order_by('-id')[pagination['start']:pagination['stop']],
-                                  'pagination': pagination,
-                                  }));
+from datetime import timedelta
+from django.core.cache import cache
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
+from django.utils import timezone
+from misago.apps.profiles.decorators import profile_view
+from misago.apps.profiles.template import RequestContext
+from misago.models import Forum
+from misago.shortcuts import render_to_response
+from misago.utils.pagination import make_pagination
+
+@profile_view('user_posts')
+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)
+    count = queryset.count()
+    try:
+        pagination = make_pagination(page, count, 12)
+    except Http404:
+        return redirect(reverse('user_posts', kwargs={'user': user.id, 'username': user.username_slug}))
+    
+    cache_key = 'user_profile_posts_graph_%s' % user.pk
+    graph = cache.get(cache_key, 'nada')
+    if graph == 'nada':
+        if user.posts:
+            graph = user.timeline(queryset.filter(date__gte=timezone.now()-timedelta(days=100)))
+        else:
+            graph = [0 for x in range(100)]
+        cache.set(cache_key, graph, 14400)
+
+    return render_to_response('profiles/posts.html',
+                              context_instance=RequestContext(request, {
+                                  'profile': user,
+                                  'tab': 'posts',
+                                  'graph_max': max(graph),
+                                  'graph': (str(i) for i in graph),
+                                  'items_total': count,
+                                  'items': queryset.select_related('thread', 'forum').order_by('-id')[pagination['start']:pagination['stop']],
+                                  'pagination': pagination,
+                                  }));

+ 40 - 40
misago/apps/profiles/threads/views.py

@@ -1,40 +1,40 @@
-from datetime import timedelta
-from django.core.cache import cache
-from django.core.urlresolvers import reverse
-from django.http import Http404
-from django.shortcuts import redirect
-from django.utils import timezone
-from misago.apps.profiles.decorators import profile_view
-from misago.apps.profiles.template import RequestContext
-from misago.models import Forum
-from misago.shortcuts import render_to_response
-from misago.utils.pagination import make_pagination
-
-@profile_view('user_threads')
-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)
-    count = queryset.count()
-    try:
-        pagination = make_pagination(page, count, 12)
-    except Http404:
-        return redirect(reverse('user_threads', kwargs={'user': user.id, 'username': user.username_slug}))
-    
-    cache_key = 'user_profile_threads_graph_%s' % user.pk
-    graph = cache.get(cache_key, 'nada')
-    if graph == 'nada':
-        if user.posts:
-            graph = user.timeline(queryset.filter(start__gte=timezone.now()-timedelta(days=100)))
-        else:
-            graph = [0 for x in range(100)]
-        cache.set(cache_key, graph, 14400)
-
-    return render_to_response('profiles/threads.html',
-                              context_instance=RequestContext(request, {
-                                  'profile': user,
-                                  'tab': 'threads',
-                                  'graph_max': max(graph),
-                                  'graph': (str(i) for i in graph),
-                                  'items_total': count,
-                                  'items': queryset.select_related('start_post', 'forum').order_by('-id')[pagination['start']:pagination['stop']],
-                                  'pagination': pagination,
-                                  }));
+from datetime import timedelta
+from django.core.cache import cache
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
+from django.utils import timezone
+from misago.apps.profiles.decorators import profile_view
+from misago.apps.profiles.template import RequestContext
+from misago.models import Forum
+from misago.shortcuts import render_to_response
+from misago.utils.pagination import make_pagination
+
+@profile_view('user_threads')
+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)
+    count = queryset.count()
+    try:
+        pagination = make_pagination(page, count, 12)
+    except Http404:
+        return redirect(reverse('user_threads', kwargs={'user': user.id, 'username': user.username_slug}))
+    
+    cache_key = 'user_profile_threads_graph_%s' % user.pk
+    graph = cache.get(cache_key, 'nada')
+    if graph == 'nada':
+        if user.posts:
+            graph = user.timeline(queryset.filter(start__gte=timezone.now()-timedelta(days=100)))
+        else:
+            graph = [0 for x in range(100)]
+        cache.set(cache_key, graph, 14400)
+
+    return render_to_response('profiles/threads.html',
+                              context_instance=RequestContext(request, {
+                                  'profile': user,
+                                  'tab': 'threads',
+                                  'graph_max': max(graph),
+                                  'graph': (str(i) for i in graph),
+                                  'items_total': count,
+                                  'items': queryset.select_related('start_post', 'forum').order_by('-id')[pagination['start']:pagination['stop']],
+                                  'pagination': pagination,
+                                  }));

+ 72 - 72
misago/apps/threads/list.py

@@ -1,73 +1,73 @@
-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 misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
-from misago.conf import settings
-from misago.models import Forum, Thread
-from misago.readstrackers import ThreadsTracker
-from misago.utils.pagination import make_pagination
-from misago.apps.threads.mixins import TypeMixin
-
-class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
-    def fetch_forum(self):
-        self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
-
-    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')
-        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)
-        if self.request.user.is_authenticated():
-            ignored_users = self.request.user.ignored_users()
-            if ignored_users:
-                threads = threads.extra(where=["`threads_thread`.`start_poster_id` IS NULL OR `threads_thread`.`start_poster_id` NOT IN (%s)" % ','.join([str(i) for i in ignored_users])])
-
-        # Add in first and last poster
-        if 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, settings.threads_per_page)
-        except Http404:
-            return self.threads_list_redirect()
-
-        tracker_forum = ThreadsTracker(self.request, self.forum)
-        for thread in list(chain(qs_announcements, qs_threads[self.pagination['start']:self.pagination['stop']])):
-            thread.is_read = tracker_forum.is_read(thread)
-            self.threads.append(thread)
-
-    def threads_actions(self):
-        acl = self.request.acl.threads.get_role(self.forum)
-        actions = []
-        try:
-            if acl['can_approve']:
-                actions.append(('accept', _('Accept threads')))
-            if acl['can_pin_threads'] == 2:
-                actions.append(('annouce', _('Change to announcements')))
-            if acl['can_pin_threads'] > 0:
-                actions.append(('sticky', _('Change to sticky threads')))
-            if acl['can_pin_threads'] > 0:
-                actions.append(('normal', _('Change to standard thread')))
-            if acl['can_move_threads_posts']:
-                actions.append(('move', _('Move threads')))
-                actions.append(('merge', _('Merge threads')))
-            if acl['can_close_threads']:
-                actions.append(('open', _('Open threads')))
-                actions.append(('close', _('Close threads')))
-            if acl['can_delete_threads']:
-                actions.append(('undelete', _('Restore threads')))
-                actions.append(('soft', _('Hide threads')))
-            if acl['can_delete_threads'] == 2:
-                actions.append(('hard', _('Delete threads')))
-        except KeyError:
-            pass
+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 misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
+from misago.conf import settings
+from misago.models import Forum, Thread
+from misago.readstrackers import ThreadsTracker
+from misago.utils.pagination import make_pagination
+from misago.apps.threads.mixins import TypeMixin
+
+class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
+    def fetch_forum(self):
+        self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
+
+    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')
+        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)
+        if self.request.user.is_authenticated():
+            ignored_users = self.request.user.ignored_users()
+            if ignored_users:
+                threads = threads.extra(where=["`threads_thread`.`start_poster_id` IS NULL OR `threads_thread`.`start_poster_id` NOT IN (%s)" % ','.join([str(i) for i in ignored_users])])
+
+        # Add in first and last poster
+        if 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, settings.threads_per_page)
+        except Http404:
+            return self.threads_list_redirect()
+
+        tracker_forum = ThreadsTracker(self.request, self.forum)
+        for thread in list(chain(qs_announcements, qs_threads[self.pagination['start']:self.pagination['stop']])):
+            thread.is_read = tracker_forum.is_read(thread)
+            self.threads.append(thread)
+
+    def threads_actions(self):
+        acl = self.request.acl.threads.get_role(self.forum)
+        actions = []
+        try:
+            if acl['can_approve']:
+                actions.append(('accept', _('Accept threads')))
+            if acl['can_pin_threads'] == 2:
+                actions.append(('annouce', _('Change to announcements')))
+            if acl['can_pin_threads'] > 0:
+                actions.append(('sticky', _('Change to sticky threads')))
+            if acl['can_pin_threads'] > 0:
+                actions.append(('normal', _('Change to standard thread')))
+            if acl['can_move_threads_posts']:
+                actions.append(('move', _('Move threads')))
+                actions.append(('merge', _('Merge threads')))
+            if acl['can_close_threads']:
+                actions.append(('open', _('Open threads')))
+                actions.append(('close', _('Close threads')))
+            if acl['can_delete_threads']:
+                actions.append(('undelete', _('Restore threads')))
+                actions.append(('soft', _('Hide threads')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Delete threads')))
+        except KeyError:
+            pass
         return actions

+ 65 - 65
misago/apps/threadtype/base.py

@@ -1,66 +1,66 @@
-from django.core.urlresolvers import reverse
-from django.http import Http404
-from django.shortcuts import redirect
-from misago.conf import settings
-from misago.models import Forum, Thread, Post
-from misago.utils.pagination import page_number
-
-class ViewBase(object):
-    def __new__(cls, request, **kwargs):
-        obj = super(ViewBase, cls).__new__(cls)
-        return obj(request, **kwargs)
-        
-    def _type_available(self):
-        try:
-            if not self.type_available():
-                raise Http404()
-        except AttributeError:
-            pass
-
-    def set_forum_context(self):
-        pass
-
-    def set_thread_context(self):
-        self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
-        self.forum = self.thread.forum
-        self.proxy = Forum.objects.parents_aware_forum(self.forum)
-        self.request.acl.forums.allow_forum_view(self.forum)
-        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
-
-    def set_post_contex(self):
-        pass
-
-    def check_forum_type(self):
-        type_prefix = self.type_prefix
-        if type_prefix == 'thread':
-            type_prefix = 'root'
-        else:
-            type_prefix = '%ss' % type_prefix
-        try:
-            if self.parents[0].parent_id != Forum.objects.special_pk(type_prefix):
-                raise Http404()
-        except (AttributeError, IndexError):
-            if self.forum.special != type_prefix:
-                raise Http404()
-
-    def _check_permissions(self):
-        try:
-            self.check_permissions()
-        except AttributeError:
-            pass
-
-    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(), 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):
-        return context
-
-    def retreat_redirect(self):
-        if self.request.POST.get('retreat'):
-            return redirect(self.request.POST.get('retreat'))
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
+from misago.conf import settings
+from misago.models import Forum, Thread, Post
+from misago.utils.pagination import page_number
+
+class ViewBase(object):
+    def __new__(cls, request, **kwargs):
+        obj = super(ViewBase, cls).__new__(cls)
+        return obj(request, **kwargs)
+        
+    def _type_available(self):
+        try:
+            if not self.type_available():
+                raise Http404()
+        except AttributeError:
+            pass
+
+    def set_forum_context(self):
+        pass
+
+    def set_thread_context(self):
+        self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
+        self.forum = self.thread.forum
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+
+    def set_post_contex(self):
+        pass
+
+    def check_forum_type(self):
+        type_prefix = self.type_prefix
+        if type_prefix == 'thread':
+            type_prefix = 'root'
+        else:
+            type_prefix = '%ss' % type_prefix
+        try:
+            if self.parents[0].parent_id != Forum.objects.special_pk(type_prefix):
+                raise Http404()
+        except (AttributeError, IndexError):
+            if self.forum.special != type_prefix:
+                raise Http404()
+
+    def _check_permissions(self):
+        try:
+            self.check_permissions()
+        except AttributeError:
+            pass
+
+    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(), 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):
+        return context
+
+    def retreat_redirect(self):
+        if self.request.POST.get('retreat'):
+            return redirect(self.request.POST.get('retreat'))
         return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))

+ 73 - 73
misago/apps/threadtype/details.py

@@ -1,74 +1,74 @@
-from django.template import RequestContext
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.apps.errors import error403, error404
-from misago.models import Forum, Thread, Post
-from misago.shortcuts import render_to_response
-from misago.apps.threadtype.base import ViewBase
-
-class ExtraBaseView(ViewBase):
-    def fetch_target(self):
-        self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
-        self.forum = self.thread.forum
-        self.proxy = Forum.objects.parents_aware_forum(self.forum)
-        self.request.acl.forums.allow_forum_view(self.forum)
-        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
-        if self.forum.level:
-            self.parents = Forum.objects.forum_parents(self.forum.pk, True)
-        self.check_forum_type()
-        self.post = Post.objects.select_related('user').get(pk=self.kwargs.get('post'), thread=self.thread.pk)
-        self.post.thread = self.thread
-        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
-
-    def __call__(self, request, **kwargs):
-        self.request = request
-        self.kwargs = kwargs
-        self.forum = None
-        self.thread = None
-        self.post = None
-        self.parents = []
-        try:
-            self._type_available()
-            self.fetch_target()
-            self.check_acl()
-            self._check_permissions()
-        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
-            return error404(self.request)
-        except ACLError403 as e:
-            return error403(request, e)
-        except ACLError404 as e:
-            return error404(request, e)
-        return self.response()
-
-
-class DetailsBaseView(ExtraBaseView):
-    def check_acl(self):
-        self.request.acl.users.allow_details_view()
-
-    def response(self):
-        return render_to_response('%ss/details.html' % self.type_prefix,
-                                  self.template_vars({
-                                      'type_prefix': self.type_prefix,
-                                      'forum': self.forum,
-                                      'parents': self.parents,
-                                      'thread': self.thread,
-                                      'post': self.post,
-                                      }),
-                                  context_instance=RequestContext(self.request))
-
-
-class KarmaVotesBaseView(ExtraBaseView):
-    def check_acl(self):
-        self.request.acl.threads.allow_post_votes_view(self.forum)
-
-    def response(self):
-        return render_to_response('%ss/karmas.html' % self.type_prefix,
-                                  self.template_vars({
-                                      'type_prefix': self.type_prefix,
-                                      'forum': self.forum,
-                                      'parents': self.parents,
-                                      'thread': self.thread,
-                                      'post': self.post,
-                                      'upvotes': self.post.karma_set.filter(score=1),
-                                      'downvotes': self.post.karma_set.filter(score=-1),
-                                      }),
+from django.template import RequestContext
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.errors import error403, error404
+from misago.models import Forum, Thread, Post
+from misago.shortcuts import render_to_response
+from misago.apps.threadtype.base import ViewBase
+
+class ExtraBaseView(ViewBase):
+    def fetch_target(self):
+        self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
+        self.forum = self.thread.forum
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+        if self.forum.level:
+            self.parents = Forum.objects.forum_parents(self.forum.pk, True)
+        self.check_forum_type()
+        self.post = Post.objects.select_related('user').get(pk=self.kwargs.get('post'), thread=self.thread.pk)
+        self.post.thread = self.thread
+        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.kwargs = kwargs
+        self.forum = None
+        self.thread = None
+        self.post = None
+        self.parents = []
+        try:
+            self._type_available()
+            self.fetch_target()
+            self.check_acl()
+            self._check_permissions()
+        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
+            return error404(self.request)
+        except ACLError403 as e:
+            return error403(request, e)
+        except ACLError404 as e:
+            return error404(request, e)
+        return self.response()
+
+
+class DetailsBaseView(ExtraBaseView):
+    def check_acl(self):
+        self.request.acl.users.allow_details_view()
+
+    def response(self):
+        return render_to_response('%ss/details.html' % self.type_prefix,
+                                  self.template_vars({
+                                      'type_prefix': self.type_prefix,
+                                      'forum': self.forum,
+                                      'parents': self.parents,
+                                      'thread': self.thread,
+                                      'post': self.post,
+                                      }),
+                                  context_instance=RequestContext(self.request))
+
+
+class KarmaVotesBaseView(ExtraBaseView):
+    def check_acl(self):
+        self.request.acl.threads.allow_post_votes_view(self.forum)
+
+    def response(self):
+        return render_to_response('%ss/karmas.html' % self.type_prefix,
+                                  self.template_vars({
+                                      'type_prefix': self.type_prefix,
+                                      'forum': self.forum,
+                                      'parents': self.parents,
+                                      'thread': self.thread,
+                                      'post': self.post,
+                                      'upvotes': self.post.karma_set.filter(score=1),
+                                      'downvotes': self.post.karma_set.filter(score=-1),
+                                      }),
                                   context_instance=RequestContext(self.request))

+ 84 - 84
misago/apps/threadtype/posting/newthread.py

@@ -1,85 +1,85 @@
-from datetime import timedelta
-from django.utils import timezone
-from misago.apps.threadtype.posting.base import PostingBaseView
-from misago.apps.threadtype.posting.forms import NewThreadForm
-from misago.conf import settings
-from misago.markdown import post_markdown
-from misago.models import Forum, Thread, Post
-from misago.monitor import monitor, UpdatingMonitor
-from misago.utils.strings import slugify
-
-class NewThreadBaseView(PostingBaseView):
-    action = 'new_thread'
-    form_type = NewThreadForm
-
-    def set_context(self):
-        self.set_forum_context()
-        self.request.acl.forums.allow_forum_view(self.forum)
-        self.proxy = Forum.objects.parents_aware_forum(self.forum)
-        self.request.acl.threads.allow_new_threads(self.proxy)
-
-    def post_form(self, form):
-        now = timezone.now()
-        moderation = (not self.request.acl.threads.acl[self.forum.pk]['can_approve']
-                      and self.request.acl.threads.acl[self.forum.pk]['can_start_threads'] == 1)
-
-        # Create empty thread
-        self.thread = Thread.objects.create(
-                                            forum=self.forum,
-                                            name=form.cleaned_data['thread_name'],
-                                            slug=slugify(form.cleaned_data['thread_name']),
-                                            start=now,
-                                            last=now,
-                                            moderated=moderation,
-                                            score=settings.thread_ranking_initial_score,
-                                            )
-
-        # Create our post
-        self.md, post_preparsed = post_markdown(form.cleaned_data['post'])
-        self.post = Post.objects.create(
-                                        forum=self.forum,
-                                        thread=self.thread,
-                                        user=self.request.user,
-                                        user_name=self.request.user.username,
-                                        ip=self.request.session.get_ip(self.request),
-                                        agent=self.request.META.get('HTTP_USER_AGENT'),
-                                        post=form.cleaned_data['post'],
-                                        post_preparsed=post_preparsed,
-                                        date=now,
-                                        moderated=moderation,
-                                        )
-
-        # Update thread stats to contain this post
-        self.thread.new_start_post(self.post)
-        self.thread.new_last_post(self.post)
-
-        # Set thread status
-        if 'close_thread' in form.cleaned_data:
-            self.thread.closed = form.cleaned_data['close_thread']
-        if 'thread_weight' in form.cleaned_data:
-            self.thread.weight = form.cleaned_data['thread_weight']
-
-        # Finally save complete thread
-        self.thread.save(force_update=True)
-
-        # Update forum monitor
-        if not moderation:
-            with UpdatingMonitor() as cm:
-                monitor.increase('threads')
-                monitor.increase('posts')
-            self.forum.threads += 1
-            self.forum.posts += 1
-            self.forum.new_last_thread(self.thread)
-            self.forum.save(force_update=True)
-
-        # Reward user for posting new thread?
-        if not moderation and (not self.request.user.last_post
-                or self.request.user.last_post < timezone.now() - timedelta(seconds=settings.score_reward_new_post_cooldown)):
-            self.request.user.score += settings.score_reward_new_thread
-
-        # Update user
-        if not moderation:
-            self.request.user.threads += 1
-            self.request.user.posts += 1
-        self.request.user.last_post = now
+from datetime import timedelta
+from django.utils import timezone
+from misago.apps.threadtype.posting.base import PostingBaseView
+from misago.apps.threadtype.posting.forms import NewThreadForm
+from misago.conf import settings
+from misago.markdown import post_markdown
+from misago.models import Forum, Thread, Post
+from misago.monitor import monitor, UpdatingMonitor
+from misago.utils.strings import slugify
+
+class NewThreadBaseView(PostingBaseView):
+    action = 'new_thread'
+    form_type = NewThreadForm
+
+    def set_context(self):
+        self.set_forum_context()
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
+        self.request.acl.threads.allow_new_threads(self.proxy)
+
+    def post_form(self, form):
+        now = timezone.now()
+        moderation = (not self.request.acl.threads.acl[self.forum.pk]['can_approve']
+                      and self.request.acl.threads.acl[self.forum.pk]['can_start_threads'] == 1)
+
+        # Create empty thread
+        self.thread = Thread.objects.create(
+                                            forum=self.forum,
+                                            name=form.cleaned_data['thread_name'],
+                                            slug=slugify(form.cleaned_data['thread_name']),
+                                            start=now,
+                                            last=now,
+                                            moderated=moderation,
+                                            score=settings.thread_ranking_initial_score,
+                                            )
+
+        # Create our post
+        self.md, post_preparsed = post_markdown(form.cleaned_data['post'])
+        self.post = Post.objects.create(
+                                        forum=self.forum,
+                                        thread=self.thread,
+                                        user=self.request.user,
+                                        user_name=self.request.user.username,
+                                        ip=self.request.session.get_ip(self.request),
+                                        agent=self.request.META.get('HTTP_USER_AGENT'),
+                                        post=form.cleaned_data['post'],
+                                        post_preparsed=post_preparsed,
+                                        date=now,
+                                        moderated=moderation,
+                                        )
+
+        # Update thread stats to contain this post
+        self.thread.new_start_post(self.post)
+        self.thread.new_last_post(self.post)
+
+        # Set thread status
+        if 'close_thread' in form.cleaned_data:
+            self.thread.closed = form.cleaned_data['close_thread']
+        if 'thread_weight' in form.cleaned_data:
+            self.thread.weight = form.cleaned_data['thread_weight']
+
+        # Finally save complete thread
+        self.thread.save(force_update=True)
+
+        # Update forum monitor
+        if not moderation:
+            with UpdatingMonitor() as cm:
+                monitor.increase('threads')
+                monitor.increase('posts')
+            self.forum.threads += 1
+            self.forum.posts += 1
+            self.forum.new_last_thread(self.thread)
+            self.forum.save(force_update=True)
+
+        # Reward user for posting new thread?
+        if not moderation and (not self.request.user.last_post
+                or self.request.user.last_post < timezone.now() - timedelta(seconds=settings.score_reward_new_post_cooldown)):
+            self.request.user.score += settings.score_reward_new_thread
+
+        # Update user
+        if not moderation:
+            self.request.user.threads += 1
+            self.request.user.posts += 1
+        self.request.user.last_post = now
         self.request.user.save(force_update=True)

+ 10 - 10
misago/apps/tos.py

@@ -1,10 +1,10 @@
-from django.template import RequestContext
-from misago.apps.errors import error404
-from misago.conf import settings
-from misago.shortcuts import render_to_response
-
-def tos(request):
-    if settings.tos_url or not settings.tos_content:
-        return error404(request)
-    return render_to_response('forum_tos.html',
-                              context_instance=RequestContext(request));
+from django.template import RequestContext
+from misago.apps.errors import error404
+from misago.conf import settings
+from misago.shortcuts import render_to_response
+
+def tos(request):
+    if settings.tos_url or not settings.tos_content:
+        return error404(request)
+    return render_to_response('forum_tos.html',
+                              context_instance=RequestContext(request));

+ 124 - 124
misago/auth.py

@@ -1,124 +1,124 @@
-from datetime import timedelta
-from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
-from misago.conf import settings
-from misago.models import Ban, SignInAttempt, Token, User
-
-"""
-Exception constants
-"""
-CREDENTIALS = 0
-ACTIVATION_USER = 1
-ACTIVATION_ADMIN = 2
-BANNED = 3
-NOT_ADMIN = 4
-
-
-class AuthException(Exception):
-    """
-    Auth Exception is thrown when auth_* method finds problem with allowing user to sign-in
-    """
-    def __init__(self, type=None, error=None, password=False, activation=False, ban=False):
-        self.type = type
-        self.error = error
-        self.password = password
-        self.activation = activation
-        self.ban = ban
-
-    def __str__(self):
-        return self.error
-
-
-def get_user(email, password, admin=False):
-    """
-    Fetch user from DB using email/pass pair, scream if either of data is incorrect
-    """
-    try:
-        user = User.objects.get_by_email(email)
-        if not user.check_password(password):
-            raise AuthException(CREDENTIALS, _("Your e-mail address or password is incorrect. Please try again."), password=True)
-        if not admin:
-            if user.activation == User.ACTIVATION_ADMIN:
-                # Only admin can activate your account.
-                raise AuthException(ACTIVATION_ADMIN, _("Board Administrator has not yet accepted your account."))
-            if user.activation != User.ACTIVATION_NONE:
-                # You have to activate your account - new member
-                raise AuthException(ACTIVATION_USER, _("You have to activate your account before you will be able to sign-in."), activation=True)
-
-    except User.DoesNotExist:
-        raise AuthException(CREDENTIALS, _("Your e-mail address or password is incorrect. Please try again."), password=True)
-    return user;
-
-
-def auth_forum(request, email, password):
-    """
-    Forum auth - check bans and if we are in maintenance - maintenance access
-    """
-    user = get_user(email, password)
-    user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
-    if user_ban:
-        if user_ban.reason_user:
-            raise AuthException(BANNED, _("Your account has been banned for following reason:"), ban=user_ban)
-        raise AuthException(BANNED, _("Your account has been banned."), ban=user_ban)
-    return user;
-
-
-def auth_remember(request, ip):
-    """
-    Remember-me auth - check if token is valid
-    Dont worry about AuthException being empty, it doesnt have to have anything
-    """
-    if request.firewall.admin:
-        raise AuthException()
-    if SignInAttempt.objects.is_jammed(ip):
-        raise AuthException()
-    cookie_token = settings.COOKIES_PREFIX + 'TOKEN'
-    try:
-        cookie_token = request.COOKIES[cookie_token]
-        if len(cookie_token) != 42:
-            raise AuthException()
-            
-        try:
-            token_rk = Token.objects.select_related().get(pk=cookie_token)
-        except Token.DoesNotExist:
-            request.cookiejar.delete('TOKEN')
-            raise AuthException()
-
-        # See if token is not expired
-        token_expires = timezone.now() - timedelta(days=settings.remember_me_lifetime)
-        if settings.remember_me_extensible and token_rk.accessed < token_expires:
-            # Token expired because it's last use is smaller than expiration date
-            raise AuthException()
-
-        if not settings.remember_me_extensible and token_rk.created < token_expires:
-            # Token expired because it was created before expiration date
-            raise AuthException()
-
-        # Update token date
-        token_rk.accessed = timezone.now()
-        token_rk.save(force_update=True)
-        request.cookiejar.set('TOKEN', token_rk.id, True)
-    except (AttributeError, KeyError):
-        raise AuthException()
-    return token_rk
-
-
-def auth_admin(request, email, password):
-    """
-    Admin auth - check ACP permissions
-    """
-    user = get_user(email, password, True)
-    if not user.is_god() and not user.acl(request).special.is_admin():
-        raise AuthException(NOT_ADMIN, _("Your account does not have admin privileges."))
-    return user;
-
-
-def sign_user_in(request, user):
-    user.set_last_visit(
-                        request.session.get_ip(request),
-                        request.META.get('HTTP_USER_AGENT', ''),
-                        )
-    user.save(force_update=True)
-    request.session.set_user(user)
-    if not request.firewall.admin:
-        request.onlines.sign_in()
+from datetime import timedelta
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+from misago.conf import settings
+from misago.models import Ban, SignInAttempt, Token, User
+
+"""
+Exception constants
+"""
+CREDENTIALS = 0
+ACTIVATION_USER = 1
+ACTIVATION_ADMIN = 2
+BANNED = 3
+NOT_ADMIN = 4
+
+
+class AuthException(Exception):
+    """
+    Auth Exception is thrown when auth_* method finds problem with allowing user to sign-in
+    """
+    def __init__(self, type=None, error=None, password=False, activation=False, ban=False):
+        self.type = type
+        self.error = error
+        self.password = password
+        self.activation = activation
+        self.ban = ban
+
+    def __str__(self):
+        return self.error
+
+
+def get_user(email, password, admin=False):
+    """
+    Fetch user from DB using email/pass pair, scream if either of data is incorrect
+    """
+    try:
+        user = User.objects.get_by_email(email)
+        if not user.check_password(password):
+            raise AuthException(CREDENTIALS, _("Your e-mail address or password is incorrect. Please try again."), password=True)
+        if not admin:
+            if user.activation == User.ACTIVATION_ADMIN:
+                # Only admin can activate your account.
+                raise AuthException(ACTIVATION_ADMIN, _("Board Administrator has not yet accepted your account."))
+            if user.activation != User.ACTIVATION_NONE:
+                # You have to activate your account - new member
+                raise AuthException(ACTIVATION_USER, _("You have to activate your account before you will be able to sign-in."), activation=True)
+
+    except User.DoesNotExist:
+        raise AuthException(CREDENTIALS, _("Your e-mail address or password is incorrect. Please try again."), password=True)
+    return user;
+
+
+def auth_forum(request, email, password):
+    """
+    Forum auth - check bans and if we are in maintenance - maintenance access
+    """
+    user = get_user(email, password)
+    user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
+    if user_ban:
+        if user_ban.reason_user:
+            raise AuthException(BANNED, _("Your account has been banned for following reason:"), ban=user_ban)
+        raise AuthException(BANNED, _("Your account has been banned."), ban=user_ban)
+    return user;
+
+
+def auth_remember(request, ip):
+    """
+    Remember-me auth - check if token is valid
+    Dont worry about AuthException being empty, it doesnt have to have anything
+    """
+    if request.firewall.admin:
+        raise AuthException()
+    if SignInAttempt.objects.is_jammed(ip):
+        raise AuthException()
+    cookie_token = settings.COOKIES_PREFIX + 'TOKEN'
+    try:
+        cookie_token = request.COOKIES[cookie_token]
+        if len(cookie_token) != 42:
+            raise AuthException()
+            
+        try:
+            token_rk = Token.objects.select_related().get(pk=cookie_token)
+        except Token.DoesNotExist:
+            request.cookiejar.delete('TOKEN')
+            raise AuthException()
+
+        # See if token is not expired
+        token_expires = timezone.now() - timedelta(days=settings.remember_me_lifetime)
+        if settings.remember_me_extensible and token_rk.accessed < token_expires:
+            # Token expired because it's last use is smaller than expiration date
+            raise AuthException()
+
+        if not settings.remember_me_extensible and token_rk.created < token_expires:
+            # Token expired because it was created before expiration date
+            raise AuthException()
+
+        # Update token date
+        token_rk.accessed = timezone.now()
+        token_rk.save(force_update=True)
+        request.cookiejar.set('TOKEN', token_rk.id, True)
+    except (AttributeError, KeyError):
+        raise AuthException()
+    return token_rk
+
+
+def auth_admin(request, email, password):
+    """
+    Admin auth - check ACP permissions
+    """
+    user = get_user(email, password, True)
+    if not user.is_god() and not user.acl(request).special.is_admin():
+        raise AuthException(NOT_ADMIN, _("Your account does not have admin privileges."))
+    return user;
+
+
+def sign_user_in(request, user):
+    user.set_last_visit(
+                        request.session.get_ip(request),
+                        request.META.get('HTTP_USER_AGENT', ''),
+                        )
+    user.save(force_update=True)
+    request.session.set_user(user)
+    if not request.firewall.admin:
+        request.onlines.sign_in()

+ 51 - 51
misago/context_processors.py

@@ -1,52 +1,52 @@
-from misago import __version__
-from misago.admin import site
-from misago.conf import settings, SafeSettings
-from misago.models import Forum
-from misago.monitor import monitor
-
-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:
-        context.update({
-            'acl': request.acl,
-            'board_address': settings.BOARD_ADDRESS,
-            'messages' : request.messages.messages,
-            'monitor': monitor,
-            'request_path': request.get_full_path(),
-            'settings': SafeSettings(),
-            'stopwatch': request.stopwatch.time(),
-            'user': request.user,
-            'version': __version__,
-            'disable_search': False,
-        })
-        context.update({
-            'csrf_id': request.csrf.csrf_id,
-            'csrf_token': request.csrf.csrf_token,
-            'is_banned': request.ban.is_banned(),
-            'is_jammed': request.jam.is_jammed(),
-            'private_threads': Forum.objects.special_model('private_threads'),
-            'reports': Forum.objects.special_model('reports'),
-        })
-    except AttributeError as e:
-        pass 
-    return context
-
-
-def admin(request):
+from misago import __version__
+from misago.admin import site
+from misago.conf import settings, SafeSettings
+from misago.models import Forum
+from misago.monitor import monitor
+
+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:
+        context.update({
+            'acl': request.acl,
+            'board_address': settings.BOARD_ADDRESS,
+            'messages' : request.messages.messages,
+            'monitor': monitor,
+            'request_path': request.get_full_path(),
+            'settings': SafeSettings(),
+            'stopwatch': request.stopwatch.time(),
+            'user': request.user,
+            'version': __version__,
+            'disable_search': False,
+        })
+        context.update({
+            'csrf_id': request.csrf.csrf_id,
+            'csrf_token': request.csrf.csrf_token,
+            'is_banned': request.ban.is_banned(),
+            'is_jammed': request.jam.is_jammed(),
+            'private_threads': Forum.objects.special_model('private_threads'),
+            'reports': Forum.objects.special_model('reports'),
+        })
+    except AttributeError as e:
+        pass 
+    return context
+
+
+def admin(request):
     return site.get_admin_navigation(request)

+ 41 - 41
misago/cookiejar.py

@@ -1,41 +1,41 @@
-from datetime import datetime, timedelta
-from django.conf import settings
-
-class CookieJar(object):
-    def __init__(self):
-        self._set_cookies = []
-        self._delete_cookies = []
-
-    def set(self, cookie, value, permanent=False):
-        if permanent:
-            # 360 days
-            max_age = 31104000
-        else:
-            # 48 hours
-            max_age = 172800
-        self._set_cookies.append({
-                                  'name': cookie,
-                                  'value': value,
-                                  'max_age': max_age,
-                                  })
-
-    def delete(self, cookie):
-        self._delete_cookies.append(cookie)
-
-    def flush(self, response):
-        for cookie in self._set_cookies:
-            response.set_cookie(
-                                settings.COOKIES_PREFIX + cookie['name'],
-                                cookie['value'],
-                                max_age=cookie['max_age'],
-                                path=settings.COOKIES_PATH,
-                                domain=settings.COOKIES_DOMAIN,
-                                secure=settings.COOKIES_SECURE
-                                )
-
-        for cookie in self._delete_cookies:
-            response.delete_cookie(
-                                   settings.COOKIES_PREFIX + cookie,
-                                   path=settings.COOKIES_PATH,
-                                   domain=settings.COOKIES_DOMAIN,
-                                   )
+from datetime import datetime, timedelta
+from django.conf import settings
+
+class CookieJar(object):
+    def __init__(self):
+        self._set_cookies = []
+        self._delete_cookies = []
+
+    def set(self, cookie, value, permanent=False):
+        if permanent:
+            # 360 days
+            max_age = 31104000
+        else:
+            # 48 hours
+            max_age = 172800
+        self._set_cookies.append({
+                                  'name': cookie,
+                                  'value': value,
+                                  'max_age': max_age,
+                                  })
+
+    def delete(self, cookie):
+        self._delete_cookies.append(cookie)
+
+    def flush(self, response):
+        for cookie in self._set_cookies:
+            response.set_cookie(
+                                settings.COOKIES_PREFIX + cookie['name'],
+                                cookie['value'],
+                                max_age=cookie['max_age'],
+                                path=settings.COOKIES_PATH,
+                                domain=settings.COOKIES_DOMAIN,
+                                secure=settings.COOKIES_SECURE
+                                )
+
+        for cookie in self._delete_cookies:
+            response.delete_cookie(
+                                   settings.COOKIES_PREFIX + cookie,
+                                   path=settings.COOKIES_PATH,
+                                   domain=settings.COOKIES_DOMAIN,
+                                   )

+ 11 - 11
misago/management/commands/countreports.py

@@ -1,12 +1,12 @@
-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['reported_posts'] = Post.objects.filter(reported=True).count()
+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['reported_posts'] = Post.objects.filter(reported=True).count()
         self.stdout.write('Reported posts were recounted.\n')

+ 44 - 44
misago/management/commands/pruneforums.py

@@ -1,45 +1,45 @@
-from datetime import timedelta
-from django.core.management.base import BaseCommand
-from django.utils import timezone
-from misago.models import Forum, Thread, Post
-from misago.monitor import monitor, UpdatingMonitor
-
-class Command(BaseCommand):
-    """
-    This command is intended to work as CRON job fired every few days to run forums pruning policies
-    """
-    help = 'Updates Popular Threads ranking'
-    def handle(self, *args, **options):
-        sync_forums = []
-        for forum in Forum.objects.all():
-            archive = forum.pruned_archive
-            deleted = 0
-            if forum.prune_start:
-                for thread in forum.thread_set.filter(weight=0).filter(start__lte=timezone.now() - timedelta(days=forum.prune_start)):
-                    if archive:
-                        thread.move_to(archive)
-                        thread.save(force_update=True)
-                    else:
-                        thread.delete()                        
-                    deleted += 1
-            if forum.prune_last:
-                for thread in forum.thread_set.filter(weight=0).filter(last__lte=timezone.now() - timedelta(days=forum.prune_last)):
-                    if archive:
-                        thread.move_to(archive)
-                        thread.save(force_update=True)
-                    else:
-                        thread.delete()
-                    deleted += 1
-            if deleted:
-                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)
-
-        with UpdatingMonitor() as cm:
-            monitor['threads'] = Thread.objects.count()
-            monitor['posts'] = Post.objects.count()
+from datetime import timedelta
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+from misago.models import Forum, Thread, Post
+from misago.monitor import monitor, UpdatingMonitor
+
+class Command(BaseCommand):
+    """
+    This command is intended to work as CRON job fired every few days to run forums pruning policies
+    """
+    help = 'Updates Popular Threads ranking'
+    def handle(self, *args, **options):
+        sync_forums = []
+        for forum in Forum.objects.all():
+            archive = forum.pruned_archive
+            deleted = 0
+            if forum.prune_start:
+                for thread in forum.thread_set.filter(weight=0).filter(start__lte=timezone.now() - timedelta(days=forum.prune_start)):
+                    if archive:
+                        thread.move_to(archive)
+                        thread.save(force_update=True)
+                    else:
+                        thread.delete()                        
+                    deleted += 1
+            if forum.prune_last:
+                for thread in forum.thread_set.filter(weight=0).filter(last__lte=timezone.now() - timedelta(days=forum.prune_last)):
+                    if archive:
+                        thread.move_to(archive)
+                        thread.save(force_update=True)
+                    else:
+                        thread.delete()
+                    deleted += 1
+            if deleted:
+                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)
+
+        with UpdatingMonitor() as cm:
+            monitor['threads'] = Thread.objects.count()
+            monitor['posts'] = Post.objects.count()
         self.stdout.write('Forums were pruned.\n')

+ 10 - 10
misago/management/commands/rebuildacls.py

@@ -1,10 +1,10 @@
-from django.core.management.base import BaseCommand
-from misago.monitor import monitor, UpdatingMonitor
-
-class Command(BaseCommand):
-    help = 'Rebuilds ACLs for all users'
-
-    def handle(self, *args, **options):
-        with UpdatingMonitor() as cm:
-            monitor.increase('acl_version')
-        self.stdout.write('\nUser ACLs cache has been set as outdated and will be rebuild when needed.\n')
+from django.core.management.base import BaseCommand
+from misago.monitor import monitor, UpdatingMonitor
+
+class Command(BaseCommand):
+    help = 'Rebuilds ACLs for all users'
+
+    def handle(self, *args, **options):
+        with UpdatingMonitor() as cm:
+            monitor.increase('acl_version')
+        self.stdout.write('\nUser ACLs cache has been set as outdated and will be rebuild when needed.\n')

+ 11 - 11
misago/management/commands/syncusermonitor.py

@@ -1,11 +1,11 @@
-from django.core.management.base import BaseCommand
-from django.utils import timezone
-from optparse import make_option
-from misago.models import User
-
-class Command(BaseCommand):
-    help = 'Updates forum monitor to contain to date user information'
-
-    def handle(self, *args, **options):
-        User.objects.resync_monitor()
-        self.stdout.write('\nForum monitor has been updated to contain to date user information.\n')
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+from optparse import make_option
+from misago.models import User
+
+class Command(BaseCommand):
+    help = 'Updates forum monitor to contain to date user information'
+
+    def handle(self, *args, **options):
+        User.objects.resync_monitor()
+        self.stdout.write('\nForum monitor has been updated to contain to date user information.\n')

+ 38 - 38
misago/management/commands/updateranking.py

@@ -1,38 +1,38 @@
-from django.core.management.base import BaseCommand, CommandError
-from django.db.models import F
-from misago.conf import settings
-from misago.models import Rank, User
-
-class Command(BaseCommand):
-    """
-    This command is intended to work as CRON job fired of once per day or less if you have more users to update user ranking.
-    """
-    help = 'Updates users ranking'
-    def handle(self, *args, **options):
-        # Find special ranks
-        special_ranks = []
-        for rank in Rank.objects.filter(special=1):
-            special_ranks.append(str(rank.pk))
-
-        # Count users that are in ranking
-        users_total = User.objects.exclude(rank__in=special_ranks).count()
-
-        # Update Ranking
-        defaulted_ranks = False
-        for rank in Rank.objects.filter(special=0).order_by('-order'):
-            if defaulted_ranks:
-                # Set ranks according to ranking
-                rank.assign_rank(users_total, special_ranks)
-            else:
-                # Set default rank first
-                User.objects.exclude(rank__in=special_ranks).update(rank=rank)
-                defaulted_ranks = True
-
-        # Inflate scores
-        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 has been updated.\n')
+from django.core.management.base import BaseCommand, CommandError
+from django.db.models import F
+from misago.conf import settings
+from misago.models import Rank, User
+
+class Command(BaseCommand):
+    """
+    This command is intended to work as CRON job fired of once per day or less if you have more users to update user ranking.
+    """
+    help = 'Updates users ranking'
+    def handle(self, *args, **options):
+        # Find special ranks
+        special_ranks = []
+        for rank in Rank.objects.filter(special=1):
+            special_ranks.append(str(rank.pk))
+
+        # Count users that are in ranking
+        users_total = User.objects.exclude(rank__in=special_ranks).count()
+
+        # Update Ranking
+        defaulted_ranks = False
+        for rank in Rank.objects.filter(special=0).order_by('-order'):
+            if defaulted_ranks:
+                # Set ranks according to ranking
+                rank.assign_rank(users_total, special_ranks)
+            else:
+                # Set default rank first
+                User.objects.exclude(rank__in=special_ranks).update(rank=rank)
+                defaulted_ranks = True
+
+        # Inflate scores
+        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 has been updated.\n')

+ 17 - 17
misago/management/commands/updatethreadranking.py

@@ -1,17 +1,17 @@
-from django.core.management.base import BaseCommand
-from django.db.models import F
-from misago.conf import settings
-from misago.models import Thread
-
-class Command(BaseCommand):
-    """
-    This command is intended to work as CRON job fired every few days to update thread popularity ranking
-    """
-    help = 'Updates Popular Threads ranking'
-    def handle(self, *args, **options):
-        if settings.thread_ranking_inflation > 0:
-            inflation = float(100 - settings.thread_ranking_inflation) / 100
-            Thread.objects.all().update(score=F('score') * inflation)
-            self.stdout.write('Thread ranking has been updated.\n')
-        else:
-            self.stdout.write('Thread ranking inflation is disabled.\n')
+from django.core.management.base import BaseCommand
+from django.db.models import F
+from misago.conf import settings
+from misago.models import Thread
+
+class Command(BaseCommand):
+    """
+    This command is intended to work as CRON job fired every few days to update thread popularity ranking
+    """
+    help = 'Updates Popular Threads ranking'
+    def handle(self, *args, **options):
+        if settings.thread_ranking_inflation > 0:
+            inflation = float(100 - settings.thread_ranking_inflation) / 100
+            Thread.objects.all().update(score=F('score') * inflation)
+            self.stdout.write('Thread ranking has been updated.\n')
+        else:
+            self.stdout.write('Thread ranking inflation is disabled.\n')