##// END OF EJS Templates
Removed django-markupfield as it is incompatible with the new migrations. Use 2 fields for storing raw and rendered text and work with them directly
neko259 -
r881:35b56a08 default
parent child Browse files
Show More
@@ -1,182 +1,183 b''
1 # coding=utf-8
1 # coding=utf-8
2
2
3 import re
3 import re
4 import bbcode
4 import bbcode
5
5
6 import boards
6 import boards
7
7
8
8
9 __author__ = 'neko259'
9 __author__ = 'neko259'
10
10
11
11
12 REFLINK_PATTERN = re.compile(r'^\d+$')
12 REFLINK_PATTERN = re.compile(r'^\d+$')
13 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
13 MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
14 ONE_NEWLINE = '\n'
14 ONE_NEWLINE = '\n'
15
15
16
16
17 class TextFormatter():
17 class TextFormatter():
18 """
18 """
19 An interface for formatter that can be used in the text format panel
19 An interface for formatter that can be used in the text format panel
20 """
20 """
21
21
22 def __init__(self):
22 def __init__(self):
23 pass
23 pass
24
24
25 name = ''
25 name = ''
26
26
27 # Left and right tags for the button preview
27 # Left and right tags for the button preview
28 preview_left = ''
28 preview_left = ''
29 preview_right = ''
29 preview_right = ''
30
30
31 # Left and right characters for the textarea input
31 # Left and right characters for the textarea input
32 format_left = ''
32 format_left = ''
33 format_right = ''
33 format_right = ''
34
34
35
35
36 class AutolinkPattern():
36 class AutolinkPattern():
37 def handleMatch(self, m):
37 def handleMatch(self, m):
38 link_element = etree.Element('a')
38 link_element = etree.Element('a')
39 href = m.group(2)
39 href = m.group(2)
40 link_element.set('href', href)
40 link_element.set('href', href)
41 link_element.text = href
41 link_element.text = href
42
42
43 return link_element
43 return link_element
44
44
45
45
46 class QuotePattern(TextFormatter):
46 class QuotePattern(TextFormatter):
47 name = 'q'
47 name = 'q'
48 preview_left = '<span class="multiquote">'
48 preview_left = '<span class="multiquote">'
49 preview_right = '</span>'
49 preview_right = '</span>'
50
50
51 format_left = '[quote]'
51 format_left = '[quote]'
52 format_right = '[/quote]'
52 format_right = '[/quote]'
53
53
54
54
55 class SpoilerPattern(TextFormatter):
55 class SpoilerPattern(TextFormatter):
56 name = 'spoiler'
56 name = 'spoiler'
57 preview_left = '<span class="spoiler">'
57 preview_left = '<span class="spoiler">'
58 preview_right = '</span>'
58 preview_right = '</span>'
59
59
60 format_left = '[spoiler]'
60 format_left = '[spoiler]'
61 format_right = '[/spoiler]'
61 format_right = '[/spoiler]'
62
62
63 def handleMatch(self, m):
63 def handleMatch(self, m):
64 quote_element = etree.Element('span')
64 quote_element = etree.Element('span')
65 quote_element.set('class', 'spoiler')
65 quote_element.set('class', 'spoiler')
66 quote_element.text = m.group(2)
66 quote_element.text = m.group(2)
67
67
68 return quote_element
68 return quote_element
69
69
70
70
71 class CommentPattern(TextFormatter):
71 class CommentPattern(TextFormatter):
72 name = ''
72 name = ''
73 preview_left = '<span class="comment">// '
73 preview_left = '<span class="comment">// '
74 preview_right = '</span>'
74 preview_right = '</span>'
75
75
76 format_left = '[comment]'
76 format_left = '[comment]'
77 format_right = '[/comment]'
77 format_right = '[/comment]'
78
78
79
79
80 # TODO Use <s> tag here
80 # TODO Use <s> tag here
81 class StrikeThroughPattern(TextFormatter):
81 class StrikeThroughPattern(TextFormatter):
82 name = 's'
82 name = 's'
83 preview_left = '<span class="strikethrough">'
83 preview_left = '<span class="strikethrough">'
84 preview_right = '</span>'
84 preview_right = '</span>'
85
85
86 format_left = '[s]'
86 format_left = '[s]'
87 format_right = '[/s]'
87 format_right = '[/s]'
88
88
89
89
90 class ItalicPattern(TextFormatter):
90 class ItalicPattern(TextFormatter):
91 name = 'i'
91 name = 'i'
92 preview_left = '<i>'
92 preview_left = '<i>'
93 preview_right = '</i>'
93 preview_right = '</i>'
94
94
95 format_left = '[i]'
95 format_left = '[i]'
96 format_right = '[/i]'
96 format_right = '[/i]'
97
97
98
98
99 class BoldPattern(TextFormatter):
99 class BoldPattern(TextFormatter):
100 name = 'b'
100 name = 'b'
101 preview_left = '<b>'
101 preview_left = '<b>'
102 preview_right = '</b>'
102 preview_right = '</b>'
103
103
104 format_left = '[b]'
104 format_left = '[b]'
105 format_right = '[/b]'
105 format_right = '[/b]'
106
106
107
107
108 class CodePattern(TextFormatter):
108 class CodePattern(TextFormatter):
109 name = 'code'
109 name = 'code'
110 preview_left = '<code>'
110 preview_left = '<code>'
111 preview_right = '</code>'
111 preview_right = '</code>'
112
112
113 format_left = '[code]'
113 format_left = '[code]'
114 format_right = '[/code]'
114 format_right = '[/code]'
115
115
116
116
117 def render_reflink(tag_name, value, options, parent, context):
117 def render_reflink(tag_name, value, options, parent, context):
118 if not REFLINK_PATTERN.match(value):
118 if not REFLINK_PATTERN.match(value):
119 return '>>%s' % value
119 return '>>%s' % value
120
120
121 post_id = int(value)
121 post_id = int(value)
122
122
123 posts = boards.models.Post.objects.filter(id=post_id)
123 posts = boards.models.Post.objects.filter(id=post_id)
124 if posts.exists():
124 if posts.exists():
125 post = posts[0]
125 post = posts[0]
126
126
127 return '<a href="%s">&gt;&gt;%s</a>' % (post.get_url(), post_id)
127 return '<a href="%s">&gt;&gt;%s</a>' % (post.get_url(), post_id)
128 else:
128 else:
129 return '>>%s' % value
129 return '>>%s' % value
130
130
131
131
132 def render_quote(tag_name, value, options, parent, context):
132 def render_quote(tag_name, value, options, parent, context):
133 source = ''
133 source = ''
134 if 'source' in options:
134 if 'source' in options:
135 source = options['source']
135 source = options['source']
136
136
137 result = ''
137 result = ''
138 if source:
138 if source:
139 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
139 result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
140 else:
140 else:
141 result = '<div class="multiquote"><div class="quote-text">%s</div></div>' % value
141 result = '<div class="multiquote"><div class="quote-text">%s</div></div>' % value
142
142
143 return result
143 return result
144
144
145
145
146 def preparse_text(text):
146 def preparse_text(text):
147 """
147 """
148 Performs manual parsing before the bbcode parser is used.
148 Performs manual parsing before the bbcode parser is used.
149 """
149 """
150
150
151 return MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
151 return MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text)
152
152
153
153
154 def bbcode_extended(markup):
154 def bbcode_extended(markup):
155 # The newline hack is added because br's margin does not work in all
155 # The newline hack is added because br's margin does not work in all
156 # browsers except firefox, when the div's does.
156 # browsers except firefox, when the div's does.
157 parser = bbcode.Parser(newline='<div class="br"></div>')
157 parser = bbcode.Parser(newline='<div class="br"></div>')
158 parser.add_formatter('post', render_reflink, strip=True)
158 parser.add_formatter('post', render_reflink, strip=True)
159 parser.add_formatter('quote', render_quote, strip=True)
159 parser.add_formatter('quote', render_quote, strip=True)
160 parser.add_simple_formatter('comment',
160 parser.add_simple_formatter('comment',
161 u'<span class="comment">//%(value)s</span>')
161 '<span class="comment">//%(value)s</span>')
162 parser.add_simple_formatter('spoiler',
162 parser.add_simple_formatter('spoiler',
163 u'<span class="spoiler">%(value)s</span>')
163 '<span class="spoiler">%(value)s</span>')
164 # TODO Use <s> here
164 # TODO Use <s> here
165 parser.add_simple_formatter('s',
165 parser.add_simple_formatter('s',
166 u'<span class="strikethrough">%(value)s</span>')
166 '<span class="strikethrough">%(value)s</span>')
167 # TODO Why not use built-in tag?
167 # TODO Why not use built-in tag?
168 parser.add_simple_formatter('code',
168 parser.add_simple_formatter('code',
169 u'<pre><code>%(value)s</pre></code>', render_embedded=False)
169 '<pre><code>%(value)s</pre></code>',
170 render_embedded=False)
170
171
171 text = preparse_text(markup)
172 text = preparse_text(markup)
172 return parser.format(text)
173 return parser.format(text)
173
174
174 formatters = [
175 formatters = [
175 QuotePattern,
176 QuotePattern,
176 SpoilerPattern,
177 SpoilerPattern,
177 ItalicPattern,
178 ItalicPattern,
178 BoldPattern,
179 BoldPattern,
179 CommentPattern,
180 CommentPattern,
180 StrikeThroughPattern,
181 StrikeThroughPattern,
181 CodePattern,
182 CodePattern,
182 ]
183 ]
@@ -1,114 +1,113 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
2 from __future__ import unicode_literals
3
3
4 from django.db import models, migrations
4 from django.db import models, migrations
5 import markupfield.fields
6 import boards.models.image
5 import boards.models.image
7 import boards.models.base
6 import boards.models.base
8 import boards.thumbs
7 import boards.thumbs
9
8
10
9
11 class Migration(migrations.Migration):
10 class Migration(migrations.Migration):
12
11
13 dependencies = [
12 dependencies = [
14 ]
13 ]
15
14
16 operations = [
15 operations = [
17 migrations.CreateModel(
16 migrations.CreateModel(
18 name='Ban',
17 name='Ban',
19 fields=[
18 fields=[
20 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
19 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
21 ('ip', models.GenericIPAddressField()),
20 ('ip', models.GenericIPAddressField()),
22 ('reason', models.CharField(max_length=200, default='Auto')),
21 ('reason', models.CharField(max_length=200, default='Auto')),
23 ('can_read', models.BooleanField(default=True)),
22 ('can_read', models.BooleanField(default=True)),
24 ],
23 ],
25 options={
24 options={
26 },
25 },
27 bases=(models.Model,),
26 bases=(models.Model,),
28 ),
27 ),
29 migrations.CreateModel(
28 migrations.CreateModel(
30 name='Post',
29 name='Post',
31 fields=[
30 fields=[
32 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
31 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
33 ('title', models.CharField(max_length=200)),
32 ('title', models.CharField(max_length=200)),
34 ('pub_time', models.DateTimeField()),
33 ('pub_time', models.DateTimeField()),
35 ('text', markupfield.fields.MarkupField()),
34 ('text', models.TextField(null=True, blank=True)),
36 ('text_markup_type', models.CharField(choices=[('', '--'), ('bbcode', 'bbcode')], max_length=30, default='bbcode')),
35 ('text_markup_type', models.CharField(choices=[('', '--'), ('bbcode', 'bbcode')], max_length=30, default='bbcode')),
37 ('poster_ip', models.GenericIPAddressField()),
36 ('poster_ip', models.GenericIPAddressField()),
38 ('_text_rendered', models.TextField(editable=False)),
37 ('_text_rendered', models.TextField(editable=False)),
39 ('poster_user_agent', models.TextField()),
38 ('poster_user_agent', models.TextField()),
40 ('last_edit_time', models.DateTimeField()),
39 ('last_edit_time', models.DateTimeField()),
41 ('refmap', models.TextField(null=True, blank=True)),
40 ('refmap', models.TextField(null=True, blank=True)),
42 ],
41 ],
43 options={
42 options={
44 'ordering': ('id',),
43 'ordering': ('id',),
45 },
44 },
46 bases=(models.Model, boards.models.base.Viewable),
45 bases=(models.Model, boards.models.base.Viewable),
47 ),
46 ),
48 migrations.CreateModel(
47 migrations.CreateModel(
49 name='PostImage',
48 name='PostImage',
50 fields=[
49 fields=[
51 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
50 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
52 ('width', models.IntegerField(default=0)),
51 ('width', models.IntegerField(default=0)),
53 ('height', models.IntegerField(default=0)),
52 ('height', models.IntegerField(default=0)),
54 ('pre_width', models.IntegerField(default=0)),
53 ('pre_width', models.IntegerField(default=0)),
55 ('pre_height', models.IntegerField(default=0)),
54 ('pre_height', models.IntegerField(default=0)),
56 ('image', boards.thumbs.ImageWithThumbsField(height_field='height', width_field='width', upload_to=boards.models.image.PostImage._update_image_filename, blank=True)),
55 ('image', boards.thumbs.ImageWithThumbsField(height_field='height', width_field='width', upload_to=boards.models.image.PostImage._update_image_filename, blank=True)),
57 ('hash', models.CharField(max_length=36)),
56 ('hash', models.CharField(max_length=36)),
58 ],
57 ],
59 options={
58 options={
60 'ordering': ('id',),
59 'ordering': ('id',),
61 },
60 },
62 bases=(models.Model,),
61 bases=(models.Model,),
63 ),
62 ),
64 migrations.CreateModel(
63 migrations.CreateModel(
65 name='Tag',
64 name='Tag',
66 fields=[
65 fields=[
67 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
66 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
68 ('name', models.CharField(db_index=True, max_length=100)),
67 ('name', models.CharField(db_index=True, max_length=100)),
69 ],
68 ],
70 options={
69 options={
71 'ordering': ('name',),
70 'ordering': ('name',),
72 },
71 },
73 bases=(models.Model, boards.models.base.Viewable),
72 bases=(models.Model, boards.models.base.Viewable),
74 ),
73 ),
75 migrations.CreateModel(
74 migrations.CreateModel(
76 name='Thread',
75 name='Thread',
77 fields=[
76 fields=[
78 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
77 ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
79 ('bump_time', models.DateTimeField()),
78 ('bump_time', models.DateTimeField()),
80 ('last_edit_time', models.DateTimeField()),
79 ('last_edit_time', models.DateTimeField()),
81 ('archived', models.BooleanField(default=False)),
80 ('archived', models.BooleanField(default=False)),
82 ('bumpable', models.BooleanField(default=True)),
81 ('bumpable', models.BooleanField(default=True)),
83 ('replies', models.ManyToManyField(null=True, related_name='tre+', to='boards.Post', blank=True)),
82 ('replies', models.ManyToManyField(null=True, related_name='tre+', to='boards.Post', blank=True)),
84 ('tags', models.ManyToManyField(to='boards.Tag')),
83 ('tags', models.ManyToManyField(to='boards.Tag')),
85 ],
84 ],
86 options={
85 options={
87 },
86 },
88 bases=(models.Model,),
87 bases=(models.Model,),
89 ),
88 ),
90 migrations.AddField(
89 migrations.AddField(
91 model_name='tag',
90 model_name='tag',
92 name='threads',
91 name='threads',
93 field=models.ManyToManyField(null=True, related_name='tag+', to='boards.Thread', blank=True),
92 field=models.ManyToManyField(null=True, related_name='tag+', to='boards.Thread', blank=True),
94 preserve_default=True,
93 preserve_default=True,
95 ),
94 ),
96 migrations.AddField(
95 migrations.AddField(
97 model_name='post',
96 model_name='post',
98 name='images',
97 name='images',
99 field=models.ManyToManyField(null=True, db_index=True, related_name='ip+', to='boards.PostImage', blank=True),
98 field=models.ManyToManyField(null=True, db_index=True, related_name='ip+', to='boards.PostImage', blank=True),
100 preserve_default=True,
99 preserve_default=True,
101 ),
100 ),
102 migrations.AddField(
101 migrations.AddField(
103 model_name='post',
102 model_name='post',
104 name='referenced_posts',
103 name='referenced_posts',
105 field=models.ManyToManyField(null=True, db_index=True, related_name='rfp+', to='boards.Post', blank=True),
104 field=models.ManyToManyField(null=True, db_index=True, related_name='rfp+', to='boards.Post', blank=True),
106 preserve_default=True,
105 preserve_default=True,
107 ),
106 ),
108 migrations.AddField(
107 migrations.AddField(
109 model_name='post',
108 model_name='post',
110 name='thread_new',
109 name='thread_new',
111 field=models.ForeignKey(null=True, default=None, to='boards.Thread'),
110 field=models.ForeignKey(null=True, default=None, to='boards.Thread'),
112 preserve_default=True,
111 preserve_default=True,
113 ),
112 ),
114 ]
113 ]
@@ -1,436 +1,447 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 from adjacent import Client
4 import logging
3 import logging
5 import re
4 import re
6
5
6 from adjacent import Client
7 from django.core.cache import cache
7 from django.core.cache import cache
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.shortcuts import get_object_or_404
10 from django.db.models import TextField
11 from django.template import RequestContext
11 from django.template import RequestContext
12 from django.template.loader import render_to_string
12 from django.template.loader import render_to_string
13 from django.utils import timezone
13 from django.utils import timezone
14 from markupfield.fields import MarkupField
14
15 from boards import settings
15 from boards import settings
16
16 from boards.mdx_neboard import bbcode_extended
17 from boards.models import PostImage
17 from boards.models import PostImage
18 from boards.models.base import Viewable
18 from boards.models.base import Viewable
19 from boards.models.thread import Thread
19 from boards.models.thread import Thread
20 from boards.utils import datetime_to_epoch
20 from boards.utils import datetime_to_epoch
21
21
22
22 WS_CHANNEL_THREAD = "thread:"
23 WS_CHANNEL_THREAD = "thread:"
23
24
24 APP_LABEL_BOARDS = 'boards'
25 APP_LABEL_BOARDS = 'boards'
25
26
26 CACHE_KEY_PPD = 'ppd'
27 CACHE_KEY_PPD = 'ppd'
27 CACHE_KEY_POST_URL = 'post_url'
28 CACHE_KEY_POST_URL = 'post_url'
28
29
29 POSTS_PER_DAY_RANGE = 7
30 POSTS_PER_DAY_RANGE = 7
30
31
31 BAN_REASON_AUTO = 'Auto'
32 BAN_REASON_AUTO = 'Auto'
32
33
33 IMAGE_THUMB_SIZE = (200, 150)
34 IMAGE_THUMB_SIZE = (200, 150)
34
35
35 TITLE_MAX_LENGTH = 200
36 TITLE_MAX_LENGTH = 200
36
37
37 DEFAULT_MARKUP_TYPE = 'bbcode'
38
39 # TODO This should be removed
38 # TODO This should be removed
40 NO_IP = '0.0.0.0'
39 NO_IP = '0.0.0.0'
41
40
42 # TODO Real user agent should be saved instead of this
41 # TODO Real user agent should be saved instead of this
43 UNKNOWN_UA = ''
42 UNKNOWN_UA = ''
44
43
45 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
44 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
46
45
47 PARAMETER_TRUNCATED = 'truncated'
46 PARAMETER_TRUNCATED = 'truncated'
48 PARAMETER_TAG = 'tag'
47 PARAMETER_TAG = 'tag'
49 PARAMETER_OFFSET = 'offset'
48 PARAMETER_OFFSET = 'offset'
50 PARAMETER_DIFF_TYPE = 'type'
49 PARAMETER_DIFF_TYPE = 'type'
51
50
52 DIFF_TYPE_HTML = 'html'
51 DIFF_TYPE_HTML = 'html'
53 DIFF_TYPE_JSON = 'json'
52 DIFF_TYPE_JSON = 'json'
54
53
55 PREPARSE_PATTERNS = {
54 PREPARSE_PATTERNS = {
56 r'>>(\d+)': r'[post]\1[/post]',
55 r'>>(\d+)': r'[post]\1[/post]',
57 r'>(.+)': r'[quote]\1[/quote]'
56 r'>(.+)': r'[quote]\1[/quote]'
58 }
57 }
59
58
60
59
61 class PostManager(models.Manager):
60 class PostManager(models.Manager):
62 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
61 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
63 tags=None):
62 tags=None):
64 """
63 """
65 Creates new post
64 Creates new post
66 """
65 """
67
66
68 if not tags:
67 if not tags:
69 tags = []
68 tags = []
70
69
71 posting_time = timezone.now()
70 posting_time = timezone.now()
72 if not thread:
71 if not thread:
73 thread = Thread.objects.create(bump_time=posting_time,
72 thread = Thread.objects.create(bump_time=posting_time,
74 last_edit_time=posting_time)
73 last_edit_time=posting_time)
75 new_thread = True
74 new_thread = True
76 else:
75 else:
77 thread.bump()
76 thread.bump()
78 thread.last_edit_time = posting_time
77 thread.last_edit_time = posting_time
79 if thread.can_bump() and (
78 if thread.can_bump() and (
80 thread.get_reply_count() >= settings.MAX_POSTS_PER_THREAD):
79 thread.get_reply_count() >= settings.MAX_POSTS_PER_THREAD):
81 thread.bumpable = False
80 thread.bumpable = False
82 thread.save()
81 thread.save()
83 new_thread = False
82 new_thread = False
84
83
85 pre_text = self._preparse_text(text)
84 pre_text = self._preparse_text(text)
86
85
87 post = self.create(title=title,
86 post = self.create(title=title,
88 text=pre_text,
87 text=pre_text,
89 pub_time=posting_time,
88 pub_time=posting_time,
90 thread_new=thread,
89 thread_new=thread,
91 poster_ip=ip,
90 poster_ip=ip,
92 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
91 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
93 # last!
92 # last!
94 last_edit_time=posting_time)
93 last_edit_time=posting_time)
95
94
96 logger = logging.getLogger('boards.post.create')
95 logger = logging.getLogger('boards.post.create')
97
96
98 logger.info('Created post #{} with title "{}" by {}'.format(
97 logger.info('Created post #{} with title "{}" by {}'.format(
99 post.id, post.title, post.poster_ip))
98 post.id, post.title, post.poster_ip))
100
99
101 if image:
100 if image:
102 post_image = PostImage.objects.create(image=image)
101 post_image = PostImage.objects.create(image=image)
103 post.images.add(post_image)
102 post.images.add(post_image)
104 logger.info('Created image #{} for post #{}'.format(
103 logger.info('Created image #{} for post #{}'.format(
105 post_image.id, post.id))
104 post_image.id, post.id))
106
105
107 thread.replies.add(post)
106 thread.replies.add(post)
108 list(map(thread.add_tag, tags))
107 list(map(thread.add_tag, tags))
109
108
110 if new_thread:
109 if new_thread:
111 Thread.objects.process_oldest_threads()
110 Thread.objects.process_oldest_threads()
112 self.connect_replies(post)
111 self.connect_replies(post)
113
112
114 return post
113 return post
115
114
116 def delete_posts_by_ip(self, ip):
115 def delete_posts_by_ip(self, ip):
117 """
116 """
118 Deletes all posts of the author with same IP
117 Deletes all posts of the author with same IP
119 """
118 """
120
119
121 posts = self.filter(poster_ip=ip)
120 posts = self.filter(poster_ip=ip)
122 for post in posts:
121 for post in posts:
123 post.delete()
122 post.delete()
124
123
125 def connect_replies(self, post):
124 def connect_replies(self, post):
126 """
125 """
127 Connects replies to a post to show them as a reflink map
126 Connects replies to a post to show them as a reflink map
128 """
127 """
129
128
130 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
129 for reply_number in re.finditer(REGEX_REPLY, post.get_raw_text()):
131 post_id = reply_number.group(1)
130 post_id = reply_number.group(1)
132 ref_post = self.filter(id=post_id)
131 ref_post = self.filter(id=post_id)
133 if ref_post.count() > 0:
132 if ref_post.count() > 0:
134 referenced_post = ref_post[0]
133 referenced_post = ref_post[0]
135 referenced_post.referenced_posts.add(post)
134 referenced_post.referenced_posts.add(post)
136 referenced_post.last_edit_time = post.pub_time
135 referenced_post.last_edit_time = post.pub_time
137 referenced_post.build_refmap()
136 referenced_post.build_refmap()
138 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
137 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
139
138
140 referenced_thread = referenced_post.get_thread()
139 referenced_thread = referenced_post.get_thread()
141 referenced_thread.last_edit_time = post.pub_time
140 referenced_thread.last_edit_time = post.pub_time
142 referenced_thread.save(update_fields=['last_edit_time'])
141 referenced_thread.save(update_fields=['last_edit_time'])
143
142
144 def get_posts_per_day(self):
143 def get_posts_per_day(self):
145 """
144 """
146 Gets average count of posts per day for the last 7 days
145 Gets average count of posts per day for the last 7 days
147 """
146 """
148
147
149 day_end = date.today()
148 day_end = date.today()
150 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
149 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
151
150
152 cache_key = CACHE_KEY_PPD + str(day_end)
151 cache_key = CACHE_KEY_PPD + str(day_end)
153 ppd = cache.get(cache_key)
152 ppd = cache.get(cache_key)
154 if ppd:
153 if ppd:
155 return ppd
154 return ppd
156
155
157 day_time_start = timezone.make_aware(datetime.combine(
156 day_time_start = timezone.make_aware(datetime.combine(
158 day_start, dtime()), timezone.get_current_timezone())
157 day_start, dtime()), timezone.get_current_timezone())
159 day_time_end = timezone.make_aware(datetime.combine(
158 day_time_end = timezone.make_aware(datetime.combine(
160 day_end, dtime()), timezone.get_current_timezone())
159 day_end, dtime()), timezone.get_current_timezone())
161
160
162 posts_per_period = float(self.filter(
161 posts_per_period = float(self.filter(
163 pub_time__lte=day_time_end,
162 pub_time__lte=day_time_end,
164 pub_time__gte=day_time_start).count())
163 pub_time__gte=day_time_start).count())
165
164
166 ppd = posts_per_period / POSTS_PER_DAY_RANGE
165 ppd = posts_per_period / POSTS_PER_DAY_RANGE
167
166
168 cache.set(cache_key, ppd)
167 cache.set(cache_key, ppd)
169 return ppd
168 return ppd
170
169
171 def _preparse_text(self, text):
170 def _preparse_text(self, text):
172 """
171 """
173 Preparses text to change patterns like '>>' to a proper bbcode
172 Preparses text to change patterns like '>>' to a proper bbcode
174 tags.
173 tags.
175 """
174 """
176
175
177 for key, value in PREPARSE_PATTERNS.items():
176 for key, value in PREPARSE_PATTERNS.items():
178 text = re.sub(key, value, text)
177 text = re.sub(key, value, text)
179
178
180 return text
179 return text
181
180
182
181
183 class Post(models.Model, Viewable):
182 class Post(models.Model, Viewable):
184 """A post is a message."""
183 """A post is a message."""
185
184
186 objects = PostManager()
185 objects = PostManager()
187
186
188 class Meta:
187 class Meta:
189 app_label = APP_LABEL_BOARDS
188 app_label = APP_LABEL_BOARDS
190 ordering = ('id',)
189 ordering = ('id',)
191
190
192 title = models.CharField(max_length=TITLE_MAX_LENGTH)
191 title = models.CharField(max_length=TITLE_MAX_LENGTH)
193 pub_time = models.DateTimeField()
192 pub_time = models.DateTimeField()
194 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
193 text = TextField(blank=True, null=True)
195 escape_html=False)
194 _text_rendered = TextField(blank=True, null=True, editable=False)
196
195
197 images = models.ManyToManyField(PostImage, null=True, blank=True,
196 images = models.ManyToManyField(PostImage, null=True, blank=True,
198 related_name='ip+', db_index=True)
197 related_name='ip+', db_index=True)
199
198
200 poster_ip = models.GenericIPAddressField()
199 poster_ip = models.GenericIPAddressField()
201 poster_user_agent = models.TextField()
200 poster_user_agent = models.TextField()
202
201
203 thread_new = models.ForeignKey('Thread', null=True, default=None,
202 thread_new = models.ForeignKey('Thread', null=True, default=None,
204 db_index=True)
203 db_index=True)
205 last_edit_time = models.DateTimeField()
204 last_edit_time = models.DateTimeField()
206
205
207 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
206 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
208 null=True,
207 null=True,
209 blank=True, related_name='rfp+',
208 blank=True, related_name='rfp+',
210 db_index=True)
209 db_index=True)
211 refmap = models.TextField(null=True, blank=True)
210 refmap = models.TextField(null=True, blank=True)
212
211
213 def __str__(self):
212 def __str__(self):
214 return 'P#{}/{}'.format(self.id, self.title)
213 return 'P#{}/{}'.format(self.id, self.title)
215
214
216 def get_title(self):
215 def get_title(self):
217 """
216 """
218 Gets original post title or part of its text.
217 Gets original post title or part of its text.
219 """
218 """
220
219
221 title = self.title
220 title = self.title
222 if not title:
221 if not title:
223 title = self.text.rendered
222 title = self.get_text()
224
223
225 return title
224 return title
226
225
227 def build_refmap(self):
226 def build_refmap(self):
228 """
227 """
229 Builds a replies map string from replies list. This is a cache to stop
228 Builds a replies map string from replies list. This is a cache to stop
230 the server from recalculating the map on every post show.
229 the server from recalculating the map on every post show.
231 """
230 """
232 map_string = ''
231 map_string = ''
233
232
234 first = True
233 first = True
235 for refpost in self.referenced_posts.all():
234 for refpost in self.referenced_posts.all():
236 if not first:
235 if not first:
237 map_string += ', '
236 map_string += ', '
238 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
237 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
239 refpost.id)
238 refpost.id)
240 first = False
239 first = False
241
240
242 self.refmap = map_string
241 self.refmap = map_string
243
242
244 def get_sorted_referenced_posts(self):
243 def get_sorted_referenced_posts(self):
245 return self.refmap
244 return self.refmap
246
245
247 def is_referenced(self):
246 def is_referenced(self):
248 if not self.refmap:
247 if not self.refmap:
249 return False
248 return False
250 else:
249 else:
251 return len(self.refmap) > 0
250 return len(self.refmap) > 0
252
251
253 def is_opening(self):
252 def is_opening(self):
254 """
253 """
255 Checks if this is an opening post or just a reply.
254 Checks if this is an opening post or just a reply.
256 """
255 """
257
256
258 return self.get_thread().get_opening_post_id() == self.id
257 return self.get_thread().get_opening_post_id() == self.id
259
258
260 @transaction.atomic
259 @transaction.atomic
261 def add_tag(self, tag):
260 def add_tag(self, tag):
262 edit_time = timezone.now()
261 edit_time = timezone.now()
263
262
264 thread = self.get_thread()
263 thread = self.get_thread()
265 thread.add_tag(tag)
264 thread.add_tag(tag)
266 self.last_edit_time = edit_time
265 self.last_edit_time = edit_time
267 self.save(update_fields=['last_edit_time'])
266 self.save(update_fields=['last_edit_time'])
268
267
269 thread.last_edit_time = edit_time
268 thread.last_edit_time = edit_time
270 thread.save(update_fields=['last_edit_time'])
269 thread.save(update_fields=['last_edit_time'])
271
270
272 @transaction.atomic
271 @transaction.atomic
273 def remove_tag(self, tag):
272 def remove_tag(self, tag):
274 edit_time = timezone.now()
273 edit_time = timezone.now()
275
274
276 thread = self.get_thread()
275 thread = self.get_thread()
277 thread.remove_tag(tag)
276 thread.remove_tag(tag)
278 self.last_edit_time = edit_time
277 self.last_edit_time = edit_time
279 self.save(update_fields=['last_edit_time'])
278 self.save(update_fields=['last_edit_time'])
280
279
281 thread.last_edit_time = edit_time
280 thread.last_edit_time = edit_time
282 thread.save(update_fields=['last_edit_time'])
281 thread.save(update_fields=['last_edit_time'])
283
282
284 def get_url(self, thread=None):
283 def get_url(self, thread=None):
285 """
284 """
286 Gets full url to the post.
285 Gets full url to the post.
287 """
286 """
288
287
289 cache_key = CACHE_KEY_POST_URL + str(self.id)
288 cache_key = CACHE_KEY_POST_URL + str(self.id)
290 link = cache.get(cache_key)
289 link = cache.get(cache_key)
291
290
292 if not link:
291 if not link:
293 if not thread:
292 if not thread:
294 thread = self.get_thread()
293 thread = self.get_thread()
295
294
296 opening_id = thread.get_opening_post_id()
295 opening_id = thread.get_opening_post_id()
297
296
298 if self.id != opening_id:
297 if self.id != opening_id:
299 link = reverse('thread', kwargs={
298 link = reverse('thread', kwargs={
300 'post_id': opening_id}) + '#' + str(self.id)
299 'post_id': opening_id}) + '#' + str(self.id)
301 else:
300 else:
302 link = reverse('thread', kwargs={'post_id': self.id})
301 link = reverse('thread', kwargs={'post_id': self.id})
303
302
304 cache.set(cache_key, link)
303 cache.set(cache_key, link)
305
304
306 return link
305 return link
307
306
308 def get_thread(self):
307 def get_thread(self):
309 """
308 """
310 Gets post's thread.
309 Gets post's thread.
311 """
310 """
312
311
313 return self.thread_new
312 return self.thread_new
314
313
315 def get_referenced_posts(self):
314 def get_referenced_posts(self):
316 return self.referenced_posts.only('id', 'thread_new')
315 return self.referenced_posts.only('id', 'thread_new')
317
316
318 def get_text(self):
317 def get_text(self):
319 return self.text
318 return self.text
320
319
321 def get_view(self, moderator=False, need_open_link=False,
320 def get_view(self, moderator=False, need_open_link=False,
322 truncated=False, *args, **kwargs):
321 truncated=False, *args, **kwargs):
323 if 'is_opening' in kwargs:
322 if 'is_opening' in kwargs:
324 is_opening = kwargs['is_opening']
323 is_opening = kwargs['is_opening']
325 else:
324 else:
326 is_opening = self.is_opening()
325 is_opening = self.is_opening()
327
326
328 if 'thread' in kwargs:
327 if 'thread' in kwargs:
329 thread = kwargs['thread']
328 thread = kwargs['thread']
330 else:
329 else:
331 thread = self.get_thread()
330 thread = self.get_thread()
332
331
333 if 'can_bump' in kwargs:
332 if 'can_bump' in kwargs:
334 can_bump = kwargs['can_bump']
333 can_bump = kwargs['can_bump']
335 else:
334 else:
336 can_bump = thread.can_bump()
335 can_bump = thread.can_bump()
337
336
338 if is_opening:
337 if is_opening:
339 opening_post_id = self.id
338 opening_post_id = self.id
340 else:
339 else:
341 opening_post_id = thread.get_opening_post_id()
340 opening_post_id = thread.get_opening_post_id()
342
341
343 return render_to_string('boards/post.html', {
342 return render_to_string('boards/post.html', {
344 'post': self,
343 'post': self,
345 'moderator': moderator,
344 'moderator': moderator,
346 'is_opening': is_opening,
345 'is_opening': is_opening,
347 'thread': thread,
346 'thread': thread,
348 'bumpable': can_bump,
347 'bumpable': can_bump,
349 'need_open_link': need_open_link,
348 'need_open_link': need_open_link,
350 'truncated': truncated,
349 'truncated': truncated,
351 'opening_post_id': opening_post_id,
350 'opening_post_id': opening_post_id,
352 })
351 })
353
352
354 def get_first_image(self):
353 def get_first_image(self):
355 return self.images.earliest('id')
354 return self.images.earliest('id')
356
355
357 def delete(self, using=None):
356 def delete(self, using=None):
358 """
357 """
359 Deletes all post images and the post itself. If the post is opening,
358 Deletes all post images and the post itself. If the post is opening,
360 thread with all posts is deleted.
359 thread with all posts is deleted.
361 """
360 """
362
361
363 self.images.all().delete()
362 self.images.all().delete()
364
363
365 if self.is_opening():
364 if self.is_opening():
366 self.get_thread().delete()
365 self.get_thread().delete()
367 else:
366 else:
368 thread = self.get_thread()
367 thread = self.get_thread()
369 thread.last_edit_time = timezone.now()
368 thread.last_edit_time = timezone.now()
370 thread.save()
369 thread.save()
371
370
372 super(Post, self).delete(using)
371 super(Post, self).delete(using)
373
372
374 logging.getLogger('boards.post.delete').info(
373 logging.getLogger('boards.post.delete').info(
375 'Deleted post P#{}/{}'.format(self.id, self.get_title()))
374 'Deleted post P#{}/{}'.format(self.id, self.get_title()))
376
375
377 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
376 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
378 include_last_update=False):
377 include_last_update=False):
379 """
378 """
380 Gets post HTML or JSON data that can be rendered on a page or used by
379 Gets post HTML or JSON data that can be rendered on a page or used by
381 API.
380 API.
382 """
381 """
383
382
384 if format_type == DIFF_TYPE_HTML:
383 if format_type == DIFF_TYPE_HTML:
385 context = RequestContext(request)
384 context = RequestContext(request)
386 context['post'] = self
385 context['post'] = self
387 if PARAMETER_TRUNCATED in request.GET:
386 if PARAMETER_TRUNCATED in request.GET:
388 context[PARAMETER_TRUNCATED] = True
387 context[PARAMETER_TRUNCATED] = True
389
388
390 # TODO Use dict here
389 # TODO Use dict here
391 return render_to_string('boards/api_post.html',
390 return render_to_string('boards/api_post.html',
392 context_instance=context)
391 context_instance=context)
393 elif format_type == DIFF_TYPE_JSON:
392 elif format_type == DIFF_TYPE_JSON:
394 post_json = {
393 post_json = {
395 'id': self.id,
394 'id': self.id,
396 'title': self.title,
395 'title': self.title,
397 'text': self.text.rendered,
396 'text': self.text.rendered,
398 }
397 }
399 if self.images.exists():
398 if self.images.exists():
400 post_image = self.get_first_image()
399 post_image = self.get_first_image()
401 post_json['image'] = post_image.image.url
400 post_json['image'] = post_image.image.url
402 post_json['image_preview'] = post_image.image.url_200x150
401 post_json['image_preview'] = post_image.image.url_200x150
403 if include_last_update:
402 if include_last_update:
404 post_json['bump_time'] = datetime_to_epoch(
403 post_json['bump_time'] = datetime_to_epoch(
405 self.thread_new.bump_time)
404 self.thread_new.bump_time)
406 return post_json
405 return post_json
407
406
408 def send_to_websocket(self, request, recursive=True):
407 def send_to_websocket(self, request, recursive=True):
409 """
408 """
410 Sends post HTML data to the thread web socket.
409 Sends post HTML data to the thread web socket.
411 """
410 """
412
411
413 if not settings.WEBSOCKETS_ENABLED:
412 if not settings.WEBSOCKETS_ENABLED:
414 return
413 return
415
414
416 client = Client()
415 client = Client()
417
416
418 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
417 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
419 client.publish(channel_name, {
418 client.publish(channel_name, {
420 'html': self.get_post_data(
419 'html': self.get_post_data(
421 format_type=DIFF_TYPE_HTML,
420 format_type=DIFF_TYPE_HTML,
422 request=request),
421 request=request),
423 'diff_type': 'added' if recursive else 'updated',
422 'diff_type': 'added' if recursive else 'updated',
424 })
423 })
425 client.send()
424 client.send()
426
425
427 logger = logging.getLogger('boards.post.websocket')
426 logger = logging.getLogger('boards.post.websocket')
428
427
429 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
428 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
430
429
431 if recursive:
430 if recursive:
432 for reply_number in re.finditer(REGEX_REPLY, self.text.raw):
431 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
433 post_id = reply_number.group(1)
432 post_id = reply_number.group(1)
434 ref_post = Post.objects.filter(id=post_id)[0]
433 ref_post = Post.objects.filter(id=post_id)[0]
435
434
436 ref_post.send_to_websocket(request, recursive=False)
435 ref_post.send_to_websocket(request, recursive=False)
436
437 def save(self, force_insert=False, force_update=False, using=None,
438 update_fields=None):
439 self._text_rendered = bbcode_extended(self.get_raw_text())
440
441 super().save(force_insert, force_update, using, update_fields)
442
443 def get_text(self):
444 return self._text_rendered
445
446 def get_raw_text(self):
447 return self.text
@@ -1,96 +1,96 b''
1 {% load i18n %}
1 {% load i18n %}
2 {% load board %}
2 {% load board %}
3 {% load cache %}
3 {% load cache %}
4
4
5 {% get_current_language as LANGUAGE_CODE %}
5 {% get_current_language as LANGUAGE_CODE %}
6
6
7 {% spaceless %}
7 {% spaceless %}
8 {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %}
8 {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %}
9 {% if thread.archived %}
9 {% if thread.archived %}
10 <div class="post archive_post" id="{{ post.id }}">
10 <div class="post archive_post" id="{{ post.id }}">
11 {% elif bumpable %}
11 {% elif bumpable %}
12 <div class="post" id="{{ post.id }}">
12 <div class="post" id="{{ post.id }}">
13 {% else %}
13 {% else %}
14 <div class="post dead_post" id="{{ post.id }}">
14 <div class="post dead_post" id="{{ post.id }}">
15 {% endif %}
15 {% endif %}
16
16
17 <div class="post-info">
17 <div class="post-info">
18 <a class="post_id" href="{% post_object_url post thread=thread %}"
18 <a class="post_id" href="{% post_object_url post thread=thread %}"
19 {% if not truncated and not thread.archived %}
19 {% if not truncated and not thread.archived %}
20 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
20 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
21 title="{% trans 'Quote' %}"
21 title="{% trans 'Quote' %}"
22 {% endif %}
22 {% endif %}
23 >({{ post.id }}) </a>
23 >({{ post.id }}) </a>
24 <span class="title">{{ post.title }} </span>
24 <span class="title">{{ post.title }} </span>
25 <span class="pub_time">{{ post.pub_time }}</span>
25 <span class="pub_time">{{ post.pub_time }}</span>
26 {% if thread.archived %}
26 {% if thread.archived %}
27 — {{ thread.bump_time }}
27 — {{ thread.bump_time }}
28 {% endif %}
28 {% endif %}
29 {% if is_opening and need_open_link %}
29 {% if is_opening and need_open_link %}
30 {% if thread.archived %}
30 {% if thread.archived %}
31 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
31 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
32 {% else %}
32 {% else %}
33 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
33 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
34 {% endif %}
34 {% endif %}
35 {% endif %}
35 {% endif %}
36
36
37 {% if moderator %}
37 {% if moderator %}
38 <span class="moderator_info">
38 <span class="moderator_info">
39 [<a href="{% url 'admin:boards_post_change' post.id %}"
39 [<a href="{% url 'admin:boards_post_change' post.id %}"
40 >{% trans 'Edit' %}</a>]
40 >{% trans 'Edit' %}</a>]
41 [<a href="{% url 'admin:boards_thread_change' thread.id %}"
41 [<a href="{% url 'admin:boards_thread_change' thread.id %}"
42 >{% trans 'Edit thread' %}</a>]
42 >{% trans 'Edit thread' %}</a>]
43 </span>
43 </span>
44 {% endif %}
44 {% endif %}
45 </div>
45 </div>
46 {% if post.images.exists %}
46 {% if post.images.exists %}
47 {% with post.images.all.0 as image %}
47 {% with post.images.all.0 as image %}
48 <div class="image">
48 <div class="image">
49 <a
49 <a
50 class="thumb"
50 class="thumb"
51 href="{{ image.image.url }}"><img
51 href="{{ image.image.url }}"><img
52 src="{{ image.image.url_200x150 }}"
52 src="{{ image.image.url_200x150 }}"
53 alt="{{ post.id }}"
53 alt="{{ post.id }}"
54 width="{{ image.pre_width }}"
54 width="{{ image.pre_width }}"
55 height="{{ image.pre_height }}"
55 height="{{ image.pre_height }}"
56 data-width="{{ image.width }}"
56 data-width="{{ image.width }}"
57 data-height="{{ image.height }}"/>
57 data-height="{{ image.height }}"/>
58 </a>
58 </a>
59 </div>
59 </div>
60 {% endwith %}
60 {% endwith %}
61 {% endif %}
61 {% endif %}
62 <div class="message">
62 <div class="message">
63 {% autoescape off %}
63 {% autoescape off %}
64 {% if truncated %}
64 {% if truncated %}
65 {{ post.text.rendered|truncatewords_html:50 }}
65 {{ post.get_text|truncatewords_html:50 }}
66 {% else %}
66 {% else %}
67 {{ post.text.rendered }}
67 {{ post.get_text }}
68 {% endif %}
68 {% endif %}
69 {% endautoescape %}
69 {% endautoescape %}
70 {% if post.is_referenced %}
70 {% if post.is_referenced %}
71 <div class="refmap">
71 <div class="refmap">
72 {% autoescape off %}
72 {% autoescape off %}
73 {% trans "Replies" %}: {{ post.refmap }}
73 {% trans "Replies" %}: {{ post.refmap }}
74 {% endautoescape %}
74 {% endautoescape %}
75 </div>
75 </div>
76 {% endif %}
76 {% endif %}
77 </div>
77 </div>
78 {% endcache %}
78 {% endcache %}
79 {% if is_opening %}
79 {% if is_opening %}
80 {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %}
80 {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %}
81 <div class="metadata">
81 <div class="metadata">
82 {% if is_opening and need_open_link %}
82 {% if is_opening and need_open_link %}
83 {{ thread.get_reply_count }} {% trans 'messages' %},
83 {{ thread.get_reply_count }} {% trans 'messages' %},
84 {{ thread.get_images_count }} {% trans 'images' %}.
84 {{ thread.get_images_count }} {% trans 'images' %}.
85 {% endif %}
85 {% endif %}
86 <span class="tags">
86 <span class="tags">
87 {% for tag in thread.get_tags %}
87 {% for tag in thread.get_tags %}
88 <a class="tag" href="{% url 'tag' tag.name %}">
88 <a class="tag" href="{% url 'tag' tag.name %}">
89 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
89 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
90 {% endfor %}
90 {% endfor %}
91 </span>
91 </span>
92 </div>
92 </div>
93 {% endcache %}
93 {% endcache %}
94 {% endif %}
94 {% endif %}
95 </div>
95 </div>
96 {% endspaceless %}
96 {% endspaceless %}
@@ -1,243 +1,239 b''
1 # Django settings for neboard project.
1 # Django settings for neboard project.
2 import os
2 import os
3 from boards.mdx_neboard import bbcode_extended
3 from boards.mdx_neboard import bbcode_extended
4
4
5 DEBUG = True
5 DEBUG = True
6 TEMPLATE_DEBUG = DEBUG
6 TEMPLATE_DEBUG = DEBUG
7
7
8 ADMINS = (
8 ADMINS = (
9 # ('Your Name', 'your_email@example.com'),
9 # ('Your Name', 'your_email@example.com'),
10 ('admin', 'admin@example.com')
10 ('admin', 'admin@example.com')
11 )
11 )
12
12
13 MANAGERS = ADMINS
13 MANAGERS = ADMINS
14
14
15 DATABASES = {
15 DATABASES = {
16 'default': {
16 'default': {
17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
18 'NAME': 'database.db', # Or path to database file if using sqlite3.
18 'NAME': 'database.db', # Or path to database file if using sqlite3.
19 'USER': '', # Not used with sqlite3.
19 'USER': '', # Not used with sqlite3.
20 'PASSWORD': '', # Not used with sqlite3.
20 'PASSWORD': '', # Not used with sqlite3.
21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
23 'CONN_MAX_AGE': None,
23 'CONN_MAX_AGE': None,
24 }
24 }
25 }
25 }
26
26
27 # Local time zone for this installation. Choices can be found here:
27 # Local time zone for this installation. Choices can be found here:
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 # although not all choices may be available on all operating systems.
29 # although not all choices may be available on all operating systems.
30 # In a Windows environment this must be set to your system time zone.
30 # In a Windows environment this must be set to your system time zone.
31 TIME_ZONE = 'Europe/Kiev'
31 TIME_ZONE = 'Europe/Kiev'
32
32
33 # Language code for this installation. All choices can be found here:
33 # Language code for this installation. All choices can be found here:
34 # http://www.i18nguy.com/unicode/language-identifiers.html
34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 LANGUAGE_CODE = 'en'
35 LANGUAGE_CODE = 'en'
36
36
37 SITE_ID = 1
37 SITE_ID = 1
38
38
39 # If you set this to False, Django will make some optimizations so as not
39 # If you set this to False, Django will make some optimizations so as not
40 # to load the internationalization machinery.
40 # to load the internationalization machinery.
41 USE_I18N = True
41 USE_I18N = True
42
42
43 # If you set this to False, Django will not format dates, numbers and
43 # If you set this to False, Django will not format dates, numbers and
44 # calendars according to the current locale.
44 # calendars according to the current locale.
45 USE_L10N = True
45 USE_L10N = True
46
46
47 # If you set this to False, Django will not use timezone-aware datetimes.
47 # If you set this to False, Django will not use timezone-aware datetimes.
48 USE_TZ = True
48 USE_TZ = True
49
49
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 # Example: "/home/media/media.lawrence.com/media/"
51 # Example: "/home/media/media.lawrence.com/media/"
52 MEDIA_ROOT = './media/'
52 MEDIA_ROOT = './media/'
53
53
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 # trailing slash.
55 # trailing slash.
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 MEDIA_URL = '/media/'
57 MEDIA_URL = '/media/'
58
58
59 # Absolute path to the directory static files should be collected to.
59 # Absolute path to the directory static files should be collected to.
60 # Don't put anything in this directory yourself; store your static files
60 # Don't put anything in this directory yourself; store your static files
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 # Example: "/home/media/media.lawrence.com/static/"
62 # Example: "/home/media/media.lawrence.com/static/"
63 STATIC_ROOT = ''
63 STATIC_ROOT = ''
64
64
65 # URL prefix for static files.
65 # URL prefix for static files.
66 # Example: "http://media.lawrence.com/static/"
66 # Example: "http://media.lawrence.com/static/"
67 STATIC_URL = '/static/'
67 STATIC_URL = '/static/'
68
68
69 # Additional locations of static files
69 # Additional locations of static files
70 # It is really a hack, put real paths, not related
70 # It is really a hack, put real paths, not related
71 STATICFILES_DIRS = (
71 STATICFILES_DIRS = (
72 os.path.dirname(__file__) + '/boards/static',
72 os.path.dirname(__file__) + '/boards/static',
73
73
74 # '/d/work/python/django/neboard/neboard/boards/static',
74 # '/d/work/python/django/neboard/neboard/boards/static',
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 # Always use forward slashes, even on Windows.
76 # Always use forward slashes, even on Windows.
77 # Don't forget to use absolute paths, not relative paths.
77 # Don't forget to use absolute paths, not relative paths.
78 )
78 )
79
79
80 # List of finder classes that know how to find static files in
80 # List of finder classes that know how to find static files in
81 # various locations.
81 # various locations.
82 STATICFILES_FINDERS = (
82 STATICFILES_FINDERS = (
83 'django.contrib.staticfiles.finders.FileSystemFinder',
83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 'compressor.finders.CompressorFinder',
85 'compressor.finders.CompressorFinder',
86 )
86 )
87
87
88 if DEBUG:
88 if DEBUG:
89 STATICFILES_STORAGE = \
89 STATICFILES_STORAGE = \
90 'django.contrib.staticfiles.storage.StaticFilesStorage'
90 'django.contrib.staticfiles.storage.StaticFilesStorage'
91 else:
91 else:
92 STATICFILES_STORAGE = \
92 STATICFILES_STORAGE = \
93 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
93 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
94
94
95 # Make this unique, and don't share it with anybody.
95 # Make this unique, and don't share it with anybody.
96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
96 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
97
97
98 # List of callables that know how to import templates from various sources.
98 # List of callables that know how to import templates from various sources.
99 TEMPLATE_LOADERS = (
99 TEMPLATE_LOADERS = (
100 'django.template.loaders.filesystem.Loader',
100 'django.template.loaders.filesystem.Loader',
101 'django.template.loaders.app_directories.Loader',
101 'django.template.loaders.app_directories.Loader',
102 )
102 )
103
103
104 TEMPLATE_CONTEXT_PROCESSORS = (
104 TEMPLATE_CONTEXT_PROCESSORS = (
105 'django.core.context_processors.media',
105 'django.core.context_processors.media',
106 'django.core.context_processors.static',
106 'django.core.context_processors.static',
107 'django.core.context_processors.request',
107 'django.core.context_processors.request',
108 'django.contrib.auth.context_processors.auth',
108 'django.contrib.auth.context_processors.auth',
109 'boards.context_processors.user_and_ui_processor',
109 'boards.context_processors.user_and_ui_processor',
110 )
110 )
111
111
112 MIDDLEWARE_CLASSES = (
112 MIDDLEWARE_CLASSES = (
113 'django.contrib.sessions.middleware.SessionMiddleware',
113 'django.contrib.sessions.middleware.SessionMiddleware',
114 'django.middleware.locale.LocaleMiddleware',
114 'django.middleware.locale.LocaleMiddleware',
115 'django.middleware.common.CommonMiddleware',
115 'django.middleware.common.CommonMiddleware',
116 'django.contrib.auth.middleware.AuthenticationMiddleware',
116 'django.contrib.auth.middleware.AuthenticationMiddleware',
117 'django.contrib.messages.middleware.MessageMiddleware',
117 'django.contrib.messages.middleware.MessageMiddleware',
118 'boards.middlewares.BanMiddleware',
118 'boards.middlewares.BanMiddleware',
119 'boards.middlewares.MinifyHTMLMiddleware',
119 'boards.middlewares.MinifyHTMLMiddleware',
120 )
120 )
121
121
122 ROOT_URLCONF = 'neboard.urls'
122 ROOT_URLCONF = 'neboard.urls'
123
123
124 # Python dotted path to the WSGI application used by Django's runserver.
124 # Python dotted path to the WSGI application used by Django's runserver.
125 WSGI_APPLICATION = 'neboard.wsgi.application'
125 WSGI_APPLICATION = 'neboard.wsgi.application'
126
126
127 TEMPLATE_DIRS = (
127 TEMPLATE_DIRS = (
128 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
128 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
129 # Always use forward slashes, even on Windows.
129 # Always use forward slashes, even on Windows.
130 # Don't forget to use absolute paths, not relative paths.
130 # Don't forget to use absolute paths, not relative paths.
131 'templates',
131 'templates',
132 )
132 )
133
133
134 INSTALLED_APPS = (
134 INSTALLED_APPS = (
135 'django.contrib.auth',
135 'django.contrib.auth',
136 'django.contrib.contenttypes',
136 'django.contrib.contenttypes',
137 'django.contrib.sessions',
137 'django.contrib.sessions',
138 # 'django.contrib.sites',
138 # 'django.contrib.sites',
139 'django.contrib.messages',
139 'django.contrib.messages',
140 'django.contrib.staticfiles',
140 'django.contrib.staticfiles',
141 # Uncomment the next line to enable the admin:
141 # Uncomment the next line to enable the admin:
142 'django.contrib.admin',
142 'django.contrib.admin',
143 # Uncomment the next line to enable admin documentation:
143 # Uncomment the next line to enable admin documentation:
144 # 'django.contrib.admindocs',
144 # 'django.contrib.admindocs',
145 'django.contrib.humanize',
145 'django.contrib.humanize',
146 'django_cleanup',
146 'django_cleanup',
147
147
148 'debug_toolbar',
148 'debug_toolbar',
149
149
150 # Search
150 # Search
151 'haystack',
151 'haystack',
152
152
153 # Static files compressor
153 # Static files compressor
154 'compressor',
154 'compressor',
155
155
156 'boards',
156 'boards',
157 )
157 )
158
158
159 # A sample logging configuration. The only tangible logging
159 # A sample logging configuration. The only tangible logging
160 # performed by this configuration is to send an email to
160 # performed by this configuration is to send an email to
161 # the site admins on every HTTP 500 error when DEBUG=False.
161 # the site admins on every HTTP 500 error when DEBUG=False.
162 # See http://docs.djangoproject.com/en/dev/topics/logging for
162 # See http://docs.djangoproject.com/en/dev/topics/logging for
163 # more details on how to customize your logging configuration.
163 # more details on how to customize your logging configuration.
164 LOGGING = {
164 LOGGING = {
165 'version': 1,
165 'version': 1,
166 'disable_existing_loggers': False,
166 'disable_existing_loggers': False,
167 'formatters': {
167 'formatters': {
168 'verbose': {
168 'verbose': {
169 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
169 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
170 },
170 },
171 'simple': {
171 'simple': {
172 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
172 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
173 },
173 },
174 },
174 },
175 'filters': {
175 'filters': {
176 'require_debug_false': {
176 'require_debug_false': {
177 '()': 'django.utils.log.RequireDebugFalse'
177 '()': 'django.utils.log.RequireDebugFalse'
178 }
178 }
179 },
179 },
180 'handlers': {
180 'handlers': {
181 'console': {
181 'console': {
182 'level': 'DEBUG',
182 'level': 'DEBUG',
183 'class': 'logging.StreamHandler',
183 'class': 'logging.StreamHandler',
184 'formatter': 'simple'
184 'formatter': 'simple'
185 },
185 },
186 },
186 },
187 'loggers': {
187 'loggers': {
188 'boards': {
188 'boards': {
189 'handlers': ['console'],
189 'handlers': ['console'],
190 'level': 'DEBUG',
190 'level': 'DEBUG',
191 }
191 }
192 },
192 },
193 }
193 }
194
194
195 HAYSTACK_CONNECTIONS = {
195 HAYSTACK_CONNECTIONS = {
196 'default': {
196 'default': {
197 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
197 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
198 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
198 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
199 },
199 },
200 }
200 }
201
201
202 MARKUP_FIELD_TYPES = (
203 ('bbcode', bbcode_extended),
204 )
205
206 THEMES = [
202 THEMES = [
207 ('md', 'Mystic Dark'),
203 ('md', 'Mystic Dark'),
208 ('md_centered', 'Mystic Dark (centered)'),
204 ('md_centered', 'Mystic Dark (centered)'),
209 ('sw', 'Snow White'),
205 ('sw', 'Snow White'),
210 ('pg', 'Photon Gray'),
206 ('pg', 'Photon Gray'),
211 ]
207 ]
212
208
213 POSTING_DELAY = 20 # seconds
209 POSTING_DELAY = 20 # seconds
214
210
215 COMPRESS_HTML = False
211 COMPRESS_HTML = False
216
212
217 # Websocket settins
213 # Websocket settins
218 CENTRIFUGE_HOST = 'localhost'
214 CENTRIFUGE_HOST = 'localhost'
219 CENTRIFUGE_PORT = '9090'
215 CENTRIFUGE_PORT = '9090'
220
216
221 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
217 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
222 CENTRIFUGE_PROJECT_ID = '<project id here>'
218 CENTRIFUGE_PROJECT_ID = '<project id here>'
223 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
219 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
224 CENTRIFUGE_TIMEOUT = 5
220 CENTRIFUGE_TIMEOUT = 5
225
221
226 # Debug mode middlewares
222 # Debug mode middlewares
227 if DEBUG:
223 if DEBUG:
228 MIDDLEWARE_CLASSES += (
224 MIDDLEWARE_CLASSES += (
229 'debug_toolbar.middleware.DebugToolbarMiddleware',
225 'debug_toolbar.middleware.DebugToolbarMiddleware',
230 )
226 )
231
227
232 def custom_show_toolbar(request):
228 def custom_show_toolbar(request):
233 return True
229 return True
234
230
235 DEBUG_TOOLBAR_CONFIG = {
231 DEBUG_TOOLBAR_CONFIG = {
236 'ENABLE_STACKTRACES': True,
232 'ENABLE_STACKTRACES': True,
237 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
233 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
238 }
234 }
239
235
240 # FIXME Uncommenting this fails somehow. Need to investigate this
236 # FIXME Uncommenting this fails somehow. Need to investigate this
241 #DEBUG_TOOLBAR_PANELS += (
237 #DEBUG_TOOLBAR_PANELS += (
242 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
238 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
243 #)
239 #)
@@ -1,55 +1,38 b''
1 # INTRO #
1 # INTRO #
2
2
3 This project aims to create centralized forum-like discussion platform with
3 This project aims to create centralized forum-like discussion platform with
4 anonymity in mind.
4 anonymity in mind.
5
5
6 Main repository: https://bitbucket.org/neko259/neboard/
6 Main repository: https://bitbucket.org/neko259/neboard/
7
7
8 Site: http://neboard.me/
8 Site: http://neboard.me/
9
9
10 # DEPENDENCIES #
11
12 ## REQUIRED ##
13
14 * pillow
15 * django >= 1.6
16 * django_cleanup
17 * django-markupfield
18 * markdown
19 * python-markdown
20 * django-simple-captcha
21 * line-profiler
22
23 ## OPTIONAL ##
24
25 * django-debug-toolbar
26
27 # INSTALLATION #
10 # INSTALLATION #
28
11
29 1. Install all dependencies over pip or system-wide
12 1. Install all dependencies over pip or system-wide
30 2. Setup a database in `neboard/settings.py`
13 2. Setup a database in `neboard/settings.py`
31 3. Run `./manage.py syncdb` and ensure the database was created
14 3. Run `./manage.py syncdb` and ensure the database was created
32 4. Run `./manage.py migrate boards` to apply all south migrations
15 4. Run `./manage.py migrate boards` to apply all south migrations
33
16
34 # RUNNING #
17 # RUNNING #
35
18
36 You can run the server using django default embedded webserver by running
19 You can run the server using django default embedded webserver by running
37
20
38 ./manage.py runserver <address>:<port>
21 ./manage.py runserver <address>:<port>
39
22
40 See django-admin command help for details
23 See django-admin command help for details
41
24
42 Also consider using wsgi or fcgi interfaces on production servers.
25 Also consider using wsgi or fcgi interfaces on production servers.
43
26
44 # UPGRADE #
27 # UPGRADE #
45
28
46 1. Backup your project data.
29 1. Backup your project data.
47 2. Save the settings in `neboard/settings.py` and `boards/settings.py`
30 2. Save the settings in `neboard/settings.py` and `boards/settings.py`
48 3. Copy the project contents over the old project directory
31 3. Copy the project contents over the old project directory
49 4. Run migrations by `./manage.py migrate boards`
32 4. Run migrations by `./manage.py migrate boards`
50
33
51 You can also just clone the mercurial project and pull it to update
34 You can also just clone the mercurial project and pull it to update
52
35
53 # CONCLUSION #
36 # CONCLUSION #
54
37
55 Enjoy our software and thank you!
38 Enjoy our software and thank you!
@@ -1,8 +1,7 b''
1 adjacent
1 adjacent
2 haystack
2 haystack
3 pillow
3 pillow
4 django>=1.7
4 django>=1.7
5 django_cleanup
5 django_cleanup
6 django-markupfield
7 bbcode
6 bbcode
8 django_compressor No newline at end of file
7 django_compressor
General Comments 0
You need to be logged in to leave comments. Login now