##// END OF EJS Templates
Connect replies to the new post link format. Strip quote tag.
neko259 -
r753:b7096f08 default
parent child Browse files
Show More
@@ -1,152 +1,152 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
14 14
15 15 class TextFormatter():
16 16 """
17 17 An interface for formatter that can be used in the text format panel
18 18 """
19 19
20 20 def __init__(self):
21 21 pass
22 22
23 23 name = ''
24 24
25 25 # Left and right tags for the button preview
26 26 preview_left = ''
27 27 preview_right = ''
28 28
29 29 # Left and right characters for the textarea input
30 30 format_left = ''
31 31 format_right = ''
32 32
33 33
34 34 class AutolinkPattern():
35 35 def handleMatch(self, m):
36 36 link_element = etree.Element('a')
37 37 href = m.group(2)
38 38 link_element.set('href', href)
39 39 link_element.text = href
40 40
41 41 return link_element
42 42
43 43
44 44 class QuotePattern(TextFormatter):
45 45 name = 'q'
46 46 preview_left = '<span class="multiquote">'
47 47 preview_right = '</span>'
48 48
49 49 format_left = '[quote]'
50 50 format_right = '[/quote]'
51 51
52 52
53 53 class SpoilerPattern(TextFormatter):
54 54 name = 'spoiler'
55 55 preview_left = '<span class="spoiler">'
56 56 preview_right = '</span>'
57 57
58 58 format_left = '[spoiler]'
59 59 format_right = '[/spoiler]'
60 60
61 61 def handleMatch(self, m):
62 62 quote_element = etree.Element('span')
63 63 quote_element.set('class', 'spoiler')
64 64 quote_element.text = m.group(2)
65 65
66 66 return quote_element
67 67
68 68
69 69 class CommentPattern(TextFormatter):
70 70 name = ''
71 71 preview_left = '<span class="comment">// '
72 72 preview_right = '</span>'
73 73
74 74 format_left = '[comment]'
75 75 format_right = '[/comment]'
76 76
77 77
78 78 class StrikeThroughPattern(TextFormatter):
79 79 name = 's'
80 80 preview_left = '<span class="strikethrough">'
81 81 preview_right = '</span>'
82 82
83 83 format_left = '[s]'
84 84 format_right = '[/s]'
85 85
86 86
87 87 class ItalicPattern(TextFormatter):
88 88 name = 'i'
89 89 preview_left = '<i>'
90 90 preview_right = '</i>'
91 91
92 92 format_left = '[i]'
93 93 format_right = '[/i]'
94 94
95 95
96 96 class BoldPattern(TextFormatter):
97 97 name = 'b'
98 98 preview_left = '<b>'
99 99 preview_right = '</b>'
100 100
101 101 format_left = '[b]'
102 102 format_right = '[/b]'
103 103
104 104
105 105 class CodePattern(TextFormatter):
106 106 name = 'code'
107 107 preview_left = '<code>'
108 108 preview_right = '</code>'
109 109
110 110 format_left = '[code]'
111 111 format_right = '[/code]'
112 112
113 113
114 114 def render_reflink(tag_name, value, options, parent, context):
115 115 if not REFLINK_PATTERN.match(value):
116 116 return u'>>%s' % value
117 117
118 118 post_id = int(value)
119 119
120 120 posts = boards.models.Post.objects.filter(id=post_id)
121 121 if posts.exists():
122 122 post = posts[0]
123 123
124 124 return u'<a href=%s>&gt;&gt;%s</a>' % (post.get_url(), post_id)
125 125 else:
126 126 return u'>>%s' % value
127 127
128 128
129 129 def bbcode_extended(markup):
130 130 parser = bbcode.Parser()
131 131 parser.add_formatter('post', render_reflink, strip=True)
132 132 parser.add_simple_formatter('quote',
133 u'<span class="multiquote">%(value)s</span>')
133 u'<span class="multiquote">%(value)s</span>', strip=True)
134 134 parser.add_simple_formatter('comment',
135 135 u'<span class="comment">//%(value)s</span>')
136 136 parser.add_simple_formatter('spoiler',
137 137 u'<span class="spoiler">%(value)s</span>')
138 138 parser.add_simple_formatter('s',
139 139 u'<span class="strikethrough">%(value)s</span>')
140 140 parser.add_simple_formatter('code',
141 141 u'<pre><code>%(value)s</pre></code>')
142 142 return parser.format(markup)
143 143
144 144 formatters = [
145 145 QuotePattern,
146 146 SpoilerPattern,
147 147 ItalicPattern,
148 148 BoldPattern,
149 149 CommentPattern,
150 150 StrikeThroughPattern,
151 151 CodePattern,
152 152 ]
@@ -1,343 +1,343 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5
6 6 from django.core.cache import cache
7 7 from django.core.urlresolvers import reverse
8 8 from django.db import models, transaction
9 9 from django.template.loader import render_to_string
10 10 from django.utils import timezone
11 11 from markupfield.fields import MarkupField
12 12
13 13 from boards.models import PostImage
14 14 from boards.models.base import Viewable
15 15 from boards.models.thread import Thread
16 16
17 17
18 18 APP_LABEL_BOARDS = 'boards'
19 19
20 20 CACHE_KEY_PPD = 'ppd'
21 21 CACHE_KEY_POST_URL = 'post_url'
22 22
23 23 POSTS_PER_DAY_RANGE = 7
24 24
25 25 BAN_REASON_AUTO = 'Auto'
26 26
27 27 IMAGE_THUMB_SIZE = (200, 150)
28 28
29 29 TITLE_MAX_LENGTH = 200
30 30
31 31 DEFAULT_MARKUP_TYPE = 'bbcode'
32 32
33 33 # TODO This should be removed
34 34 NO_IP = '0.0.0.0'
35 35
36 36 # TODO Real user agent should be saved instead of this
37 37 UNKNOWN_UA = ''
38 38
39 REGEX_REPLY = re.compile(r'&gt;&gt;(\d+)')
39 REGEX_REPLY = re.compile(ur'\[post\](\d+)\[/post\]')
40 40
41 41 logger = logging.getLogger(__name__)
42 42
43 43
44 44 class PostManager(models.Manager):
45 45 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
46 46 tags=None):
47 47 """
48 48 Creates new post
49 49 """
50 50
51 51 posting_time = timezone.now()
52 52 if not thread:
53 53 thread = Thread.objects.create(bump_time=posting_time,
54 54 last_edit_time=posting_time)
55 55 new_thread = True
56 56 else:
57 57 thread.bump()
58 58 thread.last_edit_time = posting_time
59 59 thread.save()
60 60 new_thread = False
61 61
62 62 post = self.create(title=title,
63 63 text=text,
64 64 pub_time=posting_time,
65 65 thread_new=thread,
66 66 poster_ip=ip,
67 67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
68 68 # last!
69 69 last_edit_time=posting_time)
70 70
71 71 if image:
72 72 post_image = PostImage.objects.create(image=image)
73 73 post.images.add(post_image)
74 74 logger.info('Created image #%d for post #%d' % (post_image.id,
75 75 post.id))
76 76
77 77 thread.replies.add(post)
78 78 if tags:
79 79 map(thread.add_tag, tags)
80 80
81 81 if new_thread:
82 82 Thread.objects.process_oldest_threads()
83 83 self.connect_replies(post)
84 84
85 85 logger.info('Created post #%d with title %s' % (post.id,
86 86 post.get_title()))
87 87
88 88 return post
89 89
90 90 def delete_post(self, post):
91 91 """
92 92 Deletes post and update or delete its thread
93 93 """
94 94
95 95 post_id = post.id
96 96
97 97 thread = post.get_thread()
98 98
99 99 if post.is_opening():
100 100 thread.delete()
101 101 else:
102 102 thread.last_edit_time = timezone.now()
103 103 thread.save()
104 104
105 105 post.delete()
106 106
107 107 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
108 108
109 109 def delete_posts_by_ip(self, ip):
110 110 """
111 111 Deletes all posts of the author with same IP
112 112 """
113 113
114 114 posts = self.filter(poster_ip=ip)
115 115 map(self.delete_post, posts)
116 116
117 117 def connect_replies(self, post):
118 118 """
119 119 Connects replies to a post to show them as a reflink map
120 120 """
121 121
122 for reply_number in re.finditer(REGEX_REPLY, post.text.rendered):
122 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
123 123 post_id = reply_number.group(1)
124 124 ref_post = self.filter(id=post_id)
125 125 if ref_post.count() > 0:
126 126 referenced_post = ref_post[0]
127 127 referenced_post.referenced_posts.add(post)
128 128 referenced_post.last_edit_time = post.pub_time
129 129 referenced_post.build_refmap()
130 130 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
131 131
132 132 referenced_thread = referenced_post.get_thread()
133 133 referenced_thread.last_edit_time = post.pub_time
134 134 referenced_thread.save(update_fields=['last_edit_time'])
135 135
136 136 def get_posts_per_day(self):
137 137 """
138 138 Gets average count of posts per day for the last 7 days
139 139 """
140 140
141 141 day_end = date.today()
142 142 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
143 143
144 144 cache_key = CACHE_KEY_PPD + str(day_end)
145 145 ppd = cache.get(cache_key)
146 146 if ppd:
147 147 return ppd
148 148
149 149 day_time_start = timezone.make_aware(datetime.combine(
150 150 day_start, dtime()), timezone.get_current_timezone())
151 151 day_time_end = timezone.make_aware(datetime.combine(
152 152 day_end, dtime()), timezone.get_current_timezone())
153 153
154 154 posts_per_period = float(self.filter(
155 155 pub_time__lte=day_time_end,
156 156 pub_time__gte=day_time_start).count())
157 157
158 158 ppd = posts_per_period / POSTS_PER_DAY_RANGE
159 159
160 160 cache.set(cache_key, ppd)
161 161 return ppd
162 162
163 163
164 164 class Post(models.Model, Viewable):
165 165 """A post is a message."""
166 166
167 167 objects = PostManager()
168 168
169 169 class Meta:
170 170 app_label = APP_LABEL_BOARDS
171 171 ordering = ('id',)
172 172
173 173 title = models.CharField(max_length=TITLE_MAX_LENGTH)
174 174 pub_time = models.DateTimeField()
175 175 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
176 176 escape_html=False)
177 177
178 178 images = models.ManyToManyField(PostImage, null=True, blank=True,
179 179 related_name='ip+', db_index=True)
180 180
181 181 poster_ip = models.GenericIPAddressField()
182 182 poster_user_agent = models.TextField()
183 183
184 184 thread_new = models.ForeignKey('Thread', null=True, default=None,
185 185 db_index=True)
186 186 last_edit_time = models.DateTimeField()
187 187
188 188 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
189 189 null=True,
190 190 blank=True, related_name='rfp+',
191 191 db_index=True)
192 192 refmap = models.TextField(null=True, blank=True)
193 193
194 194 def __unicode__(self):
195 195 return '#' + str(self.id) + ' ' + self.title + ' (' + \
196 196 self.text.raw[:50] + ')'
197 197
198 198 def get_title(self):
199 199 """
200 200 Gets original post title or part of its text.
201 201 """
202 202
203 203 title = self.title
204 204 if not title:
205 205 title = self.text.rendered
206 206
207 207 return title
208 208
209 209 def build_refmap(self):
210 210 """
211 211 Builds a replies map string from replies list. This is a cache to stop
212 212 the server from recalculating the map on every post show.
213 213 """
214 214 map_string = ''
215 215
216 216 first = True
217 217 for refpost in self.referenced_posts.all():
218 218 if not first:
219 219 map_string += ', '
220 220 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
221 221 refpost.id)
222 222 first = False
223 223
224 224 self.refmap = map_string
225 225
226 226 def get_sorted_referenced_posts(self):
227 227 return self.refmap
228 228
229 229 def is_referenced(self):
230 230 return len(self.refmap) > 0
231 231
232 232 def is_opening(self):
233 233 """
234 234 Checks if this is an opening post or just a reply.
235 235 """
236 236
237 237 return self.get_thread().get_opening_post_id() == self.id
238 238
239 239 @transaction.atomic
240 240 def add_tag(self, tag):
241 241 edit_time = timezone.now()
242 242
243 243 thread = self.get_thread()
244 244 thread.add_tag(tag)
245 245 self.last_edit_time = edit_time
246 246 self.save(update_fields=['last_edit_time'])
247 247
248 248 thread.last_edit_time = edit_time
249 249 thread.save(update_fields=['last_edit_time'])
250 250
251 251 @transaction.atomic
252 252 def remove_tag(self, tag):
253 253 edit_time = timezone.now()
254 254
255 255 thread = self.get_thread()
256 256 thread.remove_tag(tag)
257 257 self.last_edit_time = edit_time
258 258 self.save(update_fields=['last_edit_time'])
259 259
260 260 thread.last_edit_time = edit_time
261 261 thread.save(update_fields=['last_edit_time'])
262 262
263 263 def get_url(self, thread=None):
264 264 """
265 265 Gets full url to the post.
266 266 """
267 267
268 268 cache_key = CACHE_KEY_POST_URL + str(self.id)
269 269 link = cache.get(cache_key)
270 270
271 271 if not link:
272 272 if not thread:
273 273 thread = self.get_thread()
274 274
275 275 opening_id = thread.get_opening_post_id()
276 276
277 277 if self.id != opening_id:
278 278 link = reverse('thread', kwargs={
279 279 'post_id': opening_id}) + '#' + str(self.id)
280 280 else:
281 281 link = reverse('thread', kwargs={'post_id': self.id})
282 282
283 283 cache.set(cache_key, link)
284 284
285 285 return link
286 286
287 287 def get_thread(self):
288 288 """
289 289 Gets post's thread.
290 290 """
291 291
292 292 return self.thread_new
293 293
294 294 def get_referenced_posts(self):
295 295 return self.referenced_posts.only('id', 'thread_new')
296 296
297 297 def get_text(self):
298 298 return self.text
299 299
300 300 def get_view(self, moderator=False, need_open_link=False,
301 301 truncated=False, *args, **kwargs):
302 302 if 'is_opening' in kwargs:
303 303 is_opening = kwargs['is_opening']
304 304 else:
305 305 is_opening = self.is_opening()
306 306
307 307 if 'thread' in kwargs:
308 308 thread = kwargs['thread']
309 309 else:
310 310 thread = self.get_thread()
311 311
312 312 if 'can_bump' in kwargs:
313 313 can_bump = kwargs['can_bump']
314 314 else:
315 315 can_bump = thread.can_bump()
316 316
317 317 if is_opening:
318 318 opening_post_id = self.id
319 319 else:
320 320 opening_post_id = thread.get_opening_post_id()
321 321
322 322 return render_to_string('boards/post.html', {
323 323 'post': self,
324 324 'moderator': moderator,
325 325 'is_opening': is_opening,
326 326 'thread': thread,
327 327 'bumpable': can_bump,
328 328 'need_open_link': need_open_link,
329 329 'truncated': truncated,
330 330 'opening_post_id': opening_post_id,
331 331 })
332 332
333 333 def get_first_image(self):
334 334 return self.images.earliest('id')
335 335
336 336 def delete(self, using=None):
337 337 """
338 338 Deletes all post images and the post itself.
339 339 """
340 340
341 341 self.images.all().delete()
342 342
343 343 super(Post, self).delete(using)
General Comments 0
You need to be logged in to leave comments. Login now