diff --git a/boards/models.py b/boards/models.py --- a/boards/models.py +++ b/boards/models.py @@ -47,15 +47,17 @@ class PostManager(models.Manager): def create_post(self, title, text, image=None, thread=None, ip=NO_IP, tags=None, user=None): + posting_time = timezone.now() + post = self.create(title=title, text=text, - pub_time=timezone.now(), + pub_time=posting_time, thread=thread, image=image, poster_ip=ip, poster_user_agent=UNKNOWN_UA, - last_edit_time=timezone.now(), - bump_time=timezone.now(), + last_edit_time=posting_time, + bump_time=posting_time, user=user) if tags: @@ -66,7 +68,7 @@ class PostManager(models.Manager): if thread: thread.replies.add(post) thread.bump() - thread.last_edit_time = timezone.now() + thread.last_edit_time = posting_time thread.save() #cache_key = thread.get_cache_key() @@ -179,7 +181,10 @@ class PostManager(models.Manager): id = reply_number.group(1) ref_post = self.filter(id=id) if ref_post.count() > 0: - ref_post[0].referenced_posts.add(post) + referenced_post = ref_post[0] + referenced_post.referenced_posts.add(post) + referenced_post.last_edit_time = post.pub_time + referenced_post.save() def _get_page_count(self, thread_count): return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE))) @@ -312,7 +317,7 @@ class Post(models.Model): def can_bump(self): """Check if the thread can be bumped by replying""" - post_count = self.get_reply_count() + 1 + post_count = self.get_reply_count() return post_count <= settings.MAX_POSTS_PER_THREAD diff --git a/boards/static/js/refpopup.js b/boards/static/js/refpopup.js --- a/boards/static/js/refpopup.js +++ b/boards/static/js/refpopup.js @@ -28,7 +28,7 @@ function showPostPreview(e) { var pNum = $(this).text().match(/\d+/); if (pNum.length == 0) { - return; + return; } //position @@ -57,7 +57,7 @@ function showPostPreview(e) { }; - cln.innerHTML = gettext('Loading...'); + cln.innerHTML = "
" + gettext('Loading...') + "
"; //если пост найден в дереве. if($('div[id='+pNum+']').length > 0) { @@ -72,19 +72,20 @@ function showPostPreview(e) { //ajax api else { $.ajax({ - url: '/api/post/' + pNum - }) - .success(function(data) { - // TODO get a json, not post itself - var postdata = $(data).wrap("
").parent().html(); + url: '/api/post/' + pNum + '/?truncated' + }) + .success(function(data) { + // TODO get a json, not post itself + var postdata = $(data).wrap("
").parent().html(); - //make preview - mkPreview(cln, postdata); + //make preview + mkPreview(cln, postdata); - }) - .error(function() { - cln.innerHTML = gettext('Post not found'); - }); + }) + .error(function() { + cln.innerHTML = "
" + + gettext('Post not found') + "
"; + }); } $del(doc.getElementById(cln.id)); diff --git a/boards/static/js/thread.js b/boards/static/js/thread.js --- a/boards/static/js/thread.js +++ b/boards/static/js/thread.js @@ -77,4 +77,5 @@ function addQuickReply(postId) { $(document).ready(function(){ addGalleryPanel(); + initAutoupdate(); }); diff --git a/boards/static/js/thread_update.js b/boards/static/js/thread_update.js new file mode 100644 --- /dev/null +++ b/boards/static/js/thread_update.js @@ -0,0 +1,116 @@ +/* + @licstart The following is the entire license notice for the + JavaScript code in this page. + + + Copyright (C) 2013 neko259 + + The JavaScript code in this page is free software: you can + redistribute it and/or modify it under the terms of the GNU + General Public License (GNU GPL) as published by the Free Software + Foundation, either version 3 of the License, or (at your option) + any later version. The code is distributed WITHOUT ANY WARRANTY; + without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. + + As additional permission under GNU GPL version 3 section 7, you + may distribute non-source (e.g., minimized or compacted) forms of + that code without the copy of the GNU GPL normally required by + section 4, provided you include this license notice and a URL + through which recipients can access the Corresponding Source. + + @licend The above is the entire license notice + for the JavaScript code in this page. + */ + +var THREAD_UPDATE_DELAY = 10000; + +var loading = false; +var lastUpdateTime = null; + +function blink(node) { + var blinkCount = 2; + var blinkDelay = 250; + + var nodeToAnimate = node; + for (var i = 0; i < blinkCount; i++) { + nodeToAnimate = nodeToAnimate.fadeOut(blinkDelay).fadeIn(blinkDelay); + } +} + +function updateThread() { + if (loading) { + return; + } + + loading = true; + + var threadPosts = $('div.thread').children('.post'); + + var lastPost = threadPosts.last(); + var threadId = threadPosts.first().attr('id'); + + var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/'; + $.getJSON(diffUrl) + .success(function(data) { + var bottom = isPageBottom(); + + var addedPosts = data.added; + + for (var i = 0; i < addedPosts.length; i++) { + var postText = addedPosts[i]; + + var post = $(postText); + post.appendTo(lastPost.parent()); + addRefLinkPreview(post[0]); + + lastPost = post; + blink(post); + } + + var updatedPosts = data.updated; + for (var i = 0; i < updatedPosts.length; i++) { + var postText = updatedPosts[i]; + + var post = $(postText); + var postId = post.attr('id'); + + var oldPost = $('div.thread').children('.post[id=' + postId + ']'); + + oldPost.replaceWith(post); + addRefLinkPreview(post[0]); + + blink(post); + } + + // TODO Process deleted posts + + lastUpdateTime = data.last_update; + loading = false; + + if (bottom) { + var $target = $('html,body'); + $target.animate({scrollTop: $target.height()}, 1000); + } + }) + .error(function(data) { + // TODO Show error message that server is unavailable? + + loading = false; + }); +} + +function isPageBottom() { + var scroll = $(window).scrollTop() / ($(document).height() + - $(window).height()) + + return scroll == 1 +} + +function initAutoupdate() { + loading = false; + + lastUpdateTime = $('.metapanel').attr('data-last-update'); + + setInterval(updateThread, THREAD_UPDATE_DELAY); +} diff --git a/boards/templates/boards/post.html b/boards/templates/boards/post.html --- a/boards/templates/boards/post.html +++ b/boards/templates/boards/post.html @@ -1,14 +1,19 @@ {% load i18n %} {% load board %} -
+{% if can_bump %} +
+{% else %} +
+{% endif %} + {% if post.image %}
{% trans 'Post image' %} @@ -18,8 +23,10 @@ {% autoescape off %} - {{ post.text.rendered }} + {% if truncated %} + {{ post.text.rendered|truncatewords_html:50 }} + {% else %} + {{ post.text.rendered }} + {% endif %} {% endautoescape %} -
- {% trans "Replies" %}: - {% for ref_post in post.get_sorted_referenced_posts %} - >>{{ ref_post.id }}{% if not forloop.last %},{% endif %} - {% endfor %} -
+ {% if post.is_referenced %} +
+ {% trans "Replies" %}: + {% for ref_post in post.get_sorted_referenced_posts %} + >>{{ ref_post.id }}{% if not forloop.last %},{% endif %} + {% endfor %} +
+ {% endif %}
{% if post.tags.exists %} {% endif %}
\ No newline at end of file diff --git a/boards/templates/boards/thread.html b/boards/templates/boards/thread.html --- a/boards/templates/boards/thread.html +++ b/boards/templates/boards/thread.html @@ -13,9 +13,10 @@ {% block content %} {% get_current_language as LANGUAGE_CODE %} + - {% if posts %} + {% if posts %} {% cache 600 thread_view posts.0.last_edit_time moderator LANGUAGE_CODE %} {% if bumpable %}
@@ -27,7 +28,7 @@
{% endif %}
- {% for post in posts %} + {% for post in posts %} {% if bumpable %}
{% else %} @@ -67,7 +68,7 @@ {% autoescape off %} {{ post.text.rendered }} {% endautoescape %} - {% if post.is_referenced %} + {% if post.is_referenced %}
{% trans "Replies" %}: {% for ref_post in post.get_sorted_referenced_posts %} @@ -89,15 +90,15 @@
{% endif %}
- {% endfor %} + {% endfor %}
{% endcache %} - {% endif %} + {% endif %}
{% csrf_token %}
-
{% trans "Reply to thread" %} #{{ posts.0.id }}
+
{% trans "Reply to thread" %} #{{ posts.0.id }}
{% trans 'Title' %}
@@ -151,7 +152,7 @@ {% get_current_language as LANGUAGE_CODE %} - + {% cache 600 thread_meta posts.0.last_edit_time moderator LANGUAGE_CODE %} {{ posts.0.get_reply_count }} {% trans 'replies' %}, {{ posts.0.get_images_count }} {% trans 'images' %}. diff --git a/boards/urls.py b/boards/urls.py --- a/boards/urls.py +++ b/boards/urls.py @@ -53,4 +53,6 @@ urlpatterns = patterns('', # API url(r'^api/post/(?P\w+)/$', views.get_post, name="get_post"), + url(r'^api/diff_thread/(?P\w+)/(?P\w+)/$', + views.api_get_threaddiff, name="get_thread_diff"), ) diff --git a/boards/views.py b/boards/views.py --- a/boards/views.py +++ b/boards/views.py @@ -1,5 +1,11 @@ import hashlib +import json import string +import time +import calendar + +from datetime import datetime + from django.core import serializers from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect @@ -8,6 +14,7 @@ from django.template import RequestConte from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone from django.db import transaction +import math from boards import forms import boards @@ -210,6 +217,7 @@ def thread(request, post_id): context['bumplimit_progress'] = str( float(context['posts_left']) / neboard.settings.MAX_POSTS_PER_THREAD * 100) + context["last_update"] = _datetime_to_epoch(posts[0].last_edit_time) return render(request, 'boards/thread.html', context) @@ -245,23 +253,23 @@ def settings(request): is_moderator = user.is_moderator() if request.method == 'POST': - with transaction.commit_on_success(): - if is_moderator: - form = ModeratorSettingsForm(request.POST, - error_class=PlainErrorList) - else: - form = SettingsForm(request.POST, error_class=PlainErrorList) + with transaction.commit_on_success(): + if is_moderator: + form = ModeratorSettingsForm(request.POST, + error_class=PlainErrorList) + else: + form = SettingsForm(request.POST, error_class=PlainErrorList) - if form.is_valid(): - selected_theme = form.cleaned_data['theme'] + if form.is_valid(): + selected_theme = form.cleaned_data['theme'] - user.save_setting('theme', selected_theme) + user.save_setting('theme', selected_theme) - if is_moderator: - moderate = form.cleaned_data['moderate'] - user.save_setting(SETTING_MODERATE, moderate) + if is_moderator: + moderate = form.cleaned_data['moderate'] + user.save_setting(SETTING_MODERATE, moderate) - return redirect(settings) + return redirect(settings) else: selected_theme = _get_theme(request) @@ -318,7 +326,7 @@ def delete(request, post_id): if user.is_moderator(): # TODO Show confirmation page before deletion Post.objects.delete_post(post) - + if not post.thread: return _redirect_to_next(request) else: @@ -408,13 +416,43 @@ def api_get_post(request, post_id): return HttpResponse(content=json) +def api_get_threaddiff(request, thread_id, last_update_time): + """Get posts that were changed or added since time""" + + thread = get_object_or_404(Post, id=thread_id) + + filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000, + timezone.get_current_timezone()) + + json_data = { + 'added': [], + 'updated': [], + 'last_update': None, + } + added_posts = Post.objects.filter(thread=thread, pub_time__gt=filter_time) + updated_posts = Post.objects.filter(thread=thread, + pub_time__lt=filter_time, + last_edit_time__gt=filter_time) + for post in added_posts: + json_data['added'].append(get_post(request, post.id).content.strip()) + for post in updated_posts: + json_data['updated'].append(get_post(request, post.id).content.strip()) + json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time) + + return HttpResponse(content=json.dumps(json_data)) + + def get_post(request, post_id): """Get the html of a post. Used for popups.""" post = get_object_or_404(Post, id=post_id) + thread = post.thread context = RequestContext(request) context["post"] = post + context["can_bump"] = thread.can_bump() + if "truncated" in request.GET: + context["truncated"] = True return render(request, 'boards/post.html', context) @@ -518,3 +556,9 @@ def _remove_invalid_links(text): text = string.replace(text, '>>' + id, id) return text + + +def _datetime_to_epoch(datetime): + return int(time.mktime(timezone.localtime( + datetime,timezone.get_current_timezone()).timetuple()) + * 1000000 + datetime.microsecond) \ No newline at end of file diff --git a/neboard/settings.py b/neboard/settings.py --- a/neboard/settings.py +++ b/neboard/settings.py @@ -219,7 +219,7 @@ LAST_REPLIES_COUNT = 3 ENABLE_CAPTCHA = False # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds -POSTING_DELAY = 30 # seconds +POSTING_DELAY = 20 # seconds def custom_show_toolbar(request): diff --git a/todo.txt b/todo.txt --- a/todo.txt +++ b/todo.txt @@ -9,6 +9,8 @@ denied". Use second only for autoban for [DONE] Clean up tests and make them run ALWAYS [DONE] Use transactions in tests +[IN PROGRESS] Thread autoupdate (JS + API) + [NOT STARTED] Tree view (JS) [NOT STARTED] Adding tags to images filename [NOT STARTED] Federative network for s2s communication @@ -16,7 +18,6 @@ denied". Use second only for autoban for [NOT STARTED] Bitmessage gate [NOT STARTED] Notification engine [NOT STARTED] Javascript disabling engine -[NOT STARTED] Thread autoupdate (JS + API) [NOT STARTED] Group tags by first letter in all tags list [NOT STARTED] Show board speed in the lower panel (posts per day) [NOT STARTED] Character counter in the post field @@ -30,6 +31,7 @@ and move everything that is used only in post or its part (delimited by N characters) into quote of the new post. [NOT STARTED] Ban confirmation page with reason [NOT STARTED] Post deletion confirmation page +[NOT STARTED] Moderating page. Tags editing and adding = Bugs = [DONE] Fix bug with creating threads from tag view