Ralfp 12 лет назад
Родитель
Сommit
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.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)
     post = forms.CharField(widget=forms.Textarea)
 
@@ -26,17 +39,6 @@ class PostForm(Form):
             del self.fields['thread_name']
             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):
         data = self.cleaned_data['post']
         if len(data) < self.request.settings['post_length_min']:
@@ -50,4 +52,45 @@ class PostForm(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.utils import timezone
 from django.utils.translation import ugettext_lazy as _
+from misago.utils import slugify
 
 class ThreadManager(models.Manager):
     def filter_stats(self, start, end):
@@ -17,6 +18,7 @@ class Thread(models.Model):
     replies_reported = models.PositiveIntegerField(default=0)
     replies_moderated = 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)
     upvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
@@ -43,6 +45,35 @@ class Thread(models.Model):
     def get_date(self):
         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):
     def filter_stats(self, start, end):
@@ -52,6 +83,7 @@ class PostManager(models.Manager):
 class Post(models.Model):
     forum = models.ForeignKey('forums.Forum')
     thread = models.ForeignKey(Thread)
+    merge = models.PositiveIntegerField(default=0,db_index=True)
     user = models.ForeignKey('users.User',null=True,blank=True)
     user_name = models.CharField(max_length=255)
     ip = models.GenericIPAddressField()

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

@@ -1,18 +1,21 @@
 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.template import RequestContext
+from django.utils import timezone
 from django.utils.translation import ugettext as _
 from misago.acl.utils import ACLError403, ACLError404
 from misago.forms import FormLayout, FormFields
 from misago.forums.models import Forum
 from misago.messages import Message
 from misago.readstracker.trackers import ForumsTracker, ThreadsTracker
+from misago.threads.forms import MergeThreadsForm
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.threads.views.mixins import ThreadsFormMixin
 from misago.views import error403, error404
-from misago.utils import make_pagination
+from misago.utils import make_pagination, slugify
 
 class ThreadsView(BaseView, ThreadsFormMixin):
     def fetch_forum(self, forum):
@@ -144,6 +147,56 @@ class ThreadsView(BaseView, ThreadsFormMixin):
             Thread.objects.filter(id__in=opened).update(closed=False)
             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):
         closed = []
         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:
                                     break
                     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:
                     self.message = Message(_("You have to select at least one thread."), 'error')
             else:

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

@@ -90,6 +90,7 @@ class PostingView(BaseView):
                 post = Post.objects.create(
                                            forum=self.forum,
                                            thread=thread,
+                                           merge=thread.merges,
                                            user=request.user,
                                            user_name=request.user.username,
                                            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):
         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)
         if self.request.settings.posts_per_page < self.count:
             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>
 {% 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 %}
 <li class="active">{{ forum.name }}
 {%- 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 %}