Browse Source

Merge pull request #150 from rafalp/master

Merge in stuff from master into Develop
Rafał Pitoń 12 years ago
parent
commit
0d048a4d35
40 changed files with 1145 additions and 213 deletions
  1. 4 2
      deployment/settings.py
  2. 1 1
      misago/acl/builder.py
  3. 1 1
      misago/apps/privatethreads/posting.py
  4. 18 21
      misago/apps/privatethreads/search.py
  5. 3 3
      misago/apps/privatethreads/urls.py
  6. 16 19
      misago/apps/reports/search.py
  7. 3 3
      misago/apps/reports/urls.py
  8. 2 2
      misago/apps/search/forms.py
  9. 0 7
      misago/apps/search/urls.py
  10. 132 104
      misago/apps/search/views.py
  11. 14 0
      misago/apps/threads/search.py
  12. 4 0
      misago/apps/threads/urls.py
  13. 1 1
      misago/apps/threadtype/delete.py
  14. 0 1
      misago/apps/threadtype/jumps.py
  15. 5 0
      misago/apps/threadtype/list/moderation.py
  16. 5 5
      misago/apps/threadtype/mixins.py
  17. 2 2
      misago/apps/threadtype/posting/base.py
  18. 1 0
      misago/apps/threadtype/posting/editreply.py
  19. 4 0
      misago/apps/threadtype/posting/editthread.py
  20. 0 2
      misago/apps/threadtype/posting/newreply.py
  21. 0 1
      misago/apps/threadtype/posting/newthread.py
  22. 3 1
      misago/apps/threadtype/thread/moderation/forms.py
  23. 16 17
      misago/apps/threadtype/thread/moderation/posts.py
  24. 0 1
      misago/fixtures/forums.py
  25. 395 0
      misago/migrations/0016_auto__add_field_post_delete_date.py
  26. 392 0
      misago/migrations/0017_populate_post_delete_date.py
  27. 7 0
      misago/models/postmodel.py
  28. 3 0
      misago/models/threadmodel.py
  29. 2 11
      misago/models/usermodel.py
  30. 6 1
      misago/search.py
  31. 15 2
      misago/search_indexes.py
  32. 0 1
      misago/urls.py
  33. 3 0
      static/cranefly/css/cranefly.css
  34. 29 0
      static/cranefly/css/cranefly/search.less
  35. 1 1
      templates/cranefly/private_threads/thread.html
  36. 45 0
      templates/cranefly/reports/thread.html
  37. 1 1
      templates/cranefly/search/home.html
  38. 10 0
      templates/cranefly/search/layout.html
  39. 1 1
      templates/cranefly/threads/thread.html
  40. 0 1
      templates/search/indexes/misago/post_text.txt

+ 4 - 2
deployment/settings.py

@@ -55,7 +55,9 @@ CACHES = {
 # http://django-haystack.readthedocs.org/en/latest/tutorial.html#modify-your-settings-py
 # http://django-haystack.readthedocs.org/en/latest/tutorial.html#modify-your-settings-py
 HAYSTACK_CONNECTIONS = {
 HAYSTACK_CONNECTIONS = {
     'default': {
     'default': {
-        'ENGINE': 'haystack.backends.whoosh_backend.',
+        'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', # Misago uses Whoosh by default
+        'PATH': 'searchindex',
+        'INCLUDE_SPELLING': True,
     },
     },
 }
 }
 
 
@@ -171,4 +173,4 @@ if 'test' in sys.argv:
     CACHES['default'] = {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
     CACHES['default'] = {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}
     SKIP_SOUTH_TESTS = True
     SKIP_SOUTH_TESTS = True
     MEDIA_URL = "http://media.domain.com/"
     MEDIA_URL = "http://media.domain.com/"
-    HAYSTACK_CONNECTIONS = {'default': {'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',},}
+    HAYSTACK_CONNECTIONS = {'default': {'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',},}

+ 1 - 1
misago/acl/builder.py

@@ -53,7 +53,7 @@ def acl(request, user):
         if user_acl.version != request.monitor['acl_version']:
         if user_acl.version != request.monitor['acl_version']:
             raise InvalidCacheBackendError()
             raise InvalidCacheBackendError()
     except (AttributeError, InvalidCacheBackendError):
     except (AttributeError, InvalidCacheBackendError):
-        user_acl = build_acl(request, request.user.get_roles())
+        user_acl = build_acl(request, user.get_roles())
         cache.set(acl_key, user_acl, 2592000)
         cache.set(acl_key, user_acl, 2592000)
     return user_acl
     return user_acl
 
 

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

@@ -60,7 +60,7 @@ class NewReplyView(NewReplyBaseView, TypeMixin):
 
 
     def set_context(self):
     def set_context(self):
         super(NewReplyView, self).set_context()
         super(NewReplyView, self).set_context()
-        if not (self.request.acl.private_threads.is_mod() or len(self.thread.participants) < 2):
+        if not (self.request.acl.private_threads.is_mod() or self.thread.participants.count() > 1):
             raise ACLError403(_("This thread needs to have more than one participant to allow new replies."))
             raise ACLError403(_("This thread needs to have more than one participant to allow new replies."))
 
 
     def after_form(self, form):
     def after_form(self, form):

+ 18 - 21
misago/apps/privatethreads/search.py

@@ -1,27 +1,24 @@
-from misago.decorators import block_crawlers
 from misago.models import Post
 from misago.models import Post
-from misago.apps.errors import error404
-from misago.apps.search.views import do_search, results
+from misago.acl.exceptions import ACLError404
+from misago.apps.search.views import SearchBaseView, ResultsBaseView
 
 
-def allow_search(f):
-    def decorator(*args, **kwargs):
-        request = args[0]
-        if not (request.acl.private_threads.can_participate()
-                and request.settings['enable_private_threads']):
-            return error404()
-        return f(*args, **kwargs)
-    return decorator
+class SearchPrivateThreadsMixin(object):
+    search_route = 'private_threads_search'
+    results_route = 'private_threads_results'
 
 
+    def check_acl(self):
+        if not (self.request.acl.private_threads.can_participate()
+                and self.request.settings['enable_private_threads']):
+            raise ACLError404()
 
 
-@block_crawlers
-@allow_search
-def search_private_threads(request):
-    threads = [t.pk for t in request.user.private_thread_set.all()]
-    queryset = Post.objects.filter(thread_id__in=threads)
-    return do_search(request, queryset, 'private_threads')
+    def filter_queryset(self, sqs):
+        threads = [t.pk for t in self.request.user.private_thread_set.all()]
+        return sqs.filter(thread_id__in=threads)
 
 
 
 
-@block_crawlers
-@allow_search
-def show_private_threads_results(request, page=0):
-    return results(request, page, 'private_threads')
+class SearchView(SearchPrivateThreadsMixin, SearchBaseView):
+    pass
+
+
+class ResultsView(SearchPrivateThreadsMixin, ResultsBaseView):
+    pass

+ 3 - 3
misago/apps/privatethreads/urls.py

@@ -38,7 +38,7 @@ urlpatterns = patterns('misago.apps.privatethreads',
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="private_thread_changelog_diff"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="private_thread_changelog_diff"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="private_thread_changelog_revert"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="private_thread_changelog_revert"),
     # Extra search routes
     # Extra search routes
-    url(r'^search/$', 'search.search_private_threads', name="private_threads_search"),
-    url(r'^search/results/$', 'search.show_private_threads_results', name="private_threads_results"),
-    url(r'^search/results/(?P<page>[1-9]([0-9]+)?)/$', 'search.show_private_threads_results', name="private_threads_results"),
+    url(r'^search/$', 'search.SearchView', name="private_threads_search"),
+    url(r'^search/results/$', 'search.SearchView', name="private_threads_results"),
+    url(r'^search/results/(?P<page>[1-9]([0-9]+)?)/$', 'search.ResultsView', name="private_threads_results"),
 )
 )

+ 16 - 19
misago/apps/reports/search.py

@@ -1,25 +1,22 @@
-from misago.decorators import block_crawlers
 from misago.models import Forum, Post
 from misago.models import Forum, Post
-from misago.apps.errors import error404
-from misago.apps.search.views import do_search, results
+from misago.acl.exceptions import ACLError404
+from misago.apps.search.views import SearchBaseView, ResultsBaseView
 
 
-def allow_search(f):
-    def decorator(*args, **kwargs):
-        request = args[0]
-        if not request.acl.reports.can_handle():
-            return error404()
-        return f(*args, **kwargs)
-    return decorator
+class SearchReportsMixin(object):
+    search_route = 'reports_search'
+    results_route = 'reports_results'
+    
+    def check_acl(self):
+        if not self.request.acl.reports.can_handle():
+            raise ACLError404()
 
 
+    def filter_queryset(self, sqs):
+        return sqs.filter(forum=Forum.objects.special_pk('reports'))
 
 
-@block_crawlers
-@allow_search
-def search_reports(request):
-    queryset = Post.objects.filter(forum=Forum.objects.special_pk('reports'))
-    return do_search(request, queryset, 'reports')
 
 
+class SearchView(SearchReportsMixin, SearchBaseView):
+    pass
 
 
-@block_crawlers
-@allow_search
-def show_reports_results(request, page=0):
-    return results(request, page, 'reports')
+
+class ResultsView(SearchReportsMixin, ResultsBaseView):
+    pass

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

@@ -30,7 +30,7 @@ urlpatterns = patterns('misago.apps.reports',
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="report_changelog_diff"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="report_changelog_diff"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="report_changelog_revert"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="report_changelog_revert"),
     # Extra search routes
     # Extra search routes
-    url(r'^search/$', 'search.search_reports', name="reports_search"),
-    url(r'^search/results/$', 'search.show_reports_results', name="reports_results"),
-    url(r'^search/results/(?P<page>[1-9]([0-9]+)?)/$', 'search.show_reports_results', name="reports_results"),
+    url(r'^search/$', 'search.SearchView', name="reports_search"),
+    url(r'^search/results/$', 'search.SearchView', name="reports_results"),
+    url(r'^search/results/(?P<page>[1-9]([0-9]+)?)/$', 'search.ResultsView', name="reports_results"),
 )
 )

+ 2 - 2
misago/apps/search/forms.py

@@ -46,7 +46,7 @@ class QuickSearchForm(Form):
                 if wait_for < 5:
                 if wait_for < 5:
                     raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
                     raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
                 else:
                 else:
-                    raise forms.ValidationError(ungettext(
+                    raise forms.ValidationError(ungettext_lazy(
                             "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
                             "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
                             "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
                             "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
                         wait_for) % {
                         wait_for) % {
@@ -65,7 +65,7 @@ class QuickSearchForm(Form):
                 if wait_for < 5:
                 if wait_for < 5:
                     raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
                     raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
                 else:
                 else:
-                    raise forms.ValidationError(ungettext(
+                    raise forms.ValidationError(ungettext_lazy(
                             "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
                             "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
                             "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
                             "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
                         wait_for) % {
                         wait_for) % {

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

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

+ 132 - 104
misago/apps/search/views.py

@@ -4,7 +4,8 @@ from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
-from haystack.query import RelatedSearchQuerySet
+from haystack.query import SearchQuerySet, RelatedSearchQuerySet
+from misago.acl.exceptions import ACLError403, ACLError404
 from misago.decorators import block_crawlers
 from misago.decorators import block_crawlers
 from misago.forms import FormFields
 from misago.forms import FormFields
 from misago.models import Forum, Thread, Post, User
 from misago.models import Forum, Thread, Post, User
@@ -14,112 +15,139 @@ from misago.apps.errors import error403, error404
 from misago.apps.profiles.views import list as users_list
 from misago.apps.profiles.views import list as users_list
 from misago.apps.search.forms import QuickSearchForm
 from misago.apps.search.forms import QuickSearchForm
 
 
-@block_crawlers
-def search(request):
-    queryset = Post.objects.filter(forum__in=Forum.objects.readable_forums(request.acl))
-    return do_search(request, queryset)
 
 
+class ViewBase(object):
+    search_route = 'search'
+    results_route = 'search_results'
+    advanced_route = None
 
 
-def do_search(request, queryset, search_route=None):
-    if not request.acl.search.can_search():
-        return error403(request, _("You don't have permission to search community."))
+    def check_acl(self):
+        pass
 
 
-    search_route = search_route or 'search'
+    def queryset(self):
+        pass
 
 
-    if request.method != "POST":
-        form = QuickSearchForm(request=request)
-        return request.theme.render_to_response('search/home.html',
-                                                {
-                                                 'form': FormFields(form),
-                                                 'search_route': search_route,
-                                                 'search_result': request.session.get('%s_result' % search_route),
-                                                 'disable_search': True,
-                                                },
-                                                context_instance=RequestContext(request))
+    def search_form_type(self):
+        return QuickSearchForm
+
+    def render_to_response(self, template, form, context):
+        tpl_dict = {
+                    'form': FormFields(form),
+                    'search_route': self.search_route,
+                    'results_route': self.results_route,
+                    'search_advanced': self.advanced_route,
+                    'suggestion': None,
+                    'disable_search': True,
+                    }
+        tpl_dict.update(context)
+        return self.request.theme.render_to_response('search/%s.html' % template,
+                                                     tpl_dict,
+                                                     context_instance=RequestContext(self.request))
+
+    def __new__(cls, request, **kwargs):
+        obj = super(ViewBase, cls).__new__(cls)
+        return obj(request, **kwargs)
+
+    def __call__(self, request, **kwargs):
+        try:
+            if request.user.is_crawler():
+                raise ACLError404()
+            self.check_acl()
+            if not request.acl.search.can_search():
+                raise ACLError403(_("You don't have permission to search community."))
+            self.request = request
+            return self.call(**kwargs)
+        except ACLError403 as e:
+            return error403(request, unicode(e))
+        except ACLError404 as e:
+            return error404(request, unicode(e))
+
+
+class SearchBaseView(ViewBase):
+    def call(self, **kwargs):
+        form_type = self.search_form_type()
+        if self.request.method != "POST":
+            form = self.search_form_type()(request=self.request)
+            return self.render_to_response('home', form,  
+                                           {
+                                            'search_result': self.request.session.get(self.results_route),
+                                           })
         
         
-    try:
-        form = QuickSearchForm(request.POST, request=request)
-        if form.is_valid():
-            if form.mode == 'forum':
-                jump_to = Forum.objects.forum_by_name(form.target, request.acl)
-                if jump_to:
-                    if jump_to.level == 1:
-                        return redirect(reverse('index') + ('#%s' % jump_to.slug))
-                    return redirect(jump_to.url)
-                else:
-                    raise SearchException(_('Forum "%(forum)s" could not be found.') % {'forum': form.target})
-            if form.mode == 'user':
-                request.POST = request.POST.copy()
-                request.POST['username'] = form.target
-                return users_list(request)
-            sqs = RelatedSearchQuerySet().auto_query(form.cleaned_data['search_query']).order_by('-id').load_all()
-            sqs = sqs.load_all_queryset(Post, queryset.filter(deleted=False).filter(moderated=False).select_related('thread', 'forum'))[:120]
-
-            if request.user.is_authenticated():
-                request.user.last_search = timezone.now()
-                request.user.save(force_update=True)
-            if request.user.is_anonymous():
-                request.session['last_search'] = timezone.now()
-
-            if not sqs:
-                raise SearchException(_("Search returned no results. Change search query and try again."))
-            request.session['%s_result' % search_route] = {
-                                                           'search_query': form.cleaned_data['search_query'],
-                                                           'search_results': [p.object.pk for p in sqs],
-                                                           }
-            return redirect(reverse('%s_results' % search_route))
-        else:
-            raise SearchException(form.errors['search_query'][0])
-    except SearchException as e:
-        return request.theme.render_to_response('search/error.html',
-                                                {
-                                                 'form': FormFields(form),
-                                                 'search_route': search_route,
-                                                 'message': unicode(e),
-                                                 'disable_search': True,
-                                                },
-                                                context_instance=RequestContext(request))
-
-
-@block_crawlers
-def show_results(request, page=0):
-    return results(request, page)
-
-
-def results(request, page=0, search_route=None):
-    if not request.acl.search.can_search():
-        return error403(request, _("You don't have permission to search community."))
-
-    search_route = search_route or 'search'
-    result = request.session.get('%s_result' % search_route)
-    if not result:
-        form = QuickSearchForm(request=request)
-        return request.theme.render_to_response('search/error.html',
-                                                {
-                                                 'form': FormFields(form),
-                                                 'search_route': search_route,
-                                                 'message': _("No search results were found."),
-                                                 'disable_search': True,
-                                                },
-                                                context_instance=RequestContext(request))
-
-    queryset = Post.objects.filter(id__in=result['search_results'])
-    items_total = queryset.count();
-    try:
-        pagination = make_pagination(page, items_total, 12)
-    except Http404:
-        return redirect(reverse('%s_results' % search_route))
-
-    form = QuickSearchForm(request=request, initial={'search_query': result['search_query']})
-    return request.theme.render_to_response('search/results.html',
-                                            {
-                                             'form': FormFields(form),
-                                             'search_route': search_route,
-                                             'search_query': result['search_query'],
-                                             'results': queryset.order_by('-pk').select_related('thread', 'forum', 'user')[pagination['start']:pagination['stop']],
-                                             'disable_search': True,
-                                             'items_total': items_total,
-                                             'pagination': pagination,
-                                            },
-                                            context_instance=RequestContext(request))
+        try:
+            form = self.search_form_type()(self.request.POST, request=self.request)
+            if form.is_valid():
+                if form.mode == 'forum':
+                    jump_to = Forum.objects.forum_by_name(form.target, self.request.acl)
+                    if jump_to:
+                        if jump_to.level == 1:
+                            return redirect(reverse('index') + ('#%s' % jump_to.slug))
+                        return redirect(jump_to.url)
+                    else:
+                        raise SearchException(_('Forum "%(forum)s" could not be found.') % {'forum': form.target})
+                if form.mode == 'user':
+                    self.request.POST = self.request.POST.copy()
+                    self.request.POST['username'] = form.target
+                    return users_list(self.request)
+
+                sqs = self.filter_queryset(SearchQuerySet().auto_query(form.cleaned_data['search_query'])).load_all()[:60]
+                suggestion = SearchQuerySet().spelling_suggestion(form.cleaned_data['search_query'])
+                
+                if self.request.user.is_authenticated():
+                    self.request.user.last_search = timezone.now()
+                    self.request.user.save(force_update=True)
+                if self.request.user.is_anonymous():
+                    self.request.session['last_search'] = timezone.now()
+                
+                if not sqs:
+                    raise SearchException(_("Search returned no results. Change search query and try again."), suggestion)
+
+                if (suggestion.lower() == form.cleaned_data['search_query'].lower()
+                        or suggestion.lower() in form.cleaned_data['search_query'].lower()):
+                    suggestion = None
+
+                if suggestion:
+                    new_sqs = self.filter_queryset(SearchQuerySet().auto_query(form.cleaned_data['search_query'])).load_all()[:60]
+                    sqs_len = len(sqs)
+                    new_len = len(new_sqs)
+                    if not new_len or new_len < sqs_len * 0.8:
+                        suggestion = None # We are assuming suggestion is wrong
+
+                self.request.session[self.results_route] = {
+                                                            'search_query': form.cleaned_data['search_query'],
+                                                            'search_suggestion': suggestion,
+                                                            'search_results': [p.object for p in sqs],
+                                                            }
+                return redirect(reverse(self.results_route))
+            else:
+                if 'search_query' in form.errors:
+                    raise SearchException(form.errors['search_query'][0])
+                raise SearchException(form.errors['__all__'][0])
+        except SearchException as e:
+            return self.render_to_response('error', form,  
+                                           {'message': unicode(e), 'suggestion': unicode(e.suggestion)})
+
+
+class ResultsBaseView(ViewBase):
+    def call(self, **kwargs):
+        result = self.request.session.get(self.results_route)
+        if not result:
+            form = self.search_form_type()(request=self.request)
+            return self.render_to_response('error', form,  
+                                           {'message': _("No search results were found.")})
+
+        items = result['search_results']
+        items_total = len(items);
+        try:
+            pagination = make_pagination(kwargs.get('page', 0), items_total, 12)
+        except Http404:
+            return redirect(reverse(self.search_route))
 
 
+        form = self.search_form_type()(request=self.request, initial={'search_query': result['search_query']})
+        return self.render_to_response('results', form,  
+                                       {
+                                        'search_query': result['search_query'],
+                                        'suggestion': result['search_suggestion'],
+                                        'results': items[pagination['start']:pagination['stop']],
+                                        'items_total': items_total,
+                                        'pagination': pagination,
+                                       })

+ 14 - 0
misago/apps/threads/search.py

@@ -0,0 +1,14 @@
+from misago.models import Forum, Post
+from misago.apps.search.views import SearchBaseView, ResultsBaseView
+
+class SearchThreadsMixin(object):
+    def filter_queryset(self, sqs):
+        return sqs.filter(forum__in=Forum.objects.readable_forums(self.request.acl))
+
+
+class SearchView(SearchThreadsMixin, SearchBaseView):
+    pass
+
+
+class ResultsView(SearchThreadsMixin, ResultsBaseView):
+    pass

+ 4 - 0
misago/apps/threads/urls.py

@@ -38,4 +38,8 @@ urlpatterns = patterns('misago.apps.threads',
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="thread_changelog"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="thread_changelog"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="thread_changelog_diff"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="thread_changelog_diff"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="thread_changelog_revert"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="thread_changelog_revert"),
+    # Searching
+    url(r'^search/$', 'search.SearchView', name="search"),
+    url(r'^search/results/$', 'search.ResultsView', name="search_results"),
+    url(r'^search/results/(?P<page>[1-9]([0-9]+)?)/$', 'search.ResultsView', name="search_results"),
 )
 )

+ 1 - 1
misago/apps/threadtype/delete.py

@@ -150,7 +150,7 @@ class HideReplyBaseView(DeleteHideBaseView):
             raise ACLError403(_("Somebody has already replied to this post, you cannot delete it."))
             raise ACLError403(_("Somebody has already replied to this post, you cannot delete it."))
 
 
     def delete(self):
     def delete(self):
-        self.post.current_date = timezone.now()
+        self.post.delete_date = timezone.now()
         self.post.deleted = True
         self.post.deleted = True
         self.post.save(force_update=True)
         self.post.save(force_update=True)
         self.thread.sync()
         self.thread.sync()

+ 0 - 1
misago/apps/threadtype/jumps.py

@@ -317,7 +317,6 @@ Member @%(reporter)s has reported following post by @%(reported)s:
                                              post=reason_post,
                                              post=reason_post,
                                              post_preparsed=reason_post_preparsed,
                                              post_preparsed=reason_post_preparsed,
                                              date=now,
                                              date=now,
-                                             current_date=now,
                                              )
                                              )
 
 
                 report.start_post = reason
                 report.start_post = reason

+ 5 - 0
misago/apps/threadtype/list/moderation.py

@@ -28,6 +28,7 @@ class ThreadsListModeration(object):
                 thread.start_post.moderated = False
                 thread.start_post.moderated = False
                 thread.start_post.save(force_update=True)
                 thread.start_post.save(force_update=True)
                 thread.set_checkpoint(self.request, 'accepted')
                 thread.set_checkpoint(self.request, 'accepted')
+                thread.update_current_dates()
                 # Sync user
                 # Sync user
                 if thread.last_post.user:
                 if thread.last_post.user:
                     thread.start_post.user.threads += 1
                     thread.start_post.user.threads += 1
@@ -102,6 +103,7 @@ class ThreadsListModeration(object):
                     thread.move_to(new_forum)
                     thread.move_to(new_forum)
                     thread.save(force_update=True)
                     thread.save(force_update=True)
                     thread.set_checkpoint(self.request, 'moved', forum=self.forum)
                     thread.set_checkpoint(self.request, 'moved', forum=self.forum)
+                    thread.update_current_dates()
                 new_forum.sync()
                 new_forum.sync()
                 new_forum.save(force_update=True)
                 new_forum.save(force_update=True)
                 self.forum.sync()
                 self.forum.sync()
@@ -146,6 +148,7 @@ class ThreadsListModeration(object):
                 Thread.objects.filter(id__in=merged).delete()
                 Thread.objects.filter(id__in=merged).delete()
                 new_thread.sync()
                 new_thread.sync()
                 new_thread.save(force_update=True)
                 new_thread.save(force_update=True)
+                new_thread.update_current_dates()
                 self.forum.sync()
                 self.forum.sync()
                 self.forum.save(force_update=True)
                 self.forum.save(force_update=True)
                 if form.cleaned_data['new_forum'].pk != self.forum.pk:
                 if form.cleaned_data['new_forum'].pk != self.forum.pk:
@@ -226,6 +229,7 @@ class ThreadsListModeration(object):
                 thread.sync()
                 thread.sync()
                 thread.save(force_update=True)
                 thread.save(force_update=True)
                 thread.set_checkpoint(self.request, 'undeleted')
                 thread.set_checkpoint(self.request, 'undeleted')
+                thread.update_current_dates()
         if undeleted:
         if undeleted:
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)
@@ -247,6 +251,7 @@ class ThreadsListModeration(object):
                 thread.sync()
                 thread.sync()
                 thread.save(force_update=True)
                 thread.save(force_update=True)
                 thread.set_checkpoint(self.request, 'deleted')
                 thread.set_checkpoint(self.request, 'deleted')
+                thread.update_current_dates()
         if deleted:
         if deleted:
             self.forum.sync()
             self.forum.sync()
             self.forum.save(force_update=True)
             self.forum.save(force_update=True)

+ 5 - 5
misago/apps/threadtype/mixins.py

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

+ 2 - 2
misago/apps/threadtype/posting/base.py

@@ -12,6 +12,7 @@ from misago.apps.threadtype.thread.forms import QuickReplyForm
 
 
 class PostingBaseView(ViewBase):
 class PostingBaseView(ViewBase):
     allow_quick_reply = False
     allow_quick_reply = False
+    block_flood_requests = True
 
 
     def form_initial_data(self):
     def form_initial_data(self):
         return {}
         return {}
@@ -22,7 +23,6 @@ class PostingBaseView(ViewBase):
             self.parents = Forum.objects.forum_parents(self.forum.pk)
             self.parents = Forum.objects.forum_parents(self.forum.pk)
 
 
     def record_edit(self, form, old_name, old_post):
     def record_edit(self, form, old_name, old_post):
-        self.post.current_date = timezone.now()
         self.post.edits += 1
         self.post.edits += 1
         self.post.edit_user = self.request.user
         self.post.edit_user = self.request.user
         self.post.edit_user_name = self.request.user.username
         self.post.edit_user_name = self.request.user.username
@@ -111,6 +111,7 @@ class PostingBaseView(ViewBase):
             self._set_context()
             self._set_context()
             self.check_forum_type()
             self.check_forum_type()
             self._check_permissions()
             self._check_permissions()
+            request.block_flood_requests = self.block_flood_requests
             if request.method == 'POST':
             if request.method == 'POST':
                 # Create correct form instance
                 # Create correct form instance
                 if self.allow_quick_reply and 'quick_reply' in request.POST:
                 if self.allow_quick_reply and 'quick_reply' in request.POST:
@@ -121,7 +122,6 @@ class PostingBaseView(ViewBase):
                         form = self.form_type(request.POST, request.FILE, request=request, forum=self.forum, thread=self.thread)
                         form = self.form_type(request.POST, request.FILE, request=request, forum=self.forum, thread=self.thread)
                     except AttributeError:
                     except AttributeError:
                         form = self.form_type(request.POST, request=request, forum=self.forum, thread=self.thread)
                         form = self.form_type(request.POST, request=request, forum=self.forum, thread=self.thread)
-                
                 # Handle specific submit
                 # Handle specific submit
                 if 'preview' in request.POST:
                 if 'preview' in request.POST:
                     form.empty_errors()
                     form.empty_errors()

+ 1 - 0
misago/apps/threadtype/posting/editreply.py

@@ -6,6 +6,7 @@ from misago.markdown import post_markdown
 class EditReplyBaseView(PostingBaseView):
 class EditReplyBaseView(PostingBaseView):
     action = 'edit_reply'
     action = 'edit_reply'
     form_type = EditReplyForm
     form_type = EditReplyForm
+    block_flood_requests = False
 
 
     def set_context(self):
     def set_context(self):
         self.set_thread_context()
         self.set_thread_context()

+ 4 - 0
misago/apps/threadtype/posting/editthread.py

@@ -7,6 +7,7 @@ from misago.utils.strings import slugify
 class EditThreadBaseView(PostingBaseView):
 class EditThreadBaseView(PostingBaseView):
     action = 'edit_thread'
     action = 'edit_thread'
     form_type = EditThreadForm
     form_type = EditThreadForm
+    block_flood_requests = False
 
 
     def set_context(self):
     def set_context(self):
         self.set_thread_context()
         self.set_thread_context()
@@ -58,5 +59,8 @@ class EditThreadBaseView(PostingBaseView):
             self.md, self.post.post_preparsed = post_markdown(form.cleaned_data['post'])
             self.md, self.post.post_preparsed = post_markdown(form.cleaned_data['post'])
             self.post.save(force_update=True)
             self.post.save(force_update=True)
 
 
+        if old_name != form.cleaned_data['thread_name']:
+            self.thread.update_current_dates()
+
         if changed_thread or changed_post:
         if changed_thread or changed_post:
             self.record_edit(form, old_name, old_post)
             self.record_edit(form, old_name, old_post)

+ 0 - 2
misago/apps/threadtype/posting/newreply.py

@@ -43,7 +43,6 @@ class NewReplyBaseView(PostingBaseView):
             merged = True
             merged = True
             self.post = self.thread.last_post
             self.post = self.thread.last_post
             self.post.date = now
             self.post.date = now
-            self.post.current_date = now
             self.post.post = '%s\n\n%s' % (self.post.post, form.cleaned_data['post'])
             self.post.post = '%s\n\n%s' % (self.post.post, form.cleaned_data['post'])
             self.md, self.post.post_preparsed = post_markdown(self.post.post)
             self.md, self.post.post_preparsed = post_markdown(self.post.post)
             self.post.save(force_update=True)
             self.post.save(force_update=True)
@@ -60,7 +59,6 @@ class NewReplyBaseView(PostingBaseView):
                                             post=form.cleaned_data['post'],
                                             post=form.cleaned_data['post'],
                                             post_preparsed=post_preparsed,
                                             post_preparsed=post_preparsed,
                                             date=now,
                                             date=now,
-                                            current_date=now,
                                             moderated=moderation,
                                             moderated=moderation,
                                         )
                                         )
 
 

+ 0 - 1
misago/apps/threadtype/posting/newthread.py

@@ -44,7 +44,6 @@ class NewThreadBaseView(PostingBaseView):
                                         post=form.cleaned_data['post'],
                                         post=form.cleaned_data['post'],
                                         post_preparsed=post_preparsed,
                                         post_preparsed=post_preparsed,
                                         date=now,
                                         date=now,
-                                        current_date=now,
                                         moderated=moderation,
                                         moderated=moderation,
                                         )
                                         )
 
 

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

@@ -3,7 +3,7 @@ from django.http import Http404
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.forms import Form, ForumChoiceField
 from misago.forms import Form, ForumChoiceField
-from misago.models import Thread
+from misago.models import Forum, Thread
 from misago.validators import validate_sluggable
 from misago.validators import validate_sluggable
 from misago.apps.threadtype.mixins import ValidateThreadNameMixin
 from misago.apps.threadtype.mixins import ValidateThreadNameMixin
 
 
@@ -59,6 +59,8 @@ class MovePostsForm(Form, ValidateThreadNameMixin):
         try:
         try:
             thread_url = thread_url[len(settings.BOARD_ADDRESS):]
             thread_url = thread_url[len(settings.BOARD_ADDRESS):]
             match = resolve(thread_url)
             match = resolve(thread_url)
+            if match.url_name[0:len(self.type_prefix)] != self.type_prefix:
+                raise forms.ValidationError(_("This is not a correct thread URL."))
             thread = Thread.objects.get(pk=match.kwargs['thread'])
             thread = Thread.objects.get(pk=match.kwargs['thread'])
             self.request.acl.threads.allow_thread_view(self.request.user, thread)
             self.request.acl.threads.allow_thread_view(self.request.user, thread)
             if thread.pk == self.thread.pk:
             if thread.pk == self.thread.pk:

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

@@ -41,7 +41,6 @@ class PostsModeration(object):
             post.merge_with(new_post)
             post.merge_with(new_post)
             post.delete()
             post.delete()
         md, new_post.post_preparsed = post_markdown(new_post.post)
         md, new_post.post_preparsed = post_markdown(new_post.post)
-        new_post.current_date = timezone.now()
         new_post.save(force_update=True)
         new_post.save(force_update=True)
         self.thread.sync()
         self.thread.sync()
         self.thread.save(force_update=True)
         self.thread.save(force_update=True)
@@ -140,21 +139,6 @@ class PostsModeration(object):
                                                       },
                                                       },
                                                      context_instance=RequestContext(self.request));
                                                      context_instance=RequestContext(self.request));
 
 
-    def post_action_undelete(self, ids):
-        undeleted = []
-        for post in self.posts:
-            if post.pk in ids and post.deleted:
-                undeleted.append(post.pk)
-        if undeleted:
-            self.thread.post_set.filter(id__in=undeleted).update(deleted=False)
-            self.thread.sync()
-            self.thread.save(force_update=True)
-            self.forum.sync()
-            self.forum.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected posts have been restored.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No posts were restored.')), 'info', 'threads')
-
     def post_action_protect(self, ids):
     def post_action_protect(self, ids):
         protected = 0
         protected = 0
         for post in self.posts:
         for post in self.posts:
@@ -177,6 +161,21 @@ class PostsModeration(object):
         else:
         else:
             self.request.messages.set_flash(Message(_('No posts were unprotected.')), 'info', 'threads')
             self.request.messages.set_flash(Message(_('No posts were unprotected.')), 'info', 'threads')
 
 
+    def post_action_undelete(self, ids):
+        undeleted = []
+        for post in self.posts:
+            if post.pk in ids and post.deleted:
+                undeleted.append(post.pk)
+        if undeleted:
+            self.thread.post_set.filter(id__in=undeleted).update(deleted=False, current_date=timezone.now())
+            self.thread.sync()
+            self.thread.save(force_update=True)
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            self.request.messages.set_flash(Message(_('Selected posts have been restored.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were restored.')), 'info', 'threads')
+
     def post_action_soft(self, ids):
     def post_action_soft(self, ids):
         deleted = []
         deleted = []
         for post in self.posts:
         for post in self.posts:
@@ -185,7 +184,7 @@ class PostsModeration(object):
                     raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
                     raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
                 deleted.append(post.pk)
                 deleted.append(post.pk)
         if deleted:
         if deleted:
-            self.thread.post_set.filter(id__in=deleted).update(deleted=True, current_date=timezone.now())
+            self.thread.post_set.filter(id__in=deleted).update(deleted=True, current_date=timezone.now(), delete_date=timezone.now())
             self.thread.sync()
             self.thread.sync()
             self.thread.save(force_update=True)
             self.thread.save(force_update=True)
             self.forum.sync()
             self.forum.sync()

+ 0 - 1
misago/fixtures/forums.py

@@ -38,7 +38,6 @@ def load():
                                post='Welcome to Misago!',
                                post='Welcome to Misago!',
                                post_preparsed='Welcome to Misago!',
                                post_preparsed='Welcome to Misago!',
                                date=now,
                                date=now,
-                               current_date=now,
                                )
                                )
     thread.start_post = post
     thread.start_post = post
     thread.start_poster_name = 'MisagoProject'
     thread.start_poster_name = 'MisagoProject'

+ 395 - 0
misago/migrations/0016_auto__add_field_post_delete_date.py

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

+ 392 - 0
misago/migrations/0017_populate_post_delete_date.py

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

+ 7 - 0
misago/models/postmodel.py

@@ -33,6 +33,7 @@ class Post(models.Model):
     edit_user = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
     edit_user = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
     edit_user_name = models.CharField(max_length=255, null=True, blank=True)
     edit_user_name = models.CharField(max_length=255, null=True, blank=True)
     edit_user_slug = models.SlugField(max_length=255, null=True, blank=True)
     edit_user_slug = models.SlugField(max_length=255, null=True, blank=True)
+    delete_date = models.DateTimeField(null=True, blank=True)
     reported = models.BooleanField(default=False, db_index=True)
     reported = models.BooleanField(default=False, db_index=True)
     moderated = models.BooleanField(default=False)
     moderated = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
@@ -45,6 +46,11 @@ class Post(models.Model):
     class Meta:
     class Meta:
         app_label = 'misago'
         app_label = 'misago'
 
 
+    def save(self, *args, **kwargs):
+        self.current_date = timezone.now()
+        return super(Post, self).save(*args, **kwargs)
+
+
     def delete(self, *args, **kwargs):
     def delete(self, *args, **kwargs):
         """
         """
         FUGLY HAX for weird stuff that happens with
         FUGLY HAX for weird stuff that happens with
@@ -112,6 +118,7 @@ class Post(models.Model):
 def rename_user_handler(sender, **kwargs):
 def rename_user_handler(sender, **kwargs):
     Post.objects.filter(user=sender).update(
     Post.objects.filter(user=sender).update(
                                             user_name=sender.username,
                                             user_name=sender.username,
+                                            current_date=timezone.now(),
                                             )
                                             )
     Post.objects.filter(edit_user=sender).update(
     Post.objects.filter(edit_user=sender).update(
                                                  edit_user_name=sender.username,
                                                  edit_user_name=sender.username,

+ 3 - 0
misago/models/threadmodel.py

@@ -159,6 +159,9 @@ class Thread(models.Model):
     def merge_with(self, thread):
     def merge_with(self, thread):
         merge_thread.send(sender=self, new_thread=thread)
         merge_thread.send(sender=self, new_thread=thread)
 
 
+    def update_current_dates(self):
+        self.post_set.update(current_date=timezone.now())
+
     def sync(self):
     def sync(self):
         # Counters
         # Counters
         self.replies = self.post_set.filter(moderated=False).count() - 1
         self.replies = self.post_set.filter(moderated=False).count() - 1

+ 2 - 11
misago/models/usermodel.py

@@ -13,7 +13,7 @@ from django.db import models
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils import timezone as tz_util
 from django.utils import timezone as tz_util
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
-from misago.acl.builder import build_acl
+from misago.acl.builder import acl
 from misago.signals import delete_user_content, rename_user, sync_user_profile
 from misago.signals import delete_user_content, rename_user, sync_user_profile
 from misago.utils.avatars import avatar_size
 from misago.utils.avatars import avatar_size
 from misago.utils.strings import random_string, slugify
 from misago.utils.strings import random_string, slugify
@@ -436,16 +436,7 @@ class User(models.Model):
         return self.acl_key
         return self.acl_key
 
 
     def acl(self, request):
     def acl(self, request):
-        try:
-            self.make_acl_key()
-            acl = cache.get(self.acl_key)
-            if acl.version != request.monitor.acl_version:
-                raise InvalidCacheBackendError()
-        except AttributeError, InvalidCacheBackendError:
-            # build acl cache
-            acl = build_acl(request, self.get_roles())
-            cache.set(self.acl_key, acl, 2592000)
-        return acl
+        return acl(request, self)
 
 
     def get_avatar(self, size=None):
     def get_avatar(self, size=None):
         image_size = avatar_size(size) if size else None
         image_size = avatar_size(size) if size else None

+ 6 - 1
misago/search.py

@@ -1,7 +1,12 @@
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
 class SearchException(Exception):
 class SearchException(Exception):
-    pass
+    def __init__(self, message=None, suggestion=None):
+        self.message = message
+        self.suggestion = suggestion
+
+    def __unicode__(self):
+         return self.message
 
 
 
 
 class SearchQuery(object):
 class SearchQuery(object):

+ 15 - 2
misago/search_indexes.py

@@ -3,7 +3,7 @@ from misago.models import Post
 
 
 class PostIndex(indexes.SearchIndex, indexes.Indexable):
 class PostIndex(indexes.SearchIndex, indexes.Indexable):
     text = indexes.CharField(document=True, use_template=True)
     text = indexes.CharField(document=True, use_template=True)
-    forum = indexes.CharField(model_attr='forum')
+    forum = indexes.IntegerField(model_attr='forum_id')
     thread = indexes.CharField(model_attr='thread')
     thread = indexes.CharField(model_attr='thread')
     user = indexes.CharField(model_attr='user_name')
     user = indexes.CharField(model_attr='user_name')
     date = indexes.DateTimeField(model_attr='date')
     date = indexes.DateTimeField(model_attr='date')
@@ -11,8 +11,21 @@ class PostIndex(indexes.SearchIndex, indexes.Indexable):
     def get_model(self):
     def get_model(self):
         return Post
         return Post
 
 
+    def prepare_thread(self, obj):
+        return obj.thread.name
+
     def get_updated_field(self):
     def get_updated_field(self):
         return 'current_date'
         return 'current_date'
 
 
+    def should_update(self, instance, **kwargs):
+        if (instance.deleted or instance.moderated
+                or instance.thread.deletedor or instance.thread.moderated):
+            self.remove_object(instance, **kwargs)
+            return False
+        return True
+
+    def read_queryset(self, using=None):
+        return Post.objects.all().select_related('forum', 'thread', 'user')
+
     def index_queryset(self, using=None):
     def index_queryset(self, using=None):
-        return self.get_model().objects.all()
+        return self.get_model().objects.all().select_related('thread')

+ 0 - 1
misago/urls.py

@@ -29,7 +29,6 @@ urlpatterns += patterns('',
     (r'^reset-password/', include('misago.apps.resetpswd.urls')),
     (r'^reset-password/', include('misago.apps.resetpswd.urls')),
     (r'^private-threads/', include('misago.apps.privatethreads.urls')),
     (r'^private-threads/', include('misago.apps.privatethreads.urls')),
     (r'^reports/', include('misago.apps.reports.urls')),
     (r'^reports/', include('misago.apps.reports.urls')),
-    (r'^search/', include('misago.apps.search.urls')),
     (r'^', include('misago.apps.threads.urls')),
     (r'^', include('misago.apps.threads.urls')),
 )
 )
 
 

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

@@ -1219,6 +1219,9 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .reports-list .thread-name .report-id{color:#999999 !important;}
 .reports-list .thread-name .report-id{color:#999999 !important;}
 .reports-list .thread-name,.reports-list .thread-details{margin-left:0px !important;}
 .reports-list .thread-name,.reports-list .thread-details{margin-left:0px !important;}
 .reports-list .thread-icon{display:none !important;float:none;}
 .reports-list .thread-icon{display:none !important;float:none;}
+.search-suggestion{overflow:auto;}.search-suggestion p,.search-suggestion form{float:left;}
+.search-suggestion .lead{color:#7b7b7b;}
+.search-suggestion form .btn-link{margin-top:1px;color:#3c85a3;font-size:21px;font-style:italic;font-weight:200;text-decoration:underline;}.search-suggestion form .btn-link:hover,.search-suggestion form .btn-link:active{color:#3c85a3;text-decoration:underline !important;}
 .search-resume .muted{color:#7b7b7b;}.search-resume .muted a{color:#333333;}
 .search-resume .muted{color:#7b7b7b;}.search-resume .muted a{color:#333333;}
 .search-results .results-list .result{border-bottom:1px solid #eeeeee;margin-bottom:10px;padding-bottom:10px;}.search-results .results-list .result h3{margin:0px;line-height:20px;}.search-results .results-list .result h3 a:link,.search-results .results-list .result h3 a:visited{color:#555555;font-weight:normal;font-size:18.2px;text-decoration:underline;}
 .search-results .results-list .result{border-bottom:1px solid #eeeeee;margin-bottom:10px;padding-bottom:10px;}.search-results .results-list .result h3{margin:0px;line-height:20px;}.search-results .results-list .result h3 a:link,.search-results .results-list .result h3 a:visited{color:#555555;font-weight:normal;font-size:18.2px;text-decoration:underline;}
 .search-results .results-list .result h3 a:hover,.search-results .results-list .result h3 a:active{color:#333333;}
 .search-results .results-list .result h3 a:hover,.search-results .results-list .result h3 a:active{color:#333333;}

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

@@ -1,6 +1,35 @@
 // Search forum
 // Search forum
 // ------------------------
 // ------------------------
 
 
+.search-suggestion {
+  overflow: auto;
+
+  p, form {
+    float: left;
+  }
+
+  .lead {
+    color: lighten(@gray, 15%);
+  }
+
+  form {
+    .btn-link {
+      margin-top: 1px;
+
+      color: @bluePale;
+      font-size: @baseFontSize * 1.5;
+      font-style: italic;
+      font-weight: 200;
+      text-decoration: underline;
+
+      &:hover, &:active {
+        color: @bluePale;
+        text-decoration: underline !important;
+      }
+    }
+  }
+}
+
 .search-resume {
 .search-resume {
   .muted {
   .muted {
     color: lighten(@gray, 15%);
     color: lighten(@gray, 15%);

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

@@ -117,7 +117,7 @@
 
 
               </div>
               </div>
               <div class="post-message">
               <div class="post-message">
-                {% trans user=edit_user(post), date=post.current_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
+                {% trans user=edit_user(post), date=post.delete_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
               </div>
               </div>
             </dv>
             </dv>
           </div>
           </div>

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

@@ -72,6 +72,50 @@
         {{ macros.draw_message(post.message) }}
         {{ macros.draw_message(post.message) }}
       </div>
       </div>
       {% endif %}
       {% endif %}
+      {% if post.deleted and not acl.threads.can_see_deleted_posts(forum) %}
+      <div class="post-body post-muted">
+        {% if post.user_id %}
+        <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(50) }}" alt="" class="user-avatar"></a>
+        {% else %}
+        <img src="{{ macros.avatar_guest(60) }}" alt="" class="user-avatar">
+        {% endif %}
+        <div class="post-content">
+          <div class="post-header">
+            <div class="post-header-compact">
+              {% if post.user_id %}
+              <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="post-author">{{ post.user.username }}</a>{% if post.user.get_title() %} {{ user_label(post.user) }}{% endif %}
+              {% else %}
+              <span class="post-author">{{ post.user_name }}</span> <span class="label post-author-label post-label-guest">{% trans %}Unregistered{% endtrans %}</span>
+              {% endif %}
+              <span class="separator">&ndash;</span>
+              <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-date">{{ post.date|reltimesince }}</a>
+              {% if post.edits %}
+              <span class="separator">&ndash;</span>
+              {% if acl.threads.can_see_changelog(user, forum, post) %}
+              <a href="{% url 'report_changelog' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-changelog tooltip-bottom" title="{% trans %}Show changelog{% endtrans %}">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</a>
+              {% else %}
+              <span class="post-changelog">{% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</span>
+              {% endif %}
+              {% endif %}
+            </div>
+
+            <a href="{% url 'report_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="post-perma tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
+
+            {% if not post.is_read %}
+            <div class="post-extra">
+              <span class="label label-warning">
+                {% trans %}New{% endtrans %}
+              </span>
+            </div>
+            {% endif %}
+
+          </div>
+          <div class="post-message">
+            {% trans user=edit_user(post), date=post.delete_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
+          </div>
+        </dv>
+      </div>
+      {% else %}
       <div class="post-body">
       <div class="post-body">
         {% if post.user_id %}
         {% if post.user_id %}
         <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(100) }}" alt="" class="user-avatar"></a>
         <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}"><img src="{{ post.user.get_avatar(100) }}" alt="" class="user-avatar"></a>
@@ -226,6 +270,7 @@
           {% endfilter %}</div>
           {% endfilter %}</div>
         </div>
         </div>
       </div>
       </div>
+      {% endif %}
     </div>
     </div>
 
 
     {% if post.checkpoints_visible %}
     {% if post.checkpoints_visible %}

+ 1 - 1
templates/cranefly/search/home.html

@@ -13,5 +13,5 @@
 {% endblock %}
 {% endblock %}
 
 
 {% macro style_query(query) -%}
 {% macro style_query(query) -%}
-<a href="{{ ('%s_results'|format(search_route))|url }}">{{ query }}</a>
+<a href="{{ results_route|url }}">{{ query }}</a>
 {%- endmacro %}
 {%- endmacro %}

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

@@ -16,6 +16,16 @@
 </div>
 </div>
 
 
 <div class="container container-primary">
 <div class="container container-primary">
+  {% if suggestion %}
+  <div class="search-suggestion">
+    <p class="lead muted">{% trans %}Did you mean:{% endtrans %}</p>
+    <form action="{{ search_route|url() }}" class="inline-form" method="post">
+      <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      <input type="hidden" name="search_query" value="{{ suggestion }}">
+      <button type="submit" class="btn btn-link">{{ suggestion }}</button>
+    </form>
+  </div>
+  {% endif %}
   {% block action %}{% endblock %}
   {% block action %}{% endblock %}
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 1 - 1
templates/cranefly/threads/thread.html

@@ -109,7 +109,7 @@
 
 
           </div>
           </div>
           <div class="post-message">
           <div class="post-message">
-            {% trans user=edit_user(post), date=post.current_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
+            {% trans user=edit_user(post), date=post.delete_date|reltimesince|low %}{{ user }} has deleted this reply {{ date }}{% endtrans %}
           </div>
           </div>
         </dv>
         </dv>
       </div>
       </div>

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

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