##// END OF EJS Templates
Cache user's tags
neko259 -
r323:a0b0f2ae default
parent child Browse files
Show More
@@ -0,0 +1,1 b''
1 CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used No newline at end of file
@@ -1,382 +1,403 b''
1 import os
1 import os
2 from random import random
2 from random import random
3 import time
3 import time
4 import math
4 import math
5 from django.core.cache import cache
5
6
6 from django.db import models
7 from django.db import models
7 from django.db.models import Count
8 from django.db.models import Count
8 from django.http import Http404
9 from django.http import Http404
9 from django.utils import timezone
10 from django.utils import timezone
10 from markupfield.fields import MarkupField
11 from markupfield.fields import MarkupField
12 from boards import settings as board_settings
11
13
12 from neboard import settings
14 from neboard import settings
13 import thumbs
15 import thumbs
14
16
15 import re
17 import re
16
18
17 IMAGE_THUMB_SIZE = (200, 150)
19 IMAGE_THUMB_SIZE = (200, 150)
18
20
19 TITLE_MAX_LENGTH = 50
21 TITLE_MAX_LENGTH = 50
20
22
21 DEFAULT_MARKUP_TYPE = 'markdown'
23 DEFAULT_MARKUP_TYPE = 'markdown'
22
24
23 NO_PARENT = -1
25 NO_PARENT = -1
24 NO_IP = '0.0.0.0'
26 NO_IP = '0.0.0.0'
25 UNKNOWN_UA = ''
27 UNKNOWN_UA = ''
26 ALL_PAGES = -1
28 ALL_PAGES = -1
27 OPENING_POST_POPULARITY_WEIGHT = 2
29 OPENING_POST_POPULARITY_WEIGHT = 2
28 IMAGES_DIRECTORY = 'images/'
30 IMAGES_DIRECTORY = 'images/'
29 FILE_EXTENSION_DELIMITER = '.'
31 FILE_EXTENSION_DELIMITER = '.'
30
32
31 RANK_ADMIN = 0
33 RANK_ADMIN = 0
32 RANK_MODERATOR = 10
34 RANK_MODERATOR = 10
33 RANK_USER = 100
35 RANK_USER = 100
34
36
35 SETTING_MODERATE = "moderate"
37 SETTING_MODERATE = "moderate"
36
38
37 REGEX_REPLY = re.compile('>>(\d+)')
39 REGEX_REPLY = re.compile('>>(\d+)')
38
40
39
41
40 class PostManager(models.Manager):
42 class PostManager(models.Manager):
41
43
42 def create_post(self, title, text, image=None, thread=None,
44 def create_post(self, title, text, image=None, thread=None,
43 ip=NO_IP, tags=None, user=None):
45 ip=NO_IP, tags=None, user=None):
44 post = self.create(title=title,
46 post = self.create(title=title,
45 text=text,
47 text=text,
46 pub_time=timezone.now(),
48 pub_time=timezone.now(),
47 thread=thread,
49 thread=thread,
48 image=image,
50 image=image,
49 poster_ip=ip,
51 poster_ip=ip,
50 poster_user_agent=UNKNOWN_UA,
52 poster_user_agent=UNKNOWN_UA,
51 last_edit_time=timezone.now(),
53 last_edit_time=timezone.now(),
52 bump_time=timezone.now(),
54 bump_time=timezone.now(),
53 user=user)
55 user=user)
54
56
55 if tags:
57 if tags:
56 map(post.tags.add, tags)
58 map(post.tags.add, tags)
57 for tag in tags:
59 for tag in tags:
58 tag.threads.add(post)
60 tag.threads.add(post)
59
61
60 if thread:
62 if thread:
61 thread.replies.add(post)
63 thread.replies.add(post)
62 thread.bump()
64 thread.bump()
63 thread.last_edit_time = timezone.now()
65 thread.last_edit_time = timezone.now()
64 thread.save()
66 thread.save()
65 else:
67 else:
66 self._delete_old_threads()
68 self._delete_old_threads()
67
69
68 self.connect_replies(post)
70 self.connect_replies(post)
69
71
70 return post
72 return post
71
73
72 def delete_post(self, post):
74 def delete_post(self, post):
73 if post.replies.count() > 0:
75 if post.replies.count() > 0:
74 map(self.delete_post, post.replies.all())
76 map(self.delete_post, post.replies.all())
75
77
76 # Update thread's last edit time (used as cache key)
78 # Update thread's last edit time (used as cache key)
77 thread = post.thread
79 thread = post.thread
78 if thread:
80 if thread:
79 thread.last_edit_time = timezone.now()
81 thread.last_edit_time = timezone.now()
80 thread.save()
82 thread.save()
81
83
82 post.delete()
84 post.delete()
83
85
84 def delete_posts_by_ip(self, ip):
86 def delete_posts_by_ip(self, ip):
85 posts = self.filter(poster_ip=ip)
87 posts = self.filter(poster_ip=ip)
86 map(self.delete_post, posts)
88 map(self.delete_post, posts)
87
89
88 def get_threads(self, tag=None, page=ALL_PAGES,
90 def get_threads(self, tag=None, page=ALL_PAGES,
89 order_by='-bump_time'):
91 order_by='-bump_time'):
90 if tag:
92 if tag:
91 threads = tag.threads
93 threads = tag.threads
92
94
93 if threads.count() == 0:
95 if threads.count() == 0:
94 raise Http404
96 raise Http404
95 else:
97 else:
96 threads = self.filter(thread=None)
98 threads = self.filter(thread=None)
97
99
98 threads = threads.order_by(order_by)
100 threads = threads.order_by(order_by)
99
101
100 if page != ALL_PAGES:
102 if page != ALL_PAGES:
101 thread_count = threads.count()
103 thread_count = threads.count()
102
104
103 if page < self._get_page_count(thread_count):
105 if page < self._get_page_count(thread_count):
104 start_thread = page * settings.THREADS_PER_PAGE
106 start_thread = page * settings.THREADS_PER_PAGE
105 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
107 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
106 thread_count)
108 thread_count)
107 threads = threads[start_thread:end_thread]
109 threads = threads[start_thread:end_thread]
108
110
109 return threads
111 return threads
110
112
111 def get_thread(self, opening_post_id):
113 def get_thread(self, opening_post_id):
112 try:
114 try:
113 opening_post = self.get(id=opening_post_id, thread=None)
115 opening_post = self.get(id=opening_post_id, thread=None)
114 except Post.DoesNotExist:
116 except Post.DoesNotExist:
115 raise Http404
117 raise Http404
116
118
117 if opening_post.replies:
119 if opening_post.replies:
118 thread = [opening_post]
120 thread = [opening_post]
119 thread.extend(opening_post.replies.all().order_by('pub_time'))
121 thread.extend(opening_post.replies.all().order_by('pub_time'))
120
122
121 return thread
123 return thread
122
124
123 def exists(self, post_id):
125 def exists(self, post_id):
124 posts = self.filter(id=post_id)
126 posts = self.filter(id=post_id)
125
127
126 return posts.count() > 0
128 return posts.count() > 0
127
129
128 def get_thread_page_count(self, tag=None):
130 def get_thread_page_count(self, tag=None):
129 if tag:
131 if tag:
130 threads = self.filter(thread=None, tags=tag)
132 threads = self.filter(thread=None, tags=tag)
131 else:
133 else:
132 threads = self.filter(thread=None)
134 threads = self.filter(thread=None)
133
135
134 return self._get_page_count(threads.count())
136 return self._get_page_count(threads.count())
135
137
136 def _delete_old_threads(self):
138 def _delete_old_threads(self):
137 """
139 """
138 Preserves maximum thread count. If there are too many threads,
140 Preserves maximum thread count. If there are too many threads,
139 delete the old ones.
141 delete the old ones.
140 """
142 """
141
143
142 # TODO Move old threads to the archive instead of deleting them.
144 # TODO Move old threads to the archive instead of deleting them.
143 # Maybe make some 'old' field in the model to indicate the thread
145 # Maybe make some 'old' field in the model to indicate the thread
144 # must not be shown and be able for replying.
146 # must not be shown and be able for replying.
145
147
146 threads = self.get_threads()
148 threads = self.get_threads()
147 thread_count = threads.count()
149 thread_count = threads.count()
148
150
149 if thread_count > settings.MAX_THREAD_COUNT:
151 if thread_count > settings.MAX_THREAD_COUNT:
150 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
152 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
151 old_threads = threads[thread_count - num_threads_to_delete:]
153 old_threads = threads[thread_count - num_threads_to_delete:]
152
154
153 map(self.delete_post, old_threads)
155 map(self.delete_post, old_threads)
154
156
155 def connect_replies(self, post):
157 def connect_replies(self, post):
156 """Connect replies to a post to show them as a refmap"""
158 """Connect replies to a post to show them as a refmap"""
157
159
158 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
160 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
159 id = reply_number.group(1)
161 id = reply_number.group(1)
160 ref_post = self.filter(id=id)
162 ref_post = self.filter(id=id)
161 if ref_post.count() > 0:
163 if ref_post.count() > 0:
162 ref_post[0].referenced_posts.add(post)
164 ref_post[0].referenced_posts.add(post)
163
165
164 def _get_page_count(self, thread_count):
166 def _get_page_count(self, thread_count):
165 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
167 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
166
168
167
169
168 class TagManager(models.Manager):
170 class TagManager(models.Manager):
169
171
170 def get_not_empty_tags(self):
172 def get_not_empty_tags(self):
171 tags = self.annotate(Count('threads')) \
173 tags = self.annotate(Count('threads')) \
172 .filter(threads__count__gt=0).order_by('name')
174 .filter(threads__count__gt=0).order_by('name')
173
175
174 return tags
176 return tags
175
177
176
178
177 class Tag(models.Model):
179 class Tag(models.Model):
178 """
180 """
179 A tag is a text node assigned to the post. The tag serves as a board
181 A tag is a text node assigned to the post. The tag serves as a board
180 section. There can be multiple tags for each message
182 section. There can be multiple tags for each message
181 """
183 """
182
184
183 objects = TagManager()
185 objects = TagManager()
184
186
185 name = models.CharField(max_length=100)
187 name = models.CharField(max_length=100)
186 threads = models.ManyToManyField('Post', null=True,
188 threads = models.ManyToManyField('Post', null=True,
187 blank=True, related_name='tag+')
189 blank=True, related_name='tag+')
188 linked = models.ForeignKey('Tag', null=True, blank=True)
190 linked = models.ForeignKey('Tag', null=True, blank=True)
189
191
190 def __unicode__(self):
192 def __unicode__(self):
191 return self.name
193 return self.name
192
194
193 def is_empty(self):
195 def is_empty(self):
194 return self.get_post_count() == 0
196 return self.get_post_count() == 0
195
197
196 def get_post_count(self):
198 def get_post_count(self):
197 return self.threads.count()
199 return self.threads.count()
198
200
199 def get_popularity(self):
201 def get_popularity(self):
200 posts_with_tag = Post.objects.get_threads(tag=self)
202 posts_with_tag = Post.objects.get_threads(tag=self)
201 reply_count = 0
203 reply_count = 0
202 for post in posts_with_tag:
204 for post in posts_with_tag:
203 reply_count += post.get_reply_count()
205 reply_count += post.get_reply_count()
204 reply_count += OPENING_POST_POPULARITY_WEIGHT
206 reply_count += OPENING_POST_POPULARITY_WEIGHT
205
207
206 return reply_count
208 return reply_count
207
209
208 def get_linked_tags(self):
210 def get_linked_tags(self):
209 tag_list = []
211 tag_list = []
210 self.get_linked_tags_list(tag_list)
212 self.get_linked_tags_list(tag_list)
211
213
212 return tag_list
214 return tag_list
213
215
214 def get_linked_tags_list(self, tag_list=[]):
216 def get_linked_tags_list(self, tag_list=[]):
215 """
217 """
216 Returns the list of tags linked to current. The list can be got
218 Returns the list of tags linked to current. The list can be got
217 through returned value or tag_list parameter
219 through returned value or tag_list parameter
218 """
220 """
219
221
220 linked_tag = self.linked
222 linked_tag = self.linked
221
223
222 if linked_tag and not (linked_tag in tag_list):
224 if linked_tag and not (linked_tag in tag_list):
223 tag_list.append(linked_tag)
225 tag_list.append(linked_tag)
224
226
225 linked_tag.get_linked_tags_list(tag_list)
227 linked_tag.get_linked_tags_list(tag_list)
226
228
227
229
228 class Post(models.Model):
230 class Post(models.Model):
229 """A post is a message."""
231 """A post is a message."""
230
232
231 objects = PostManager()
233 objects = PostManager()
232
234
233 def _update_image_filename(self, filename):
235 def _update_image_filename(self, filename):
234 """Get unique image filename"""
236 """Get unique image filename"""
235
237
236 path = IMAGES_DIRECTORY
238 path = IMAGES_DIRECTORY
237 new_name = str(int(time.mktime(time.gmtime())))
239 new_name = str(int(time.mktime(time.gmtime())))
238 new_name += str(int(random() * 1000))
240 new_name += str(int(random() * 1000))
239 new_name += FILE_EXTENSION_DELIMITER
241 new_name += FILE_EXTENSION_DELIMITER
240 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
242 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
241
243
242 return os.path.join(path, new_name)
244 return os.path.join(path, new_name)
243
245
244 title = models.CharField(max_length=TITLE_MAX_LENGTH)
246 title = models.CharField(max_length=TITLE_MAX_LENGTH)
245 pub_time = models.DateTimeField()
247 pub_time = models.DateTimeField()
246 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
248 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
247 escape_html=False)
249 escape_html=False)
248
250
249 image_width = models.IntegerField(default=0)
251 image_width = models.IntegerField(default=0)
250 image_height = models.IntegerField(default=0)
252 image_height = models.IntegerField(default=0)
251
253
252 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
254 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
253 blank=True, sizes=(IMAGE_THUMB_SIZE,),
255 blank=True, sizes=(IMAGE_THUMB_SIZE,),
254 width_field='image_width',
256 width_field='image_width',
255 height_field='image_height')
257 height_field='image_height')
256
258
257 poster_ip = models.GenericIPAddressField()
259 poster_ip = models.GenericIPAddressField()
258 poster_user_agent = models.TextField()
260 poster_user_agent = models.TextField()
259
261
260 thread = models.ForeignKey('Post', null=True, default=None)
262 thread = models.ForeignKey('Post', null=True, default=None)
261 tags = models.ManyToManyField(Tag)
263 tags = models.ManyToManyField(Tag)
262 last_edit_time = models.DateTimeField()
264 last_edit_time = models.DateTimeField()
263 bump_time = models.DateTimeField()
265 bump_time = models.DateTimeField()
264 user = models.ForeignKey('User', null=True, default=None)
266 user = models.ForeignKey('User', null=True, default=None)
265
267
266 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
268 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
267 blank=True, related_name='re+')
269 blank=True, related_name='re+')
268 referenced_posts = models.ManyToManyField('Post', symmetrical=False, null=True,
270 referenced_posts = models.ManyToManyField('Post', symmetrical=False, null=True,
269 blank=True, related_name='rfp+')
271 blank=True, related_name='rfp+')
270
272
271 def __unicode__(self):
273 def __unicode__(self):
272 return '#' + str(self.id) + ' ' + self.title + ' (' + \
274 return '#' + str(self.id) + ' ' + self.title + ' (' + \
273 self.text.raw[:50] + ')'
275 self.text.raw[:50] + ')'
274
276
275 def get_title(self):
277 def get_title(self):
276 title = self.title
278 title = self.title
277 if len(title) == 0:
279 if len(title) == 0:
278 title = self.text.raw[:20]
280 title = self.text.raw[:20]
279
281
280 return title
282 return title
281
283
282 def get_reply_count(self):
284 def get_reply_count(self):
283 return self.replies.count()
285 return self.replies.count()
284
286
285 def get_images_count(self):
287 def get_images_count(self):
286 images_count = 1 if self.image else 0
288 images_count = 1 if self.image else 0
287 images_count += self.replies.filter(image_width__gt=0).count()
289 images_count += self.replies.filter(image_width__gt=0).count()
288
290
289 return images_count
291 return images_count
290
292
291 def can_bump(self):
293 def can_bump(self):
292 """Check if the thread can be bumped by replying"""
294 """Check if the thread can be bumped by replying"""
293
295
294 post_count = self.get_reply_count() + 1
296 post_count = self.get_reply_count() + 1
295
297
296 return post_count <= settings.MAX_POSTS_PER_THREAD
298 return post_count <= settings.MAX_POSTS_PER_THREAD
297
299
298 def bump(self):
300 def bump(self):
299 """Bump (move to up) thread"""
301 """Bump (move to up) thread"""
300
302
301 if self.can_bump():
303 if self.can_bump():
302 self.bump_time = timezone.now()
304 self.bump_time = timezone.now()
303
305
304 def get_last_replies(self):
306 def get_last_replies(self):
305 if settings.LAST_REPLIES_COUNT > 0:
307 if settings.LAST_REPLIES_COUNT > 0:
306 reply_count = self.get_reply_count()
308 reply_count = self.get_reply_count()
307
309
308 if reply_count > 0:
310 if reply_count > 0:
309 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
311 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
310 reply_count)
312 reply_count)
311 last_replies = self.replies.all().order_by('pub_time')[reply_count -
313 last_replies = self.replies.all().order_by('pub_time')[reply_count -
312 reply_count_to_show:]
314 reply_count_to_show:]
313
315
314 return last_replies
316 return last_replies
315
317
316 def get_tags(self):
318 def get_tags(self):
317 """Get a sorted tag list"""
319 """Get a sorted tag list"""
318
320
319 return self.tags.order_by('name')
321 return self.tags.order_by('name')
320
322
321
323
322 class User(models.Model):
324 class User(models.Model):
323
325
324 user_id = models.CharField(max_length=50)
326 user_id = models.CharField(max_length=50)
325 rank = models.IntegerField()
327 rank = models.IntegerField()
326
328
327 registration_time = models.DateTimeField()
329 registration_time = models.DateTimeField()
328
330
329 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
331 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
330 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
332 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
331 blank=True)
333 blank=True)
332
334
333 def save_setting(self, name, value):
335 def save_setting(self, name, value):
334 setting, created = Setting.objects.get_or_create(name=name, user=self)
336 setting, created = Setting.objects.get_or_create(name=name, user=self)
335 setting.value = str(value)
337 setting.value = str(value)
336 setting.save()
338 setting.save()
337
339
338 return setting
340 return setting
339
341
340 def get_setting(self, name):
342 def get_setting(self, name):
341 if Setting.objects.filter(name=name, user=self).exists():
343 if Setting.objects.filter(name=name, user=self).exists():
342 setting = Setting.objects.get(name=name, user=self)
344 setting = Setting.objects.get(name=name, user=self)
343 setting_value = setting.value
345 setting_value = setting.value
344 else:
346 else:
345 setting_value = None
347 setting_value = None
346
348
347 return setting_value
349 return setting_value
348
350
349 def is_moderator(self):
351 def is_moderator(self):
350 return RANK_MODERATOR >= self.rank
352 return RANK_MODERATOR >= self.rank
351
353
352 def get_sorted_fav_tags(self):
354 def get_sorted_fav_tags(self):
355 cache_key = self._get_tag_cache_key()
356 fav_tags = cache.get(cache_key)
357 if fav_tags:
358 return fav_tags
359
353 tags = self.fav_tags.annotate(Count('threads'))\
360 tags = self.fav_tags.annotate(Count('threads'))\
354 .filter(threads__count__gt=0).order_by('name')
361 .filter(threads__count__gt=0).order_by('name')
355
362
363 if tags:
364 cache.set(cache_key, tags, board_settings.CACHE_TIMEOUT)
365
356 return tags
366 return tags
357
367
358 def get_post_count(self):
368 def get_post_count(self):
359 return Post.objects.filter(user=self).count()
369 return Post.objects.filter(user=self).count()
360
370
361 def __unicode__(self):
371 def __unicode__(self):
362 return self.user_id + '(' + str(self.rank) + ')'
372 return self.user_id + '(' + str(self.rank) + ')'
363
373
364 def get_last_access_time(self):
374 def get_last_access_time(self):
365 posts = Post.objects.filter(user=self)
375 posts = Post.objects.filter(user=self)
366 if posts.count() > 0:
376 if posts.count() > 0:
367 return posts.latest('pub_time').pub_time
377 return posts.latest('pub_time').pub_time
368
378
379 def add_tag(self, tag):
380 self.fav_tags.add(tag)
381 cache.delete(self._get_tag_cache_key())
382
383 def remove_tag(self, tag):
384 self.fav_tags.remove(tag)
385 cache.delete(self._get_tag_cache_key())
386
387 def _get_tag_cache_key(self):
388 return self.user_id + '_tags'
389
369
390
370 class Setting(models.Model):
391 class Setting(models.Model):
371
392
372 name = models.CharField(max_length=50)
393 name = models.CharField(max_length=50)
373 value = models.CharField(max_length=50)
394 value = models.CharField(max_length=50)
374 user = models.ForeignKey(User)
395 user = models.ForeignKey(User)
375
396
376
397
377 class Ban(models.Model):
398 class Ban(models.Model):
378
399
379 ip = models.GenericIPAddressField()
400 ip = models.GenericIPAddressField()
380
401
381 def __unicode__(self):
402 def __unicode__(self):
382 return self.ip
403 return self.ip
@@ -1,485 +1,485 b''
1 import hashlib
1 import hashlib
2 import string
2 import string
3 from django.core import serializers
3 from django.core import serializers
4 from django.core.urlresolvers import reverse
4 from django.core.urlresolvers import reverse
5 from django.http import HttpResponseRedirect
5 from django.http import HttpResponseRedirect
6 from django.http.response import HttpResponse
6 from django.http.response import HttpResponse
7 from django.template import RequestContext
7 from django.template import RequestContext
8 from django.shortcuts import render, redirect, get_object_or_404
8 from django.shortcuts import render, redirect, get_object_or_404
9 from django.utils import timezone
9 from django.utils import timezone
10
10
11 from boards import forms
11 from boards import forms
12 import boards
12 import boards
13 from boards import utils
13 from boards import utils
14 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
14 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
15 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
15 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
16
16
17 from boards.models import Post, Tag, Ban, User, RANK_USER, SETTING_MODERATE, \
17 from boards.models import Post, Tag, Ban, User, RANK_USER, SETTING_MODERATE, \
18 REGEX_REPLY
18 REGEX_REPLY
19 from boards import authors
19 from boards import authors
20 from boards.utils import get_client_ip
20 from boards.utils import get_client_ip
21 import neboard
21 import neboard
22 import re
22 import re
23
23
24
24
25 def index(request, page=0):
25 def index(request, page=0):
26 context = _init_default_context(request)
26 context = _init_default_context(request)
27
27
28 if utils.need_include_captcha(request):
28 if utils.need_include_captcha(request):
29 threadFormClass = ThreadCaptchaForm
29 threadFormClass = ThreadCaptchaForm
30 kwargs = {'request': request}
30 kwargs = {'request': request}
31 else:
31 else:
32 threadFormClass = ThreadForm
32 threadFormClass = ThreadForm
33 kwargs = {}
33 kwargs = {}
34
34
35 if request.method == 'POST':
35 if request.method == 'POST':
36 form = threadFormClass(request.POST, request.FILES,
36 form = threadFormClass(request.POST, request.FILES,
37 error_class=PlainErrorList, **kwargs)
37 error_class=PlainErrorList, **kwargs)
38 form.session = request.session
38 form.session = request.session
39
39
40 if form.is_valid():
40 if form.is_valid():
41 return _new_post(request, form)
41 return _new_post(request, form)
42 if form.need_to_ban:
42 if form.need_to_ban:
43 # Ban user because he is suspected to be a bot
43 # Ban user because he is suspected to be a bot
44 _ban_current_user(request)
44 _ban_current_user(request)
45 else:
45 else:
46 form = threadFormClass(error_class=PlainErrorList, **kwargs)
46 form = threadFormClass(error_class=PlainErrorList, **kwargs)
47
47
48 threads = []
48 threads = []
49 for thread in Post.objects.get_threads(page=int(page)):
49 for thread in Post.objects.get_threads(page=int(page)):
50 threads.append({
50 threads.append({
51 'thread': thread,
51 'thread': thread,
52 'bumpable': thread.can_bump(),
52 'bumpable': thread.can_bump(),
53 'last_replies': thread.get_last_replies(),
53 'last_replies': thread.get_last_replies(),
54 })
54 })
55
55
56 context['threads'] = None if len(threads) == 0 else threads
56 context['threads'] = None if len(threads) == 0 else threads
57 context['form'] = form
57 context['form'] = form
58 context['pages'] = range(Post.objects.get_thread_page_count())
58 context['pages'] = range(Post.objects.get_thread_page_count())
59
59
60 return render(request, 'boards/posting_general.html',
60 return render(request, 'boards/posting_general.html',
61 context)
61 context)
62
62
63
63
64 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
64 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
65 """Add a new post (in thread or as a reply)."""
65 """Add a new post (in thread or as a reply)."""
66
66
67 ip = get_client_ip(request)
67 ip = get_client_ip(request)
68 is_banned = Ban.objects.filter(ip=ip).exists()
68 is_banned = Ban.objects.filter(ip=ip).exists()
69
69
70 if is_banned:
70 if is_banned:
71 return redirect(you_are_banned)
71 return redirect(you_are_banned)
72
72
73 data = form.cleaned_data
73 data = form.cleaned_data
74
74
75 title = data['title']
75 title = data['title']
76 text = data['text']
76 text = data['text']
77
77
78 text = _remove_invalid_links(text)
78 text = _remove_invalid_links(text)
79
79
80 if 'image' in data.keys():
80 if 'image' in data.keys():
81 image = data['image']
81 image = data['image']
82 else:
82 else:
83 image = None
83 image = None
84
84
85 tags = []
85 tags = []
86
86
87 new_thread = thread_id == boards.models.NO_PARENT
87 new_thread = thread_id == boards.models.NO_PARENT
88 if new_thread:
88 if new_thread:
89 tag_strings = data['tags']
89 tag_strings = data['tags']
90
90
91 if tag_strings:
91 if tag_strings:
92 tag_strings = tag_strings.split(' ')
92 tag_strings = tag_strings.split(' ')
93 for tag_name in tag_strings:
93 for tag_name in tag_strings:
94 tag_name = string.lower(tag_name.strip())
94 tag_name = string.lower(tag_name.strip())
95 if len(tag_name) > 0:
95 if len(tag_name) > 0:
96 tag, created = Tag.objects.get_or_create(name=tag_name)
96 tag, created = Tag.objects.get_or_create(name=tag_name)
97 tags.append(tag)
97 tags.append(tag)
98
98
99 linked_tags = tag.get_linked_tags()
99 linked_tags = tag.get_linked_tags()
100 if len(linked_tags) > 0:
100 if len(linked_tags) > 0:
101 tags.extend(linked_tags)
101 tags.extend(linked_tags)
102
102
103 op = None if thread_id == boards.models.NO_PARENT else \
103 op = None if thread_id == boards.models.NO_PARENT else \
104 get_object_or_404(Post, id=thread_id)
104 get_object_or_404(Post, id=thread_id)
105 post = Post.objects.create_post(title=title, text=text, ip=ip,
105 post = Post.objects.create_post(title=title, text=text, ip=ip,
106 thread=op, image=image,
106 thread=op, image=image,
107 tags=tags, user=_get_user(request))
107 tags=tags, user=_get_user(request))
108
108
109 thread_to_show = (post.id if new_thread else thread_id)
109 thread_to_show = (post.id if new_thread else thread_id)
110
110
111 if new_thread:
111 if new_thread:
112 return redirect(thread, post_id=thread_to_show)
112 return redirect(thread, post_id=thread_to_show)
113 else:
113 else:
114 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
114 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
115 '#' + str(post.id))
115 '#' + str(post.id))
116
116
117
117
118 def tag(request, tag_name, page=0):
118 def tag(request, tag_name, page=0):
119 """
119 """
120 Get all tag threads. Threads are split in pages, so some page is
120 Get all tag threads. Threads are split in pages, so some page is
121 requested. Default page is 0.
121 requested. Default page is 0.
122 """
122 """
123
123
124 tag = get_object_or_404(Tag, name=tag_name)
124 tag = get_object_or_404(Tag, name=tag_name)
125 threads = []
125 threads = []
126 for thread in Post.objects.get_threads(tag=tag, page=int(page)):
126 for thread in Post.objects.get_threads(tag=tag, page=int(page)):
127 threads.append({
127 threads.append({
128 'thread': thread,
128 'thread': thread,
129 'bumpable': thread.can_bump(),
129 'bumpable': thread.can_bump(),
130 'last_replies': thread.get_last_replies(),
130 'last_replies': thread.get_last_replies(),
131 })
131 })
132
132
133 if request.method == 'POST':
133 if request.method == 'POST':
134 form = ThreadForm(request.POST, request.FILES,
134 form = ThreadForm(request.POST, request.FILES,
135 error_class=PlainErrorList)
135 error_class=PlainErrorList)
136 form.session = request.session
136 form.session = request.session
137
137
138 if form.is_valid():
138 if form.is_valid():
139 return _new_post(request, form)
139 return _new_post(request, form)
140 if form.need_to_ban:
140 if form.need_to_ban:
141 # Ban user because he is suspected to be a bot
141 # Ban user because he is suspected to be a bot
142 _ban_current_user(request)
142 _ban_current_user(request)
143 else:
143 else:
144 form = forms.ThreadForm(initial={'tags': tag_name},
144 form = forms.ThreadForm(initial={'tags': tag_name},
145 error_class=PlainErrorList)
145 error_class=PlainErrorList)
146
146
147 context = _init_default_context(request)
147 context = _init_default_context(request)
148 context['threads'] = None if len(threads) == 0 else threads
148 context['threads'] = None if len(threads) == 0 else threads
149 context['tag'] = tag
149 context['tag'] = tag
150 context['pages'] = range(Post.objects.get_thread_page_count(tag=tag))
150 context['pages'] = range(Post.objects.get_thread_page_count(tag=tag))
151
151
152 context['form'] = form
152 context['form'] = form
153
153
154 return render(request, 'boards/posting_general.html',
154 return render(request, 'boards/posting_general.html',
155 context)
155 context)
156
156
157
157
158 def thread(request, post_id):
158 def thread(request, post_id):
159 """Get all thread posts"""
159 """Get all thread posts"""
160
160
161 if utils.need_include_captcha(request):
161 if utils.need_include_captcha(request):
162 postFormClass = PostCaptchaForm
162 postFormClass = PostCaptchaForm
163 kwargs = {'request': request}
163 kwargs = {'request': request}
164 else:
164 else:
165 postFormClass = PostForm
165 postFormClass = PostForm
166 kwargs = {}
166 kwargs = {}
167
167
168 if request.method == 'POST':
168 if request.method == 'POST':
169 form = postFormClass(request.POST, request.FILES,
169 form = postFormClass(request.POST, request.FILES,
170 error_class=PlainErrorList, **kwargs)
170 error_class=PlainErrorList, **kwargs)
171 form.session = request.session
171 form.session = request.session
172
172
173 if form.is_valid():
173 if form.is_valid():
174 return _new_post(request, form, post_id)
174 return _new_post(request, form, post_id)
175 if form.need_to_ban:
175 if form.need_to_ban:
176 # Ban user because he is suspected to be a bot
176 # Ban user because he is suspected to be a bot
177 _ban_current_user(request)
177 _ban_current_user(request)
178 else:
178 else:
179 form = postFormClass(error_class=PlainErrorList, **kwargs)
179 form = postFormClass(error_class=PlainErrorList, **kwargs)
180
180
181 posts = Post.objects.get_thread(post_id)
181 posts = Post.objects.get_thread(post_id)
182
182
183 context = _init_default_context(request)
183 context = _init_default_context(request)
184
184
185 context['posts'] = posts
185 context['posts'] = posts
186 context['form'] = form
186 context['form'] = form
187 context['bumpable'] = posts[0].can_bump()
187 context['bumpable'] = posts[0].can_bump()
188 if context['bumpable']:
188 if context['bumpable']:
189 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - len(
189 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - len(
190 posts)
190 posts)
191 context['bumplimit_progress'] = str(
191 context['bumplimit_progress'] = str(
192 float(context['posts_left']) /
192 float(context['posts_left']) /
193 neboard.settings.MAX_POSTS_PER_THREAD * 100)
193 neboard.settings.MAX_POSTS_PER_THREAD * 100)
194
194
195 return render(request, 'boards/thread.html', context)
195 return render(request, 'boards/thread.html', context)
196
196
197
197
198 def login(request):
198 def login(request):
199 """Log in with user id"""
199 """Log in with user id"""
200
200
201 context = _init_default_context(request)
201 context = _init_default_context(request)
202
202
203 if request.method == 'POST':
203 if request.method == 'POST':
204 form = LoginForm(request.POST, request.FILES,
204 form = LoginForm(request.POST, request.FILES,
205 error_class=PlainErrorList)
205 error_class=PlainErrorList)
206 form.session = request.session
206 form.session = request.session
207
207
208 if form.is_valid():
208 if form.is_valid():
209 user = User.objects.get(user_id=form.cleaned_data['user_id'])
209 user = User.objects.get(user_id=form.cleaned_data['user_id'])
210 request.session['user_id'] = user.id
210 request.session['user_id'] = user.id
211 return redirect(index)
211 return redirect(index)
212
212
213 else:
213 else:
214 form = LoginForm()
214 form = LoginForm()
215
215
216 context['form'] = form
216 context['form'] = form
217
217
218 return render(request, 'boards/login.html', context)
218 return render(request, 'boards/login.html', context)
219
219
220
220
221 def settings(request):
221 def settings(request):
222 """User's settings"""
222 """User's settings"""
223
223
224 context = _init_default_context(request)
224 context = _init_default_context(request)
225 user = _get_user(request)
225 user = _get_user(request)
226 is_moderator = user.is_moderator()
226 is_moderator = user.is_moderator()
227
227
228 if request.method == 'POST':
228 if request.method == 'POST':
229 if is_moderator:
229 if is_moderator:
230 form = ModeratorSettingsForm(request.POST,
230 form = ModeratorSettingsForm(request.POST,
231 error_class=PlainErrorList)
231 error_class=PlainErrorList)
232 else:
232 else:
233 form = SettingsForm(request.POST, error_class=PlainErrorList)
233 form = SettingsForm(request.POST, error_class=PlainErrorList)
234
234
235 if form.is_valid():
235 if form.is_valid():
236 selected_theme = form.cleaned_data['theme']
236 selected_theme = form.cleaned_data['theme']
237
237
238 user.save_setting('theme', selected_theme)
238 user.save_setting('theme', selected_theme)
239
239
240 if is_moderator:
240 if is_moderator:
241 moderate = form.cleaned_data['moderate']
241 moderate = form.cleaned_data['moderate']
242 user.save_setting(SETTING_MODERATE, moderate)
242 user.save_setting(SETTING_MODERATE, moderate)
243
243
244 return redirect(settings)
244 return redirect(settings)
245 else:
245 else:
246 selected_theme = _get_theme(request)
246 selected_theme = _get_theme(request)
247
247
248 if is_moderator:
248 if is_moderator:
249 form = ModeratorSettingsForm(initial={'theme': selected_theme,
249 form = ModeratorSettingsForm(initial={'theme': selected_theme,
250 'moderate': context['moderator']},
250 'moderate': context['moderator']},
251 error_class=PlainErrorList)
251 error_class=PlainErrorList)
252 else:
252 else:
253 form = SettingsForm(initial={'theme': selected_theme},
253 form = SettingsForm(initial={'theme': selected_theme},
254 error_class=PlainErrorList)
254 error_class=PlainErrorList)
255
255
256 context['form'] = form
256 context['form'] = form
257
257
258 return render(request, 'boards/settings.html', context)
258 return render(request, 'boards/settings.html', context)
259
259
260
260
261 def all_tags(request):
261 def all_tags(request):
262 """All tags list"""
262 """All tags list"""
263
263
264 context = _init_default_context(request)
264 context = _init_default_context(request)
265 context['all_tags'] = Tag.objects.get_not_empty_tags()
265 context['all_tags'] = Tag.objects.get_not_empty_tags()
266
266
267 return render(request, 'boards/tags.html', context)
267 return render(request, 'boards/tags.html', context)
268
268
269
269
270 def jump_to_post(request, post_id):
270 def jump_to_post(request, post_id):
271 """Determine thread in which the requested post is and open it's page"""
271 """Determine thread in which the requested post is and open it's page"""
272
272
273 post = get_object_or_404(Post, id=post_id)
273 post = get_object_or_404(Post, id=post_id)
274
274
275 if not post.thread:
275 if not post.thread:
276 return redirect(thread, post_id=post.id)
276 return redirect(thread, post_id=post.id)
277 else:
277 else:
278 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
278 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
279 + '#' + str(post.id))
279 + '#' + str(post.id))
280
280
281
281
282 def authors(request):
282 def authors(request):
283 """Show authors list"""
283 """Show authors list"""
284
284
285 context = _init_default_context(request)
285 context = _init_default_context(request)
286 context['authors'] = boards.authors.authors
286 context['authors'] = boards.authors.authors
287
287
288 return render(request, 'boards/authors.html', context)
288 return render(request, 'boards/authors.html', context)
289
289
290
290
291 def delete(request, post_id):
291 def delete(request, post_id):
292 """Delete post"""
292 """Delete post"""
293
293
294 user = _get_user(request)
294 user = _get_user(request)
295 post = get_object_or_404(Post, id=post_id)
295 post = get_object_or_404(Post, id=post_id)
296
296
297 if user.is_moderator():
297 if user.is_moderator():
298 # TODO Show confirmation page before deletion
298 # TODO Show confirmation page before deletion
299 Post.objects.delete_post(post)
299 Post.objects.delete_post(post)
300
300
301 if not post.thread:
301 if not post.thread:
302 return _redirect_to_next(request)
302 return _redirect_to_next(request)
303 else:
303 else:
304 return redirect(thread, post_id=post.thread.id)
304 return redirect(thread, post_id=post.thread.id)
305
305
306
306
307 def ban(request, post_id):
307 def ban(request, post_id):
308 """Ban user"""
308 """Ban user"""
309
309
310 user = _get_user(request)
310 user = _get_user(request)
311 post = get_object_or_404(Post, id=post_id)
311 post = get_object_or_404(Post, id=post_id)
312
312
313 if user.is_moderator():
313 if user.is_moderator():
314 # TODO Show confirmation page before ban
314 # TODO Show confirmation page before ban
315 Ban.objects.get_or_create(ip=post.poster_ip)
315 Ban.objects.get_or_create(ip=post.poster_ip)
316
316
317 return _redirect_to_next(request)
317 return _redirect_to_next(request)
318
318
319
319
320 def you_are_banned(request):
320 def you_are_banned(request):
321 """Show the page that notifies that user is banned"""
321 """Show the page that notifies that user is banned"""
322
322
323 context = _init_default_context(request)
323 context = _init_default_context(request)
324 return render(request, 'boards/staticpages/banned.html', context)
324 return render(request, 'boards/staticpages/banned.html', context)
325
325
326
326
327 def page_404(request):
327 def page_404(request):
328 """Show page 404 (not found error)"""
328 """Show page 404 (not found error)"""
329
329
330 context = _init_default_context(request)
330 context = _init_default_context(request)
331 return render(request, 'boards/404.html', context)
331 return render(request, 'boards/404.html', context)
332
332
333
333
334 def tag_subscribe(request, tag_name):
334 def tag_subscribe(request, tag_name):
335 """Add tag to favorites"""
335 """Add tag to favorites"""
336
336
337 user = _get_user(request)
337 user = _get_user(request)
338 tag = get_object_or_404(Tag, name=tag_name)
338 tag = get_object_or_404(Tag, name=tag_name)
339
339
340 if not tag in user.fav_tags.all():
340 if not tag in user.fav_tags.all():
341 user.fav_tags.add(tag)
341 user.add_tag(tag)
342
342
343 return _redirect_to_next(request)
343 return _redirect_to_next(request)
344
344
345
345
346 def tag_unsubscribe(request, tag_name):
346 def tag_unsubscribe(request, tag_name):
347 """Remove tag from favorites"""
347 """Remove tag from favorites"""
348
348
349 user = _get_user(request)
349 user = _get_user(request)
350 tag = get_object_or_404(Tag, name=tag_name)
350 tag = get_object_or_404(Tag, name=tag_name)
351
351
352 if tag in user.fav_tags.all():
352 if tag in user.fav_tags.all():
353 user.fav_tags.remove(tag)
353 user.remove_tag(tag)
354
354
355 return _redirect_to_next(request)
355 return _redirect_to_next(request)
356
356
357
357
358 def static_page(request, name):
358 def static_page(request, name):
359 """Show a static page that needs only tags list and a CSS"""
359 """Show a static page that needs only tags list and a CSS"""
360
360
361 context = _init_default_context(request)
361 context = _init_default_context(request)
362 return render(request, 'boards/staticpages/' + name + '.html', context)
362 return render(request, 'boards/staticpages/' + name + '.html', context)
363
363
364
364
365 def api_get_post(request, post_id):
365 def api_get_post(request, post_id):
366 """
366 """
367 Get the JSON of a post. This can be
367 Get the JSON of a post. This can be
368 used as and API for external clients.
368 used as and API for external clients.
369 """
369 """
370
370
371 post = get_object_or_404(Post, id=post_id)
371 post = get_object_or_404(Post, id=post_id)
372
372
373 json = serializers.serialize("json", [post], fields=(
373 json = serializers.serialize("json", [post], fields=(
374 "pub_time", "_text_rendered", "title", "text", "image",
374 "pub_time", "_text_rendered", "title", "text", "image",
375 "image_width", "image_height", "replies", "tags"
375 "image_width", "image_height", "replies", "tags"
376 ))
376 ))
377
377
378 return HttpResponse(content=json)
378 return HttpResponse(content=json)
379
379
380
380
381 def get_post(request, post_id):
381 def get_post(request, post_id):
382 """Get the html of a post. Used for popups."""
382 """Get the html of a post. Used for popups."""
383
383
384 post = get_object_or_404(Post, id=post_id)
384 post = get_object_or_404(Post, id=post_id)
385
385
386 context = RequestContext(request)
386 context = RequestContext(request)
387 context["post"] = post
387 context["post"] = post
388
388
389 return render(request, 'boards/post.html', context)
389 return render(request, 'boards/post.html', context)
390
390
391
391
392 def _get_theme(request, user=None):
392 def _get_theme(request, user=None):
393 """Get user's CSS theme"""
393 """Get user's CSS theme"""
394
394
395 if not user:
395 if not user:
396 user = _get_user(request)
396 user = _get_user(request)
397 theme = user.get_setting('theme')
397 theme = user.get_setting('theme')
398 if not theme:
398 if not theme:
399 theme = neboard.settings.DEFAULT_THEME
399 theme = neboard.settings.DEFAULT_THEME
400
400
401 return theme
401 return theme
402
402
403
403
404 def _init_default_context(request):
404 def _init_default_context(request):
405 """Create context with default values that are used in most views"""
405 """Create context with default values that are used in most views"""
406
406
407 context = RequestContext(request)
407 context = RequestContext(request)
408
408
409 user = _get_user(request)
409 user = _get_user(request)
410 context['user'] = user
410 context['user'] = user
411 context['tags'] = user.get_sorted_fav_tags()
411 context['tags'] = user.get_sorted_fav_tags()
412
412
413 theme = _get_theme(request, user)
413 theme = _get_theme(request, user)
414 context['theme'] = theme
414 context['theme'] = theme
415 context['theme_css'] = 'css/' + theme + '/base_page.css'
415 context['theme_css'] = 'css/' + theme + '/base_page.css'
416
416
417 # This shows the moderator panel
417 # This shows the moderator panel
418 moderate = user.get_setting(SETTING_MODERATE)
418 moderate = user.get_setting(SETTING_MODERATE)
419 if moderate == 'True':
419 if moderate == 'True':
420 context['moderator'] = user.is_moderator()
420 context['moderator'] = user.is_moderator()
421 else:
421 else:
422 context['moderator'] = False
422 context['moderator'] = False
423
423
424 return context
424 return context
425
425
426
426
427 def _get_user(request):
427 def _get_user(request):
428 """
428 """
429 Get current user from the session. If the user does not exist, create
429 Get current user from the session. If the user does not exist, create
430 a new one.
430 a new one.
431 """
431 """
432
432
433 session = request.session
433 session = request.session
434 if not 'user_id' in session:
434 if not 'user_id' in session:
435 request.session.save()
435 request.session.save()
436
436
437 md5 = hashlib.md5()
437 md5 = hashlib.md5()
438 md5.update(session.session_key)
438 md5.update(session.session_key)
439 new_id = md5.hexdigest()
439 new_id = md5.hexdigest()
440
440
441 time_now = timezone.now()
441 time_now = timezone.now()
442 user = User.objects.create(user_id=new_id, rank=RANK_USER,
442 user = User.objects.create(user_id=new_id, rank=RANK_USER,
443 registration_time=time_now)
443 registration_time=time_now)
444
444
445 session['user_id'] = user.id
445 session['user_id'] = user.id
446 else:
446 else:
447 user = User.objects.get(id=session['user_id'])
447 user = User.objects.get(id=session['user_id'])
448
448
449 return user
449 return user
450
450
451
451
452 def _redirect_to_next(request):
452 def _redirect_to_next(request):
453 """
453 """
454 If a 'next' parameter was specified, redirect to the next page. This is
454 If a 'next' parameter was specified, redirect to the next page. This is
455 used when the user is required to return to some page after the current
455 used when the user is required to return to some page after the current
456 view has finished its work.
456 view has finished its work.
457 """
457 """
458
458
459 if 'next' in request.GET:
459 if 'next' in request.GET:
460 next_page = request.GET['next']
460 next_page = request.GET['next']
461 return HttpResponseRedirect(next_page)
461 return HttpResponseRedirect(next_page)
462 else:
462 else:
463 return redirect(index)
463 return redirect(index)
464
464
465
465
466 def _ban_current_user(request):
466 def _ban_current_user(request):
467 """Add current user to the IP ban list"""
467 """Add current user to the IP ban list"""
468
468
469 ip = utils.get_client_ip(request)
469 ip = utils.get_client_ip(request)
470 Ban.objects.get_or_create(ip=ip)
470 Ban.objects.get_or_create(ip=ip)
471
471
472
472
473 def _remove_invalid_links(text):
473 def _remove_invalid_links(text):
474 """
474 """
475 Replace invalid links in posts so that they won't be parsed.
475 Replace invalid links in posts so that they won't be parsed.
476 Invalid links are links to non-existent posts
476 Invalid links are links to non-existent posts
477 """
477 """
478
478
479 for reply_number in re.finditer(REGEX_REPLY, text):
479 for reply_number in re.finditer(REGEX_REPLY, text):
480 post_id = reply_number.group(1)
480 post_id = reply_number.group(1)
481 post = Post.objects.filter(id=post_id)
481 post = Post.objects.filter(id=post_id)
482 if not post.exists():
482 if not post.exists():
483 text = string.replace(text, '>>' + id, id)
483 text = string.replace(text, '>>' + id, id)
484
484
485 return text
485 return text
General Comments 0
You need to be logged in to leave comments. Login now