Browse Source

- Forums app was split into Forums, Threads and Posts
- MPTT was implemented into Forums and Forms app
- Basic Forums admin

Ralfp 12 years ago
parent
commit
983bb5b2db

+ 37 - 2
misago/admin/layout/forums.py

@@ -1,6 +1,7 @@
 from django.conf.urls import patterns, include, url
 from django.utils.translation import ugettext_lazy as _
 from misago.admin import AdminAction
+from misago.forums.models import Forum
 
 ADMIN_ACTIONS=(
    AdminAction(
@@ -9,9 +10,43 @@ ADMIN_ACTIONS=(
                name=_("Forums List"),
                help=_("Create, edit and delete forums."),
                icon='comment',
+               model=Forum,
+               actions=[
+                        {
+                         'id': 'list',
+                         'name': _("Forums List"),
+                         'help': _("All existing forums"),
+                         'route': 'admin_forums'
+                         },
+                        {
+                         'id': 'new_category',
+                         'name': _("New Category"),
+                         'help': _("Create new category"),
+                         'route': 'admin_forums_new_category'
+                         },
+                        {
+                         'id': 'new_forum',
+                         'name': _("New Forum"),
+                         'help': _("Create new forum"),
+                         'route': 'admin_forums_new_forum'
+                         },
+                        {
+                         'id': 'new_redirect',
+                         'name': _("New Redirect"),
+                         'help': _("Create new redirect"),
+                         'route': 'admin_forums_new_redirect'
+                         },
+                        ],
                route='admin_forums',
-               urlpatterns=patterns('misago.admin.views',
-                        url(r'^$', 'todo', name='admin_forums'),
+               urlpatterns=patterns('misago.forums.views',
+                        url(r'^$', 'List', name='admin_forums'),
+                        url(r'^new/category/$', 'NewCategory', name='admin_forums_new_category'),
+                        url(r'^new/forum/$', 'NewForum', name='admin_forums_new_forum'),
+                        url(r'^new/redirect/$', 'NewRedirect', name='admin_forums_new_redirect'),
+                        url(r'^up/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Up', name='admin_forums_up'),
+                        url(r'^down/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Down', name='admin_forums_down'),
+                        url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_forums_edit'),
+                        url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_forums_delete'),
                     ),
                ),
    AdminAction(

+ 1 - 2
misago/admin/layout/users.py

@@ -39,8 +39,7 @@ ADMIN_ACTIONS=(
                         url(r'^edit/(?P<slug>[a-z0-9]+)-(?P<target>\d+)/$', 'Edit', name='admin_users_edit'),
                         url(r'^delete/(?P<slug>[a-z0-9]+)-(?P<target>\d+)/$', 'Delete', name='admin_users_delete'),
                     ),
-               ),
-               
+               ),               
    AdminAction(
                section='users',
                id='roles',

+ 15 - 17
misago/admin/widgets.py

@@ -247,16 +247,18 @@ class ListWidget(BaseWidget):
         pagination['stop'] = pagination['start'] + self.pagination
         return pagination
     
+    def get_items(self, request):
+        if request.session.get(self.get_token('filter')):
+            self.is_filtering = True
+            return self.set_filters(self.admin.model.objects, request.session.get(self.get_token('filter')))
+        return self.admin.model.objects
+            
     def __call__(self, request, page=0):
         """
         Use widget as view
         """
-        # Get basic list attributes
-        if request.session.get(self.get_token('filter')):
-            self.is_filtering = True
-            items_total = self.set_filters(self.admin.model.objects, request.session.get(self.get_token('filter')))
-        else:
-            items_total = self.admin.model.objects
+        # Get basic list items
+        items_total = self.get_items(request)
             
         # Set extra filters?
         try:
@@ -269,12 +271,8 @@ class ListWidget(BaseWidget):
         paginating_method = self.get_pagination(request, items_total, page)
         
         # List items
-        items = self.admin.model.objects
-        
-        # Filter items?
-        if request.session.get(self.get_token('filter')):
-            items = self.set_filters(items, request.session.get(self.get_token('filter')))
-        else:
+        items = self.get_items(request)
+        if not request.session.get(self.get_token('filter')):
             items = items.all()
          
         # Set extra filters?
@@ -412,13 +410,13 @@ class FormWidget(BaseWidget):
     def get_url(self, request, model):
         return reverse(self.admin.get_action_attr(self.id, 'route'))
     
-    def get_form(self, request, model):
+    def get_form(self, request, target):
         return self.form
     
-    def get_form_instance(self, form, request, model, initial, post=False):
+    def get_form_instance(self, form, request, target, initial, post=False):
         if post:
-            return form(request.POST, request=request, initial=self.get_initial_data(request, model))
-        return form(request=request, initial=self.get_initial_data(request, model))
+            return form(request.POST, request=request, initial=self.get_initial_data(request, target))
+        return form(request=request, initial=self.get_initial_data(request, target))
     
     def get_layout(self, request, form, model):
         if self.layout:
@@ -445,7 +443,7 @@ class FormWidget(BaseWidget):
         original_model = model
         
         # Get form type to instantiate
-        FormType = self.get_form(request, target)
+        FormType = self.get_form(request, model)
         
         #Submit form
         message = None

+ 1 - 1
misago/forms/__init__.py

@@ -45,7 +45,7 @@ class Form(forms.Form):
         """
         for key, field in self.base_fields.iteritems():
             try:
-                if field.__class__.__name__ == 'ModelChoiceField' and data[key]:
+                if field.__class__.__name__ in ['ModelChoiceField', 'TreeForeignKey'] and data[key]:
                     data[key] = int(data[key])
                 elif field.__class__.__name__ == 'ModelMultipleChoiceField':
                     data.setlist(key, [int(x) for x in data.getlist(key, [])])

+ 0 - 4
misago/forums/admin/views.py

@@ -1,4 +0,0 @@
-from django.http import HttpResponse
-
-def forums_list(request):
-    return HttpResponse("UNIMPLEMENTED ADMIN ACTION!")

+ 9 - 1
misago/forums/fixtures.py

@@ -1,4 +1,5 @@
 from misago.monitor.fixtures import load_monitor_fixture
+from misago.forums.models import Forum
 
 monitor_fixtures = {
                   'threads': 0,
@@ -7,4 +8,11 @@ monitor_fixtures = {
 
 
 def load_fixtures():
-    load_monitor_fixture(monitor_fixtures)
+    load_monitor_fixture(monitor_fixtures)
+    
+    root_forum = Forum(
+                       token='root',
+                       name='root',
+                       slug='root',
+                       )
+    Forum.objects.insert_node(root_forum,target=None,save=True)

+ 91 - 0
misago/forums/forms.py

@@ -0,0 +1,91 @@
+from django.utils.translation import ugettext_lazy as _
+from django import forms
+from mptt.forms import TreeNodeChoiceField
+from misago.forms import Form
+from misago.forums.models import Forum
+
+class CategoryForm(Form):
+    parent = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(include_self=True),level_indicator=u'- - ')
+    name = forms.CharField(max_length=255)
+    description = forms.CharField(widget=forms.Textarea,required=False)
+    template = forms.ChoiceField(choices=(
+                                          ('rows', _('One forum per row')),
+                                          ('fifty', _('Two forums per row')),
+                                          ('thirty', _('Three forums per row')),
+                                          ))
+    
+    layout = (
+              (
+               _("Category Options"),
+               (
+                ('parent', {'label': _("Category Parent")}),
+                ('name', {'label': _("Category Name")}),
+                ('description', {'label': _("Category Description")}),
+                ('template', {'label': _("Category Layout")}),
+                ),
+              ),
+             )
+    
+
+class ForumForm(Form):
+    parent = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ')
+    name = forms.CharField(max_length=255)
+    description = forms.CharField(widget=forms.Textarea,required=False)
+    template = forms.ChoiceField(choices=(
+                                          ('rows', _('One forum per row')),
+                                          ('fifty', _('Two forums per row')),
+                                          ('thirty', _('Three forums per row')),
+                                          ))
+    prune_start = forms.IntegerField(min_value=0,initial=0)
+    prune_last = forms.IntegerField(min_value=0,initial=0)
+    
+    layout = (
+              (
+               _("Forum Options"),
+               (
+                ('parent', {'label': _("Forum Parent")}),
+                ('name', {'label': _("Forum Name")}),
+                ('description', {'label': _("Forum Description")}),
+                ('template', {'label': _("Subforums Layout")}),
+                ),
+              ),
+              (
+               _("Prune Forum"),
+               (
+                ('prune_start', {'label': _("Delete threads with first post older than"), 'help_text': _('Enter number of days since topic start after which topic will be deleted or zero to don\'t delete topics.')}),
+                ('prune_last', {'label': _("Delete threads with last post older than"), 'help_text': _('Enter number of days since since last reply in topic after which topic will be deleted or zero to don\'t delete topics.')}),
+                ),
+              ),
+             )
+
+
+class RedirectForm(Form):
+    parent = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ')
+    name = forms.CharField(max_length=255)
+    description = forms.CharField(widget=forms.Textarea,required=False)
+    redirect = forms.URLField(max_length=255)
+    
+    layout = (
+              (
+               _("Redirect Options"),
+               (
+                ('parent', {'label': _("Redirect Parent")}),
+                ('name', {'label': _("Redirect Name")}),
+                ('redirect', {'label': _("Redirect URL")}),
+                ('description', {'label': _("Redirect Description")}),
+                ),
+              ),
+             )
+    
+
+class DeleteForm(Form):
+    parent = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),required=False,empty_label=_("Remove with forum"),level_indicator=u'- - ')
+   
+    layout = (
+              (
+               _("Delete Options"),
+               (
+                ('parent', {'label': _("Move deleted Forum contents to")}),
+                ),
+              ),
+             )

+ 22 - 137
misago/forums/models.py

@@ -5,157 +5,42 @@ from mptt.models import MPTTModel, TreeForeignKey
 
 class Forum(MPTTModel):
     parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
-    position = models.IntegerField()   
-    role = models.CharField(max_length=12, choices=(
-        ('cat', 'Category'),
-        ('for', 'Forum'),
-        ('red', 'Redirect')
-    ))
-    special = models.CharField(max_length=255,null=True, blank=True)
+    type = models.CharField(max_length=12)
+    token = models.CharField(max_length=255,null=True, blank=True)
     name = models.CharField(max_length=255)
     slug = models.SlugField(max_length=255)
-    style = models.CharField(max_length=255)
     description = models.TextField(null=True, blank=True)
     description_preparsed = models.TextField(null=True, blank=True)
     threads = models.PositiveIntegerField(default=0)
     posts = models.PositiveIntegerField(default=0)
-    last_thread = models.ForeignKey('Thread', related_name='+', null=True, blank=True)
+    #last_thread = models.ForeignKey('threads.Thread', related_name='+', null=True, blank=True)
     last_thread_name = models.CharField(max_length=255, null=True, blank=True)
     last_thread_slug = models.SlugField(null=True, blank=True)
     last_thread_date = models.DateTimeField(null=True, blank=True)
-    last_poster = models.ForeignKey('users.User', related_name='+')
-    last_poster_name = models.CharField(max_length=255, null=True, blank=True)
-    last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
-    last_poster_style = models.CharField(max_length=255, null=True, blank=True)
-    closed = models.BooleanField(default=False)
-    
-    class MPTTMeta:
-            order_insertion_by = ['position']
-
-
-class ThreadManager(models.Manager):
-    def filter_stats(self, start, end):
-        return self.filter(start__gte=start).filter(start__lte=end)
-
-
-class Thread(models.Model):
-    forum = models.ForeignKey(Forum, related_name='+')
-    name = models.CharField(max_length=255)
-    slug = models.SlugField(max_length=255)
-    replies = models.PositiveIntegerField()
-    views = models.PositiveIntegerField(default=0)
-    start = models.DateTimeField(default=0)
-    start_post = models.ForeignKey('Post', related_name='+', null=True, blank=True)
-    start_poster = models.ForeignKey('users.User', related_name='+', null=True, blank=True)
-    start_poster_name = models.CharField(max_length=255)
-    start_poster_slug = models.SlugField(max_length=255)
-    start_poster_style = models.CharField(max_length=255)
-    last = models.DateTimeField()
-    last_post = models.ForeignKey('Post', related_name='+', null=True, blank=True)
     last_poster = models.ForeignKey('users.User', related_name='+', null=True, blank=True)
     last_poster_name = models.CharField(max_length=255, null=True, blank=True)
     last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
     last_poster_style = models.CharField(max_length=255, null=True, blank=True)
-    poster_styles_list = models.CharField(max_length=255, null=True, blank=True)
-    hidden = models.BooleanField(default=False)
+    prune_start = models.PositiveIntegerField(default=0)
+    prune_last = models.PositiveIntegerField(default=0)
+    redirect = models.CharField(max_length=255, null=True, blank=True)
+    template = models.CharField(max_length=255, null=True, blank=True)
     closed = models.BooleanField(default=False)
     
-    objects = ThreadManager()
-    
-    statistics_name = _('New Threads')
-        
-    def get_date(self):
-        return self.start
-
-
-class PostManager(models.Manager):
-    def filter_stats(self, start, end):
-        return self.filter(date__gte=start).filter(date__lte=end)
-    
-
-class Post(models.Model):
-    forum = models.ForeignKey(Forum, related_name='+')
-    thread = models.ForeignKey(Thread, related_name='+')
-    user = models.ForeignKey('users.User', related_name='+', null=True, blank=True)
-    user_name = models.CharField(max_length=255)
-    ip = models.GenericIPAddressField()
-    agent = models.CharField(max_length=255)
-    post = models.TextField()
-    post_preparsed = models.TextField()
-    upvotes = models.IntegerField(default=0)
-    downvotes = models.IntegerField(default=0)
-    date = models.DateTimeField()
-    attachments = models.BooleanField(default=False)
-    attachments_list = models.CommaSeparatedIntegerField(max_length=255)
-    edited = models.BooleanField(default=False)
-    edits = models.PositiveIntegerField(default=0)
-    edit_date = models.DateTimeField(null=True, blank=True)
-    edit_reason = models.CharField(max_length=255, null=True, blank=True)
-    edit_user = models.ForeignKey('users.User', related_name='+', null=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)
-    reported = models.BooleanField(default=False)
-    hidden = models.BooleanField(default=False)
-    protected = models.BooleanField(default=False)
-    
-    objects = PostManager()
-    
-    statistics_name = _('New Posts')
-    
-    def get_date(self):
-        return self.date
-
-
-class AttachmentType(models.Model):
-    mime = models.CharField(max_length=255)
-    extension = models.CharField(max_length=255)
-    
+    def __unicode__(self):
+        if self.token == 'root':
+           return unicode(_('Root Category')) 
+        return unicode(self.name)
+    
+    def set_description(self, description):
+        self.description = description.strip()
+        self.description_preparsed = ''
+        if self.description:
+            import markdown
+            self.description_preparsed = markdown.markdown(description, safe_mode='escape', output_format=settings.OUTPUT_FORMAT)
         
-class Attachment(models.Model):
-    forum = models.ForeignKey(Forum, related_name='+')
-    thread = models.ForeignKey(Thread, related_name='+')
-    post = models.ForeignKey(Post, related_name='+')
-    type = models.ForeignKey(AttachmentType, related_name='+')
-    user = models.ForeignKey('users.User', related_name='+', null=True, blank=True)
-    created = models.DateTimeField()
-    size = models.PositiveIntegerField(default=0)
-    name = models.CharField(max_length=255)
-    file = models.FileField(upload_to=settings.MEDIA_ROOT + '/attachments/%m_%Y/',max_length=255)
-    downloads = models.PositiveIntegerField(default=0)
-    
-    
-class Poll(models.Model):
-    forum = models.ForeignKey(Forum, related_name='+')
-    thread = models.ForeignKey(Thread, related_name='+')
-    name = models.CharField(max_length=255)
-    name_slug = models.SlugField(max_length=255)
-    user = models.ForeignKey('users.User', related_name='+')
-    user_name = models.CharField(max_length=255)
-    user_slug = models.SlugField(max_length=255)
-    public = models.BooleanField(default=False)
-    multiple = models.BooleanField(default=False)
-    changing = models.BooleanField(default=False)
-    created = models.DateTimeField()
-    length = models.PositiveIntegerField(default=0)
-    votes = models.PositiveIntegerField(default=0)
-    
-    
-class Vote(models.Model):
-    forum = models.ForeignKey(Forum, related_name='+')
-    thread = models.ForeignKey(Thread, related_name='+')
-    poll = models.ForeignKey(Poll, related_name='+')
-    user = models.ForeignKey('users.User', related_name='+', null=True, blank=True)
-    ip = models.GenericIPAddressField()
-    option = models.PositiveIntegerField()
-    
-    
-class Report(models.Model):
-    forum = models.ForeignKey(Forum, related_name='+')
-    thread = models.ForeignKey(Thread, related_name='+')
-    post = models.ForeignKey(Post, related_name='+')
-    
+    def move_content(self, target):
+        pass
     
-class Edit(models.Model):
-    forum = models.ForeignKey(Forum, related_name='+')
-    thread = models.ForeignKey(Thread, related_name='+')
-    post = models.ForeignKey(Post, related_name='+')    
+    def prune(self):
+        pass

+ 287 - 1
misago/forums/views.py

@@ -1 +1,287 @@
-# Create your views here.
+from django.core.urlresolvers import reverse as django_reverse
+from django.db.models import Q
+from django.utils.translation import ugettext as _
+from mptt.forms import TreeNodeChoiceField
+from misago.admin import site
+from misago.admin.widgets import *
+from misago.utils import slugify
+from misago.forums.forms import CategoryForm, ForumForm, RedirectForm, DeleteForm
+from misago.forums.models import Forum
+
+def reverse(route, target=None):
+    if target:
+        return django_reverse(route, kwargs={'target': target.pk, 'slug': target.slug})
+    return django_reverse(route)
+
+"""
+Views
+"""
+class List(ListWidget):
+    admin = site.get_action('forums')
+    id = 'list'
+    columns=(
+             ('forum', _("Forum")),
+             )
+    nothing_checked_message = _('You have to select at least one forum.')
+    actions=(
+             ('resync', _("Resynchronise forums")),
+             ('prune', _("Prune forums"), _("Are you sure you want to delete all content from selected forums?")),
+             )
+    empty_message = _('No forums are currently defined.')
+    
+    def get_items(self, request):
+        return self.admin.model.objects.get(token='root').get_descendants()
+    
+    def sort_items(self, request, page_items, sorting_method):
+        return page_items.order_by('lft')
+    
+    def get_item_actions(self, request, item):
+        if item.type == 'category':
+            return (
+                    self.action('chevron-up', _("Move Category Up"), reverse('admin_forums_up', item), post=True),
+                    self.action('chevron-down', _("Move Category Down"), reverse('admin_forums_down', item), post=True),
+                    self.action('pencil', _("Edit Category"), reverse('admin_forums_edit', item)),
+                    self.action('remove', _("Delete Category"), reverse('admin_forums_delete', item)),
+                    )
+            
+        if item.type == 'forum':
+            return (
+                    self.action('chevron-up', _("Move Forum Up"), reverse('admin_forums_up', item), post=True),
+                    self.action('chevron-down', _("Move Forum Down"), reverse('admin_forums_down', item), post=True),
+                    self.action('pencil', _("Edit Forum"), reverse('admin_forums_edit', item)),
+                    self.action('remove', _("Delete Forum"), reverse('admin_forums_delete', item)),
+                    )
+            
+        return (
+                self.action('chevron-up', _("Move Redirect Up"), reverse('admin_forums_up', item), post=True),
+                self.action('chevron-down', _("Move Redirect Down"), reverse('admin_forums_down', item), post=True),
+                self.action('pencil', _("Edit Redirect"), reverse('admin_forums_edit', item)),
+                self.action('remove', _("Delete Redirect"), reverse('admin_forums_delete', item)),
+                )
+
+    def action_resync(self, request, items, checked):
+        return Message(_('Selected forums have been resynchronised successfully.'), 'success'), reverse('admin_forums')
+
+    def action_prune(self, request, items, checked):
+        return Message(_('Selected forums have been pruned successfully.'), 'success'), reverse('admin_forums')
+
+
+class NewCategory(FormWidget):
+    admin = site.get_action('forums')
+    id = 'new_category'
+    fallback = 'admin_forums' 
+    form = CategoryForm
+    submit_button = _("Save Category")
+        
+    def get_new_url(self, request, model):
+        return reverse('admin_forums_new_category')
+    
+    def get_edit_url(self, request, model):
+        return reverse('admin_forums_edit', model)
+    
+    def submit_form(self, request, form, target):
+        new_forum = Forum(
+                     name=form.cleaned_data['name'],
+                     slug=slugify(form.cleaned_data['name']),
+                     type='category',
+                     template=form.cleaned_data['template'],
+                     )
+        new_forum.set_description(form.cleaned_data['description'])
+        new_forum.insert_at(form.cleaned_data['parent'], position='last-child', save=True)
+        return new_forum, Message(_('New Category has been created.'), 'success')
+
+
+class NewForum(FormWidget):
+    admin = site.get_action('forums')
+    id = 'new_forum'
+    fallback = 'admin_forums' 
+    form = ForumForm
+    submit_button = _("Save Forum")
+        
+    def get_new_url(self, request, model):
+        return reverse('admin_forums_new_forum')
+    
+    def get_edit_url(self, request, model):
+        return reverse('admin_forums_edit', model)
+    
+    def submit_form(self, request, form, target):
+        new_forum = Forum(
+                     name=form.cleaned_data['name'],
+                     slug=slugify(form.cleaned_data['name']),
+                     type='forum',
+                     template=form.cleaned_data['template'],
+                     prune_days=form.cleaned_data['prune_days'],
+                     prune_start=form.cleaned_data['prune_start'],
+                     )
+        new_forum.set_description(form.cleaned_data['description'])
+        new_forum.insert_at(form.cleaned_data['parent'], position='last-child', save=True)
+        return new_forum, Message(_('New Forum has been created.'), 'success')
+
+    def __call__(self, request):
+        if self.admin.model.objects.get(token='root').get_descendants().count() == 0:
+            request.messages.set_flash(Message(_("You have to create at least one category before you will be able to create forums.")), 'error', self.admin.id)
+            return redirect(self.get_fallback_url(request))
+        return super(NewForum, self).__call__(request)
+
+
+class NewRedirect(FormWidget):
+    admin = site.get_action('forums')
+    id = 'new_redirect'
+    fallback = 'admin_forums' 
+    form = RedirectForm
+    submit_button = _("Save Forum")
+        
+    def get_new_url(self, request, model):
+        return reverse('admin_forums_new_redirect')
+    
+    def get_edit_url(self, request, model):
+        return reverse('admin_forums_edit', model)
+    
+    def submit_form(self, request, form, target):
+        new_forum = Forum(
+                     name=form.cleaned_data['name'],
+                     slug=slugify(form.cleaned_data['name']),
+                     redirect=form.cleaned_data['redirect'],
+                     type='redirect',
+                     )
+        new_forum.set_description(form.cleaned_data['description'])
+        new_forum.insert_at(form.cleaned_data['parent'], position='last-child', save=True)
+        return new_forum, Message(_('New Redirect has been created.'), 'success')
+    
+    def __call__(self, request):
+        if self.admin.model.objects.get(token='root').get_descendants().count() == 0:
+            request.messages.set_flash(Message(_("You have to create at least one category before you will be able to create redirects.")), 'error', self.admin.id)
+            return redirect(self.get_fallback_url(request))
+        return super(NewRedirect, self).__call__(request)
+
+
+class Up(ButtonWidget):
+    admin = site.get_action('forums')
+    id = 'up'
+    fallback = 'admin_forums'
+    notfound_message = _('Requested Forum could not be found.')
+    
+    def action(self, request, target):
+        previous_sibling = target.get_previous_sibling()
+        if previous_sibling:
+            target.move_to(previous_sibling, 'left')
+            return Message(_('Forum "%(name)s" has been moved up.') % {'name': target.name}, 'success'), False
+        return Message(_('Forum "%(name)s" is first child of its parent node and cannot be moved up.') % {'name': target.name}, 'info'), False
+
+
+class Down(ButtonWidget):
+    admin = site.get_action('forums')
+    id = 'down'
+    fallback = 'admin_forums'
+    notfound_message = _('Requested Forum could not be found.')
+    
+    def action(self, request, target):
+        next_sibling = target.get_next_sibling()
+        if next_sibling:
+            target.move_to(next_sibling, 'right')
+            return Message(_('Forum "%(name)s" has been moved down.') % {'name': target.name}, 'success'), False
+        return Message(_('Forum "%(name)s" is last child of its parent node and cannot be moved down.') % {'name': target.name}, 'info'), False
+  
+   
+class Edit(FormWidget):
+    admin = site.get_action('forums')
+    id = 'edit'
+    name = _("Edit Forum")
+    fallback = 'admin_forums'
+    form = ForumForm
+    target_name = 'name'
+    notfound_message = _('Requested Forum could not be found.')
+    submit_fallback = True
+    
+    def get_url(self, request, model):
+        return reverse('admin_forums_edit', model)
+    
+    def get_edit_url(self, request, model):
+        return self.get_url(request, model)
+    
+    def get_form(self, request, target):
+        if target.type == 'category':
+            self.name= _("Edit Category")
+            self.form = CategoryForm
+        if target.type == 'redirect':
+            self.name= _("Edit Redirect")
+            self.form = RedirectForm
+        
+        # Remove invalid targets from parent select
+        valid_targets = Forum.tree.get(token='root').get_descendants(include_self=target.type == 'category').exclude(Q(lft__gte=target.lft) & Q(rght__lte=target.rght))
+        self.form.base_fields['parent'] = TreeNodeChoiceField(queryset=valid_targets,level_indicator=u'- - ')
+        
+        return self.form
+    
+    def get_initial_data(self, request, model):
+        initial = {
+                   'parent': model.parent,
+                   'name': model.name,
+                   'description': model.description,
+                   }
+            
+        if model.type == 'redirect':
+            initial['redirect'] = model.redirect
+        else:
+            initial['template'] = model.template
+            
+        if model.type == 'forum':
+            initial['prune_start'] = model.prune_start
+            initial['prune_last'] = model.prune_last
+        
+        return initial
+    
+    def submit_form(self, request, form, target):
+        target.name = form.cleaned_data['name']
+        target.set_description(form.cleaned_data['description'])
+        if target.type == 'redirect':
+            target.redirect = form.cleaned_data['redirect']
+        else:
+            target.template = form.cleaned_data['template']
+        if target.type == 'forum':
+            target.prune_start = form.cleaned_data['prune_start']
+            target.prune_last = form.cleaned_data['prune_last']
+        if form.cleaned_data['parent'].pk != target.parent.pk:
+            target.move_to(form.cleaned_data['parent'], 'last-child')
+        target.save(force_update=True)
+        
+        return target, Message(_('Changes in forum "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
+
+
+class Delete(FormWidget):
+    admin = site.get_action('forums')
+    id = 'delete'
+    name = _("Delete Forum")
+    fallback = 'admin_forums'
+    form = DeleteForm
+    target_name = 'name'
+    notfound_message = _('Requested Forum could not be found.')
+    submit_fallback = True
+    
+    def get_url(self, request, model):
+        return reverse('admin_forums_delete', model)
+   
+    def get_form(self, request, target):
+        if target.type == 'category':
+            self.name= _("Delete Category")
+        if target.type == 'redirect':
+            self.name= _("Delete Redirect")
+        
+        # Remove invalid targets from parent select
+        valid_targets = Forum.tree.get(token='root').get_descendants(include_self=target.type == 'category').exclude(Q(lft__gte=target.lft) & Q(rght__lte=target.rght))
+        self.form.base_fields['parent'] = TreeNodeChoiceField(queryset=valid_targets,required=False,empty_label=_("Remove with forum"),level_indicator=u'- - ')
+        
+        return self.form
+        
+    def submit_form(self, request, form, target):
+        new_parent = form.cleaned_data['parent']
+        if new_parent:
+            target.move_content(new_parent)
+            for child in target.get_descendants():
+                child.move_to(new_parent, 'last-child')
+                child.save(force_update=True)
+            target.delete()
+        else:
+            for child in target.get_descendants(include_self=True):
+                child.delete()
+        return target, Message(_('Forum "%(name)s" has been deleted.') % {'name': self.original_name}, 'success')

+ 0 - 0
misago/forums/admin/__init__.py → misago/posts/__init__.py


+ 10 - 0
misago/posts/fixtures.py

@@ -0,0 +1,10 @@
+from misago.monitor.fixtures import load_monitor_fixture
+
+monitor_fixtures = {
+                  'threads': 0,
+                  'posts': 0,
+                  }
+
+
+def load_fixtures():
+    load_monitor_fixture(monitor_fixtures)

+ 39 - 0
misago/posts/models.py

@@ -0,0 +1,39 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+class PostManager(models.Manager):
+    def filter_stats(self, start, end):
+        return self.filter(date__gte=start).filter(date__lte=end)
+    
+
+class Post(models.Model):
+    forum = models.ForeignKey(Forum, related_name='+')
+    thread = models.ForeignKey(Thread, related_name='+')
+    user = models.ForeignKey('users.User', related_name='+', null=True, blank=True)
+    user_name = models.CharField(max_length=255)
+    ip = models.GenericIPAddressField()
+    agent = models.CharField(max_length=255)
+    post = models.TextField()
+    post_preparsed = models.TextField()
+    upvotes = models.IntegerField(default=0)
+    downvotes = models.IntegerField(default=0)
+    date = models.DateTimeField()
+    attachments = models.BooleanField(default=False)
+    attachments_list = models.CommaSeparatedIntegerField(max_length=255)
+    edited = models.BooleanField(default=False)
+    edits = models.PositiveIntegerField(default=0)
+    edit_date = models.DateTimeField(null=True, blank=True)
+    edit_reason = models.CharField(max_length=255, null=True, blank=True)
+    edit_user = models.ForeignKey('users.User', related_name='+', null=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)
+    reported = models.BooleanField(default=False)
+    hidden = models.BooleanField(default=False)
+    protected = models.BooleanField(default=False)
+    
+    objects = PostManager()
+    
+    statistics_name = _('New Posts')
+    
+    def get_date(self):
+        return self.date

+ 1 - 0
misago/posts/views.py

@@ -0,0 +1 @@
+# Create your views here.

+ 0 - 1
misago/roles/views.py

@@ -20,7 +20,6 @@ class List(ListWidget):
     columns=(
              ('role', _("Role")),
              )
-    table_form_button = _('Reorder Roles')
     nothing_checked_message = _('You have to check at least one role.')
     actions=(
              ('delete', _("Delete selected roles"), _("Are you sure you want to delete selected roles?")),

+ 0 - 0
misago/threads/__init__.py


+ 10 - 0
misago/threads/fixtures.py

@@ -0,0 +1,10 @@
+from misago.monitor.fixtures import load_monitor_fixture
+
+monitor_fixtures = {
+                  'threads': 0,
+                  'posts': 0,
+                  }
+
+
+def load_fixtures():
+    load_monitor_fixture(monitor_fixtures)

+ 36 - 0
misago/threads/models.py

@@ -0,0 +1,36 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+class ThreadManager(models.Manager):
+    def filter_stats(self, start, end):
+        return self.filter(start__gte=start).filter(start__lte=end)
+
+
+class Thread(models.Model):
+    forum = models.ForeignKey(Forum, related_name='+')
+    name = models.CharField(max_length=255)
+    slug = models.SlugField(max_length=255)
+    replies = models.PositiveIntegerField()
+    views = models.PositiveIntegerField(default=0)
+    start = models.DateTimeField(default=0)
+    start_post = models.ForeignKey('Post', related_name='+', null=True, blank=True)
+    start_poster = models.ForeignKey('users.User', related_name='+', null=True, blank=True)
+    start_poster_name = models.CharField(max_length=255)
+    start_poster_slug = models.SlugField(max_length=255)
+    start_poster_style = models.CharField(max_length=255)
+    last = models.DateTimeField()
+    last_post = models.ForeignKey('Post', related_name='+', null=True, blank=True)
+    last_poster = models.ForeignKey('users.User', related_name='+', null=True, blank=True)
+    last_poster_name = models.CharField(max_length=255, null=True, blank=True)
+    last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
+    last_poster_style = models.CharField(max_length=255, null=True, blank=True)
+    poster_styles_list = models.CharField(max_length=255, null=True, blank=True)
+    hidden = models.BooleanField(default=False)
+    closed = models.BooleanField(default=False)
+    
+    objects = ThreadManager()
+    
+    statistics_name = _('New Threads')
+        
+    def get_date(self):
+        return self.start

+ 1 - 0
misago/threads/views.py

@@ -0,0 +1 @@
+# Create your views here.

+ 1 - 1
misago/users/models.py

@@ -285,7 +285,7 @@ class User(models.Model):
         self.signature_preparsed = ''
         if self.signature:
             import markdown
-            self.signature_preparsed = markdown.markdown(value, safe_mode='escape', output_format=settings.OUTPUT_FORMAT)
+            self.signature_preparsed = markdown.markdown(signature, safe_mode='escape', output_format=settings.OUTPUT_FORMAT)
         
     def is_username_valid(self, e):
         try:

+ 3 - 3
misago/views.py

@@ -3,10 +3,10 @@ from django.shortcuts import redirect
 from django.template import RequestContext
 
 
-def home(request):    
+def home(request):
     return request.theme.render_to_response('index.html',
-                                            {'page_title': 'Hello World!'},
-                                            context_instance=RequestContext(request));
+                                        {'page_title': 'Hello World!'},
+                                        context_instance=RequestContext(request));
 
 
 def redirect_message(request, message, type='info', owner=None):

+ 20 - 0
templates/admin/forums/list.html

@@ -0,0 +1,20 @@
+{% extends "admin/admin/list.html" %}
+{% load i18n %}
+{% load l10n %}
+{% load url from future %}
+
+{% block table_row scoped %}
+  <td class="lead-cell" style="padding-left: {{ 8 + ((item.level - 1) * 24) }}px;">
+  	<i class="icon-{{ forum_icon(item.type) }}"></i> <strong>{{ item.name }}</strong>
+  </td>
+{% endblock%}
+
+{% macro forum_icon(forum_type) -%}
+{%- if forum_type == 'category' -%}
+folder-open
+{%- elif forum_type == 'forum' -%}
+list
+{%- else -%}
+globe
+{%- endif -%}
+{%- endmacro %}