Browse Source

WIP Goto links and posting overhaul

Rafał Pitoń 10 years ago
parent
commit
00186012cd

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

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

+ 43 - 25
misago/static/misago/css/misago/posting.less

@@ -3,46 +3,50 @@
 // --------------------------------------------------
 
 
-.editor-preview {
-  padding-bottom: @line-height-computed;
-
-  .misago-markup {
-    // finetuned height to avoid page height form changing for most common posts
-    margin-top: 6px;
-    min-height: 33px;
-  }
-}
-
-
 .posting-container {
   background: @form-panel-bg;
-  border: 1px solid @form-panel-border;
+  border-top: 1px solid darken(@form-panel-border, 8%);
   border-width: 1px 0px;
-  box-shadow: 0px 0px 0px 3px @form-panel-shadow;
+  box-shadow: 0px 0px 0px 5px darken(@form-panel-shadow, 8%);
   padding: @form-panel-padding;
   padding-bottom: 0px;
   padding-left: 0px;
   padding-right: 0px;
 
+  position: fixed;
+  bottom: -100%;
+  width: 100%;
+  z-index: 2;
+
   &.fixed {
-    border-bottom: none;
-    position: fixed;
+    transition-duration: 0.5s;
     bottom: 0px;
-    width: 100%;
-    z-index: 2;
   }
 
-  form>.container-fluid {
-    padding-left: @grid-gutter-width;
-    padding-right: @grid-gutter-width;
+  .editor-preview {
+    border: 2px dashed @form-panel-border;
+    border-radius: @border-radius-large;
+    padding: 0px;
+
+    .empty-message {
+      padding: @padding-base-vertical @padding-base-horizontal;
+    }
+
+    .misago-markup {
+      padding: @padding-base-vertical @padding-base-horizontal;
+    }
+
+    .preview-footer  {
+      margin: 0px;
+      padding: @padding-base-vertical @padding-base-horizontal;
+    }
   }
 
   .form-main {
-    &>* {
-      margin: @form-panel-padding;
-      margin-top: 0px;
-      margin-left: 0px;
-      margin-right: 0px;
+    padding: 0px;
+
+    &>.form-control {
+      margin-bottom: @line-height-computed / 2;
     }
   }
 
@@ -66,9 +70,23 @@
     background-color: @form-panel-footer-bg;
     border-top: 1px solid @form-panel-border;
     padding: @form-panel-padding;
+    padding-left: 0px;
+    padding-right: 0px;
 
     strong {
       margin-left: @line-height-computed / 2;
     }
   }
 }
+
+
+.reply-to-thread {
+  .btn {
+    margin: 0px auto;
+    max-width: 300px;
+
+    img {
+      height: 30px;
+    }
+  }
+}

+ 106 - 39
misago/static/misago/js/misago-posting.js

@@ -3,7 +3,8 @@ $(function() {
   MisagoPreview = function(_controller, options) {
 
     this.$form = options.form;
-    this.$area = $(options.selector);
+    this.$area = options.$area;
+    this.$frame = this.$area.find('.frame');
 
     this.$markup = this.$area.find('.misago-markup');
     this.$message = this.$area.find('.empty-message');
@@ -16,7 +17,6 @@ $(function() {
     this.active = true;
     this.previewed_data = this.$form.serialize() + '&preview=1';
 
-
     this.frequency = 1500;
 
     this.height = this.$markup.height();
@@ -28,36 +28,39 @@ $(function() {
       _this.last_key_press = (new Date().getTime() / 1000);
     })
 
+    this.$frame.height(this.$form.find('.misago-editor').innerHeight() - this.$area.find('.preview-footer').outerHeight());
+
     this.update = function() {
       var form_data = _this.$form.serialize() + '&preview=1';
       var last_key = (new Date().getTime() / 1000) - _this.last_key_press;
 
       if (_this.previewed_data != form_data && last_key > 2) {
         $.post(_this.api_url, form_data, function(data) {
-          var scroll = $(document).height() - $(document).scrollTop();
+          var scroll = _this.$markup.height() - _this.$frame.scrollTop();
 
           if (data.preview) {
             if (_this.$message.is(":visible")) {
               _this.$message.fadeOut(function() {
                 _this.$markup.html(data.preview);
+                Misago.Onebox.activate(_this.$markup);
                 _this.$markup.fadeIn();
               });
             } else {
               _this.$markup.html(data.preview);
+              Misago.Onebox.activate(_this.$markup);
             }
 
             if (_this.$markup.height() > _this.height) {
-              $(document).scrollTop($(document).height() - scroll);
+              _this.$frame.scrollTop(_this.$markup.height() - scroll);
             }
           } else {
             _this.$markup.fadeOut(function() {
               _this.$markup.html("");
               _this.$message.fadeIn();
+              _this.$frame.scrollTop(0);
             });
           }
 
-          _controller.update_affix_end();
-          _controller.update_affix();
           Misago.DOM.changed();
 
           _this.previewed_data = form_data;
@@ -85,47 +88,55 @@ $(function() {
 
   MisagoPosting = function() {
 
-    this.$spacer = null;
-    this.$container = null;
-    this.$form = null;
+    this._clear = function() {
+
+      this.$spacer = null;
+      this.$container = null;
+      this.$form = null;
+
+      this.$ajax_loader = null;
+      this.$ajax_complete = null;
 
-    this.$ajax_loader = null;
-    this.$ajax_complete = null;
+      this.$preview = null;
 
-    this.$preview = null;
+      this.submitted = false;
+      this.posted = false;
 
-    this.submitted = false;
-    this.posted = false;
+      this.affix_end = 0;
+
+      this.on_cancel = null;
+
+    }
 
-    this.affix_end = 0;
+    this._clear();
 
     var _this = this;
 
     this.init = function(options) {
 
+      if (this.$form !== null) {
+        return false;
+      }
+
       this.$form = $('#posting-form');
       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();
+      if (options.on_cancel !== undefined) {
+        this.on_cancel = options.on_cancel
+      } else {
+        this.on_cancel = null;
       }
 
-      this.container_height = this.$container.innerHeight();
-      this.$spacer.height(this.container_height);
-
-      this.heights_diff = this.$container.outerHeight() - this.$spacer.innerHeight();
+      this.$ajax_loader = this.$container.find('.ajax-loader');
+      this.$ajax_complete = this.$container.find('.ajax-complete');
 
-      this.update_affix_end();
-      this.update_affix();
+      this.$preview = new MisagoPreview(this, {$area: this.$form.find('.editor-preview'), form: this.$form, api_url: options.api_url});
+      this.$preview.update();
 
-      $(document).scroll(function() {
-        _this.update_affix()
-      });
+      // target height is 26 px too big
+      this.$spacer.height(this.$container.outerHeight() - ($(document).height() - this.$spacer.offset().top));
+      this.$container.addClass('fixed');
 
       this.$container.find('button[name="submit"]').click(function() {
         if (!_this.submitted && !_this.posted) {
@@ -133,7 +144,7 @@ $(function() {
           _this.$ajax_loader.addClass('in');
 
           var form_data = _this.$form.serialize() + '&submit=1';
-          $.post(_this.api_url, form_data, function(data) {
+          $.post(options.api_url, form_data, function(data) {
             _this.$ajax_loader.removeClass('in');
             if (data.thread_url !== undefined) {
               _this.posted = true;
@@ -143,7 +154,7 @@ $(function() {
             } else if (data.errors !== undefined) {
               Misago.Alerts.error(data.errors[0]);
             } else if (data.interrupt !== undefined) {
-              Misago.Alerts.error(data.interrupt);
+              Misago.Alerts.info(data.interrupt);
             } else {
               Misago.Alerts.error();
             }
@@ -151,24 +162,80 @@ $(function() {
             _this.submitted = false;
           });
         }
+
         return false;
       })
 
+      this.$container.find('button[name="cancel"]').click(function() {
+
+        if (_this.has_content()) {
+          var decision = confirm(lang_dismiss_editor);
+          if (decision) {
+            _this.cancel();
+          }
+        } else {
+          _this.cancel();
+        }
+
+        return false;
+      });
+
+      return true;
+
     }
 
-    this.update_affix_end = function() {
-      this.spacer_end = this.$spacer.offset().top + this.$spacer.height();
+    this.load = function(options) {
+
+      if (this.$form !== null) {
+        return false;
+      }
+
+      $.get(options.api_url, function(data) {
+        $('#reply-form-placeholder').html(data);
+        Misago.DOM.changed();
+        _this.init(options);
+        Misago.DOM.changed();
+
+        if (options.on_load !== undefined) {
+          options.on_load();
+        }
+      });
+
     }
 
-    this.update_affix = function() {
-      if (this.spacer_end - $(document).scrollTop() > $(window).height()) {
-        this.$container.addClass('fixed');
-      } else {
-        this.$container.removeClass('fixed');
+    this.cancel = function() {
+
+      if (this.$form !== null) {
+
+        if (this.$preview !== null) {
+          this.$preview.stop();
+        }
+
+        this.$spacer.fadeOut(function() {
+          $(this).remove();
+          $('.main-footer').show();
+        });
+
+        this._clear();
       }
+
+    }
+
+    this.has_content = function() {
+
+        var length = $.trim(_this.$form.find('input[name="title"]').val()).length;
+        length += $.trim(_this.$form.find('textarea').val()).length;
+        return length > 0;
+
     }
 
   }
 
   Misago.Posting = new MisagoPosting();
+
+  $(window).on("beforeunload", function() {
+    if (Misago.Posting.has_content() && !Misago.Posting.posted) {
+      return lang_dismiss_editor;
+    }
+  })
 });

+ 1 - 0
misago/templates/misago/base.html

@@ -36,6 +36,7 @@
       var is_authenticated = {{ user.is_authenticated|yesno:"true,false" }};
 
       var lang_time_units = "{% trans "smhd" %}";
+      var lang_dismiss_editor = "{% trans "Are you sure you want to abandon your message?" %}";
 
       var ajax_errors = {
         generic: "{% trans "Server returned unspecified error. Refresh page and try again." %}",

+ 1 - 1
misago/templates/misago/editor/body.html

@@ -59,7 +59,7 @@
   </div>
 
   <div class="editor-textarea">
-    <textarea id="{{ editor.field.auto_id }}" name="{{ editor.field.html_name }}" rows="6">{% if editor.field.value %}{{ editor.field.value }}{% endif %}</textarea>
+    <textarea id="{{ editor.field.auto_id }}" name="{{ editor.field.html_name }}" class="scrollable" rows="6">{% if editor.field.value %}{{ editor.field.value }}{% endif %}</textarea>
   </div>
 
   <div class="editor-footer">

+ 1 - 1
misago/templates/misago/forums/forums.html

@@ -49,7 +49,7 @@
         {% if not forum.acl.can_see_all_threads %}
         <em class="text-muted">{% trans "Forum is private" %}</em>
         {% elif forum.last_thread_title %}
-        <a href="{% url 'misago:thread' thread_slug=forum.last_thread_slug thread_id=forum.last_thread_id %}" class="item-title">{{ forum.last_thread_title }}</a>
+        <a href="{% url 'misago:thread_new' thread_slug=forum.last_thread_slug thread_id=forum.last_thread_id %}" class="item-title">{{ forum.last_thread_title }}</a>
         <div class="text-muted">
           {% capture trimmed as last_poster %}
             {% if forum.last_poster_id %}

+ 0 - 59
misago/templates/misago/posting/editorview.html

@@ -1,59 +0,0 @@
-{% extends "misago/base.html" %}
-{% load i18n %}
-
-
-{% block title %}{% trans "Start new thread" %} | {{ forum }} | {{ block.super }}{% endblock title %}
-
-
-{% block content %}
-<div{% if forum.css %} class="page-{{ forum.css_class }}"{% endif %}>
-  <div class="page-header">
-    <div class="container">
-      {% if path %}
-      <ol class="breadcrumb">
-        {% for crumb in path %}
-        <li>
-          <a href="{{ crumb.get_absolute_url }}">{{ crumb }}</a>{% if not forloop.last %}<span class="fa fa-chevron-right"></span>{% endif %}
-        </li>
-        {% endfor %}
-      </ol>
-      {% endif %}
-
-      <h1>
-        <span class="main">
-          <a href="{{ forum.get_absolute_url }}">{{ forum }}</a>
-        </span>
-        <span class="sub">
-          <span class="fa fa-chevron-right"></span>
-          {% trans "Start new thread" %}
-        </span>
-      </h1>
-    </div>
-  </div>
-  <div class="container">
-    <div id="preview-area" class="editor-preview">
-      <p class="lead empty-message">
-        {% trans "Once you start writing mesage, it's preview will be displayed here." %}
-      </p>
-      <article class="misago-markup"></article>
-      <p class="text-muted small">
-        <span class="fa fa-refresh fa-fw"></span>
-        {% trans "Preview updates automatically when you pause typing." %}
-      </p>
-    </div>
-  </div>
-
-  {% include "misago/posting/form.html" %}
-
-</div>
-{% endblock content %}
-
-
-{% block javascripts %}
-{{ block.super }}
-<script lang="JavaScript">
-  $(function() {
-    var reply_editor = Misago.Posting.init({preview: '#preview-area', api: '{{ api_url }}'});
-  });
-</script>
-{% endblock javascripts %}

+ 12 - 7
misago/templates/misago/posting/form.html → misago/templates/misago/posting/formset.html

@@ -3,22 +3,25 @@
   <div class="posting-container">
     <form id="posting-form" method="POST">
 
-      <div class="container-fluid">
+      <div class="container">
         {% csrf_token %}
 
+        {% comment "Disabled" %}
         <div class="row">
           {% if supporting_forms %}
           <div class="col-md-9 form-main">
           {% else %}
           <div class="col-md-12 form-main">
           {% endif %}
+        {% endcomment %}
 
+          <div class="form-main">
             {% for form in main_forms %}
             {% include form.template %}
             {% endfor %}
-
           </div>
 
+          {% comment "Disabled" %}
           {% if supporting_forms %}
           <div class="col-md-3 form-side">
 
@@ -33,21 +36,23 @@
           </div>
           {% endif %}
         </div><!-- /.row -->
-
+        {% endcomment %}
 
       </div>
 
       <div class="form-footer">
-        <div class="container-fluid">
+        <div class="container">
 
           {% if formset.start_form %}
-          <button class="btn btn-primary" name="submit">{% trans "Post thread" %}</button>
+          <button class="btn btn-primary" type="button" name="submit">{% trans "Post thread" %}</button>
           {% elif formset.reply_form %}
-          <button class="btn btn-primary" name="submit">{% trans "Post reply" %}</button>
+          <button class="btn btn-primary" type="button" name="submit">{% trans "Post reply" %}</button>
           {% elif formset.edit_form %}
-          <button class="btn btn-primary" name="submit">{% trans "Save changes" %}</button>
+          <button class="btn btn-primary" type="button" name="submit">{% trans "Save changes" %}</button>
           {% endif %}
 
+          <button class="btn btn-default" type="button" name="cancel">{% trans "Cancel" %}</button>
+
           <strong class="ajax-loader text-muted fade">
             <i class="fa fa-cog fa-fw fa-spin"></i>
             {% trans "Posting..." %}

+ 23 - 1
misago/templates/misago/posting/replyform.html

@@ -5,4 +5,26 @@
 <input class="textinput textInput form-control input-lg" id="{{ form.title.auto_id }}" name="title" type="text" {% if form.title.value %}value="{{ form.title.value }}"{% endif %} placeholder="{% trans "Thread title..." %}"/>
 {% endif %}
 
-{% editor_body form.post_editor %}
+<div class="row">
+  <div class="col-md-6">
+
+    {% editor_body form.post_editor %}
+
+  </div>
+  <div class="col-md-6">
+
+    <div class="editor-preview">
+      <div class="frame scrollable">
+        <p class="lead empty-message">
+          {% trans "Once you start writing mesage, it's preview will be displayed here." %}
+        </p>
+        <article class="misago-markup"></article>
+      </div>
+      <p class="preview-footer text-muted small">
+        <span class="fa fa-refresh fa-fw"></span>
+        {% trans "Preview updates automatically when you pause typing." %}
+      </p>
+    </div>
+
+  </div>
+</div>

+ 1 - 1
misago/templates/misago/thread/post.html

@@ -26,7 +26,7 @@
 
         <span class="separator">&ndash;</span>
 
-        <a href="" class="post-date tooltip-top dynamic time-ago" title="{{ post.posted_on }}" data-timestamp="{{ post.posted_on|date:"c" }}">
+        <a href="{{ post.get_absolute_url }}" class="post-date tooltip-top dynamic time-ago" title="{{ post.posted_on }}" data-timestamp="{{ post.posted_on|date:"c" }}">
           {{ post.posted_on|date }}
         </a>
 

+ 35 - 0
misago/templates/misago/thread/replies.html

@@ -80,8 +80,25 @@
 
     </div>
 
+    <div class="reply-to-thread">
+
+      {% if thread_reply_message %}
+      <p class="text-center lead">
+        <span class="fa fa-ban fa-fw fa-lg"></span>
+        {{ thread_reply_message }}
+      </p>
+      {% else %}
+      <button type="button" class="btn btn-success btn-block btn-lg">
+        <img class="img-rounded" src="{{ user|avatar:100 }}" alt="{% trans "Your avatar" %}">
+        {% trans "Reply thread" %}
+      </button>
+      {% endif %}
+
+    </div>
+
   </div>
 </div>
+<div id="reply-form-placeholder"></div>
 {% endblock %}
 
 
@@ -92,5 +109,23 @@
   {% if forum.acl.can_hide_events %}
     {% include "misago/thread/events_js.html" %}
   {% endif %}
+
+  {% if thread.acl.can_reply %}
+  <script lang="JavaScript">
+    $(function() {
+      $('.reply-to-thread button').click(function() {
+        Misago.Posting.load({
+          api_url: "{% url 'misago:reply_thread' forum_id=forum.id thread_id=thread.id %}",
+          on_load: function() {
+            $('.reply-to-thread').hide();
+          },
+          on_cancel: function() {
+            $('.reply-to-thread').fadeIn();
+          }
+        });
+      });
+    });
+  </script>
+  {% endif %}
 {% endif %}
 {% endblock javascripts %}

+ 6 - 1
misago/templates/misago/threads/base.html

@@ -29,7 +29,11 @@
                 {% endif %}
               {% endif %}
 
+              {% if thread.is_read %}
               <a href="{{ thread.get_absolute_url }}" class="item-title">
+              {% else %}
+              <a href="{{ thread.get_new_reply_url }}" class="item-title">
+              {% endif %}
                 {{ thread }}
               </a>
 
@@ -44,7 +48,7 @@
               {% endif %}
 
               {% block thread-stats %}
-              <a href="#" class="last-post">
+              <a href="{{ thread.get_last_reply_url }}" class="last-post">
                 <span class="dynamic time-ago-compact tooltip-top" data-timestamp="{{ thread.last_post_on|date:"c" }}" title="{% blocktrans with last_post=thread.last_post_on|date:"DATETIME_FORMAT" %}Last post from {{ last_post }}{% endblocktrans %}">{{ thread.last_post_on|compact_date|lower }}</span>
               </a>
 
@@ -143,4 +147,5 @@
   </div>
   {% endblock threads-list %}
 </div>
+<div id="reply-form-placeholder"></div>
 {% endblock content %}

+ 12 - 0
misago/templates/misago/threads/forum.html

@@ -99,4 +99,16 @@
 {% if user.is_authenticated and list_actions %}
   {% include "misago/threads/actions_js.html" %}
 {% endif %}
+{% if forum.acl.can_start_threads %}
+<script lang="JavaScript">
+  $(function() {
+    $('.btn-reply').click(function() {
+      var $btn = $(this);
+      Misago.Posting.load({
+        api_url: "{% url 'misago:start_thread' forum_id=forum.id %}"
+      });
+    });
+  });
+</script>
+{% endif %}
 {% endblock javascripts %}

+ 2 - 2
misago/templates/misago/threads/reply_btn.html

@@ -11,10 +11,10 @@
     {% trans "Sign in to start thread" %}
   </a>
   {% elif forum.acl.can_start_threads %}
-  <a href="{% url 'misago:start_thread' forum_slug=forum.slug forum_id=forum.id %}" class="btn btn-success pull-right">
+  <button class="btn btn-reply btn-success pull-right" type="button">
     <span class="fa fa-plus-circle"></span>
     {% trans "Start thread" %}
-  </a>
+  </button>
   {% else %}
   <span class="btn btn-default btn-closed pull-right">
     <span class="fa fa-ban"></span>

+ 125 - 0
misago/threads/goto.py

@@ -0,0 +1,125 @@
+from math import ceil
+
+from django.core.urlresolvers import reverse
+
+from misago.readtracker.threadstracker import make_read_aware
+
+from misago.threads.models import Post
+from misago.threads.permissions import exclude_invisible_posts
+
+
+def posts_queryset(user, thread):
+    qs = exclude_invisible_posts(thread.post_set, user, thread.forum)
+    return qs.count(), qs.order_by('id')
+
+
+def get_thread_pages(posts):
+    if posts <= 13:
+        return 1
+
+    thread_pages = posts / 10
+    thread_tail = posts - thread_pages * 10
+    if thread_tail and thread_tail > 3:
+        thread_pages += 1
+    return thread_pages
+
+
+def get_post_page(posts, post_qs):
+    post_no = post_qs.count()
+    if posts <= 13:
+        return 1
+
+    thread_pages = get_thread_pages(posts)
+
+    post_page = int(ceil(float(post_no) / 10))
+    if post_page > thread_pages:
+        post_page = thread_pages
+    return post_page
+
+
+def hashed_reverse(thread, post, page=1):
+    link_name = thread.get_url_name()
+
+    if page > 1:
+        post_url = reverse(link_name, kwargs={
+            'thread_id': thread.id,
+            'thread_slug': thread.slug,
+            'page': page
+        })
+    else:
+        post_url = reverse(link_name, kwargs={
+            'thread_id': thread.id,
+            'thread_slug': thread.slug
+        })
+
+    return '%s#post-%s' % (post_url, post.pk)
+
+
+def last(user, thread):
+    posts, qs = posts_queryset(user, thread)
+    thread_pages = get_thread_pages(posts)
+
+    link_name = thread.get_url_name()
+    if thread_pages > 1:
+        post_url = reverse(link_name, kwargs={
+            'thread_id': thread.id,
+            'thread_slug': thread.slug,
+            'page': thread_pages
+        })
+    else:
+        post_url = reverse(link_name, kwargs={
+            'thread_id': thread.id,
+            'thread_slug': thread.slug
+        })
+
+    return '%s#post-%s' % (post_url, thread.last_post_id)
+
+
+def get_post_link(posts, qs, thread, post):
+    post_page = get_post_page(posts, qs.filter(id__lte=post.pk))
+    return hashed_reverse(thread, post, post_page)
+
+
+def new(user, thread):
+    make_read_aware(user, thread)
+    if thread.is_read:
+        return last(user, thread)
+
+    posts, qs = posts_queryset(user, thread)
+    try:
+        first_unread = qs.filter(posted_on__gt=thread.last_read_on)[:1][0]
+    except IndexError:
+        return last(user, thread)
+
+    return get_post_link(posts, qs, thread, first_unread)
+
+
+def reported(user, thread):
+    if thread.is_read:
+        return thread.get_absolute_url()
+
+    posts, qs = posts_queryset(user, thread)
+    try:
+        first_reported = qs.filter(is_reported=True)[:1][0]
+    except IndexError:
+        return thread.get_absolute_url()
+
+    return get_post_link(posts, qs, thread, first_reported)
+
+
+def moderated(user, thread):
+    if not thread.has_moderated_posts or not thread.acl['can_review']:
+        return thread.get_absolute_url()
+
+    posts, qs = posts_queryset(user, thread)
+    try:
+        first_moderated = qs.filter(is_moderated=True)[:1][0]
+    except IndexError:
+        return thread.get_absolute_url()
+
+    return get_post_link(posts, qs, thread, first_moderated)
+
+
+def post(user, thread, post):
+    posts, qs = posts_queryset(user, thread)
+    return get_post_link(posts, qs, thread, post)

+ 8 - 0
misago/threads/models/post.py

@@ -1,3 +1,4 @@
+from django.core.urlresolvers import reverse
 from django.db import models
 from django.dispatch import receiver
 
@@ -84,3 +85,10 @@ class Post(models.Model):
     @property
     def is_valid(self):
         return is_post_valid(self)
+
+    def get_absolute_url(self):
+        return reverse(self.thread.get_url_name('post'), kwargs={
+            'thread_slug': self.thread.slug,
+            'thread_id': self.thread.id,
+            'post_id': self.id
+        })

+ 8 - 2
misago/threads/models/thread.py

@@ -84,10 +84,10 @@ class Thread(models.Model):
         if self.replies > 0:
             self.replies -= 1
 
-        reported_post_qs = self.post_set.filter(is_reported=True)[:1]
+        reported_post_qs = self.post_set.filter(is_reported=True)
         self.has_reported_posts = reported_post_qs.exists()
 
-        moderated_post_qs = self.post_set.filter(is_moderated=True)[:1]
+        moderated_post_qs = self.post_set.filter(is_moderated=True)
         self.has_moderated_posts = moderated_post_qs.exists()
 
         hidden_post_qs = self.post_set.filter(is_hidden=True)[:1]
@@ -133,6 +133,12 @@ class Thread(models.Model):
     def get_new_reply_url(self):
         return self.get_url('new')
 
+    def get_reported_reply_url(self):
+        return self.get_url('reported')
+
+    def get_moderated_reply_url(self):
+        return self.get_url('moderated')
+
     def get_last_reply_url(self):
         return self.get_url('last')
 

+ 13 - 5
misago/threads/permissions.py

@@ -379,6 +379,14 @@ def can_change_owned_thread(user, target):
     return True
 
 
+def allow_see_post(user, target):
+    forum_acl = user.acl['forums'].get(target.forum_id, {})
+    if not forum_acl.get('can_review_moderated_content'):
+        if user.is_anonymous() or user.pk != target.poster_id:
+            raise Http404()
+can_see_post = return_boolean(allow_see_post)
+
+
 """
 Queryset helpers
 """
@@ -462,12 +470,12 @@ def exclude_all_invisible_threads(queryset, user):
 
 
 def exclude_invisible_posts(queryset, user, forum):
-    if user.is_authenticated():
-        if not forum.acl['can_review_moderated_content']:
-            condition_author = Q(starter_id=user.id)
+    if not forum.acl['can_review_moderated_content']:
+        if user.is_authenticated():
+            condition_author = Q(poster=user.id)
             condition = Q(is_moderated=False)
             queryset = queryset.filter(condition_author | condition)
-    elif not forum.acl['can_review_moderated_content']:
-        queryset = queryset.filter(is_moderated=False)
+        elif not forum.acl['can_review_moderated_content']:
+            queryset = queryset.filter(is_moderated=False)
 
     return queryset

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

@@ -123,6 +123,7 @@ class EditorFormset(object):
                 all_forms_valid = False
                 for error in form.non_field_errors():
                     self.errors.append(unicode(error))
+
         return all_forms_valid
 
     def save(self):

+ 41 - 0
misago/threads/tests/test_goto.py

@@ -0,0 +1,41 @@
+from misago.forums.models import Forum
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from misago.threads import goto
+from misago.threads.testutils import post_thread, reply_thread
+
+
+class GotoTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(GotoTests, self).setUp()
+
+        self.forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
+        self.forum.labels = []
+
+        self.thread = post_thread(self.forum)
+
+    def test_get_thread_pages(self):
+        """get_thread_pages returns valid count of pages for given positions"""
+        self.assertEqual(goto.get_thread_pages(1), 1)
+        self.assertEqual(goto.get_thread_pages(10), 1)
+        self.assertEqual(goto.get_thread_pages(13), 1)
+        self.assertEqual(goto.get_thread_pages(14), 2)
+        self.assertEqual(goto.get_thread_pages(19), 2)
+        self.assertEqual(goto.get_thread_pages(20), 2)
+        self.assertEqual(goto.get_thread_pages(23), 2)
+        self.assertEqual(goto.get_thread_pages(24), 3)
+        self.assertEqual(goto.get_thread_pages(27), 3)
+        self.assertEqual(goto.get_thread_pages(36), 4)
+        self.assertEqual(goto.get_thread_pages(373), 37)
+
+    def test_get_post_page(self):
+        """get_post_page returns valid page number for given queryset"""
+        self.assertEqual(goto.get_post_page(1, self.thread.post_set), 1)
+
+        # add 12 posts, bumping no of posts on page to to 13
+        [reply_thread(self.thread) for p in xrange(12)]
+        self.assertEqual(goto.get_post_page(13, self.thread.post_set), 1)
+
+        # add 2 posts
+        [reply_thread(self.thread) for p in xrange(2)]
+        self.assertEqual(goto.get_post_page(15, self.thread.post_set), 2)

+ 13 - 4
misago/threads/urls.py

@@ -1,9 +1,7 @@
 from django.conf.urls import patterns, include, url
 
-from misago.threads.views.threads import (ForumView, ThreadView,
-                                          StartThreadView, ReplyView, EditView)
-
 
+from misago.threads.views.threads import ForumView
 urlpatterns = patterns('',
     url(r'^forum/(?P<forum_slug>[\w\d-]+)-(?P<forum_id>\d+)/$', ForumView.as_view(), name='forum'),
     url(r'^forum/(?P<forum_slug>[\w\d-]+)-(?P<forum_id>\d+)/(?P<page>\d+)/$', ForumView.as_view(), name='forum'),
@@ -13,13 +11,24 @@ urlpatterns = patterns('',
     url(r'^forum/(?P<forum_slug>[\w\d-]+)-(?P<forum_id>\d+)/show-(?P<show>[\w-]+)/(?P<page>\d+)/$', ForumView.as_view(), name='forum'),
     url(r'^forum/(?P<forum_slug>[\w\d-]+)-(?P<forum_id>\d+)/sort-(?P<sort>[\w-]+)/show-(?P<show>[\w-]+)/$', ForumView.as_view(), name='forum'),
     url(r'^forum/(?P<forum_slug>[\w\d-]+)-(?P<forum_id>\d+)/sort-(?P<sort>[\w-]+)/show-(?P<show>[\w-]+)/(?P<page>\d+)/$', ForumView.as_view(), name='forum'),
-    url(r'^forum/(?P<forum_slug>[\w\d-]+)-(?P<forum_id>\d+)/start-thread/$', StartThreadView.as_view(), name='start_thread'),
 )
 
 
+from misago.threads.views.threads import (ThreadView, GotoLastView,
+                                          GotoNewView, GotoPostView)
 urlpatterns += patterns('',
     url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/$', ThreadView.as_view(), name='thread'),
     url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/(?P<page>\d+)/$', ThreadView.as_view(), name='thread'),
+    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/last/$', GotoLastView.as_view(), name='thread_last'),
+    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/new/$', GotoNewView.as_view(), name='thread_new'),
+    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/post-(?P<post_id>\d+)/$', GotoPostView.as_view(), name='thread_post'),
+)
+
+
+from misago.threads.views.threads import StartThreadView, ReplyView, EditView
+urlpatterns += patterns('',
+    url(r'^start-thread/(?P<forum_id>\d+)/$', StartThreadView.as_view(), name='start_thread'),
+    url(r'^reply-thread/(?P<forum_id>\d+)/(?P<thread_id>\d+)/$', ReplyView.as_view(), name='reply_thread'),
 )
 
 

+ 1 - 0
misago/threads/views/generic/__init__.py

@@ -1,5 +1,6 @@
 # flake8: noqa
 from misago.threads.views.generic.base import *
+from misago.threads.views.generic.goto import *
 from misago.threads.views.generic.post import *
 from misago.threads.views.generic.posting import *
 from misago.threads.views.generic.thread import *

+ 23 - 4
misago/threads/views/generic/base.py

@@ -7,7 +7,7 @@ from misago.forums.models import Forum
 from misago.forums.permissions import allow_see_forum, allow_browse_forum
 
 from misago.threads.models import Thread, Post
-from misago.threads.permissions import allow_see_thread
+from misago.threads.permissions import allow_see_thread, allow_see_post
 
 
 __all__ = ['ForumMixin', 'ThreadMixin', 'PostMixin', 'ViewBase']
@@ -53,8 +53,9 @@ class ThreadMixin(object):
 
         return thread
 
-    def fetch_thread(self, request, lock=False, select_related=None, **kwargs):
-        queryset = Thread.objects
+    def fetch_thread(self, request, lock=False, select_related=None,
+                     queryset=None, **kwargs):
+        queryset = queryset or Thread.objects
         if lock:
             queryset = queryset.select_for_update()
         if select_related:
@@ -68,7 +69,25 @@ class ThreadMixin(object):
 
 
 class PostMixin(object):
-    pass
+    def get_post(self, request, lock=False, **kwargs):
+        thread = self.fetch_post(request, lock, **kwargs)
+        self.check_post_permissions(request, thread)
+
+        return thread
+
+    def fetch_post(self, request, lock=False, select_related=None,
+                   queryset=None, **kwargs):
+        queryset = queryset or Post.objects
+        if lock:
+            queryset = queryset.select_for_update()
+        if select_related:
+            queryset = queryset.select_related(*select_related)
+
+        return get_object_or_404(queryset, id=kwargs.get('post_id'))
+
+    def check_post_permissions(self, request, post):
+        add_acl(request.user, post)
+        allow_see_post(request.user, post)
 
 
 class ViewBase(ForumMixin, ThreadMixin, PostMixin, View):

+ 51 - 0
misago/threads/views/generic/goto.py

@@ -0,0 +1,51 @@
+from django.http import Http404
+from django.shortcuts import redirect
+
+from misago.threads import goto
+from misago.threads.views.generic.base import ViewBase
+
+
+__all__ = ['BaseGotoView', 'GotoLastView', 'GotoNewView', 'GotoPostView']
+
+
+class BaseGotoView(ViewBase):
+    def get_redirect(self, user, thread):
+        raise NotImplementedError("views inheriting form BaseGotoView "
+                                  "should define get_redirect method")
+
+    def dispatch(self, request, *args, **kwargs):
+        thread = self.fetch_thread(request, select_related=['forum'], **kwargs)
+        forum = thread.forum
+
+        self.check_forum_permissions(request, forum)
+        self.check_thread_permissions(request, thread)
+
+        return redirect(self.get_redirect(request.user, thread))
+
+
+class GotoLastView(BaseGotoView):
+    def get_redirect(self, user, thread):
+        return goto.last(user, thread)
+
+
+class GotoNewView(BaseGotoView):
+    def get_redirect(self, user, thread):
+        return goto.new(user, thread)
+
+
+class GotoPostView(BaseGotoView):
+    def get_redirect(self, user, thread, post):
+        return goto.post(user, thread, post)
+
+    def dispatch(self, request, *args, **kwargs):
+        post = self.fetch_post(
+            request, select_related=['thread', 'forum'], **kwargs)
+        forum = post.forum
+        thread = post.thread
+
+        self.check_forum_permissions(request, forum)
+        thread.forum = forum
+        self.check_thread_permissions(request, thread)
+        self.check_post_permissions(request, post)
+
+        return redirect(self.get_redirect(request.user, thread, post))

+ 37 - 40
misago/threads/views/generic/posting.py

@@ -12,7 +12,7 @@ from misago.forums.lists import get_forum_path
 from misago.threads.posting import (PostingInterrupt, EditorFormset,
                                     START, REPLY, EDIT)
 from misago.threads.models import Thread, Post, Label
-from misago.threads.permissions import allow_start_thread
+from misago.threads.permissions import allow_start_thread, allow_reply_thread
 from misago.threads.views.generic.base import ViewBase
 
 
@@ -23,7 +23,7 @@ class EditorView(ViewBase):
     """
     Basic view for starting/replying/editing
     """
-    template = 'misago/posting/editorview.html'
+    template = 'misago/posting/formset.html'
 
     def find_mode(self, request, *args, **kwargs):
         """
@@ -31,36 +31,42 @@ class EditorView(ViewBase):
         """
         is_post = request.method == 'POST'
 
-        if 'forum_id' in kwargs:
-            mode = START
-            user = request.user
+        forum = self.get_forum(request, lock=is_post, **kwargs)
+
+        thread = None
+        post = None
+
+        if 'thread_id' in kwargs:
+            thread = self.get_thread(
+                request, lock=is_post, queryset=forum.thread_set, **kwargs)
 
-            forum = self.get_forum(request, lock=is_post, **kwargs)
+        if thread:
+            mode = REPLY
+        else:
+            mode = START
             thread = Thread(forum=forum)
+
+        if not post:
             post = Post(forum=forum, thread=thread)
-            quote = Post(0)
-        elif 'thread_id' in kwargs:
-            thread = self.get_thread(request, lock=is_post, **kwargs)
-            forum = thread.forum
 
-        return mode, forum, thread, post, quote
+        return mode, forum, thread, post
 
-    def allow_mode(self, user, mode, forum, thread, post, quote):
+    def allow_mode(self, user, mode, forum, thread, post):
         """
         Second step: check start/reply/edit permissions
         """
         if mode == START:
             self.allow_start(user, forum)
         if mode == REPLY:
-            self.allow_reply(user, forum, thread, quote)
+            self.allow_reply(user, forum, thread)
         if mode == EDIT:
             self.allow_edit(user, forum, thread, post)
 
     def allow_start(self, user, forum):
         allow_start_thread(user, forum)
 
-    def allow_reply(self, user, forum, thread, quote):
-        raise NotImplementedError()
+    def allow_reply(self, user, forum, thread):
+        allow_reply_thread(user, thread)
 
     def allow_edit(self, user, forum, thread, post):
         raise NotImplementedError()
@@ -76,16 +82,20 @@ class EditorView(ViewBase):
     def real_dispatch(self, request, *args, **kwargs):
         mode_context = self.find_mode(request, *args, **kwargs)
         self.allow_mode(request.user, *mode_context)
+        mode, forum, thread, post = mode_context
+
+        if not request.is_ajax():
+            response = render(request, 'misago/errorpages/wrong_way.html')
+            response.status_code = 405
+            return response
 
-        mode, forum, thread, post, quote = mode_context
         forum.labels = Label.objects.get_forum_labels(forum)
         formset = EditorFormset(request=request,
                                 mode=mode,
                                 user=request.user,
                                 forum=forum,
                                 thread=thread,
-                                post=post,
-                                quote=quote)
+                                post=post)
 
         if request.method == 'POST':
             if 'submit' in request.POST:
@@ -93,29 +103,17 @@ class EditorView(ViewBase):
                     try:
                         formset.save()
                         messages.success(request, _("New thread was posted."))
-                        if request.is_ajax():
-                            return JsonResponse({
-                                'thread_url': thread.get_absolute_url()
-                            })
-                        else:
-                            return redirect(thread.get_absolute_url())
+                        return JsonResponse({
+                            'thread_url': thread.get_absolute_url()
+                        })
                     except PostingInterrupt as e:
-                        if request.is_ajax():
-                            return JsonResponse({
-                                'interrupt': e.message
-                            })
-                        else:
-                            messages.error(request, e.message)
-                elif request.is_ajax():
-                    return JsonResponse({
-                        'errors': formset.errors
-                    })
-
-            if request.is_ajax() and 'preview' in request.POST:
+                        return JsonResponse({'interrupt': e.message})
+                else:
+                    return JsonResponse({'errors': formset.errors})
+
+            if 'preview' in request.POST:
                 formset.update()
-                return JsonResponse({
-                    'preview': formset.post.parsed
-                })
+                return JsonResponse({'preview': formset.post.parsed})
 
         return self.render(request, {
             'mode': mode,
@@ -127,6 +125,5 @@ class EditorView(ViewBase):
             'path': get_forum_path(forum),
             'thread': thread,
             'post': post,
-            'quote': quote,
             'api_url': request.path
         })

+ 4 - 11
misago/threads/views/generic/thread/view.py

@@ -10,7 +10,8 @@ from misago.users.online.utils import get_user_state
 
 from misago.threads.events import add_events_to_posts
 from misago.threads.paginator import paginate
-from misago.threads.permissions import allow_reply_thread
+from misago.threads.permissions import (allow_reply_thread,
+                                        exclude_invisible_posts)
 from misago.threads.views.generic.base import ViewBase
 from misago.threads.views.generic.thread.postsactions import PostsActions
 from misago.threads.views.generic.thread.threadactions import ThreadActions
@@ -29,7 +30,7 @@ class ThreadView(ViewBase):
 
     def get_posts(self, user, forum, thread, kwargs):
         queryset = self.get_posts_queryset(user, forum, thread)
-        page = paginate(queryset, kwargs.get('page', 0), 10, 5)
+        page = paginate(queryset, kwargs.get('page', 0), 10, 3)
 
         posts = []
         for post in page.object_list:
@@ -50,15 +51,7 @@ class ThreadView(ViewBase):
     def get_posts_queryset(self, user, forum, thread):
         queryset = thread.post_set.select_related(
             'poster', 'poster__rank', 'poster__bancache', 'poster__online')
-
-        if user.is_authenticated():
-            if forum.acl['can_review_moderated_content']:
-                visibility_condition = Q(is_moderated=False) | Q(poster=user)
-                queryset = queryset.filter(visibility_condition)
-        else:
-            queryset = queryset.filter(is_moderated=False)
-
-        return queryset.order_by('id')
+        return exclude_invisible_posts(queryset, user, forum).order_by('id')
 
     def dispatch(self, request, *args, **kwargs):
         relations = ['forum', 'starter', 'last_poster', 'first_post']

+ 12 - 0
misago/threads/views/threads.py

@@ -13,6 +13,18 @@ class ThreadView(ThreadsMixin, generic.ThreadView):
     pass
 
 
+class GotoLastView(ThreadsMixin, generic.GotoLastView):
+    pass
+
+
+class GotoNewView(ThreadsMixin, generic.GotoNewView):
+    pass
+
+
+class GotoPostView(ThreadsMixin, generic.GotoPostView):
+    pass
+
+
 class StartThreadView(ThreadsMixin, generic.EditorView):
     pass