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