# HG changeset patch # User bodqhrohro # Date 2016-05-13 17:14:08 # Node ID b47850ce08d5f8cfecc7b8649c44a4f27de36dd8 # Parent dcc73245714b7d5ac6710aa3058b19ec9cd1802c # Parent df109e7c56ae1ad04dc587705c9376d423e77d53 Merge remote changes diff --git a/boards/admin.py b/boards/admin.py --- a/boards/admin.py +++ b/boards/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse +from django.db.models import F from boards.models import Post, Tag, Ban, Thread, Banner, PostImage, KeyPair, GlobalId @@ -12,7 +13,8 @@ class PostAdmin(admin.ModelAdmin): search_fields = ('id', 'title', 'text', 'poster_ip') exclude = ('referenced_posts', 'refmap', 'images', 'global_id') readonly_fields = ('poster_ip', 'threads', 'thread', 'linked_images', - 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id') + 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id', + 'version') def ban_poster(self, request, queryset): bans = 0 @@ -54,6 +56,11 @@ class PostAdmin(admin.ModelAdmin): args=[global_id.id]), str(global_id)) linked_global_id.allow_tags = True + def save_model(self, request, obj, form, change): + obj.increment_version() + obj.save() + obj.clear_cache() + actions = ['ban_poster', 'ban_with_hiding'] @@ -96,6 +103,14 @@ class ThreadAdmin(admin.ModelAdmin): def save_related(self, request, form, formsets, change): super().save_related(request, form, formsets, change) form.instance.refresh_tags() + + def save_model(self, request, obj, form, change): + op = obj.get_opening_post() + op.increment_version() + op.save(update_fields=['version']) + obj.save() + op.clear_cache() + list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip', 'display_tags') list_filter = ('bump_time', 'status') diff --git a/boards/management/commands/invalidate_sync_cache.py b/boards/management/commands/invalidate_sync_cache.py --- a/boards/management/commands/invalidate_sync_cache.py +++ b/boards/management/commands/invalidate_sync_cache.py @@ -12,9 +12,11 @@ class Command(BaseCommand): @transaction.atomic def handle(self, *args, **options): count = 0 - for global_id in GlobalId.objects.all(): + for global_id in GlobalId.objects.exclude(content__isnull=True).exclude( + content=''): if global_id.is_local() and global_id.content is not None: global_id.content = None global_id.save() + global_id.signature_set.all().delete() count += 1 print('Invalidated {} caches.'.format(count)) diff --git a/boards/management/commands/sync_with_server.py b/boards/management/commands/sync_with_server.py --- a/boards/management/commands/sync_with_server.py +++ b/boards/management/commands/sync_with_server.py @@ -5,7 +5,7 @@ import httplib2 from django.core.management import BaseCommand from boards.models import GlobalId -from boards.models.post.sync import SyncManager +from boards.models.post.sync import SyncManager, TAG_ID, TAG_VERSION __author__ = 'neko259' @@ -27,7 +27,7 @@ class Command(BaseCommand): def handle(self, *args, **options): url = options.get('url') - pull_url = url + 'api/sync/pull/' + list_url = url + 'api/sync/list/' get_url = url + 'api/sync/get/' file_url = url[:-1] @@ -52,8 +52,8 @@ class Command(BaseCommand): raise Exception('Invalid global ID') else: h = httplib2.Http() - xml = GlobalId.objects.generate_request_pull() - response, content = h.request(pull_url, method="POST", body=xml) + xml = GlobalId.objects.generate_request_list() + response, content = h.request(list_url, method="POST", body=xml) print(content.decode() + '\n') @@ -64,11 +64,16 @@ class Command(BaseCommand): models = root.findall('models')[0] for model in models: - global_id, exists = GlobalId.from_xml_element(model) - if not exists: - print(global_id) + tag_id = model.find(TAG_ID) + global_id, exists = GlobalId.from_xml_element(tag_id) + tag_version = model.find(TAG_VERSION) + if tag_version is not None: + version = int(tag_version.text) or 1 + else: + version = 1 + if not exists or global_id.post.version < version: ids_to_sync.append(global_id) - print() + print('Starting sync...') if len(ids_to_sync) > 0: limit = options.get('split_query', len(ids_to_sync)) diff --git a/boards/migrations/0045_post_version.py b/boards/migrations/0045_post_version.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0045_post_version.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-05-11 09:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('boards', '0044_globalid_content'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='version', + field=models.IntegerField(default=1), + ), + ] diff --git a/boards/models/post/__init__.py b/boards/models/post/__init__.py --- a/boards/models/post/__init__.py +++ b/boards/models/post/__init__.py @@ -11,7 +11,7 @@ from boards.utils import datetime_to_epo from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse from django.db import models -from django.db.models import TextField, QuerySet +from django.db.models import TextField, QuerySet, F from django.template.defaultfilters import truncatewords, striptags from django.template.loader import render_to_string @@ -104,6 +104,7 @@ class Post(models.Model, Viewable): tripcode = models.CharField(max_length=50, blank=True, default='') opening = models.BooleanField(db_index=True) hidden = models.BooleanField(default=False) + version = models.IntegerField(default=1) def __str__(self): return 'P#{}/{}'.format(self.id, self.get_title()) @@ -337,8 +338,11 @@ class Post(models.Model, Viewable): replacements = dict() for post_id in REGEX_REPLY.findall(self.get_raw_text()): - absolute_post_id = str(Post.objects.get(id=post_id).global_id) - replacements[post_id] = absolute_post_id + try: + absolute_post_id = str(Post.objects.get(id=post_id).global_id) + replacements[post_id] = absolute_post_id + except Post.DoesNotExist: + pass text = self.get_raw_text() or '' for key in replacements: @@ -379,3 +383,14 @@ class Post(models.Model, Viewable): def set_hidden(self, hidden): self.hidden = hidden + + def increment_version(self): + self.version = F('version') + 1 + + def clear_cache(self): + global_id = self.global_id + if global_id is not None and global_id.is_local()\ + and global_id.content is not None: + global_id.content = None + global_id.save() + global_id.signature_set.all().delete() diff --git a/boards/models/post/sync.py b/boards/models/post/sync.py --- a/boards/models/post/sync.py +++ b/boards/models/post/sync.py @@ -32,6 +32,7 @@ TAG_TAG = 'tag' TAG_ATTACHMENT_REFS = 'attachment-refs' TAG_ATTACHMENT_REF = 'attachment-ref' TAG_TRIPCODE = 'tripcode' +TAG_VERSION = 'version' TYPE_GET = 'get' @@ -126,6 +127,8 @@ class SyncManager: SyncManager._attachment_to_xml( attachments_tag, attachment_refs, file.file.file, file.hash, file.file.url) + version_tag = et.SubElement(content_tag, TAG_VERSION) + version_tag.text = str(post.version) global_id.content = et.tostring(content_tag, ENCODING_UNICODE) global_id.save() @@ -227,11 +230,12 @@ class SyncManager: title=title, text=text, pub_time=pub_time, opening_post=opening_post, tags=tags, global_id=global_id, files=files, tripcode=tripcode) + print('Parsed post {}'.format(global_id)) else: raise SyncException(EXCEPTION_NODE.format(tag_status.text)) @staticmethod - def generate_response_pull(): + def generate_response_list(): response = et.Element(TAG_RESPONSE) status = et.SubElement(response, TAG_STATUS) @@ -240,8 +244,11 @@ class SyncManager: models = et.SubElement(response, TAG_MODELS) for post in Post.objects.prefetch_related('global_id').all(): - tag_id = et.SubElement(models, TAG_ID) + tag_model = et.SubElement(models, TAG_MODEL) + tag_id = et.SubElement(tag_model, TAG_ID) post.global_id.to_xml_element(tag_id) + tag_version = et.SubElement(tag_model, TAG_VERSION) + tag_version.text = str(post.version) return et.tostring(response, ENCODING_UNICODE) diff --git a/boards/models/signature.py b/boards/models/signature.py --- a/boards/models/signature.py +++ b/boards/models/signature.py @@ -8,7 +8,7 @@ TAG_REQUEST = 'request' TAG_ID = 'id' TYPE_GET = 'get' -TYPE_PULL = 'pull' +TYPE_LIST = 'list' ATTR_VERSION = 'version' ATTR_TYPE = 'type' @@ -39,13 +39,13 @@ class GlobalIdManager(models.Manager): return et.tostring(request, 'unicode') - def generate_request_pull(self): + def generate_request_list(self): """ Form a pull request from a list of ModelId objects. """ request = et.Element(TAG_REQUEST) - request.set(ATTR_TYPE, TYPE_PULL) + request.set(ATTR_TYPE, TYPE_LIST) request.set(ATTR_VERSION, '1.0') model = et.SubElement(request, TAG_MODEL) 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 @@ -96,17 +96,24 @@ function addQuickReply(postId) { } function addQuickQuote() { + var textAreaJq = $('textarea'); + + var quoteButton = $("#quote-button"); + var postId = quoteButton.attr('data-post-id'); + if (postId != null && getForm().prev().attr('id') != postId) { + addQuickReply(postId); + } + var textToAdd = ''; - var textAreaJq = $('textarea'); var selection = window.getSelection().toString(); if (selection.length == 0) { - selection = $("#quote-button").attr('data-text'); + selection = quoteButton.attr('data-text'); } if (selection.length > 0) { textToAdd += '[quote]' + selection + '[/quote]\n'; } - textAreaJq.val(textAreaJq.val()+ textToAdd); + textAreaJq.val(textAreaJq.val() + textToAdd); textAreaJq.focus(); @@ -128,6 +135,15 @@ function showQuoteButton() { var text = window.getSelection().toString(); quoteButton.attr('data-text', text); + + var rect = window.getSelection().getRangeAt(0).getBoundingClientRect(); + var element = $(document.elementFromPoint(rect.x, rect.y)); + var postParent = element.parents('.post'); + if (postParent.length > 0) { + quoteButton.attr('data-post-id', postParent.attr('id')); + } else { + quoteButton.attr('data-post-id', null); + } } else { quoteButton.hide(); } diff --git a/boards/tests/test_keys.py b/boards/tests/test_keys.py --- a/boards/tests/test_keys.py +++ b/boards/tests/test_keys.py @@ -71,6 +71,7 @@ class KeyTest(TestCase): '[post]%s[/post]' '' '%s' + '%s' '' % ( key.public_key, reply_post.id, @@ -80,6 +81,7 @@ class KeyTest(TestCase): post.id, key.key_type, str(reply_post.get_pub_time_str()), + post.version, ) in response, 'Wrong XML generated for the GET response.') diff --git a/boards/tests/test_sync.py b/boards/tests/test_sync.py --- a/boards/tests/test_sync.py +++ b/boards/tests/test_sync.py @@ -43,6 +43,7 @@ class SyncTest(TestCase): '%s' '%s' '%s' + '%s' '' % ( post.global_id.key, post.global_id.local_id, @@ -51,6 +52,7 @@ class SyncTest(TestCase): post.get_sync_text(), post.get_thread().get_tags().first().name, post.get_pub_time_str(), + post.version, ) in response, 'Wrong response generated for the GET request.') @@ -90,6 +92,7 @@ class SyncTest(TestCase): '%s' '%s' '%s' + '%s' '' % ( post.global_id.key, post.global_id.local_id, @@ -98,5 +101,6 @@ class SyncTest(TestCase): post.get_sync_text(), post.get_thread().get_tags().first().name, post.get_pub_time_str(), + post.version, ) in response, 'Wrong response generated for the GET request.') diff --git a/boards/urls.py b/boards/urls.py --- a/boards/urls.py +++ b/boards/urls.py @@ -1,5 +1,4 @@ from django.conf.urls import url -#from django.views.i18n import javascript_catalog import neboard @@ -9,10 +8,9 @@ from boards.views import api, tag_thread settings, all_tags, feed from boards.views.authors import AuthorsView from boards.views.notifications import NotificationView -from boards.views.search import BoardSearchView from boards.views.static import StaticPageView from boards.views.preview import PostPreviewView -from boards.views.sync import get_post_sync_data, response_get, response_pull +from boards.views.sync import get_post_sync_data, response_get, response_list from boards.views.random import RandomImageView from boards.views.tag_gallery import TagGalleryView from boards.views.translation import cached_javascript_catalog @@ -78,9 +76,8 @@ urlpatterns = [ url(r'^api/new_posts/$', api.api_get_new_posts, name='new_posts'), # Sync protocol API - url(r'^api/sync/pull/$', response_pull, name='api_sync_pull'), - url(r'^api/sync/get/$', response_get, name='api_sync_pull'), - # TODO 'get' request + url(r'^api/sync/list/$', response_list, name='api_sync_list'), + url(r'^api/sync/get/$', response_get, name='api_sync_get'), # Notifications url(r'^notifications/(?P\w+)/$', NotificationView.as_view(), name='notifications'), diff --git a/boards/views/sync.py b/boards/views/sync.py --- a/boards/views/sync.py +++ b/boards/views/sync.py @@ -1,18 +1,17 @@ import xml.etree.ElementTree as et -import xml.dom.minidom from django.http import HttpResponse, Http404 from boards.models import GlobalId, Post from boards.models.post.sync import SyncManager -def response_pull(request): +def response_list(request): request_xml = request.body - if request_xml is None: + if request_xml is None or len(request_xml) == 0: return HttpResponse(content='Use the API') - response_xml = SyncManager.generate_response_pull() + response_xml = SyncManager.generate_response_list() return HttpResponse(content=response_xml) @@ -25,7 +24,7 @@ def response_get(request): request_xml = request.body - if request_xml is None: + if request_xml is None or len(request_xml) == 0: return HttpResponse(content='Use the API') posts = [] @@ -50,13 +49,7 @@ def get_post_sync_data(request, post_id) xml_str = SyncManager.generate_response_get([post]) - xml_repr = xml.dom.minidom.parseString(xml_str) - xml_repr = xml_repr.toprettyxml() - - content = '=Global ID=\n%s\n\n=XML=\n%s' \ - % (post.global_id, xml_repr) - return HttpResponse( - content_type='text/plain; charset=utf-8', - content=content, - ) \ No newline at end of file + content_type='text/xml; charset=utf-8', + content=xml_str, + ) diff --git a/docs/api.markdown b/docs/api.markdown --- a/docs/api.markdown +++ b/docs/api.markdown @@ -43,7 +43,7 @@ Parameters: * ``last_update``: last update timestamp * ``last_post``: last added post id -Get the diff of the thread in the `O`` +Get the diff of the thread in the ``O`` format. 2 formats are available: ``html`` (used in AJAX thread update) and ``json``. The default format is ``html``. Return list format: diff --git a/docs/dip-1.markdown b/docs/dip-1.markdown --- a/docs/dip-1.markdown +++ b/docs/dip-1.markdown @@ -61,15 +61,15 @@ responsible for validating it. The server is required to return the status of request. See 3.2 for details. -### 3.1.1 pull ### +### 3.1.1 list ### -"pull" request gets the desired model id list by the given filter (e.g. thread, tags, +"list" request gets the desired model id list by the given filter (e.g. thread, tags, author) Sample request is as follows: - + 0 0 @@ -95,10 +95,17 @@ Sample response: success - - - - + + + 1 + + + + + + + value + diff --git a/docs/dip-2.markdown b/docs/dip-2.markdown --- a/docs/dip-2.markdown +++ b/docs/dip-2.markdown @@ -13,6 +13,7 @@ * title -- text field. * text -- text field. * pub-time -- timestamp (TBD: Define format). +* version -- when post content changes, version should be incremented. # 2.2 Optional fields #