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