##// END OF EJS Templates
Split up post model into post and thread to normalise models. Still need some refactoring
neko259 -
r398:b2448061 default
parent child Browse files
Show More
@@ -0,0 +1,115 b''
1 # -*- coding: utf-8 -*-
2 from south.db import db
3 from south.v2 import SchemaMigration
4 from django.db import models
5
6
7 class Migration(SchemaMigration):
8
9 def forwards(self, orm):
10 # Adding model 'Thread'
11 db.create_table(u'boards_thread', (
12 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
13 ('bump_time', self.gf('django.db.models.fields.DateTimeField')()),
14 ('last_edit_time', self.gf('django.db.models.fields.DateTimeField')()),
15 ))
16 db.send_create_signal('boards', ['Thread'])
17
18 # Adding M2M table for field tags on 'Thread'
19 m2m_table_name = db.shorten_name(u'boards_thread_tags')
20 db.create_table(m2m_table_name, (
21 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
22 ('thread', models.ForeignKey(orm['boards.thread'], null=False)),
23 ('tag', models.ForeignKey(orm['boards.tag'], null=False))
24 ))
25 db.create_unique(m2m_table_name, ['thread_id', 'tag_id'])
26
27 db.delete_table(db.shorten_name(u'boards_tag_threads'))
28 # Adding M2M table for field threads on 'Tag'
29 m2m_table_name = db.shorten_name(u'boards_tag_threads')
30 db.create_table(m2m_table_name, (
31 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
32 ('thread', models.ForeignKey(orm['boards.thread'], null=False)),
33 ('tag', models.ForeignKey(orm['boards.tag'], null=False))
34 ))
35 db.create_unique(m2m_table_name, ['thread_id', 'tag_id'])
36
37 # Adding M2M table for field replies on 'Thread'
38 m2m_table_name = db.shorten_name(u'boards_thread_replies')
39 db.create_table(m2m_table_name, (
40 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
41 ('thread', models.ForeignKey(orm['boards.thread'], null=False)),
42 ('post', models.ForeignKey(orm['boards.post'], null=False))
43 ))
44 db.create_unique(m2m_table_name, ['thread_id', 'post_id'])
45
46 db.add_column(u'boards_post', 'thread_new',
47 self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['boards.Thread'], null=True),
48 keep_default=False)
49
50 def backwards(self, orm):
51 pass
52
53 models = {
54 'boards.ban': {
55 'Meta': {'object_name': 'Ban'},
56 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
57 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
58 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
59 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
60 },
61 'boards.post': {
62 'Meta': {'object_name': 'Post'},
63 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
64 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
65 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
66 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
67 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
68 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
69 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
70 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
71 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
72 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
73 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
74 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
75 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}),
76 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
77 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
78 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
79 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
80 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
81 },
82 'boards.setting': {
83 'Meta': {'object_name': 'Setting'},
84 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
85 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
86 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
87 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
88 },
89 'boards.tag': {
90 'Meta': {'object_name': 'Tag'},
91 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
92 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
93 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
94 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
95 },
96 'boards.thread': {
97 'Meta': {'object_name': 'Thread'},
98 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
99 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
100 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
101 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
102 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
103 },
104 'boards.user': {
105 'Meta': {'object_name': 'User'},
106 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
107 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
108 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
109 'rank': ('django.db.models.fields.IntegerField', [], {}),
110 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
111 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
112 }
113 }
114
115 complete_apps = ['boards'] No newline at end of file
@@ -0,0 +1,98 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import DataMigration
5 from django.db import models
6 from boards import views
7
8
9 class Migration(DataMigration):
10
11 def forwards(self, orm):
12 for post in orm.Post.objects.filter(thread=None):
13 thread = orm.Thread.objects.create(
14 bump_time=post.bump_time,
15 last_edit_time=post.last_edit_time)
16
17 thread.replies.add(post)
18 post.thread_new = thread
19 post.save()
20 print str(post.thread_new.id)
21
22 for reply in post.replies.all():
23 thread.replies.add(reply)
24 reply.thread_new = thread
25 reply.save()
26
27 for tag in post.tags.all():
28 thread.tags.add(tag)
29 tag.threads.add(thread)
30
31 def backwards(self, orm):
32 pass
33
34 models = {
35 'boards.ban': {
36 'Meta': {'object_name': 'Ban'},
37 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
38 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
39 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
40 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
41 },
42 'boards.post': {
43 'Meta': {'object_name': 'Post'},
44 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
45 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
46 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
47 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
48 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
49 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
50 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
51 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
52 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
53 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
54 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
55 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
56 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'}),
57 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
58 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
59 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}),
60 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
61 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
62 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
63 },
64 'boards.setting': {
65 'Meta': {'object_name': 'Setting'},
66 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
67 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
68 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
69 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
70 },
71 'boards.tag': {
72 'Meta': {'object_name': 'Tag'},
73 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
74 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
75 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
76 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
77 },
78 'boards.thread': {
79 'Meta': {'object_name': 'Thread'},
80 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
81 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
82 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
83 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
84 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
85 },
86 'boards.user': {
87 'Meta': {'object_name': 'User'},
88 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
89 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
90 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
91 'rank': ('django.db.models.fields.IntegerField', [], {}),
92 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
93 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
94 }
95 }
96
97 complete_apps = ['boards']
98 symmetrical = True
@@ -0,0 +1,110 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import SchemaMigration
5 from django.db import models
6
7
8 class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Deleting field 'Post.bump_time'
12 db.delete_column(u'boards_post', 'bump_time')
13
14 # Removing M2M table for field tags on 'Post'
15 db.delete_table(db.shorten_name(u'boards_post_tags'))
16
17 # Removing M2M table for field replies on 'Post'
18 db.delete_table(db.shorten_name(u'boards_post_replies'))
19
20
21 def backwards(self, orm):
22
23 # User chose to not deal with backwards NULL issues for 'Post.bump_time'
24 raise RuntimeError("Cannot reverse this migration. 'Post.bump_time' and its values cannot be restored.")
25
26 # The following code is provided here to aid in writing a correct migration # Adding field 'Post.bump_time'
27 db.add_column(u'boards_post', 'bump_time',
28 self.gf('django.db.models.fields.DateTimeField')(),
29 keep_default=False)
30
31 # Adding M2M table for field tags on 'Post'
32 m2m_table_name = db.shorten_name(u'boards_post_tags')
33 db.create_table(m2m_table_name, (
34 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
35 ('post', models.ForeignKey(orm['boards.post'], null=False)),
36 ('tag', models.ForeignKey(orm['boards.tag'], null=False))
37 ))
38 db.create_unique(m2m_table_name, ['post_id', 'tag_id'])
39
40 # Adding M2M table for field replies on 'Post'
41 m2m_table_name = db.shorten_name(u'boards_post_replies')
42 db.create_table(m2m_table_name, (
43 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
44 ('from_post', models.ForeignKey(orm['boards.post'], null=False)),
45 ('to_post', models.ForeignKey(orm['boards.post'], null=False))
46 ))
47 db.create_unique(m2m_table_name, ['from_post_id', 'to_post_id'])
48
49
50 models = {
51 'boards.ban': {
52 'Meta': {'object_name': 'Ban'},
53 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
54 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
55 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
56 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
57 },
58 'boards.post': {
59 'Meta': {'object_name': 'Post'},
60 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
61 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
63 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
64 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
65 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
66 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
67 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
68 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
69 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
70 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
71 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
72 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}),
73 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
74 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
75 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
76 },
77 'boards.setting': {
78 'Meta': {'object_name': 'Setting'},
79 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
80 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
81 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
82 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
83 },
84 'boards.tag': {
85 'Meta': {'object_name': 'Tag'},
86 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
87 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
88 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
89 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
90 },
91 'boards.thread': {
92 'Meta': {'object_name': 'Thread'},
93 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
94 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
95 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
96 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
97 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
98 },
99 'boards.user': {
100 'Meta': {'object_name': 'User'},
101 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
102 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
103 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
104 'rank': ('django.db.models.fields.IntegerField', [], {}),
105 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
106 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
107 }
108 }
109
110 complete_apps = ['boards'] No newline at end of file
@@ -1,25 +1,25 b''
1 1 from django.contrib import admin
2 2 from boards.models import Post, Tag, User, Ban
3 3
4 4
5 5 class PostAdmin(admin.ModelAdmin):
6 6
7 7 list_display = ('id', 'title', 'text')
8 list_filter = ('pub_time', 'tags')
8 list_filter = ('pub_time', 'thread_new')
9 9 search_fields = ('id', 'title', 'text')
10 10
11 11
12 12 class TagAdmin(admin.ModelAdmin):
13 13
14 14 list_display = ('name', 'linked')
15 15 list_filter = ('linked',)
16 16
17 17
18 18 class UserAdmin(admin.ModelAdmin):
19 19
20 20 list_display = ('user_id', 'rank')
21 21
22 22 admin.site.register(Post, PostAdmin)
23 23 admin.site.register(Tag, TagAdmin)
24 24 admin.site.register(User, UserAdmin)
25 25 admin.site.register(Ban)
@@ -1,118 +1,119 b''
1 1 from django.core.urlresolvers import reverse
2 2 import markdown
3 3 from markdown.inlinepatterns import Pattern
4 4 from markdown.util import etree
5 5 import boards
6 6
7 7 __author__ = 'neko259'
8 8
9 9
10 10 AUTOLINK_PATTERN = r'(https?://\S+)'
11 11 QUOTE_PATTERN = r'^(?<!>)(>[^>].+)$'
12 12 REFLINK_PATTERN = r'((>>)(\d+))'
13 13 SPOILER_PATTERN = r'%%(.+)%%'
14 14 COMMENT_PATTERN = r'^(//(.+))'
15 15 STRIKETHROUGH_PATTERN = r'~(.+)~'
16 16
17
17 18 class AutolinkPattern(Pattern):
18 19 def handleMatch(self, m):
19 20 link_element = etree.Element('a')
20 21 href = m.group(2)
21 22 link_element.set('href', href)
22 23 link_element.text = href
23 24
24 25 return link_element
25 26
26 27
27 28 class QuotePattern(Pattern):
28 29 def handleMatch(self, m):
29 30 quote_element = etree.Element('span')
30 31 quote_element.set('class', 'quote')
31 32 quote_element.text = m.group(2)
32 33
33 34 return quote_element
34 35
35 36
36 37 class ReflinkPattern(Pattern):
37 38 def handleMatch(self, m):
38 39 post_id = m.group(4)
39 40
40 41 posts = boards.models.Post.objects.filter(id=post_id)
41 42 if posts.count() > 0:
42 43 ref_element = etree.Element('a')
43 44
44 post = posts[0]
45 post = posts[0]
45 46 if post.thread:
46 47 link = reverse(boards.views.thread, kwargs={'post_id': post.thread.id}) \
47 48 + '#' + post_id
48 49 else:
49 link = reverse(boards.views.thread, kwargs={'post_id': post_id})
50 link = reverse(boards.views.thread, kwargs={'post_id': post_id})
50 51
51 52 ref_element.set('href', link)
52 53 ref_element.text = m.group(2)
53 54
54 return ref_element
55 return ref_element
55 56
56 57
57 58 class SpoilerPattern(Pattern):
58 59 def handleMatch(self, m):
59 60 quote_element = etree.Element('span')
60 61 quote_element.set('class', 'spoiler')
61 62 quote_element.text = m.group(2)
62 63
63 64 return quote_element
64 65
65 66
66 67 class CommentPattern(Pattern):
67 68 def handleMatch(self, m):
68 69 quote_element = etree.Element('span')
69 70 quote_element.set('class', 'comment')
70 71 quote_element.text = '//' + m.group(3)
71 72
72 73 return quote_element
73 74
74 75
75 76 class StrikeThroughPattern(Pattern):
76 77 def handleMatch(self, m):
77 78 quote_element = etree.Element('span')
78 79 quote_element.set('class', 'strikethrough')
79 80 quote_element.text = m.group(2)
80 81
81 82 return quote_element
82 83
83 84
84 85 class NeboardMarkdown(markdown.Extension):
85 86 def extendMarkdown(self, md, md_globals):
86 87 self._add_neboard_patterns(md)
87 88 self._delete_patterns(md)
88 89
89 90 def _delete_patterns(self, md):
90 91 del md.parser.blockprocessors['quote']
91 92
92 93 del md.inlinePatterns['image_link']
93 94 del md.inlinePatterns['image_reference']
94 95
95 96 def _add_neboard_patterns(self, md):
96 97 autolink = AutolinkPattern(AUTOLINK_PATTERN, md)
97 98 quote = QuotePattern(QUOTE_PATTERN, md)
98 99 reflink = ReflinkPattern(REFLINK_PATTERN, md)
99 100 spoiler = SpoilerPattern(SPOILER_PATTERN, md)
100 101 comment = CommentPattern(COMMENT_PATTERN, md)
101 102 strikethrough = StrikeThroughPattern(STRIKETHROUGH_PATTERN, md)
102 103
103 104 md.inlinePatterns[u'autolink_ext'] = autolink
104 105 md.inlinePatterns[u'spoiler'] = spoiler
105 106 md.inlinePatterns[u'strikethrough'] = strikethrough
106 107 md.inlinePatterns[u'comment'] = comment
107 108 md.inlinePatterns[u'reflink'] = reflink
108 109 md.inlinePatterns[u'quote'] = quote
109 110
110 111
111 112 def makeExtension(configs=None):
112 113 return NeboardMarkdown(configs=configs)
113 114
114 115 neboard_extension = makeExtension()
115 116
116 117
117 118 def markdown_extended(markup):
118 119 return markdown.markdown(markup, [neboard_extension], safe_mode=True)
@@ -1,7 +1,8 b''
1 1 __author__ = 'neko259'
2 2
3 3 from boards.models.post import Post
4 from boards.models.post import Thread
4 5 from boards.models.tag import Tag
5 6 from boards.models.user import Ban
6 7 from boards.models.user import Setting
7 8 from boards.models.user import User
@@ -1,272 +1,296 b''
1 1 import os
2 2 from random import random
3 3 import time
4 4 import math
5 5 import re
6 6
7 7 from django.db import models
8 8 from django.http import Http404
9 9 from django.utils import timezone
10 10 from markupfield.fields import MarkupField
11 11
12 12 from neboard import settings
13 13 from boards import settings as boards_settings
14 14 from boards import thumbs
15 15
16 16 BAN_REASON_AUTO = 'Auto'
17 17
18 18 IMAGE_THUMB_SIZE = (200, 150)
19 19
20 20 TITLE_MAX_LENGTH = 50
21 21
22 22 DEFAULT_MARKUP_TYPE = 'markdown'
23 23
24 24 NO_PARENT = -1
25 25 NO_IP = '0.0.0.0'
26 26 UNKNOWN_UA = ''
27 27 ALL_PAGES = -1
28 28 IMAGES_DIRECTORY = 'images/'
29 29 FILE_EXTENSION_DELIMITER = '.'
30 30
31 31 SETTING_MODERATE = "moderate"
32 32
33 33 REGEX_REPLY = re.compile('>>(\d+)')
34 34
35 35
36 36 class PostManager(models.Manager):
37 37
38 38 def create_post(self, title, text, image=None, thread=None,
39 39 ip=NO_IP, tags=None, user=None):
40 40 posting_time = timezone.now()
41 if not thread:
42 thread = Thread.objects.create(bump_time=posting_time,
43 last_edit_time=posting_time)
44 else:
45 thread.bump()
46 thread.last_edit_time = posting_time
47 thread.save()
41 48
42 49 post = self.create(title=title,
43 50 text=text,
44 51 pub_time=posting_time,
45 thread=thread,
52 thread_new=thread,
46 53 image=image,
47 54 poster_ip=ip,
48 55 poster_user_agent=UNKNOWN_UA,
49 56 last_edit_time=posting_time,
50 bump_time=posting_time,
51 57 user=user)
52 58
59 thread.replies.add(post)
53 60 if tags:
54 61 linked_tags = []
55 62 for tag in tags:
56 63 tag_linked_tags = tag.get_linked_tags()
57 64 if len(tag_linked_tags) > 0:
58 65 linked_tags.extend(tag_linked_tags)
59 66
60 67 tags.extend(linked_tags)
61 map(post.tags.add, tags)
62 for tag in tags:
63 tag.threads.add(post)
68 map(thread.add_tag, tags)
64 69
65 if thread:
66 thread.replies.add(post)
67 thread.bump()
68 thread.last_edit_time = posting_time
69 thread.save()
70 else:
71 self._delete_old_threads()
72
70 self._delete_old_threads()
73 71 self.connect_replies(post)
74 72
75 73 return post
76 74
75 # TODO Remove this method after migration
77 76 def delete_post(self, post):
78 if post.replies.count() > 0:
79 map(self.delete_post, post.replies.all())
80
81 # Update thread's last edit time
82 thread = post.thread
83 if thread:
84 thread.last_edit_time = timezone.now()
85 thread.save()
77 thread = post.thread_new
78 thread.last_edit_time = timezone.now()
79 thread.save()
86 80
87 81 post.delete()
88 82
89 83 def delete_posts_by_ip(self, ip):
90 84 posts = self.filter(poster_ip=ip)
91 85 map(self.delete_post, posts)
92 86
87 # TODO Remove this method after migration
93 88 def get_threads(self, tag=None, page=ALL_PAGES,
94 89 order_by='-bump_time'):
95 90 if tag:
96 91 threads = tag.threads
97 92
98 if threads.count() == 0:
93 if not threads.exists():
99 94 raise Http404
100 95 else:
101 threads = self.filter(thread=None)
96 threads = Thread.objects.all()
102 97
103 98 threads = threads.order_by(order_by)
104 99
105 100 if page != ALL_PAGES:
106 101 thread_count = threads.count()
107 102
108 103 if page < self._get_page_count(thread_count):
109 104 start_thread = page * settings.THREADS_PER_PAGE
110 105 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
111 106 thread_count)
112 107 threads = threads[start_thread:end_thread]
113 108
114 109 return threads
115 110
111 # TODO Remove this method after migration
116 112 def get_thread(self, opening_post_id):
117 113 try:
118 opening_post = self.get(id=opening_post_id, thread=None)
114 opening_post = self.get(id=opening_post_id)
119 115 except Post.DoesNotExist:
120 116 raise Http404
121 117
122 if opening_post.replies:
123 thread = [opening_post]
124 thread.extend(opening_post.replies.all().order_by('pub_time'))
118 return opening_post.thread_new
125 119
126 return thread
127
120 # TODO Move this method to thread manager
128 121 def get_thread_page_count(self, tag=None):
129 122 if tag:
130 threads = self.filter(thread=None, tags=tag)
123 threads = Thread.objects.filter(tags=tag)
131 124 else:
132 threads = self.filter(thread=None)
125 threads = Thread.objects.all()
133 126
134 127 return self._get_page_count(threads.count())
135 128
129 # TODO Move this method to thread manager
136 130 def _delete_old_threads(self):
137 131 """
138 132 Preserves maximum thread count. If there are too many threads,
139 133 delete the old ones.
140 134 """
141 135
142 136 # TODO Move old threads to the archive instead of deleting them.
143 137 # Maybe make some 'old' field in the model to indicate the thread
144 138 # must not be shown and be able for replying.
145 139
146 threads = self.get_threads()
140 threads = Thread.objects.all()
147 141 thread_count = threads.count()
148 142
149 143 if thread_count > settings.MAX_THREAD_COUNT:
150 144 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
151 145 old_threads = threads[thread_count - num_threads_to_delete:]
152 146
153 map(self.delete_post, old_threads)
147 map(Thread.delete_with_posts, old_threads)
154 148
155 149 def connect_replies(self, post):
156 150 """Connect replies to a post to show them as a refmap"""
157 151
158 152 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
159 153 post_id = reply_number.group(1)
160 154 ref_post = self.filter(id=post_id)
161 155 if ref_post.count() > 0:
162 156 referenced_post = ref_post[0]
163 157 referenced_post.referenced_posts.add(post)
164 158 referenced_post.last_edit_time = post.pub_time
165 159 referenced_post.save()
166 160
167 161 def _get_page_count(self, thread_count):
168 162 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
169 163
170 164
171 165 class Post(models.Model):
172 166 """A post is a message."""
173 167
174 168 objects = PostManager()
175 169
176 170 class Meta:
177 171 app_label = 'boards'
178 172
179 173 def _update_image_filename(self, filename):
180 174 """Get unique image filename"""
181 175
182 176 path = IMAGES_DIRECTORY
183 177 new_name = str(int(time.mktime(time.gmtime())))
184 178 new_name += str(int(random() * 1000))
185 179 new_name += FILE_EXTENSION_DELIMITER
186 180 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
187 181
188 182 return os.path.join(path, new_name)
189 183
190 184 title = models.CharField(max_length=TITLE_MAX_LENGTH)
191 185 pub_time = models.DateTimeField()
192 186 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
193 187 escape_html=False)
194 188
195 189 image_width = models.IntegerField(default=0)
196 190 image_height = models.IntegerField(default=0)
197 191
198 192 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
199 193 blank=True, sizes=(IMAGE_THUMB_SIZE,),
200 194 width_field='image_width',
201 195 height_field='image_height')
202 196
203 197 poster_ip = models.GenericIPAddressField()
204 198 poster_user_agent = models.TextField()
205 199
206 200 thread = models.ForeignKey('Post', null=True, default=None)
207 tags = models.ManyToManyField('Tag')
201 thread_new = models.ForeignKey('Thread', null=True, default=None)
208 202 last_edit_time = models.DateTimeField()
209 bump_time = models.DateTimeField()
210 203 user = models.ForeignKey('User', null=True, default=None)
211 204
212 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
213 blank=True, related_name='re+')
214 205 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
215 206 null=True,
216 207 blank=True, related_name='rfp+')
217 208
218 209 def __unicode__(self):
219 210 return '#' + str(self.id) + ' ' + self.title + ' (' + \
220 211 self.text.raw[:50] + ')'
221 212
222 213 def get_title(self):
223 214 title = self.title
224 215 if len(title) == 0:
225 216 title = self.text.raw[:20]
226 217
227 218 return title
228 219
220 def get_sorted_referenced_posts(self):
221 return self.referenced_posts.order_by('id')
222
223 def is_referenced(self):
224 return self.referenced_posts.all().exists()
225
226
227 class Thread(models.Model):
228
229 class Meta:
230 app_label = 'boards'
231
232 tags = models.ManyToManyField('Tag')
233 bump_time = models.DateTimeField()
234 last_edit_time = models.DateTimeField()
235 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
236 blank=True, related_name='tre+')
237
238 def get_tags(self):
239 """Get a sorted tag list"""
240
241 return self.tags.order_by('name')
242
243 def bump(self):
244 """Bump (move to up) thread"""
245
246 if self.can_bump():
247 self.bump_time = timezone.now()
248
229 249 def get_reply_count(self):
230 250 return self.replies.count()
231 251
232 252 def get_images_count(self):
233 images_count = 1 if self.image else 0
234 images_count += self.replies.filter(image_width__gt=0).count()
235
236 return images_count
253 return self.replies.filter(image_width__gt=0).count()
237 254
238 255 def can_bump(self):
239 256 """Check if the thread can be bumped by replying"""
240 257
241 258 post_count = self.get_reply_count()
242 259
243 260 return post_count <= settings.MAX_POSTS_PER_THREAD
244 261
245 def bump(self):
246 """Bump (move to up) thread"""
262 def delete_with_posts(self):
263 """Completely delete thread"""
247 264
248 if self.can_bump():
249 self.bump_time = timezone.now()
265 if self.replies.count() > 0:
266 map(Post.objects.delete_post, self.replies.all())
267
268 self.delete()
250 269
251 270 def get_last_replies(self):
271 """Get last replies, not including opening post"""
272
252 273 if settings.LAST_REPLIES_COUNT > 0:
253 274 reply_count = self.get_reply_count()
254 275
255 276 if reply_count > 0:
256 277 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
257 reply_count)
278 reply_count - 1)
258 279 last_replies = self.replies.all().order_by('pub_time')[
259 280 reply_count - reply_count_to_show:]
260 281
261 282 return last_replies
262 283
263 def get_tags(self):
264 """Get a sorted tag list"""
284 def get_replies(self):
285 """Get sorted thread posts"""
265 286
266 return self.tags.order_by('name')
287 return self.replies.all().order_by('pub_time')
267 288
268 def get_sorted_referenced_posts(self):
269 return self.referenced_posts.order_by('id')
289 def add_tag(self, tag):
290 """Connect thread to a tag and tag to a thread"""
270 291
271 def is_referenced(self):
272 return self.referenced_posts.all().exists()
292 self.tags.add(tag)
293 tag.threads.add(self)
294
295 def __unicode__(self):
296 return str(self.get_replies()[0].id) No newline at end of file
@@ -1,83 +1,84 b''
1 from boards.models import Post
1 from boards.models import Thread
2 2 from django.db import models
3 3 from django.db.models import Count
4 4
5 5 __author__ = 'neko259'
6 6
7 7 TAG_FONT_MULTIPLIER = 0.1
8 8 MAX_TAG_FONT = 10
9 9 OPENING_POST_POPULARITY_WEIGHT = 2
10 10
11 11
12 12 class TagManager(models.Manager):
13 13
14 14 def get_not_empty_tags(self):
15 15 tags = self.annotate(Count('threads')) \
16 16 .filter(threads__count__gt=0).order_by('name')
17 17
18 18 return tags
19 19
20 20
21 21 class Tag(models.Model):
22 22 """
23 A tag is a text node assigned to the post. The tag serves as a board
24 section. There can be multiple tags for each message
23 A tag is a text node assigned to the thread. The tag serves as a board
24 section. There can be multiple tags for each thread
25 25 """
26 26
27 27 objects = TagManager()
28 28
29 29 class Meta:
30 30 app_label = 'boards'
31 31
32 32 name = models.CharField(max_length=100)
33 threads = models.ManyToManyField('Post', null=True,
33 threads = models.ManyToManyField(Thread, null=True,
34 34 blank=True, related_name='tag+')
35 35 linked = models.ForeignKey('Tag', null=True, blank=True)
36 36
37 37 def __unicode__(self):
38 38 return self.name
39 39
40 40 def is_empty(self):
41 41 return self.get_post_count() == 0
42 42
43 43 def get_post_count(self):
44 44 return self.threads.count()
45 45
46 def get_popularity(self):
47 posts_with_tag = Post.objects.get_threads(tag=self)
48 reply_count = 0
49 for post in posts_with_tag:
50 reply_count += post.get_reply_count()
51 reply_count += OPENING_POST_POPULARITY_WEIGHT
52
53 return reply_count
46 # TODO Reenable this method after migration
47 # def get_popularity(self):
48 # posts_with_tag = Thread.objects.get_threads(tag=self)
49 # reply_count = 0
50 # for post in posts_with_tag:
51 # reply_count += post.get_reply_count()
52 # reply_count += OPENING_POST_POPULARITY_WEIGHT
53 #
54 # return reply_count
54 55
55 56 def get_linked_tags(self):
56 57 tag_list = []
57 58 self.get_linked_tags_list(tag_list)
58 59
59 60 return tag_list
60 61
61 62 def get_linked_tags_list(self, tag_list=[]):
62 63 """
63 64 Returns the list of tags linked to current. The list can be got
64 65 through returned value or tag_list parameter
65 66 """
66 67
67 68 linked_tag = self.linked
68 69
69 70 if linked_tag and not (linked_tag in tag_list):
70 71 tag_list.append(linked_tag)
71 72
72 73 linked_tag.get_linked_tags_list(tag_list)
73 74
74 75 def get_font_value(self):
75 76 """Get tag font value to differ most popular tags in the list"""
76 77
77 78 post_count = self.get_post_count()
78 79 if post_count > MAX_TAG_FONT:
79 80 post_count = MAX_TAG_FONT
80 81
81 82 font_value = str(1 + (post_count - 1) * TAG_FONT_MULTIPLIER)
82 83
83 84 return font_value No newline at end of file
@@ -1,354 +1,354 b''
1 1 html {
2 2 background: #555;
3 3 color: #ffffff;
4 4 }
5 5
6 6 #admin_panel {
7 7 background: #FF0000;
8 8 color: #00FF00
9 9 }
10 10
11 11 .input_field {
12 12
13 13 }
14 14
15 15 .input_field_name {
16 16
17 17 }
18 18
19 19 .input_field_error {
20 20 color: #FF0000;
21 21 }
22 22
23 23
24 24 .title {
25 25 font-weight: bold;
26 26 color: #ffcc00;
27 27 }
28 28
29 29 .link, a {
30 30 color: #afdcec;
31 31 }
32 32
33 33 .block {
34 34 display: inline-block;
35 35 vertical-align: top;
36 36 }
37 37
38 38 .tag {
39 39 color: #b4cfec;
40 40 }
41 41
42 42 .post_id {
43 43 color: #fff380;
44 44 }
45 45
46 46 .post, .dead_post, #posts-table {
47 47 background: #333;
48 48 margin: 5px;
49 49 padding: 10px;
50 50 border: solid 1px #888;
51 51 clear: left;
52 52 word-wrap: break-word;
53 53 }
54 54
55 55 .metadata {
56 56 padding-top: 5px;
57 57 margin-top: 10px;
58 58 border-top: solid 1px #666;
59 59 font-size: 0.9em;
60 60 color: #ddd;
61 61 }
62 62
63 63 .navigation_panel, .tag_info {
64 64 background: #444;
65 65 margin: 5px;
66 66 padding: 10px;
67 67 border: solid 1px #888;
68 68 color: #eee;
69 69 }
70 70
71 71 .navigation_panel .link {
72 72 border-right: 1px solid #fff;
73 73 font-weight: bold;
74 74 margin-right: 1ex;
75 75 padding-right: 1ex;
76 76 }
77 77 .navigation_panel .link:last-child {
78 78 border-left: 1px solid #fff;
79 79 border-right: none;
80 80 float: right;
81 81 margin-left: 1ex;
82 82 margin-right: 0;
83 83 padding-left: 1ex;
84 84 padding-right: 0;
85 85 }
86 86
87 87 .navigation_panel::after, .post::after {
88 88 clear: both;
89 89 content: ".";
90 90 display: block;
91 91 height: 0;
92 92 line-height: 0;
93 93 visibility: hidden;
94 94 }
95 95
96 96 p {
97 97 margin-top: .5em;
98 98 margin-bottom: .5em;
99 99 }
100 100
101 101 .post-form-w {
102 102 display: table;
103 103 background: #333344;
104 104 border: solid 1px #888;
105 105 color: #fff;
106 106 padding: 10px;
107 margin: 5;
107 margin: 5px;
108 108 }
109 109
110 110 .form-row {
111 111 display: table-row;
112 112 }
113 113
114 114 .form-label, .form-input, .form-errors {
115 115 display: table-cell;
116 116 }
117 117
118 118 .form-label {
119 119 padding: .25em 1ex .25em 0;
120 120 vertical-align: top;
121 121 }
122 122
123 123 .form-input {
124 124 padding: .25em 0;
125 125 }
126 126
127 127 .form-errors {
128 128 padding-left: 1ex;
129 129 font-weight: bold;
130 130 vertical-align: middle;
131 131 }
132 132
133 133 .post-form input, .post-form textarea {
134 134 background: #333;
135 135 color: #fff;
136 136 border: solid 1px;
137 137 padding: 0;
138 138 width: 100%;
139 139 font: medium sans;
140 140 }
141 141
142 142 .form-submit {
143 143 border-bottom: 2px solid #ddd;
144 144 margin-bottom: .5em;
145 145 padding-bottom: .5em;
146 146 }
147 147
148 148 .form-title {
149 149 font-weight: bold;
150 150 font-size: 2.5ex;
151 151 text-decoration: underline;
152 152 }
153 153
154 154 input[type="submit"] {
155 155 background: #222;
156 156 border: solid 1px #fff;
157 157 color: #fff;
158 158 }
159 159
160 160 blockquote {
161 161 border-left: solid 2px;
162 162 padding-left: 5px;
163 163 color: #B1FB17;
164 164 margin: 0;
165 165 }
166 166
167 167 .post > .image {
168 168 float: left;
169 169 margin: 0 1ex .5ex 0;
170 170 min-width: 1px;
171 171 text-align: center;
172 172 display: table-row;
173 173
174 174 height: 150px;
175 175 }
176 176
177 177 .post > .metadata {
178 178 clear: left;
179 179 }
180 180
181 181 .get {
182 182 font-weight: bold;
183 183 color: #d55;
184 184 }
185 185
186 186 * {
187 187 text-decoration: none;
188 188 }
189 189
190 190 .dead_post {
191 191 background-color: #442222;
192 192 }
193 193
194 194 .mark_btn {
195 195 padding: 2px 4px;
196 196 border: 1px solid;
197 197 }
198 198
199 199 .mark_btn:hover {
200 200 background: #555;
201 201 }
202 202
203 203 .quote {
204 204 color: #92cf38;
205 205 font-style: italic;
206 206 }
207 207
208 208 .spoiler {
209 209 background: white;
210 210 color: white;
211 211 }
212 212
213 213 .spoiler:hover {
214 214 color: black;
215 215 }
216 216
217 217 .comment {
218 218 color: #eb2;
219 219 font-style: italic;
220 220 }
221 221
222 222 a:hover {
223 223 text-decoration: underline;
224 224 }
225 225
226 226 .last-replies {
227 227 margin-left: 3ex;
228 228 }
229 229
230 230 .thread {
231 231 margin-bottom: 3ex;
232 232 }
233 233
234 234 .post:target {
235 235 border: solid 2px white;
236 236 }
237 237
238 238 pre{
239 239 white-space:pre-wrap
240 240 }
241 241
242 242 li {
243 243 list-style-position: inside;
244 244 }
245 245
246 246 .fancybox-skin {
247 247 position: relative;
248 248 background-color: #fff;
249 249 color: #ddd;
250 250 text-shadow: none;
251 251 }
252 252
253 253 .fancybox-image {
254 254 border: 1px solid black;
255 255 }
256 256
257 257 .image-mode-tab {
258 258 background: #444;
259 259 color: #eee;
260 260 display: table;
261 261 margin: 5px;
262 262 padding: 5px;
263 263 border: 1px solid #888;
264 264 }
265 265
266 266 .image-mode-tab > label {
267 267 margin: 0 1ex;
268 268 }
269 269
270 270 .image-mode-tab > label > input {
271 271 margin-right: .5ex;
272 272 }
273 273
274 274 #posts-table {
275 275 margin: 5px;
276 276 }
277 277
278 278 .tag_info {
279 279 display: table;
280 280 }
281 281
282 282 .tag_info > h2 {
283 283 margin: 0;
284 284 }
285 285
286 286 .post-info {
287 287 color: #ddd;
288 288 }
289 289
290 290 .moderator_info {
291 291 color: #e99d41;
292 292 border: dashed 1px;
293 293 padding: 3px;
294 294 }
295 295
296 296 .refmap {
297 297 font-size: 0.9em;
298 298 color: #ccc;
299 299 margin-top: 1em;
300 300 }
301 301
302 302 input[type="submit"]:hover {
303 303 background: #555;
304 304 }
305 305
306 306 .fav {
307 307 color: yellow;
308 308 }
309 309
310 310 .not_fav {
311 311 color: #ccc;
312 312 }
313 313
314 314 .role {
315 315 text-decoration: underline;
316 316 }
317 317
318 318 .form-email {
319 319 display: none;
320 320 }
321 321
322 322 .footer {
323 323 margin: 5px;
324 324 }
325 325
326 326 .bar-value {
327 327 background: rgba(50, 55, 164, 0.45);
328 328 font-size: 0.9em;
329 329 height: 1.5em;
330 330 }
331 331
332 332 .bar-bg {
333 333 position: relative;
334 334 border: solid 1px #888;
335 335 margin: 5px;
336 336 overflow: hidden;
337 337 }
338 338
339 339 .bar-text {
340 340 padding: 2px;
341 341 position: absolute;
342 342 left: 0;
343 343 top: 0;
344 344 }
345 345
346 346 .page_link {
347 347 display: table;
348 348 background: #444;
349 349 margin: 5px;
350 350 border: solid 1px #888;
351 351 padding: 5px;
352 352 font-weight: bolder;
353 353 color: #eee;
354 354 }
@@ -1,68 +1,68 b''
1 1 {% load i18n %}
2 2 {% load board %}
3 3
4 4 {% if can_bump %}
5 5 <div class="post" id="{{ post.id }}">
6 6 {% else %}
7 7 <div class="post dead_post" id="{{ post.id }}">
8 8 {% endif %}
9 9
10 10 {% if post.image %}
11 11 <div class="image">
12 12 <a
13 13 class="thumb"
14 14 href="{{ post.image.url }}"><img
15 15 src="{{ post.image.url_200x150 }}"
16 16 alt="{{ post.id }}"
17 17 data-width="{{ post.image_width }}"
18 18 data-height="{{ post.image_height }}"/>
19 19 </a>
20 20 </div>
21 21 {% endif %}
22 22 <div class="message">
23 23 <div class="post-info">
24 24 <span class="title">{{ post.title }}</span>
25 25 <a class="post_id" href="#{{ post.id }}">
26 26 ({{ post.id }})</a>
27 27 [{{ post.pub_time }}]
28 28 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
29 29 ; return false;">&gt;&gt;</a>]
30 30
31 31 {% if moderator %}
32 32 <span class="moderator_info">
33 33 [<a href="{% url 'delete' post_id=post.id %}"
34 34 >{% trans 'Delete' %}</a>]
35 35 ({{ post.poster_ip }})
36 36 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
37 37 >{% trans 'Ban IP' %}</a>]
38 38 </span>
39 39 {% endif %}
40 40 </div>
41 41 {% autoescape off %}
42 42 {% if truncated %}
43 43 {{ post.text.rendered|truncatewords_html:50 }}
44 44 {% else %}
45 45 {{ post.text.rendered }}
46 46 {% endif %}
47 47 {% endautoescape %}
48 48 {% if post.is_referenced %}
49 49 <div class="refmap">
50 50 {% trans "Replies" %}:
51 51 {% for ref_post in post.get_sorted_referenced_posts %}
52 52 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
53 53 >{% if not forloop.last %},{% endif %}
54 54 {% endfor %}
55 55 </div>
56 56 {% endif %}
57 57 </div>
58 {% if post.tags.exists %}
58 {% if post.thread.tags.exists %}
59 59 <div class="metadata">
60 60 <span class="tags">{% trans 'Tags' %}:
61 {% for tag in post.tags.all %}
61 {% for tag in post.thread.get_tags %}
62 62 <a class="tag" href="{% url 'tag' tag.name %}">
63 63 {{ tag.name }}</a>
64 64 {% endfor %}
65 65 </span>
66 66 </div>
67 67 {% endif %}
68 68 </div>
@@ -1,274 +1,277 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load board %}
6 6
7 7 {% block head %}
8 8 {% if tag %}
9 9 <title>Neboard - {{ tag.name }}</title>
10 10 {% else %}
11 11 <title>Neboard</title>
12 12 {% endif %}
13 13
14 14 {% if prev_page %}
15 15 <link rel="next" href="
16 16 {% if tag %}
17 17 {% url "tag" tag_name=tag page=prev_page %}
18 18 {% else %}
19 19 {% url "index" page=prev_page %}
20 20 {% endif %}
21 21 " />
22 22 {% endif %}
23 23 {% if next_page %}
24 24 <link rel="next" href="
25 25 {% if tag %}
26 26 {% url "tag" tag_name=tag page=next_page %}
27 27 {% else %}
28 28 {% url "index" page=next_page %}
29 29 {% endif %}
30 30 " />
31 31 {% endif %}
32 32
33 33 {% endblock %}
34 34
35 35 {% block content %}
36 36
37 37 {% get_current_language as LANGUAGE_CODE %}
38 38
39 39 {% if tag %}
40 40 <div class="tag_info">
41 41 <h2>
42 42 {% if tag in user.fav_tags.all %}
43 43 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
44 44 class="fav">β˜…</a>
45 45 {% else %}
46 46 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
47 47 class="not_fav">β˜…</a>
48 48 {% endif %}
49 49 #{{ tag.name }}
50 50 </h2>
51 51 </div>
52 52 {% endif %}
53 53
54 54 {% if threads %}
55 55 {% if prev_page %}
56 56 <div class="page_link">
57 57 <a href="
58 58 {% if tag %}
59 59 {% url "tag" tag_name=tag page=prev_page %}
60 60 {% else %}
61 61 {% url "index" page=prev_page %}
62 62 {% endif %}
63 63 ">{% trans "Previous page" %}</a>
64 64 </div>
65 65 {% endif %}
66 66
67 67 {% for thread in threads %}
68 68 {% cache 600 thread_short thread.thread.last_edit_time moderator LANGUAGE_CODE %}
69 69 <div class="thread">
70 70 {% if thread.bumpable %}
71 <div class="post" id="{{ thread.thread.id }}">
71 <div class="post" id="{{ thread.op.id }}">
72 72 {% else %}
73 <div class="post dead_post" id="{{ thread.thread.id }}">
73 <div class="post dead_post" id="{{ thread.op.id }}">
74 74 {% endif %}
75 {% if thread.thread.image %}
75 {% if thread.op.image %}
76 76 <div class="image">
77 77 <a class="thumb"
78 href="{{ thread.thread.image.url }}"><img
79 src="{{ thread.thread.image.url_200x150 }}"
80 alt="{{ thread.thread.id }}"
81 data-width="{{ thread.thread.image_width }}"
82 data-height="{{ thread.thread.image_height }}" />
78 href="{{ thread.op.image.url }}"><img
79 src="{{ thread.op.image.url_200x150 }}"
80 alt="{{ thread.op.id }}"
81 data-width="{{ thread.op.image_width }}"
82 data-height="{{ thread.op.image_height }}"/>
83 83 </a>
84 84 </div>
85 85 {% endif %}
86 86 <div class="message">
87 87 <div class="post-info">
88 <span class="title">{{ thread.thread.title }}</span>
89 <a class="post_id" href="{% url 'thread' thread.thread.id %}"
90 >({{ thread.thread.id }})</a>
91 [{{ thread.thread.pub_time }}]
92 [<a class="link" href="{% url 'thread' thread.thread.id %}#form"
88 <span class="title">{{ thread.op.title }}</span>
89 <a class="post_id" href="{% url 'thread' thread.op.id %}"
90 >({{ thread.op.id }})</a>
91 [{{ thread.op.pub_time }}]
92 [<a class="link" href="
93 {% url 'thread' thread.op.id %}#form"
93 94 >{% trans "Reply" %}</a>]
94 95
95 96 {% if moderator %}
96 97 <span class="moderator_info">
97 [<a href="{% url 'delete' post_id=thread.thread.id %}?next={{ request.path }}"
98 [<a href="
99 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
98 100 >{% trans 'Delete' %}</a>]
99 101 ({{ thread.thread.poster_ip }})
100 [<a href="{% url 'ban' post_id=thread.thread.id %}?next={{ request.path }}"
102 [<a href="
103 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
101 104 >{% trans 'Ban IP' %}</a>]
102 105 </span>
103 106 {% endif %}
104 107 </div>
105 108 {% autoescape off %}
106 {{ thread.thread.text.rendered|truncatewords_html:50 }}
109 {{ thread.op.text.rendered|truncatewords_html:50 }}
107 110 {% endautoescape %}
108 {% if thread.thread.is_referenced %}
111 {% if thread.op.is_referenced %}
109 112 <div class="refmap">
110 113 {% trans "Replies" %}:
111 {% for ref_post in thread.thread.get_sorted_referenced_posts %}
114 {% for ref_post in thread.op.get_sorted_referenced_posts %}
112 115 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
113 116 >{% if not forloop.last %},{% endif %}
114 117 {% endfor %}
115 118 </div>
116 119 {% endif %}
117 120 </div>
118 121 <div class="metadata">
119 122 {{ thread.thread.get_reply_count }} {% trans 'replies' %},
120 123 {{ thread.thread.get_images_count }} {% trans 'images' %}.
121 124 {% if thread.thread.tags %}
122 125 <span class="tags">
123 126 {% for tag in thread.thread.get_tags %}
124 127 <a class="tag" href="
125 128 {% url 'tag' tag_name=tag.name %}">
126 129 #{{ tag.name }}</a
127 130 >{% if not forloop.last %},{% endif %}
128 131 {% endfor %}
129 132 </span>
130 133 {% endif %}
131 134 </div>
132 135 </div>
133 136 {% if thread.last_replies.exists %}
134 137 <div class="last-replies">
135 138 {% for post in thread.last_replies %}
136 139 {% if thread.bumpable %}
137 140 <div class="post" id="{{ post.id }}">
138 141 {% else %}
139 142 <div class="post dead_post" id="{{ post.id }}">
140 143 {% endif %}
141 144 {% if post.image %}
142 145 <div class="image">
143 146 <a class="thumb"
144 147 href="{{ post.image.url }}"><img
145 148 src=" {{ post.image.url_200x150 }}"
146 149 alt="{{ post.id }}"
147 150 data-width="{{ post.image_width }}"
148 151 data-height="{{ post.image_height }}"/>
149 152 </a>
150 153 </div>
151 154 {% endif %}
152 155 <div class="message">
153 156 <div class="post-info">
154 157 <span class="title">{{ post.title }}</span>
155 158 <a class="post_id" href="
156 {% url 'thread' thread.thread.id %}#{{ post.id }}">
159 {% url 'thread' thread.op.id %}#{{ post.id }}">
157 160 ({{ post.id }})</a>
158 161 [{{ post.pub_time }}]
159 162 </div>
160 163 {% autoescape off %}
161 164 {{ post.text.rendered|truncatewords_html:50 }}
162 165 {% endautoescape %}
163 166 </div>
164 167 {% if post.is_referenced %}
165 168 <div class="refmap">
166 169 {% trans "Replies" %}:
167 170 {% for ref_post in post.get_sorted_referenced_posts %}
168 171 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
169 172 >{% if not forloop.last %},{% endif %}
170 173 {% endfor %}
171 174 </div>
172 175 {% endif %}
173 176 </div>
174 177 {% endfor %}
175 178 </div>
176 179 {% endif %}
177 180 </div>
178 181 {% endcache %}
179 182 {% endfor %}
180 183
181 184 {% if next_page %}
182 185 <div class="page_link">
183 186 <a href="
184 187 {% if tag %}
185 188 {% url "tag" tag_name=tag page=next_page %}
186 189 {% else %}
187 190 {% url "index" page=next_page %}
188 191 {% endif %}
189 192 ">{% trans "Next page" %}</a>
190 193 </div>
191 194 {% endif %}
192 195 {% else %}
193 196 <div class="post">
194 197 {% trans 'No threads exist. Create the first one!' %}</div>
195 198 {% endif %}
196 199
197 200 <form enctype="multipart/form-data" method="post">{% csrf_token %}
198 201 <div class="post-form-w">
199 202
200 203 <div class="form-title">{% trans "Create new thread" %}</div>
201 204 <div class="post-form">
202 205 <div class="form-row">
203 206 <div class="form-label">{% trans 'Title' %}</div>
204 207 <div class="form-input">{{ form.title }}</div>
205 208 <div class="form-errors">{{ form.title.errors }}</div>
206 209 </div>
207 210 <div class="form-row">
208 211 <div class="form-label">{% trans 'Formatting' %}</div>
209 212 <div class="form-input" id="mark_panel">
210 213 <span class="mark_btn" id="quote"><span class="quote">&gt;{% trans 'quote' %}</span></span>
211 214 <span class="mark_btn" id="italic"><i>{% trans 'italic' %}</i></span>
212 215 <span class="mark_btn" id="bold"><b>{% trans 'bold' %}</b></span>
213 216 <span class="mark_btn" id="spoiler"><span class="spoiler">{% trans 'spoiler' %}</span></span>
214 217 <span class="mark_btn" id="comment"><span class="comment">// {% trans 'comment' %}</span></span>
215 218 </div>
216 219 </div>
217 220 <div class="form-row">
218 221 <div class="form-label">{% trans 'Text' %}</div>
219 222 <div class="form-input">{{ form.text }}</div>
220 223 <div class="form-errors">{{ form.text.errors }}</div>
221 224 </div>
222 225 <div class="form-row">
223 226 <div class="form-label">{% trans 'Image' %}</div>
224 227 <div class="form-input">{{ form.image }}</div>
225 228 <div class="form-errors">{{ form.image.errors }}</div>
226 229 </div>
227 230 <div class="form-row">
228 231 <div class="form-label">{% trans 'Tags' %}</div>
229 232 <div class="form-input">{{ form.tags }}</div>
230 233 <div class="form-errors">{{ form.tags.errors }}</div>
231 234 </div>
232 235 <div class="form-row form-email">
233 236 <div class="form-label">{% trans 'e-mail' %}</div>
234 237 <div class="form-input">{{ form.email }}</div>
235 238 <div class="form-errors">{{ form.email.errors }}</div>
236 239 </div>
237 240 <div class="form-row">
238 241 {{ form.captcha }}
239 242 <div class="form-errors">{{ form.captcha.errors }}</div>
240 243 </div>
241 244 <div class="form-row">
242 245 <div class="form-errors">{{ form.other.errors }}</div>
243 246 </div>
244 247 </div>
245 248 <div class="form-submit">
246 249 <input type="submit" value="{% trans "Post" %}"/></div>
247 250 <div>
248 251 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
249 252 </div>
250 253 <div><a href="{% url "staticpage" name="help" %}">
251 254 {% trans 'Text syntax' %}</a></div>
252 255 </div>
253 256 </form>
254 257
255 258 {% endblock %}
256 259
257 260 {% block metapanel %}
258 261
259 262 <span class="metapanel">
260 <b><a href="{% url "authors" %}">Neboard</a> 1.3</b>
263 <b><a href="{% url "authors" %}">Neboard</a> 1.4</b>
261 264 {% trans "Pages:" %}
262 265 {% for page in pages %}
263 266 [<a href="
264 267 {% if tag %}
265 268 {% url "tag" tag_name=tag page=page %}
266 269 {% else %}
267 270 {% url "index" page=page %}
268 271 {% endif %}
269 272 ">{{ page }}</a>]
270 273 {% endfor %}
271 274 [<a href="rss/">RSS</a>]
272 275 </span>
273 276
274 277 {% endblock %}
@@ -1,163 +1,161 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7
8 8 {% block head %}
9 9 <title>Neboard - {{ posts.0.get_title }}</title>
10 10 {% endblock %}
11 11
12 12 {% block content %}
13 13 {% get_current_language as LANGUAGE_CODE %}
14 14
15 15 <script src="{% static 'js/thread_update.js' %}"></script>
16 16 <script src="{% static 'js/thread.js' %}"></script>
17 17
18 {% if posts %}
19 {% cache 600 thread_view posts.0.last_edit_time moderator LANGUAGE_CODE %}
20 {% if bumpable %}
21 <div class="bar-bg">
22 <div class="bar-value" style="width:{{ bumplimit_progress }}%">
23 </div>
24 <div class="bar-text">
25 {{ posts_left }} {% trans 'posts to bumplimit' %}
26 </div>
18 {% cache 600 thread_view thread.last_edit_time moderator LANGUAGE_CODE %}
19 {% if bumpable %}
20 <div class="bar-bg">
21 <div class="bar-value" style="width:{{ bumplimit_progress }}%">
27 22 </div>
28 {% endif %}
29 <div class="thread">
30 {% for post in posts %}
31 {% if bumpable %}
32 <div class="post" id="{{ post.id }}">
33 {% else %}
34 <div class="post dead_post" id="{{ post.id }}">
35 {% endif %}
36 {% if post.image %}
37 <div class="image">
38 <a
39 class="thumb"
40 href="{{ post.image.url }}"><img
41 src="{{ post.image.url_200x150 }}"
42 alt="{{ post.id }}"
43 data-width="{{ post.image_width }}"
44 data-height="{{ post.image_height }}"/>
45 </a>
23 <div class="bar-text">
24 {{ posts_left }} {% trans 'posts to bumplimit' %}
46 25 </div>
47 {% endif %}
48 <div class="message">
49 <div class="post-info">
50 <span class="title">{{ post.title }}</span>
51 <a class="post_id" href="#{{ post.id }}">
52 ({{ post.id }})</a>
53 [{{ post.pub_time }}]
54 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
55 ; return false;">&gt;&gt;</a>]
56
57 {% if moderator %}
58 <span class="moderator_info">
59 [<a href="{% url 'delete' post_id=post.id %}"
60 >{% trans 'Delete' %}</a>]
61 ({{ post.poster_ip }})
62 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
63 >{% trans 'Ban IP' %}</a>]
64 </span>
65 {% endif %}
66 </div>
67 {% autoescape off %}
68 {{ post.text.rendered }}
69 {% endautoescape %}
70 {% if post.is_referenced %}
71 <div class="refmap">
72 {% trans "Replies" %}:
73 {% for ref_post in post.get_sorted_referenced_posts %}
74 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
75 >{% if not forloop.last %},{% endif %}
76 {% endfor %}
26 </div>
27 {% endif %}
28 <div class="thread">
29 {% for post in thread.get_replies %}
30 {% if bumpable %}
31 <div class="post" id="{{ post.id }}">
32 {% else %}
33 <div class="post dead_post" id="{{ post.id }}">
34 {% endif %}
35 {% if post.image %}
36 <div class="image">
37 <a
38 class="thumb"
39 href="{{ post.image.url }}"><img
40 src="{{ post.image.url_200x150 }}"
41 alt="{{ post.id }}"
42 data-width="{{ post.image_width }}"
43 data-height="{{ post.image_height }}"/>
44 </a>
77 45 </div>
78 46 {% endif %}
47 <div class="message">
48 <div class="post-info">
49 <span class="title">{{ post.title }}</span>
50 <a class="post_id" href="#{{ post.id }}">
51 ({{ post.id }})</a>
52 [{{ post.pub_time }}]
53 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
54 ; return false;">&gt;&gt;</a>]
55
56 {% if moderator %}
57 <span class="moderator_info">
58 [<a href="{% url 'delete' post_id=post.id %}"
59 >{% trans 'Delete' %}</a>]
60 ({{ post.poster_ip }})
61 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
62 >{% trans 'Ban IP' %}</a>]
63 </span>
64 {% endif %}
79 65 </div>
80 {% if forloop.first %}
81 <div class="metadata">
82 <span class="tags">
83 {% for tag in post.get_tags %}
84 <a class="tag" href="{% url 'tag' tag.name %}">
85 #{{ tag.name }}</a
86 >{% if not forloop.last %},{% endif %}
87 {% endfor %}
88 </span>
89 </div>
90 {% endif %}
91 </div>
66 {% autoescape off %}
67 {{ post.text.rendered }}
68 {% endautoescape %}
69 {% if post.is_referenced %}
70 <div class="refmap">
71 {% trans "Replies" %}:
72 {% for ref_post in post.get_sorted_referenced_posts %}
73 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
74 >{% if not forloop.last %},{% endif %}
92 75 {% endfor %}
93 76 </div>
94 {% endcache %}
95 77 {% endif %}
78 </div>
79 {% if forloop.first %}
80 <div class="metadata">
81 <span class="tags">
82 {% for tag in thread.get_tags %}
83 <a class="tag" href="{% url 'tag' tag.name %}">
84 #{{ tag.name }}</a
85 >{% if not forloop.last %},{% endif %}
86 {% endfor %}
87 </span>
88 </div>
89 {% endif %}
90 </div>
91 {% endfor %}
92 </div>
93 {% endcache %}
96 94
97 95 <form id="form" enctype="multipart/form-data" method="post"
98 96 >{% csrf_token %}
99 97 <div class="post-form-w">
100 98 <div class="form-title">{% trans "Reply to thread" %} #{{ posts.0.id }}</div>
101 99 <div class="post-form">
102 100 <div class="form-row">
103 101 <div class="form-label">{% trans 'Title' %}</div>
104 102 <div class="form-input">{{ form.title }}</div>
105 103 <div class="form-errors">{{ form.title.errors }}</div>
106 104 </div>
107 105 <div class="form-row">
108 106 <div class="form-label">{% trans 'Formatting' %}</div>
109 107 <div class="form-input" id="mark_panel">
110 108 <span class="mark_btn" id="quote"><span class="quote">&gt;{% trans 'quote' %}</span></span>
111 109 <span class="mark_btn" id="italic"><i>{% trans 'italic' %}</i></span>
112 110 <span class="mark_btn" id="bold"><b>{% trans 'bold' %}</b></span>
113 111 <span class="mark_btn" id="spoiler"><span class="spoiler">{% trans 'spoiler' %}</span></span>
114 112 <span class="mark_btn" id="comment"><span class="comment">// {% trans 'comment' %}</span></span>
115 113 </div>
116 114 </div>
117 115 <div class="form-row">
118 116 <div class="form-label">{% trans 'Text' %}</div>
119 117 <div class="form-input">{{ form.text }}</div>
120 118 <div class="form-errors">{{ form.text.errors }}</div>
121 119 </div>
122 120 <div class="form-row">
123 121 <div class="form-label">{% trans 'Image' %}</div>
124 122 <div class="form-input">{{ form.image }}</div>
125 123 <div class="form-errors">{{ form.image.errors }}</div>
126 124 </div>
127 125 <div class="form-row form-email">
128 126 <div class="form-label">{% trans 'e-mail' %}</div>
129 127 <div class="form-input">{{ form.email }}</div>
130 128 <div class="form-errors">{{ form.email.errors }}</div>
131 129 </div>
132 130 <div class="form-row">
133 131 {{ form.captcha }}
134 132 <div class="form-errors">{{ form.captcha.errors }}</div>
135 133 </div>
136 134 <div class="form-row">
137 135 <div class="form-errors">{{ form.other.errors }}</div>
138 136 </div>
139 137 </div>
140 138
141 139 <div class="form-submit"><input type="submit"
142 140 value="{% trans "Post" %}"/></div>
143 141 <div><a href="{% url "staticpage" name="help" %}">
144 142 {% trans 'Text syntax' %}</a></div>
145 143 </div>
146 144 </form>
147 145
148 146 {% endblock %}
149 147
150 148 {% block metapanel %}
151 149
152 150 {% get_current_language as LANGUAGE_CODE %}
153 151
154 152 <span class="metapanel" data-last-update="{{ last_update }}">
155 {% cache 600 thread_meta posts.0.last_edit_time moderator LANGUAGE_CODE %}
156 <span id="reply-count">{{ posts.0.get_reply_count }}</span> {% trans 'replies' %},
157 <span id="image-count">{{ posts.0.get_images_count }}</span> {% trans 'images' %}.
158 {% trans 'Last update: ' %}{{ posts.0.last_edit_time }}
153 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
154 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
155 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
156 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
159 157 [<a href="rss/">RSS</a>]
160 158 {% endcache %}
161 159 </span>
162 160
163 161 {% endblock %}
@@ -1,218 +1,224 b''
1 1 # coding=utf-8
2 2 from django.test import TestCase
3 3 from django.test.client import Client
4 4 import time
5 5
6 6 from boards.models import Post, Tag
7 7 from neboard import settings
8 8
9 9 PAGE_404 = 'boards/404.html'
10 10
11 11 TEST_TEXT = 'test text'
12 12
13 13 NEW_THREAD_PAGE = '/'
14 14 THREAD_PAGE_ONE = '/thread/1/'
15 15 THREAD_PAGE = '/thread/'
16 16 TAG_PAGE = '/tag/'
17 17 HTTP_CODE_REDIRECT = 302
18 18 HTTP_CODE_OK = 200
19 19 HTTP_CODE_NOT_FOUND = 404
20 20
21 21
22 22 class PostTests(TestCase):
23 23
24 24 def _create_post(self):
25 25 return Post.objects.create_post(title='title',
26 26 text='text')
27 27
28 28 def test_post_add(self):
29 29 """Test adding post"""
30 30
31 31 post = self._create_post()
32 32
33 self.assertIsNotNone(post)
34 self.assertIsNone(post.thread, 'Opening post has a thread')
33 self.assertIsNotNone(post, 'No post was created')
35 34
36 35 def test_delete_post(self):
37 36 """Test post deletion"""
38 37
39 38 post = self._create_post()
40 39 post_id = post.id
41 40
42 41 Post.objects.delete_post(post)
43 42
44 43 self.assertFalse(Post.objects.filter(id=post_id).exists())
45 44
46 45 def test_delete_thread(self):
47 46 """Test thread deletion"""
48 47
49 thread = self._create_post()
48 opening_post = self._create_post()
49 thread = opening_post.thread_new
50 50 reply = Post.objects.create_post("", "", thread=thread)
51 51
52 Post.objects.delete_post(thread)
52 thread.delete_with_posts()
53 53
54 54 self.assertFalse(Post.objects.filter(id=reply.id).exists())
55 55
56 56 def test_post_to_thread(self):
57 57 """Test adding post to a thread"""
58 58
59 59 op = self._create_post()
60 post = Post.objects.create_post("", "", thread=op)
60 post = Post.objects.create_post("", "", thread=op.thread_new)
61 61
62 62 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
63 self.assertEqual(op.last_edit_time, post.pub_time,
63 self.assertEqual(op.thread_new.last_edit_time, post.pub_time,
64 64 'Post\'s create time doesn\'t match thread last edit'
65 65 ' time')
66 66
67 67 def test_delete_posts_by_ip(self):
68 68 """Test deleting posts with the given ip"""
69 69
70 70 post = self._create_post()
71 71 post_id = post.id
72 72
73 73 Post.objects.delete_posts_by_ip('0.0.0.0')
74 74
75 75 self.assertFalse(Post.objects.filter(id=post_id).exists())
76 76
77 77 def test_get_thread(self):
78 78 """Test getting all posts of a thread"""
79 79
80 80 opening_post = self._create_post()
81 81
82 82 for i in range(0, 2):
83 Post.objects.create_post('title', 'text', thread=opening_post)
83 Post.objects.create_post('title', 'text',
84 thread=opening_post.thread_new)
84 85
85 86 thread = Post.objects.get_thread(opening_post.id)
86 87
87 self.assertEqual(3, len(thread))
88 self.assertEqual(3, thread.replies.count())
88 89
89 90 def test_create_post_with_tag(self):
90 91 """Test adding tag to post"""
91 92
92 93 tag = Tag.objects.create(name='test_tag')
93 94 post = Post.objects.create_post(title='title', text='text', tags=[tag])
94 self.assertIsNotNone(post)
95
96 thread = post.thread_new
97 self.assertIsNotNone(post, 'Post not created')
98 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
99 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
95 100
96 101 def test_thread_max_count(self):
97 102 """Test deletion of old posts when the max thread count is reached"""
98 103
99 104 for i in range(settings.MAX_THREAD_COUNT + 1):
100 105 self._create_post()
101 106
102 107 self.assertEqual(settings.MAX_THREAD_COUNT,
103 108 len(Post.objects.get_threads()))
104 109
105 110 def test_pages(self):
106 111 """Test that the thread list is properly split into pages"""
107 112
108 113 for i in range(settings.MAX_THREAD_COUNT):
109 114 self._create_post()
110 115
111 116 all_threads = Post.objects.get_threads()
112 117
113 118 posts_in_second_page = Post.objects.get_threads(page=1)
114 119 first_post = posts_in_second_page[0]
115 120
116 121 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
117 122 first_post.id)
118 123
119 124 def test_linked_tag(self):
120 125 """Test adding a linked tag"""
121 126
122 127 linked_tag = Tag.objects.create(name=u'tag1')
123 128 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
124 129
125 130 post = Post.objects.create_post("", "", tags=[tag])
126 131
127 self.assertTrue(linked_tag in post.tags.all(),
132 self.assertTrue(linked_tag in post.thread_new.tags.all(),
128 133 'Linked tag was not added')
129 134
130 135
131 136 class PagesTest(TestCase):
132 137
133 138 def test_404(self):
134 139 """Test receiving error 404 when opening a non-existent page"""
135 140
136 141 tag_name = u'test_tag'
137 142 tag = Tag.objects.create(name=tag_name)
138 143 client = Client()
139 144
140 145 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
141 146
142 147 existing_post_id = Post.objects.all()[0].id
143 148 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
144 149 '/')
145 150 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
146 151 u'Cannot open existing thread')
147 152
148 153 response_not_existing = client.get(THREAD_PAGE + str(
149 154 existing_post_id + 1) + '/')
150 155 self.assertEqual(PAGE_404,
151 156 response_not_existing.templates[0].name,
152 157 u'Not existing thread is opened')
153 158
154 159 response_existing = client.get(TAG_PAGE + tag_name + '/')
155 160 self.assertEqual(HTTP_CODE_OK,
156 161 response_existing.status_code,
157 162 u'Cannot open existing tag')
158 163
159 164 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
160 165 self.assertEqual(PAGE_404,
161 166 response_not_existing.templates[0].name,
162 167 u'Not existing tag is opened')
163 168
164 169 reply_id = Post.objects.create_post('', TEST_TEXT,
165 thread=Post.objects.all()[0])
170 thread=Post.objects.all()[0]
171 .thread)
166 172 response_not_existing = client.get(THREAD_PAGE + str(
167 173 reply_id) + '/')
168 174 self.assertEqual(PAGE_404,
169 175 response_not_existing.templates[0].name,
170 176 u'Reply is opened as a thread')
171 177
172 178
173 179 class FormTest(TestCase):
174 180 def test_post_validation(self):
175 181 """Test the validation of the post form"""
176 182
177 183 # Disable captcha for the test
178 184 captcha_enabled = settings.ENABLE_CAPTCHA
179 185 settings.ENABLE_CAPTCHA = False
180 186
181 187 client = Client()
182 188
183 189 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
184 190 invalid_tags = u'$%_356 ---'
185 191
186 192 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
187 193 'text': TEST_TEXT,
188 194 'tags': valid_tags})
189 195 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
190 196 msg='Posting new message failed: got code ' +
191 197 str(response.status_code))
192 198
193 199 self.assertEqual(1, Post.objects.count(),
194 200 msg='No posts were created')
195 201
196 202 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
197 203 'tags': invalid_tags})
198 204 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
199 205 'where it should fail')
200 206
201 207 # Change posting delay so we don't have to wait for 30 seconds or more
202 208 old_posting_delay = settings.POSTING_DELAY
203 209 # Wait fot the posting delay or we won't be able to post
204 210 settings.POSTING_DELAY = 1
205 211 time.sleep(settings.POSTING_DELAY + 1)
206 212 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
207 213 'tags': valid_tags})
208 214 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
209 215 msg=u'Posting new message failed: got code ' +
210 216 str(response.status_code))
211 217 # Restore posting delay
212 218 settings.POSTING_DELAY = old_posting_delay
213 219
214 220 self.assertEqual(2, Post.objects.count(),
215 221 msg=u'No posts were created')
216 222
217 223 # Restore captcha setting
218 224 settings.ENABLE_CAPTCHA = captcha_enabled
@@ -1,561 +1,567 b''
1 1 import hashlib
2 2 import json
3 3 import string
4 4 import time
5 5 from datetime import datetime
6 6 import re
7 7
8 8 from django.core import serializers
9 9 from django.core.urlresolvers import reverse
10 10 from django.http import HttpResponseRedirect
11 11 from django.http.response import HttpResponse
12 12 from django.template import RequestContext
13 13 from django.shortcuts import render, redirect, get_object_or_404
14 14 from django.utils import timezone
15 15 from django.db import transaction
16 16
17 17 from boards import forms
18 18 import boards
19 19 from boards import utils
20 20 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
21 21 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
22 22 from boards.models import Post, Tag, Ban, User
23 23 from boards.models.post import SETTING_MODERATE, REGEX_REPLY
24 24 from boards.models.user import RANK_USER
25 25 from boards import authors
26 26 from boards.utils import get_client_ip
27 27 import neboard
28 28
29 29
30 30 BAN_REASON_SPAM = 'Autoban: spam bot'
31 31
32 32
33 33 def index(request, page=0):
34 34 context = _init_default_context(request)
35 35
36 36 if utils.need_include_captcha(request):
37 37 threadFormClass = ThreadCaptchaForm
38 38 kwargs = {'request': request}
39 39 else:
40 40 threadFormClass = ThreadForm
41 41 kwargs = {}
42 42
43 43 if request.method == 'POST':
44 44 form = threadFormClass(request.POST, request.FILES,
45 45 error_class=PlainErrorList, **kwargs)
46 46 form.session = request.session
47 47
48 48 if form.is_valid():
49 49 return _new_post(request, form)
50 50 if form.need_to_ban:
51 51 # Ban user because he is suspected to be a bot
52 52 _ban_current_user(request)
53 53 else:
54 54 form = threadFormClass(error_class=PlainErrorList, **kwargs)
55 55
56 56 threads = []
57 for thread in Post.objects.get_threads(page=int(page)):
57 for thread_to_show in Post.objects.get_threads(page=int(page)):
58 58 threads.append({
59 'thread': thread,
60 'bumpable': thread.can_bump(),
61 'last_replies': thread.get_last_replies(),
59 'thread': thread_to_show,
60 'op': thread_to_show.get_replies()[0],
61 'bumpable': thread_to_show.can_bump(),
62 'last_replies': thread_to_show.get_last_replies(),
62 63 })
63 64
64 65 # TODO Make this generic for tag and threads list pages
65 66 context['threads'] = None if len(threads) == 0 else threads
66 67 context['form'] = form
67 68
68 69 page_count = Post.objects.get_thread_page_count()
69 70 context['pages'] = range(page_count)
70 71 page = int(page)
71 72 if page < page_count - 1:
72 73 context['next_page'] = str(page + 1)
73 74 if page > 0:
74 75 context['prev_page'] = str(page - 1)
75 76
76 77 return render(request, 'boards/posting_general.html',
77 78 context)
78 79
79 80
80 81 @transaction.atomic
81 82 def _new_post(request, form, opening_post=None):
82 83 """Add a new post (in thread or as a reply)."""
83 84
84 85 ip = get_client_ip(request)
85 86 is_banned = Ban.objects.filter(ip=ip).exists()
86 87
87 88 if is_banned:
88 89 return redirect(you_are_banned)
89 90
90 91 data = form.cleaned_data
91 92
92 93 title = data['title']
93 94 text = data['text']
94 95
95 96 text = _remove_invalid_links(text)
96 97
97 98 if 'image' in data.keys():
98 99 image = data['image']
99 100 else:
100 101 image = None
101 102
102 103 tags = []
103 104
104 105 if not opening_post:
105 106 tag_strings = data['tags']
106 107
107 108 if tag_strings:
108 109 tag_strings = tag_strings.split(' ')
109 110 for tag_name in tag_strings:
110 111 tag_name = string.lower(tag_name.strip())
111 112 if len(tag_name) > 0:
112 113 tag, created = Tag.objects.get_or_create(name=tag_name)
113 114 tags.append(tag)
115 post_thread = None
116 else:
117 post_thread = opening_post.thread_new
114 118
115 119 post = Post.objects.create_post(title=title, text=text, ip=ip,
116 thread=opening_post, image=image,
120 thread=post_thread, image=image,
117 121 tags=tags, user=_get_user(request))
118 122
119 123 thread_to_show = (opening_post.id if opening_post else post.id)
120 124
121 125 if opening_post:
122 126 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
123 127 '#' + str(post.id))
124 128 else:
125 129 return redirect(thread, post_id=thread_to_show)
126 130
127 131
128 132 def tag(request, tag_name, page=0):
129 133 """
130 134 Get all tag threads. Threads are split in pages, so some page is
131 135 requested. Default page is 0.
132 136 """
133 137
134 138 tag = get_object_or_404(Tag, name=tag_name)
135 139 threads = []
136 for thread in Post.objects.get_threads(tag=tag, page=int(page)):
140 for thread_to_show in Post.objects.get_threads(page=int(page)):
137 141 threads.append({
138 'thread': thread,
139 'bumpable': thread.can_bump(),
140 'last_replies': thread.get_last_replies(),
141 })
142 'thread': thread_to_show,
143 'op': thread_to_show.get_replies()[0],
144 'bumpable': thread_to_show.can_bump(),
145 'last_replies': thread_to_show.get_last_replies(),
146 })
142 147
143 148 if request.method == 'POST':
144 149 form = ThreadForm(request.POST, request.FILES,
145 150 error_class=PlainErrorList)
146 151 form.session = request.session
147 152
148 153 if form.is_valid():
149 154 return _new_post(request, form)
150 155 if form.need_to_ban:
151 156 # Ban user because he is suspected to be a bot
152 157 _ban_current_user(request)
153 158 else:
154 159 form = forms.ThreadForm(initial={'tags': tag_name},
155 160 error_class=PlainErrorList)
156 161
157 162 context = _init_default_context(request)
158 163 context['threads'] = None if len(threads) == 0 else threads
159 164 context['tag'] = tag
160 165
161 166 page_count = Post.objects.get_thread_page_count(tag=tag)
162 167 context['pages'] = range(page_count)
163 168 page = int(page)
164 169 if page < page_count - 1:
165 170 context['next_page'] = str(page + 1)
166 171 if page > 0:
167 172 context['prev_page'] = str(page - 1)
168 173
169 174 context['form'] = form
170 175
171 176 return render(request, 'boards/posting_general.html',
172 177 context)
173 178
174 179
175 180 def thread(request, post_id):
176 181 """Get all thread posts"""
177 182
178 183 if utils.need_include_captcha(request):
179 184 postFormClass = PostCaptchaForm
180 185 kwargs = {'request': request}
181 186 else:
182 187 postFormClass = PostForm
183 188 kwargs = {}
184 189
185 190 if request.method == 'POST':
186 191 form = postFormClass(request.POST, request.FILES,
187 192 error_class=PlainErrorList, **kwargs)
188 193 form.session = request.session
189 194
190 195 opening_post = get_object_or_404(Post, id=post_id)
191 196 if form.is_valid():
192 197 return _new_post(request, form, opening_post)
193 198 if form.need_to_ban:
194 199 # Ban user because he is suspected to be a bot
195 200 _ban_current_user(request)
196 201 else:
197 202 form = postFormClass(error_class=PlainErrorList, **kwargs)
198 203
199 posts = Post.objects.get_thread(post_id)
204 thread_to_show = get_object_or_404(Post, id=post_id).thread_new
200 205
201 206 context = _init_default_context(request)
202 207
203 context['posts'] = posts
208 posts = thread_to_show.get_replies()
204 209 context['form'] = form
205 context['bumpable'] = posts[0].can_bump()
210 context['bumpable'] = thread_to_show.can_bump()
206 211 if context['bumpable']:
207 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - len(
208 posts)
212 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts\
213 .count()
209 214 context['bumplimit_progress'] = str(
210 215 float(context['posts_left']) /
211 216 neboard.settings.MAX_POSTS_PER_THREAD * 100)
212 context["last_update"] = _datetime_to_epoch(posts[0].last_edit_time)
217 context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time)
218 context["thread"] = thread_to_show
213 219
214 220 return render(request, 'boards/thread.html', context)
215 221
216 222
217 223 def login(request):
218 224 """Log in with user id"""
219 225
220 226 context = _init_default_context(request)
221 227
222 228 if request.method == 'POST':
223 229 form = LoginForm(request.POST, request.FILES,
224 230 error_class=PlainErrorList)
225 231 form.session = request.session
226 232
227 233 if form.is_valid():
228 234 user = User.objects.get(user_id=form.cleaned_data['user_id'])
229 235 request.session['user_id'] = user.id
230 236 return redirect(index)
231 237
232 238 else:
233 239 form = LoginForm()
234 240
235 241 context['form'] = form
236 242
237 243 return render(request, 'boards/login.html', context)
238 244
239 245
240 246 def settings(request):
241 247 """User's settings"""
242 248
243 249 context = _init_default_context(request)
244 250 user = _get_user(request)
245 251 is_moderator = user.is_moderator()
246 252
247 253 if request.method == 'POST':
248 254 with transaction.commit_on_success():
249 255 if is_moderator:
250 256 form = ModeratorSettingsForm(request.POST,
251 257 error_class=PlainErrorList)
252 258 else:
253 259 form = SettingsForm(request.POST, error_class=PlainErrorList)
254 260
255 261 if form.is_valid():
256 262 selected_theme = form.cleaned_data['theme']
257 263
258 264 user.save_setting('theme', selected_theme)
259 265
260 266 if is_moderator:
261 267 moderate = form.cleaned_data['moderate']
262 268 user.save_setting(SETTING_MODERATE, moderate)
263 269
264 270 return redirect(settings)
265 271 else:
266 272 selected_theme = _get_theme(request)
267 273
268 274 if is_moderator:
269 275 form = ModeratorSettingsForm(initial={'theme': selected_theme,
270 276 'moderate': context['moderator']},
271 277 error_class=PlainErrorList)
272 278 else:
273 279 form = SettingsForm(initial={'theme': selected_theme},
274 280 error_class=PlainErrorList)
275 281
276 282 context['form'] = form
277 283
278 284 return render(request, 'boards/settings.html', context)
279 285
280 286
281 287 def all_tags(request):
282 288 """All tags list"""
283 289
284 290 context = _init_default_context(request)
285 291 context['all_tags'] = Tag.objects.get_not_empty_tags()
286 292
287 293 return render(request, 'boards/tags.html', context)
288 294
289 295
290 296 def jump_to_post(request, post_id):
291 297 """Determine thread in which the requested post is and open it's page"""
292 298
293 299 post = get_object_or_404(Post, id=post_id)
294 300
295 301 if not post.thread:
296 302 return redirect(thread, post_id=post.id)
297 303 else:
298 304 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
299 305 + '#' + str(post.id))
300 306
301 307
302 308 def authors(request):
303 309 """Show authors list"""
304 310
305 311 context = _init_default_context(request)
306 312 context['authors'] = boards.authors.authors
307 313
308 314 return render(request, 'boards/authors.html', context)
309 315
310 316
311 317 @transaction.atomic
312 318 def delete(request, post_id):
313 319 """Delete post"""
314 320
315 321 user = _get_user(request)
316 322 post = get_object_or_404(Post, id=post_id)
317 323
318 324 if user.is_moderator():
319 325 # TODO Show confirmation page before deletion
320 326 Post.objects.delete_post(post)
321 327
322 328 if not post.thread:
323 329 return _redirect_to_next(request)
324 330 else:
325 331 return redirect(thread, post_id=post.thread.id)
326 332
327 333
328 334 @transaction.atomic
329 335 def ban(request, post_id):
330 336 """Ban user"""
331 337
332 338 user = _get_user(request)
333 339 post = get_object_or_404(Post, id=post_id)
334 340
335 341 if user.is_moderator():
336 342 # TODO Show confirmation page before ban
337 343 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
338 344 if created:
339 345 ban.reason = 'Banned for post ' + str(post_id)
340 346 ban.save()
341 347
342 348 return _redirect_to_next(request)
343 349
344 350
345 351 def you_are_banned(request):
346 352 """Show the page that notifies that user is banned"""
347 353
348 354 context = _init_default_context(request)
349 355
350 356 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
351 357 context['ban_reason'] = ban.reason
352 358 return render(request, 'boards/staticpages/banned.html', context)
353 359
354 360
355 361 def page_404(request):
356 362 """Show page 404 (not found error)"""
357 363
358 364 context = _init_default_context(request)
359 365 return render(request, 'boards/404.html', context)
360 366
361 367
362 368 @transaction.atomic
363 369 def tag_subscribe(request, tag_name):
364 370 """Add tag to favorites"""
365 371
366 372 user = _get_user(request)
367 373 tag = get_object_or_404(Tag, name=tag_name)
368 374
369 375 if not tag in user.fav_tags.all():
370 376 user.add_tag(tag)
371 377
372 378 return _redirect_to_next(request)
373 379
374 380
375 381 @transaction.atomic
376 382 def tag_unsubscribe(request, tag_name):
377 383 """Remove tag from favorites"""
378 384
379 385 user = _get_user(request)
380 386 tag = get_object_or_404(Tag, name=tag_name)
381 387
382 388 if tag in user.fav_tags.all():
383 389 user.remove_tag(tag)
384 390
385 391 return _redirect_to_next(request)
386 392
387 393
388 394 def static_page(request, name):
389 395 """Show a static page that needs only tags list and a CSS"""
390 396
391 397 context = _init_default_context(request)
392 398 return render(request, 'boards/staticpages/' + name + '.html', context)
393 399
394 400
395 401 def api_get_post(request, post_id):
396 402 """
397 403 Get the JSON of a post. This can be
398 404 used as and API for external clients.
399 405 """
400 406
401 407 post = get_object_or_404(Post, id=post_id)
402 408
403 409 json = serializers.serialize("json", [post], fields=(
404 410 "pub_time", "_text_rendered", "title", "text", "image",
405 411 "image_width", "image_height", "replies", "tags"
406 412 ))
407 413
408 414 return HttpResponse(content=json)
409 415
410 416
411 417 @transaction.atomic
412 418 def api_get_threaddiff(request, thread_id, last_update_time):
413 419 """Get posts that were changed or added since time"""
414 420
415 421 thread = get_object_or_404(Post, id=thread_id)
416 422
417 423 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
418 424 timezone.get_current_timezone())
419 425
420 426 json_data = {
421 427 'added': [],
422 428 'updated': [],
423 429 'last_update': None,
424 430 }
425 431 added_posts = Post.objects.filter(thread=thread,
426 432 pub_time__gt=filter_time)\
427 433 .order_by('pub_time')
428 434 updated_posts = Post.objects.filter(thread=thread,
429 435 pub_time__lte=filter_time,
430 436 last_edit_time__gt=filter_time)
431 437 for post in added_posts:
432 438 json_data['added'].append(get_post(request, post.id).content.strip())
433 439 for post in updated_posts:
434 440 json_data['updated'].append(get_post(request, post.id).content.strip())
435 441 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
436 442
437 443 return HttpResponse(content=json.dumps(json_data))
438 444
439 445
440 446 def get_post(request, post_id):
441 447 """Get the html of a post. Used for popups."""
442 448
443 449 post = get_object_or_404(Post, id=post_id)
444 450 thread = post.thread
445 451 if not thread:
446 452 thread = post
447 453
448 454 context = RequestContext(request)
449 455 context["post"] = post
450 456 context["can_bump"] = thread.can_bump()
451 457 if "truncated" in request.GET:
452 458 context["truncated"] = True
453 459
454 460 return render(request, 'boards/post.html', context)
455 461
456 462
457 463 def _get_theme(request, user=None):
458 464 """Get user's CSS theme"""
459 465
460 466 if not user:
461 467 user = _get_user(request)
462 468 theme = user.get_setting('theme')
463 469 if not theme:
464 470 theme = neboard.settings.DEFAULT_THEME
465 471
466 472 return theme
467 473
468 474
469 475 def _init_default_context(request):
470 476 """Create context with default values that are used in most views"""
471 477
472 478 context = RequestContext(request)
473 479
474 480 user = _get_user(request)
475 481 context['user'] = user
476 482 context['tags'] = user.get_sorted_fav_tags()
477 483
478 484 theme = _get_theme(request, user)
479 485 context['theme'] = theme
480 486 context['theme_css'] = 'css/' + theme + '/base_page.css'
481 487
482 488 # This shows the moderator panel
483 489 moderate = user.get_setting(SETTING_MODERATE)
484 490 if moderate == 'True':
485 491 context['moderator'] = user.is_moderator()
486 492 else:
487 493 context['moderator'] = False
488 494
489 495 return context
490 496
491 497
492 498 def _get_user(request):
493 499 """
494 500 Get current user from the session. If the user does not exist, create
495 501 a new one.
496 502 """
497 503
498 504 session = request.session
499 505 if not 'user_id' in session:
500 506 request.session.save()
501 507
502 508 md5 = hashlib.md5()
503 509 md5.update(session.session_key)
504 510 new_id = md5.hexdigest()
505 511
506 512 time_now = timezone.now()
507 513 user = User.objects.create(user_id=new_id, rank=RANK_USER,
508 514 registration_time=time_now)
509 515
510 516 session['user_id'] = user.id
511 517 else:
512 518 user = User.objects.get(id=session['user_id'])
513 519
514 520 return user
515 521
516 522
517 523 def _redirect_to_next(request):
518 524 """
519 525 If a 'next' parameter was specified, redirect to the next page. This is
520 526 used when the user is required to return to some page after the current
521 527 view has finished its work.
522 528 """
523 529
524 530 if 'next' in request.GET:
525 531 next_page = request.GET['next']
526 532 return HttpResponseRedirect(next_page)
527 533 else:
528 534 return redirect(index)
529 535
530 536
531 537 @transaction.atomic
532 538 def _ban_current_user(request):
533 539 """Add current user to the IP ban list"""
534 540
535 541 ip = utils.get_client_ip(request)
536 542 ban, created = Ban.objects.get_or_create(ip=ip)
537 543 if created:
538 544 ban.can_read = False
539 545 ban.reason = BAN_REASON_SPAM
540 546 ban.save()
541 547
542 548
543 549 def _remove_invalid_links(text):
544 550 """
545 551 Replace invalid links in posts so that they won't be parsed.
546 552 Invalid links are links to non-existent posts
547 553 """
548 554
549 555 for reply_number in re.finditer(REGEX_REPLY, text):
550 556 post_id = reply_number.group(1)
551 557 post = Post.objects.filter(id=post_id)
552 558 if not post.exists():
553 559 text = string.replace(text, '>>' + post_id, post_id)
554 560
555 561 return text
556 562
557 563
558 564 def _datetime_to_epoch(datetime):
559 565 return int(time.mktime(timezone.localtime(
560 566 datetime,timezone.get_current_timezone()).timetuple())
561 567 * 1000000 + datetime.microsecond)
@@ -1,48 +1,48 b''
1 1 = Features =
2 2 [DONE] Connecting tags to each other
3 3 [DONE] Connect posts to the replies (in messages), get rid of the JS reply map
4 4 [DONE] Better django admin pages to simplify admin operations
5 5 [DONE] Regen script to update all posts
6 6 [DONE] Remove jump links from refmaps
7 7 [DONE] Ban reasons. Split bans into 2 types "read-only" and "read
8 8 denied". Use second only for autoban for spam
9 9 [DONE] Clean up tests and make them run ALWAYS
10 10 [DONE] Use transactions in tests
11 11 [DONE] Thread autoupdate (JS + API)
12 [IN PROGRESS] Split up post model into post and thread,
13 and move everything that is used only in 1st post to thread model.
12 14
13 15 [NOT STARTED] Tree view (JS)
14 16 [NOT STARTED] Adding tags to images filename
15 17 [NOT STARTED] Federative network for s2s communication
16 18 [NOT STARTED] XMPP gate
17 19 [NOT STARTED] Bitmessage gate
18 20 [NOT STARTED] Notification engine
19 21 [NOT STARTED] Javascript disabling engine
20 22 [NOT STARTED] Group tags by first letter in all tags list
21 23 [NOT STARTED] Show board speed in the lower panel (posts per day)
22 24 [NOT STARTED] Character counter in the post field
23 25 [NOT STARTED] Save image thumbnails size to the separate field
24 26 [NOT STARTED] Whitelist functionality. Permin autoban of an address
25 [NOT STARTED] Split up post model into post and thread,
26 and move everything that is used only in 1st post to thread model.
27 27 [NOT STARTED] Statistics module. Count views (optional, may result in bad
28 28 performance), posts per day/week/month, users (or IPs)
29 29 [NOT STARTED] Quote button next to "reply" for posts in thread to include full
30 30 post or its part (delimited by N characters) into quote of the new post.
31 31 [NOT STARTED] Ban confirmation page with reason
32 32 [NOT STARTED] Post deletion confirmation page
33 33 [NOT STARTED] Moderating page. Tags editing and adding
34 34 [NOT STARTED] Get thread graph image using pygraphviz
35 35 [NOT STARTED] Creating post via AJAX without reloading page
36 36 [NOT STARTED] Subscribing to tag via AJAX
37 37
38 38 = Bugs =
39 39 [DONE] Fix bug with creating threads from tag view
40 40 [DONE] Quote characters within quote causes quote parsing to fail
41 41
42 42 [IN PROGRESS] Replies, images, last update time in bottom panel doesn't change when
43 43 thread updates
44 44
45 45 = Testing =
46 46 [NOT STARTED] Make tests for every view
47 47 [NOT STARTED] Make tests for every model
48 48 [NOT STARTED] Make tests for every form
General Comments 0
You need to be logged in to leave comments. Login now