Browse Source

- Merge threads
- Sync threads

Ralfp 12 years ago
parent
commit
e046d454e0

+ 56 - 13
misago/threads/forms.py

@@ -3,7 +3,20 @@ from django.utils.translation import ungettext, ugettext_lazy as _
 from misago.forms import Form
 from misago.forms import Form
 from misago.utils import slugify
 from misago.utils import slugify
 
 
-class PostForm(Form):
+class ThreadNameMixin(object):
+    def clean_thread_name(self):
+        data = self.cleaned_data['thread_name']
+        slug = slugify(data)
+        if len(slug) < self.request.settings['thread_name_min']:
+            raise forms.ValidationError(ungettext(
+                                                  "Thread name must contain at least one alpha-numeric character.",
+                                                  "Thread name must contain at least %(count)d alpha-numeric characters.",
+                                                  self.request.settings['thread_name_min']
+                                                  ) % {'count': self.request.settings['thread_name_min']})
+        return data
+
+
+class PostForm(Form, ThreadNameMixin):
     thread_name = forms.CharField(max_length=255)
     thread_name = forms.CharField(max_length=255)
     post = forms.CharField(widget=forms.Textarea)
     post = forms.CharField(widget=forms.Textarea)
 
 
@@ -26,17 +39,6 @@ class PostForm(Form):
             del self.fields['thread_name']
             del self.fields['thread_name']
             del self.layout[0][1][0]
             del self.layout[0][1][0]
             
             
-    def clean_thread_name(self):
-        data = self.cleaned_data['thread_name']
-        slug = slugify(data)
-        if len(slug) < self.request.settings['thread_name_min']:
-            raise forms.ValidationError(ungettext(
-                                                  "Thread name must contain at least one alpha-numeric character.",
-                                                  "Thread name must contain at least %(count)d alpha-numeric characters.",
-                                                  self.request.settings['thread_name_min']
-                                                  ) % {'count': self.request.settings['thread_name_min']})
-        return data
-            
     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']:
@@ -50,4 +52,45 @@ class PostForm(Form):
         
         
 
 
 class QuickReplyForm(Form):
 class QuickReplyForm(Form):
-    post = forms.CharField(widget=forms.Textarea)
+    post = forms.CharField(widget=forms.Textarea)
+
+
+class MergeThreadsForm(Form, ThreadNameMixin):
+    def __init__(self, data=None, request=None, threads=[], *args, **kwargs):
+        self.threads = threads
+        super(MergeThreadsForm, self).__init__(data, request=request, *args, **kwargs)
+    
+    def finalize_form(self):
+        self.fields['thread_name'] = forms.CharField(max_length=255, initial=self.threads[0].name)
+        self.layout = [
+                       [
+                        _("Thread Options"),
+                        [
+                         ('thread_name', {'label': _("Thread Name"), 'help_text': _("Name of new thread that will be created as result of merge.")}),
+                         ],
+                        ],
+                       [
+                        _("Merge Order"),
+                        [
+                         ],
+                        ],
+                       ]
+        
+        choices = []
+        for i, thread in enumerate(self.threads):
+            choices.append((str(i), i + 1))
+        for i, thread in enumerate(self.threads):
+            self.fields['thread_%s' % thread.pk] = forms.ChoiceField(choices=choices,initial=str(i))
+            self.layout[1][1].append(('thread_%s' % thread.pk, {'label': thread.name}))
+            
+    def clean(self):        
+        cleaned_data = super(MergeThreadsForm, self).clean()
+        self.merge_order = {}
+        lookback = []
+        for thread in self.threads:
+            order = int(cleaned_data['thread_%s' % thread.pk])
+            if order in lookback:
+                raise forms.ValidationError(_("One or more threads have same position in merge order."))
+            lookback.append(order)
+            self.merge_order[order] = thread
+        return cleaned_data

+ 32 - 0
misago/threads/models.py

@@ -1,6 +1,7 @@
 from django.db import models
 from django.db import models
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
+from misago.utils import slugify
 
 
 class ThreadManager(models.Manager):
 class ThreadManager(models.Manager):
     def filter_stats(self, start, end):
     def filter_stats(self, start, end):
@@ -17,6 +18,7 @@ class Thread(models.Model):
     replies_reported = models.PositiveIntegerField(default=0)
     replies_reported = models.PositiveIntegerField(default=0)
     replies_moderated = models.PositiveIntegerField(default=0)
     replies_moderated = models.PositiveIntegerField(default=0)
     replies_deleted = models.PositiveIntegerField(default=0)
     replies_deleted = models.PositiveIntegerField(default=0)
+    merges = models.PositiveIntegerField(default=0,db_index=True)
     score = models.PositiveIntegerField(default=30,db_index=True)
     score = models.PositiveIntegerField(default=30,db_index=True)
     upvotes = models.PositiveIntegerField(default=0)
     upvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
@@ -43,6 +45,35 @@ class Thread(models.Model):
     def get_date(self):
     def get_date(self):
         return self.start
         return self.start
     
     
+    def sync(self):
+        # First post
+        start_post = self.post_set.order_by('merge', 'id')[1:][0]
+        self.start = start_post.date
+        self.start_post = start_post
+        self.start_poster = start_post.user
+        self.start_poster_name = start_post.user_name
+        self.start_poster_slug = slugify(start_post.user_name)
+        self.start_poster_style = start_post.user.rank.style if start_post.user else None
+        self.upvotes = start_post.upvotes
+        self.downvotes = start_post.downvotes
+        # Last post
+        last_post = self.post_set.order_by('-merge', '-id')[1:][0]
+        self.last = last_post.date
+        self.last_post = last_post
+        self.last_poster = last_post.user
+        self.last_poster_name = last_post.user_name
+        self.last_poster_slug = slugify(last_post.user_name)
+        self.last_poster_style = last_post.user.rank.style if last_post.user else None
+        # Flags
+        self.moderated = start_post.moderated
+        self.deleted = start_post.deleted
+        self.merges = last_post.merge
+        # Counters
+        self.replies = self.post_set.filter(moderated=False).filter(deleted=False).count() - 1
+        self.replies_reported = self.post_set.filter(reported=True).count()
+        self.replies_moderated = self.post_set.filter(moderated=True).count()
+        self.replies_deleted = self.post_set.filter(deleted=True).count()
+    
 
 
 class PostManager(models.Manager):
 class PostManager(models.Manager):
     def filter_stats(self, start, end):
     def filter_stats(self, start, end):
@@ -52,6 +83,7 @@ class PostManager(models.Manager):
 class Post(models.Model):
 class Post(models.Model):
     forum = models.ForeignKey('forums.Forum')
     forum = models.ForeignKey('forums.Forum')
     thread = models.ForeignKey(Thread)
     thread = models.ForeignKey(Thread)
+    merge = models.PositiveIntegerField(default=0,db_index=True)
     user = models.ForeignKey('users.User',null=True,blank=True)
     user = models.ForeignKey('users.User',null=True,blank=True)
     user_name = models.CharField(max_length=255)
     user_name = models.CharField(max_length=255)
     ip = models.GenericIPAddressField()
     ip = models.GenericIPAddressField()

+ 55 - 2
misago/threads/views/list.py

@@ -1,18 +1,21 @@
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
-from django.db.models import Q
+from django.db.models import Q, F
+from django.forms import ValidationError
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
+from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.acl.utils import ACLError403, ACLError404
 from misago.acl.utils import ACLError403, ACLError404
 from misago.forms import FormLayout, FormFields
 from misago.forms import FormLayout, FormFields
 from misago.forums.models import Forum
 from misago.forums.models import Forum
 from misago.messages import Message
 from misago.messages import Message
 from misago.readstracker.trackers import ForumsTracker, ThreadsTracker
 from misago.readstracker.trackers import ForumsTracker, ThreadsTracker
+from misago.threads.forms import MergeThreadsForm
 from misago.threads.models import Thread, Post
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.threads.views.base import BaseView
 from misago.threads.views.mixins import ThreadsFormMixin
 from misago.threads.views.mixins import ThreadsFormMixin
 from misago.views import error403, error404
 from misago.views import error403, error404
-from misago.utils import make_pagination
+from misago.utils import make_pagination, slugify
 
 
 class ThreadsView(BaseView, ThreadsFormMixin):
 class ThreadsView(BaseView, ThreadsFormMixin):
     def fetch_forum(self, forum):
     def fetch_forum(self, forum):
@@ -144,6 +147,56 @@ class ThreadsView(BaseView, ThreadsFormMixin):
             Thread.objects.filter(id__in=opened).update(closed=False)
             Thread.objects.filter(id__in=opened).update(closed=False)
             self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
             self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
     
     
+    def action_merge(self, ids):
+        if len(ids) < 2:
+            raise ValidationError(_("You have to pick two or more threads to merge."))
+        threads = []
+        for thread in self.threads:
+            if thread.pk in ids:
+                threads.append(thread)
+        if self.request.POST.get('origin') == 'merge_form':
+            form = MergeThreadsForm(self.request.POST,request=self.request,threads=threads)
+            if form.is_valid():
+                new_thread = Thread.objects.create(
+                                                   forum=self.forum,
+                                                   name=form.cleaned_data['thread_name'],
+                                                   slug=slugify(form.cleaned_data['thread_name']),
+                                                   start=timezone.now(),
+                                                   last=timezone.now()
+                                                   )
+                last_merge = 0
+                last_thread = None
+                merged = []
+                for i in range(0, len(threads)):
+                    thread = form.merge_order[i]
+                    merged.append(thread.pk)
+                    if last_thread and last_thread.last > thread.start:
+                        last_merge += thread.merges + 1
+                    thread.post_set.update(thread=new_thread,merge=F('merge') + last_merge)
+                    thread.change_set.update(thread=new_thread)
+                    thread.checkpoint_set.update(thread=new_thread)
+                    last_thread = thread
+                Thread.objects.filter(id__in=merged).delete()
+                new_thread.sync()
+                new_thread.save(force_update=True)
+                self.forum.sync()
+                self.forum.save(force_update=True)
+                self.request.messages.set_flash(Message(_('Selected threads have been merged into new one.')), 'success', 'threads')
+                return None
+            else:
+                self.message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = MergeThreadsForm(request=self.request,threads=threads)  
+        return self.request.theme.render_to_response('threads/merge.html',
+                                                {
+                                                 'message': self.message,
+                                                 'forum': self.forum,
+                                                 'parents': self.parents,
+                                                 'threads': threads,
+                                                 'form': FormLayout(form),
+                                                 },
+                                                context_instance=RequestContext(self.request));      
+        
     def action_close(self, ids):
     def action_close(self, ids):
         closed = []
         closed = []
         for thread in self.threads:
         for thread in self.threads:

+ 7 - 4
misago/threads/views/mixins.py

@@ -47,10 +47,13 @@ class ThreadsFormMixin(object):
                                 if thread.start_post_id == post.pk or thread.last_post_id == post.pk:
                                 if thread.start_post_id == post.pk or thread.last_post_id == post.pk:
                                     break
                                     break
                     form_action = getattr(self, 'action_' + self.form.cleaned_data['list_action'])
                     form_action = getattr(self, 'action_' + self.form.cleaned_data['list_action'])
-                    response = form_action(checked_items)
-                    if response:
-                        return response
-                    return redirect(self.request.path)
+                    try:
+                        response = form_action(checked_items)
+                        if response:
+                            return response
+                        return redirect(self.request.path)
+                    except forms.ValidationError as e:
+                        self.message = Message(e.messages[0], 'error')
                 else:
                 else:
                     self.message = Message(_("You have to select at least one thread."), 'error')
                     self.message = Message(_("You have to select at least one thread."), 'error')
             else:
             else:

+ 1 - 0
misago/threads/views/posting.py

@@ -90,6 +90,7 @@ class PostingView(BaseView):
                 post = Post.objects.create(
                 post = Post.objects.create(
                                            forum=self.forum,
                                            forum=self.forum,
                                            thread=thread,
                                            thread=thread,
+                                           merge=thread.merges,
                                            user=request.user,
                                            user=request.user,
                                            user_name=request.user.username,
                                            user_name=request.user.username,
                                            ip=request.session.get_ip(request),
                                            ip=request.session.get_ip(request),

+ 5 - 1
misago/threads/views/thread.py

@@ -23,7 +23,11 @@ class ThreadView(BaseView):
     
     
     def fetch_posts(self, page):
     def fetch_posts(self, page):
         self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
         self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
-        self.posts = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).order_by('pk').prefetch_related('checkpoint_set', 'user', 'user__rank')
+        self.posts = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).prefetch_related('checkpoint_set', 'user', 'user__rank')
+        if self.thread.merges > 0:
+            self.posts = self.posts.order_by('merge', 'pk')
+        else:
+            self.posts = self.posts.order_by('pk')
         self.pagination = make_pagination(page, self.count, self.request.settings.posts_per_page)
         self.pagination = make_pagination(page, self.count, self.request.settings.posts_per_page)
         if self.request.settings.posts_per_page < self.count:
         if self.request.settings.posts_per_page < self.count:
             self.posts = self.posts[self.pagination['start']:self.pagination['stop']]
             self.posts = self.posts[self.pagination['start']:self.pagination['stop']]

+ 1 - 1
templates/sora/threads/list.html

@@ -8,7 +8,7 @@
 
 
 {% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
 {% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
 {% for parent in parents %}
 {% for parent in parents %}
-<li class="first"><a href="{{ parent.type|url(forum=forum.pk, slug=forum.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
+<li class="first"><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
 {% endfor %}
 {% endfor %}
 <li class="active">{{ forum.name }}
 <li class="active">{{ forum.name }}
 {%- endblock %}
 {%- endblock %}

+ 42 - 0
templates/sora/threads/merge.html

@@ -0,0 +1,42 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "_forms.html" as form_theme with context %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_("Merge Threads"),parent=forum.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
+{% for parent in parents %}
+<li class="first"><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
+{% endfor %}
+<li class="first"><a href="{% url 'forum' forum=forum.pk, slug=forum.slug %}">{{ forum.name }}</a> <span class="divider">/</span></li>
+<li class="active">{% trans %}Merge Threads{% endtrans %}
+{%- endblock %}
+
+{% block content %}
+<div class="page-header">
+  <ul class="breadcrumb">
+    {{ self.breadcrumb() }}</li>
+  </ul>
+  <h1>{% trans %}Merge Threads{% endtrans %} <small>{{ forum.name }}</small></h1>
+</div>
+<div class="row">
+  <div class="span8 offset2">
+    {% if message %}{{ macros.draw_message(message) }}{% endif %}
+    <form action="{% url 'forum' forum=forum.pk, slug=forum.slug %}" method="post">
+      <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      <input type="hidden" name="origin" value="merge_form">
+      <input type="hidden" name="list_action" value="merge">
+      {% for thread in threads -%}
+      <input type="hidden" name="list_items" value="{{ thread.pk }}">
+      {% endfor %}
+      {{ form_theme.form_widget(form, width=8) }}
+      <div class="form-actions">
+        <button name="save" type="submit" class="btn btn-primary">{% trans %}Merge Threads{% endtrans %}</button>
+        <a href="{% url 'forum' forum=forum.pk, slug=forum.slug %}" class="btn">{% trans %}Cancel{% endtrans %}</a>
+      </div>
+    </form>
+  </div>
+</div>
+{% endblock %}