diff --git a/boards/admin.py b/boards/admin.py --- a/boards/admin.py +++ b/boards/admin.py @@ -5,7 +5,7 @@ from boards.models import Post, Tag, Use class PostAdmin(admin.ModelAdmin): list_display = ('id', 'title', 'text') - list_filter = ('pub_time', 'tags') + list_filter = ('pub_time', 'thread_new') search_fields = ('id', 'title', 'text') diff --git a/boards/mdx_neboard.py b/boards/mdx_neboard.py --- a/boards/mdx_neboard.py +++ b/boards/mdx_neboard.py @@ -14,6 +14,7 @@ SPOILER_PATTERN = r'%%(.+)%%' COMMENT_PATTERN = r'^(//(.+))' STRIKETHROUGH_PATTERN = r'~(.+)~' + class AutolinkPattern(Pattern): def handleMatch(self, m): link_element = etree.Element('a') @@ -41,17 +42,17 @@ class ReflinkPattern(Pattern): if posts.count() > 0: ref_element = etree.Element('a') - post = posts[0] + post = posts[0] if post.thread: link = reverse(boards.views.thread, kwargs={'post_id': post.thread.id}) \ + '#' + post_id else: - link = reverse(boards.views.thread, kwargs={'post_id': post_id}) + link = reverse(boards.views.thread, kwargs={'post_id': post_id}) ref_element.set('href', link) ref_element.text = m.group(2) - return ref_element + return ref_element class SpoilerPattern(Pattern): diff --git a/boards/migrations/0014_auto__add_thread__chg_field_post_thread.py b/boards/migrations/0014_auto__add_thread__chg_field_post_thread.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0014_auto__add_thread__chg_field_post_thread.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Thread' + db.create_table(u'boards_thread', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('bump_time', self.gf('django.db.models.fields.DateTimeField')()), + ('last_edit_time', self.gf('django.db.models.fields.DateTimeField')()), + )) + db.send_create_signal('boards', ['Thread']) + + # Adding M2M table for field tags on 'Thread' + m2m_table_name = db.shorten_name(u'boards_thread_tags') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('thread', models.ForeignKey(orm['boards.thread'], null=False)), + ('tag', models.ForeignKey(orm['boards.tag'], null=False)) + )) + db.create_unique(m2m_table_name, ['thread_id', 'tag_id']) + + db.delete_table(db.shorten_name(u'boards_tag_threads')) + # Adding M2M table for field threads on 'Tag' + m2m_table_name = db.shorten_name(u'boards_tag_threads') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('thread', models.ForeignKey(orm['boards.thread'], null=False)), + ('tag', models.ForeignKey(orm['boards.tag'], null=False)) + )) + db.create_unique(m2m_table_name, ['thread_id', 'tag_id']) + + # Adding M2M table for field replies on 'Thread' + m2m_table_name = db.shorten_name(u'boards_thread_replies') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('thread', models.ForeignKey(orm['boards.thread'], null=False)), + ('post', models.ForeignKey(orm['boards.post'], null=False)) + )) + db.create_unique(m2m_table_name, ['thread_id', 'post_id']) + + db.add_column(u'boards_post', 'thread_new', + self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['boards.Thread'], null=True), + keep_default=False) + + def backwards(self, orm): + pass + + models = { + 'boards.ban': { + 'Meta': {'object_name': 'Ban'}, + 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'}) + }, + 'boards.post': { + 'Meta': {'object_name': 'Post'}, + '_text_rendered': ('django.db.models.fields.TextField', [], {}), + 'bump_time': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), + 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), + 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), + 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}), + 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), + 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'}) + }, + 'boards.setting': { + 'Meta': {'object_name': 'Setting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'boards.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"}) + }, + 'boards.thread': { + 'Meta': {'object_name': 'Thread'}, + 'bump_time': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}) + }, + 'boards.user': { + 'Meta': {'object_name': 'User'}, + 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rank': ('django.db.models.fields.IntegerField', [], {}), + 'registration_time': ('django.db.models.fields.DateTimeField', [], {}), + 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['boards'] \ No newline at end of file diff --git a/boards/migrations/0015_post_to_thread.py b/boards/migrations/0015_post_to_thread.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0015_post_to_thread.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +from boards import views + + +class Migration(DataMigration): + + def forwards(self, orm): + for post in orm.Post.objects.filter(thread=None): + thread = orm.Thread.objects.create( + bump_time=post.bump_time, + last_edit_time=post.last_edit_time) + + thread.replies.add(post) + post.thread_new = thread + post.save() + print str(post.thread_new.id) + + for reply in post.replies.all(): + thread.replies.add(reply) + reply.thread_new = thread + reply.save() + + for tag in post.tags.all(): + thread.tags.add(tag) + tag.threads.add(thread) + + def backwards(self, orm): + pass + + models = { + 'boards.ban': { + 'Meta': {'object_name': 'Ban'}, + 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'}) + }, + 'boards.post': { + 'Meta': {'object_name': 'Post'}, + '_text_rendered': ('django.db.models.fields.TextField', [], {}), + 'bump_time': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), + 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), + 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), + 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}), + 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), + 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}), + 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'}) + }, + 'boards.setting': { + 'Meta': {'object_name': 'Setting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'boards.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"}) + }, + 'boards.thread': { + 'Meta': {'object_name': 'Thread'}, + 'bump_time': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}) + }, + 'boards.user': { + 'Meta': {'object_name': 'User'}, + 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rank': ('django.db.models.fields.IntegerField', [], {}), + 'registration_time': ('django.db.models.fields.DateTimeField', [], {}), + 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['boards'] + symmetrical = True diff --git a/boards/migrations/0016_auto__del_field_post_bump_time.py b/boards/migrations/0016_auto__del_field_post_bump_time.py new file mode 100644 --- /dev/null +++ b/boards/migrations/0016_auto__del_field_post_bump_time.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting field 'Post.bump_time' + db.delete_column(u'boards_post', 'bump_time') + + # Removing M2M table for field tags on 'Post' + db.delete_table(db.shorten_name(u'boards_post_tags')) + + # Removing M2M table for field replies on 'Post' + db.delete_table(db.shorten_name(u'boards_post_replies')) + + + def backwards(self, orm): + + # User chose to not deal with backwards NULL issues for 'Post.bump_time' + raise RuntimeError("Cannot reverse this migration. 'Post.bump_time' and its values cannot be restored.") + + # The following code is provided here to aid in writing a correct migration # Adding field 'Post.bump_time' + db.add_column(u'boards_post', 'bump_time', + self.gf('django.db.models.fields.DateTimeField')(), + keep_default=False) + + # Adding M2M table for field tags on 'Post' + m2m_table_name = db.shorten_name(u'boards_post_tags') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('post', models.ForeignKey(orm['boards.post'], null=False)), + ('tag', models.ForeignKey(orm['boards.tag'], null=False)) + )) + db.create_unique(m2m_table_name, ['post_id', 'tag_id']) + + # Adding M2M table for field replies on 'Post' + m2m_table_name = db.shorten_name(u'boards_post_replies') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('from_post', models.ForeignKey(orm['boards.post'], null=False)), + ('to_post', models.ForeignKey(orm['boards.post'], null=False)) + )) + db.create_unique(m2m_table_name, ['from_post_id', 'to_post_id']) + + + models = { + 'boards.ban': { + 'Meta': {'object_name': 'Ban'}, + 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'}) + }, + 'boards.post': { + 'Meta': {'object_name': 'Post'}, + '_text_rendered': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}), + 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'poster_user_agent': ('django.db.models.fields.TextField', [], {}), + 'pub_time': ('django.db.models.fields.DateTimeField', [], {}), + 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}), + 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}), + 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'}) + }, + 'boards.setting': { + 'Meta': {'object_name': 'Setting'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'boards.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"}) + }, + 'boards.thread': { + 'Meta': {'object_name': 'Thread'}, + 'bump_time': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}), + 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}) + }, + 'boards.user': { + 'Meta': {'object_name': 'User'}, + 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}), + 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rank': ('django.db.models.fields.IntegerField', [], {}), + 'registration_time': ('django.db.models.fields.DateTimeField', [], {}), + 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['boards'] \ No newline at end of file diff --git a/boards/models/__init__.py b/boards/models/__init__.py --- a/boards/models/__init__.py +++ b/boards/models/__init__.py @@ -1,6 +1,7 @@ __author__ = 'neko259' from boards.models.post import Post +from boards.models.post import Thread from boards.models.tag import Tag from boards.models.user import Ban from boards.models.user import Setting diff --git a/boards/models/post.py b/boards/models/post.py --- a/boards/models/post.py +++ b/boards/models/post.py @@ -38,18 +38,25 @@ 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() + if not thread: + thread = Thread.objects.create(bump_time=posting_time, + last_edit_time=posting_time) + else: + thread.bump() + thread.last_edit_time = posting_time + thread.save() post = self.create(title=title, text=text, pub_time=posting_time, - thread=thread, + thread_new=thread, image=image, poster_ip=ip, poster_user_agent=UNKNOWN_UA, last_edit_time=posting_time, - bump_time=posting_time, user=user) + thread.replies.add(post) if tags: linked_tags = [] for tag in tags: @@ -58,31 +65,18 @@ class PostManager(models.Manager): linked_tags.extend(tag_linked_tags) tags.extend(linked_tags) - map(post.tags.add, tags) - for tag in tags: - tag.threads.add(post) + map(thread.add_tag, tags) - if thread: - thread.replies.add(post) - thread.bump() - thread.last_edit_time = posting_time - thread.save() - else: - self._delete_old_threads() - + self._delete_old_threads() self.connect_replies(post) return post + # TODO Remove this method after migration def delete_post(self, post): - if post.replies.count() > 0: - map(self.delete_post, post.replies.all()) - - # Update thread's last edit time - thread = post.thread - if thread: - thread.last_edit_time = timezone.now() - thread.save() + thread = post.thread_new + thread.last_edit_time = timezone.now() + thread.save() post.delete() @@ -90,15 +84,16 @@ class PostManager(models.Manager): posts = self.filter(poster_ip=ip) map(self.delete_post, posts) + # TODO Remove this method after migration def get_threads(self, tag=None, page=ALL_PAGES, order_by='-bump_time'): if tag: threads = tag.threads - if threads.count() == 0: + if not threads.exists(): raise Http404 else: - threads = self.filter(thread=None) + threads = Thread.objects.all() threads = threads.order_by(order_by) @@ -113,26 +108,25 @@ class PostManager(models.Manager): return threads + # TODO Remove this method after migration def get_thread(self, opening_post_id): try: - opening_post = self.get(id=opening_post_id, thread=None) + opening_post = self.get(id=opening_post_id) except Post.DoesNotExist: raise Http404 - if opening_post.replies: - thread = [opening_post] - thread.extend(opening_post.replies.all().order_by('pub_time')) + return opening_post.thread_new - return thread - + # TODO Move this method to thread manager def get_thread_page_count(self, tag=None): if tag: - threads = self.filter(thread=None, tags=tag) + threads = Thread.objects.filter(tags=tag) else: - threads = self.filter(thread=None) + threads = Thread.objects.all() return self._get_page_count(threads.count()) + # TODO Move this method to thread manager def _delete_old_threads(self): """ Preserves maximum thread count. If there are too many threads, @@ -143,14 +137,14 @@ class PostManager(models.Manager): # Maybe make some 'old' field in the model to indicate the thread # must not be shown and be able for replying. - threads = self.get_threads() + threads = Thread.objects.all() thread_count = threads.count() if thread_count > settings.MAX_THREAD_COUNT: num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT old_threads = threads[thread_count - num_threads_to_delete:] - map(self.delete_post, old_threads) + map(Thread.delete_with_posts, old_threads) def connect_replies(self, post): """Connect replies to a post to show them as a refmap""" @@ -204,13 +198,10 @@ class Post(models.Model): poster_user_agent = models.TextField() thread = models.ForeignKey('Post', null=True, default=None) - tags = models.ManyToManyField('Tag') + thread_new = models.ForeignKey('Thread', null=True, default=None) last_edit_time = models.DateTimeField() - bump_time = models.DateTimeField() user = models.ForeignKey('User', null=True, default=None) - replies = models.ManyToManyField('Post', symmetrical=False, null=True, - blank=True, related_name='re+') referenced_posts = models.ManyToManyField('Post', symmetrical=False, null=True, blank=True, related_name='rfp+') @@ -226,14 +217,40 @@ class Post(models.Model): return title + def get_sorted_referenced_posts(self): + return self.referenced_posts.order_by('id') + + def is_referenced(self): + return self.referenced_posts.all().exists() + + +class Thread(models.Model): + + class Meta: + app_label = 'boards' + + tags = models.ManyToManyField('Tag') + bump_time = models.DateTimeField() + last_edit_time = models.DateTimeField() + replies = models.ManyToManyField('Post', symmetrical=False, null=True, + blank=True, related_name='tre+') + + def get_tags(self): + """Get a sorted tag list""" + + return self.tags.order_by('name') + + def bump(self): + """Bump (move to up) thread""" + + if self.can_bump(): + self.bump_time = timezone.now() + def get_reply_count(self): return self.replies.count() def get_images_count(self): - images_count = 1 if self.image else 0 - images_count += self.replies.filter(image_width__gt=0).count() - - return images_count + return self.replies.filter(image_width__gt=0).count() def can_bump(self): """Check if the thread can be bumped by replying""" @@ -242,31 +259,38 @@ class Post(models.Model): return post_count <= settings.MAX_POSTS_PER_THREAD - def bump(self): - """Bump (move to up) thread""" + def delete_with_posts(self): + """Completely delete thread""" - if self.can_bump(): - self.bump_time = timezone.now() + if self.replies.count() > 0: + map(Post.objects.delete_post, self.replies.all()) + + self.delete() def get_last_replies(self): + """Get last replies, not including opening post""" + if settings.LAST_REPLIES_COUNT > 0: reply_count = self.get_reply_count() if reply_count > 0: reply_count_to_show = min(settings.LAST_REPLIES_COUNT, - reply_count) + reply_count - 1) last_replies = self.replies.all().order_by('pub_time')[ reply_count - reply_count_to_show:] return last_replies - def get_tags(self): - """Get a sorted tag list""" + def get_replies(self): + """Get sorted thread posts""" - return self.tags.order_by('name') + return self.replies.all().order_by('pub_time') - def get_sorted_referenced_posts(self): - return self.referenced_posts.order_by('id') + def add_tag(self, tag): + """Connect thread to a tag and tag to a thread""" - def is_referenced(self): - return self.referenced_posts.all().exists() + self.tags.add(tag) + tag.threads.add(self) + + def __unicode__(self): + return str(self.get_replies()[0].id) \ No newline at end of file diff --git a/boards/models/tag.py b/boards/models/tag.py --- a/boards/models/tag.py +++ b/boards/models/tag.py @@ -1,4 +1,4 @@ -from boards.models import Post +from boards.models import Thread from django.db import models from django.db.models import Count @@ -20,8 +20,8 @@ class TagManager(models.Manager): class Tag(models.Model): """ - A tag is a text node assigned to the post. The tag serves as a board - section. There can be multiple tags for each message + A tag is a text node assigned to the thread. The tag serves as a board + section. There can be multiple tags for each thread """ objects = TagManager() @@ -30,7 +30,7 @@ class Tag(models.Model): app_label = 'boards' name = models.CharField(max_length=100) - threads = models.ManyToManyField('Post', null=True, + threads = models.ManyToManyField(Thread, null=True, blank=True, related_name='tag+') linked = models.ForeignKey('Tag', null=True, blank=True) @@ -43,14 +43,15 @@ class Tag(models.Model): def get_post_count(self): return self.threads.count() - def get_popularity(self): - posts_with_tag = Post.objects.get_threads(tag=self) - reply_count = 0 - for post in posts_with_tag: - reply_count += post.get_reply_count() - reply_count += OPENING_POST_POPULARITY_WEIGHT - - return reply_count + # TODO Reenable this method after migration + # def get_popularity(self): + # posts_with_tag = Thread.objects.get_threads(tag=self) + # reply_count = 0 + # for post in posts_with_tag: + # reply_count += post.get_reply_count() + # reply_count += OPENING_POST_POPULARITY_WEIGHT + # + # return reply_count def get_linked_tags(self): tag_list = [] diff --git a/boards/static/css/md/base_page.css b/boards/static/css/md/base_page.css --- a/boards/static/css/md/base_page.css +++ b/boards/static/css/md/base_page.css @@ -104,7 +104,7 @@ p { border: solid 1px #888; color: #fff; padding: 10px; - margin: 5; + margin: 5px; } .form-row { 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 @@ -55,10 +55,10 @@ {% endif %} -{% if post.tags.exists %} +{% if post.thread.tags.exists %}
{% trans 'Tags' %}: - {% for tag in post.tags.all %} + {% for tag in post.thread.get_tags %} {{ tag.name }} {% endfor %} diff --git a/boards/templates/boards/posting_general.html b/boards/templates/boards/posting_general.html --- a/boards/templates/boards/posting_general.html +++ b/boards/templates/boards/posting_general.html @@ -68,47 +68,50 @@ {% cache 600 thread_short thread.thread.last_edit_time moderator LANGUAGE_CODE %}
{% if thread.bumpable %} -
+
{% else %} -
+
{% endif %} - {% if thread.thread.image %} + {% if thread.op.image %} {% endif %}
{% autoescape off %} - {{ thread.thread.text.rendered|truncatewords_html:50 }} + {{ thread.op.text.rendered|truncatewords_html:50 }} {% endautoescape %} - {% if thread.thread.is_referenced %} + {% if thread.op.is_referenced %}
{% trans "Replies" %}: - {% for ref_post in thread.thread.get_sorted_referenced_posts %} + {% for ref_post in thread.op.get_sorted_referenced_posts %} >>{{ ref_post.id }}{% if not forloop.last %},{% endif %} {% endfor %} @@ -153,7 +156,7 @@ @@ -257,7 +260,7 @@ {% block metapanel %} - Neboard 1.3 + Neboard 1.4 {% trans "Pages:" %} {% for page in pages %} [ - {% if posts %} - {% cache 600 thread_view posts.0.last_edit_time moderator LANGUAGE_CODE %} - {% if bumpable %} -
-
-
-
- {{ posts_left }} {% trans 'posts to bumplimit' %} -
+ {% cache 600 thread_view thread.last_edit_time moderator LANGUAGE_CODE %} + {% if bumpable %} +
+
- {% endif %} -
- {% for post in posts %} - {% if bumpable %} -
- {% else %} -
- {% endif %} - {% if post.image %} -
- {{ post.id }} - +
+ {{ posts_left }} {% trans 'posts to bumplimit' %}
- {% endif %} -
- - {% autoescape off %} - {{ post.text.rendered }} - {% endautoescape %} - {% 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 %} +
+ {% for post in thread.get_replies %} + {% if bumpable %} +
+ {% else %} +
+ {% endif %} + {% if post.image %} +
+ {{ post.id }} +
{% endif %} +
+ - {% if forloop.first %} - - {% endif %} -
+ {% autoescape off %} + {{ post.text.rendered }} + {% endautoescape %} + {% if post.is_referenced %} +
+ {% trans "Replies" %}: + {% for ref_post in post.get_sorted_referenced_posts %} + >>{{ ref_post.id }}{% if not forloop.last %},{% endif %} {% endfor %}
- {% endcache %} {% endif %} +
+ {% if forloop.first %} + + {% endif %} +
+ {% endfor %} +
+ {% endcache %}
{% csrf_token %} @@ -152,10 +150,10 @@ {% 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' %}. - {% trans 'Last update: ' %}{{ posts.0.last_edit_time }} + {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %} + {{ thread.get_reply_count }} {% trans 'replies' %}, + {{ thread.get_images_count }} {% trans 'images' %}. + {% trans 'Last update: ' %}{{ thread.last_edit_time }} [RSS] {% endcache %} diff --git a/boards/tests.py b/boards/tests.py --- a/boards/tests.py +++ b/boards/tests.py @@ -30,8 +30,7 @@ class PostTests(TestCase): post = self._create_post() - self.assertIsNotNone(post) - self.assertIsNone(post.thread, 'Opening post has a thread') + self.assertIsNotNone(post, 'No post was created') def test_delete_post(self): """Test post deletion""" @@ -46,10 +45,11 @@ class PostTests(TestCase): def test_delete_thread(self): """Test thread deletion""" - thread = self._create_post() + opening_post = self._create_post() + thread = opening_post.thread_new reply = Post.objects.create_post("", "", thread=thread) - Post.objects.delete_post(thread) + thread.delete_with_posts() self.assertFalse(Post.objects.filter(id=reply.id).exists()) @@ -57,10 +57,10 @@ class PostTests(TestCase): """Test adding post to a thread""" op = self._create_post() - post = Post.objects.create_post("", "", thread=op) + post = Post.objects.create_post("", "", thread=op.thread_new) self.assertIsNotNone(post, 'Reply to thread wasn\'t created') - self.assertEqual(op.last_edit_time, post.pub_time, + self.assertEqual(op.thread_new.last_edit_time, post.pub_time, 'Post\'s create time doesn\'t match thread last edit' ' time') @@ -80,18 +80,23 @@ class PostTests(TestCase): opening_post = self._create_post() for i in range(0, 2): - Post.objects.create_post('title', 'text', thread=opening_post) + Post.objects.create_post('title', 'text', + thread=opening_post.thread_new) thread = Post.objects.get_thread(opening_post.id) - self.assertEqual(3, len(thread)) + self.assertEqual(3, thread.replies.count()) def test_create_post_with_tag(self): """Test adding tag to post""" tag = Tag.objects.create(name='test_tag') post = Post.objects.create_post(title='title', text='text', tags=[tag]) - self.assertIsNotNone(post) + + thread = post.thread_new + self.assertIsNotNone(post, 'Post not created') + self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread') + self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag') def test_thread_max_count(self): """Test deletion of old posts when the max thread count is reached""" @@ -124,7 +129,7 @@ class PostTests(TestCase): post = Post.objects.create_post("", "", tags=[tag]) - self.assertTrue(linked_tag in post.tags.all(), + self.assertTrue(linked_tag in post.thread_new.tags.all(), 'Linked tag was not added') @@ -162,7 +167,8 @@ class PagesTest(TestCase): u'Not existing tag is opened') reply_id = Post.objects.create_post('', TEST_TEXT, - thread=Post.objects.all()[0]) + thread=Post.objects.all()[0] + .thread) response_not_existing = client.get(THREAD_PAGE + str( reply_id) + '/') self.assertEqual(PAGE_404, diff --git a/boards/views.py b/boards/views.py --- a/boards/views.py +++ b/boards/views.py @@ -54,11 +54,12 @@ def index(request, page=0): form = threadFormClass(error_class=PlainErrorList, **kwargs) threads = [] - for thread in Post.objects.get_threads(page=int(page)): + for thread_to_show in Post.objects.get_threads(page=int(page)): threads.append({ - 'thread': thread, - 'bumpable': thread.can_bump(), - 'last_replies': thread.get_last_replies(), + 'thread': thread_to_show, + 'op': thread_to_show.get_replies()[0], + 'bumpable': thread_to_show.can_bump(), + 'last_replies': thread_to_show.get_last_replies(), }) # TODO Make this generic for tag and threads list pages @@ -111,9 +112,12 @@ def _new_post(request, form, opening_pos if len(tag_name) > 0: tag, created = Tag.objects.get_or_create(name=tag_name) tags.append(tag) + post_thread = None + else: + post_thread = opening_post.thread_new post = Post.objects.create_post(title=title, text=text, ip=ip, - thread=opening_post, image=image, + thread=post_thread, image=image, tags=tags, user=_get_user(request)) thread_to_show = (opening_post.id if opening_post else post.id) @@ -133,12 +137,13 @@ def tag(request, tag_name, page=0): tag = get_object_or_404(Tag, name=tag_name) threads = [] - for thread in Post.objects.get_threads(tag=tag, page=int(page)): + for thread_to_show in Post.objects.get_threads(page=int(page)): threads.append({ - 'thread': thread, - 'bumpable': thread.can_bump(), - 'last_replies': thread.get_last_replies(), - }) + 'thread': thread_to_show, + 'op': thread_to_show.get_replies()[0], + 'bumpable': thread_to_show.can_bump(), + 'last_replies': thread_to_show.get_last_replies(), + }) if request.method == 'POST': form = ThreadForm(request.POST, request.FILES, @@ -196,20 +201,21 @@ def thread(request, post_id): else: form = postFormClass(error_class=PlainErrorList, **kwargs) - posts = Post.objects.get_thread(post_id) + thread_to_show = get_object_or_404(Post, id=post_id).thread_new context = _init_default_context(request) - context['posts'] = posts + posts = thread_to_show.get_replies() context['form'] = form - context['bumpable'] = posts[0].can_bump() + context['bumpable'] = thread_to_show.can_bump() if context['bumpable']: - context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - len( - posts) + context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts\ + .count() 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) + context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time) + context["thread"] = thread_to_show return render(request, 'boards/thread.html', context) 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 [DONE] Thread autoupdate (JS + API) +[IN PROGRESS] Split up post model into post and thread, +and move everything that is used only in 1st post to thread model. [NOT STARTED] Tree view (JS) [NOT STARTED] Adding tags to images filename @@ -22,8 +24,6 @@ denied". Use second only for autoban for [NOT STARTED] Character counter in the post field [NOT STARTED] Save image thumbnails size to the separate field [NOT STARTED] Whitelist functionality. Permin autoban of an address -[NOT STARTED] Split up post model into post and thread, -and move everything that is used only in 1st post to thread model. [NOT STARTED] Statistics module. Count views (optional, may result in bad performance), posts per day/week/month, users (or IPs) [NOT STARTED] Quote button next to "reply" for posts in thread to include full