Browse Source

#448: post new thread + flood protection

Rafał Pitoń 10 years ago
parent
commit
95999aa07e

+ 1 - 1
docs/developers/posting_process.rst

@@ -99,6 +99,6 @@ When different middlewares add custom fields to ``update_fields`` and set ``upda
 Interrupting posting process from middleware
 ============================================
 
-Middlewares can always interrupt (and rollback) posting process during ``pre_save`` phrase by raising :py:class:`misago.threads.posting.PostingInterruptPostingInterrupt` exception with error message as its only argument.
+Middlewares can always interrupt (and rollback) posting process during ``pre_save`` phrase by raising :py:class:`misago.threads.posting.PostingInterrupt` exception with error message as its only argument.
 
 All ``PostingInterrupt`` raised outside that phrase will be escalated to ``ValueError`` that will result in 500 error response from Misago. However as this will happen inside database transaction, there is chance that no data loss has occured during that.

+ 2 - 0
misago/conf/defaults.py

@@ -182,6 +182,8 @@ MISAGO_ACL_EXTENSIONS = (
 MISAGO_MARKUP_EXTENSIONS = ()
 
 MISAGO_POSTING_MIDDLEWARES = (
+    # Note: always keep FloodProtectionMiddleware middleware first one
+    'misago.threads.posting.floodprotection.FloodProtectionMiddleware',
     'misago.threads.posting.reply.ReplyFormMiddleware',
     'misago.threads.posting.threadlabel.ThreadLabelFormMiddleware',
     'misago.threads.posting.threadpin.ThreadPinFormMiddleware',

+ 2 - 0
misago/static/misago/css/misago/editor.less

@@ -37,6 +37,7 @@
           text-align: center;
 
           &:hover {
+            background: fadeOut(@state-default, 50%);
             border-color: @state-default;
             top: 0px;
 
@@ -46,6 +47,7 @@
           }
 
           &.active, &:active {
+            background: fadeOut(@state-clicked, 50%);
             border-color: @state-clicked;
             top: 0px;
 

+ 4 - 0
misago/static/misago/css/misago/posting.less

@@ -66,5 +66,9 @@
     background-color: @form-panel-footer-bg;
     border-top: 1px solid @form-panel-border;
     padding: @form-panel-padding;
+
+    strong {
+      margin-left: @line-height-computed / 2;
+    }
   }
 }

+ 32 - 10
misago/static/misago/js/misago-posting.js

@@ -89,8 +89,14 @@ $(function() {
     this.$container = null;
     this.$form = null;
 
+    this.$ajax_loader = null;
+    this.$ajax_complete = null;
+
     this.$preview = null;
 
+    this.submitted = false;
+    this.posted = false;
+
     this.affix_end = 0;
 
     var _this = this;
@@ -101,6 +107,9 @@ $(function() {
       this.$container = this.$form.parent();
       this.$spacer = this.$container.parent();
 
+      this.$ajax_loader = this.$container.find('.ajax-loader');
+      this.$ajax_complete = this.$container.find('.ajax-complete');
+
       if (options.preview !== undefined) {
         this.$preview = new MisagoPreview(this, {selector: options.preview, form: this.$form, api_url: options.api_url});
         this.$preview.update();
@@ -119,16 +128,29 @@ $(function() {
       });
 
       this.$container.find('button[name="submit"]').click(function() {
-        var form_data = _this.$form.serialize() + '&submit=1';
-        $.post(_this.api_url, form_data, function(data) {
-          if (data.thread_url !== undefined) {
-            window.location.replace(data.thread_url);
-          } else if (data.errors !== undefined) {
-            Misago.Alerts.error(data.errors[0]);
-          } else {
-            Misago.Alerts.error();
-          }
-        });
+        if (!_this.submitted && !_this.posted) {
+          _this.submitted = true; // lock submit process until after response
+          _this.$ajax_loader.addClass('in');
+
+          var form_data = _this.$form.serialize() + '&submit=1';
+          $.post(_this.api_url, form_data, function(data) {
+            _this.$ajax_loader.removeClass('in');
+            if (data.thread_url !== undefined) {
+              _this.posted = true;
+              _this.$ajax_loader.hide();
+              _this.$ajax_complete.addClass('in')
+              window.location.replace(data.thread_url);
+            } else if (data.errors !== undefined) {
+              Misago.Alerts.error(data.errors[0]);
+            } else if (data.interrupt !== undefined) {
+              Misago.Alerts.error(data.interrupt);
+            } else {
+              Misago.Alerts.error();
+            }
+
+            _this.submitted = false;
+          });
+        }
         return false;
       })
 

+ 10 - 0
misago/templates/misago/posting/form.html

@@ -48,6 +48,16 @@
           <button class="btn btn-primary" name="submit">{% trans "Save changes" %}</button>
           {% endif %}
 
+          <strong class="ajax-loader text-muted fade">
+            <i class="fa fa-cog fa-fw fa-spin"></i>
+            {% trans "Posting..." %}
+          </strong>
+
+          <strong class="ajax-complete text-success fade">
+            <span class="fa fa-check fa-fw"></span>
+            {% trans "Posted! Redirecting..." %}
+          </strong>
+
         </div>
       </div>
 

+ 8 - 3
misago/threads/posting/__init__.py

@@ -130,14 +130,19 @@ class EditorFormset(object):
         forms_dict = self.get_forms_dict()
         for middleware, obj in self.middlewares:
             obj.pre_save(forms_dict.get(middleware))
+
         try:
             for middleware, obj in self.middlewares:
                 obj.save(forms_dict.get(middleware))
             for middleware, obj in self.middlewares:
                 obj.post_save(forms_dict.get(middleware))
-        except PostingInterrupt:
-            raise ValueError("Posting process can only be "
-                             "interrupted during pre_save phase")
+        except PostingInterrupt as e:
+            from misago.threads.posting import floodprotection
+            if isinstance(obj, floodprotection.FloodProtectionMiddleware):
+                raise e
+            else:
+                raise ValueError("Posting process can only be "
+                                 "interrupted during pre_save phase")
 
     def update(self):
         """handle POST that shouldn't result in state change"""

+ 19 - 0
misago/threads/posting/floodprotection.py

@@ -0,0 +1,19 @@
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+
+from misago.threads.posting import PostingMiddleware, PostingInterrupt
+
+
+MIN_POSTING_PAUSE = 3
+
+
+class FloodProtectionMiddleware(PostingMiddleware):
+    def save(self, form):
+        message = _("You can't post message so quickly after previous one.")
+        if self.user.last_post:
+            previous_post = timezone.now() - self.user.last_post
+            if previous_post.total_seconds() < MIN_POSTING_PAUSE:
+                raise PostingInterrupt(message)
+
+        self.user.last_post = timezone.now()
+        self.user.update_fields.append('last_post')

+ 9 - 10
misago/threads/views/generic/posting.py

@@ -68,6 +68,7 @@ class EditorView(ViewBase):
     def dispatch(self, request, *args, **kwargs):
         if request.method == 'POST':
             with atomic():
+                request.user.lock()
                 return self.real_dispatch(request, *args, **kwargs)
         else:
             return self.real_dispatch(request, *args, **kwargs)
@@ -100,7 +101,9 @@ class EditorView(ViewBase):
                             return redirect(thread.get_absolute_url())
                     except PostingInterrupt as e:
                         if request.is_ajax():
-                            raise AjaxError(e.message)
+                            return JsonResponse({
+                                'interrupt': e.message
+                            })
                         else:
                             messages.error(request, e.message)
                 elif request.is_ajax():
@@ -108,15 +111,11 @@ class EditorView(ViewBase):
                         'errors': formset.errors
                     })
 
-            if request.is_ajax():
-                if 'form' in request.POST:
-                    pass
-
-                if 'preview' in request.POST:
-                    formset.update()
-                    return JsonResponse({
-                        'preview': formset.post.parsed
-                    })
+            if request.is_ajax() and 'preview' in request.POST:
+                formset.update()
+                return JsonResponse({
+                    'preview': formset.post.parsed
+                })
 
         return self.render(request, {
             'mode': mode,