##// END OF EJS Templates
Get opening post only once when reversing post url
neko259 -
r591:12e7d699 default
parent child Browse files
Show More
@@ -1,443 +1,444 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import os
3 import os
4 from random import random
4 from random import random
5 import time
5 import time
6 import math
6 import math
7 import re
7 import re
8 import hashlib
8 import hashlib
9
9
10 from django.core.cache import cache
10 from django.core.cache import cache
11 from django.core.paginator import Paginator
11 from django.core.paginator import Paginator
12 from django.core.urlresolvers import reverse
12 from django.core.urlresolvers import reverse
13
13
14 from django.db import models, transaction
14 from django.db import models, transaction
15 from django.http import Http404
15 from django.http import Http404
16 from django.utils import timezone
16 from django.utils import timezone
17 from markupfield.fields import MarkupField
17 from markupfield.fields import MarkupField
18
18
19 from neboard import settings
19 from neboard import settings
20 from boards import thumbs
20 from boards import thumbs
21
21
22 MAX_TITLE_LENGTH = 50
22 MAX_TITLE_LENGTH = 50
23
23
24 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
25
25
26 CACHE_KEY_PPD = 'ppd'
26 CACHE_KEY_PPD = 'ppd'
27 CACHE_KEY_POST_URL = 'post_url'
27 CACHE_KEY_POST_URL = 'post_url'
28
28
29 POSTS_PER_DAY_RANGE = range(7)
29 POSTS_PER_DAY_RANGE = range(7)
30
30
31 BAN_REASON_AUTO = 'Auto'
31 BAN_REASON_AUTO = 'Auto'
32
32
33 IMAGE_THUMB_SIZE = (200, 150)
33 IMAGE_THUMB_SIZE = (200, 150)
34
34
35 TITLE_MAX_LENGTH = 50
35 TITLE_MAX_LENGTH = 50
36
36
37 DEFAULT_MARKUP_TYPE = 'markdown'
37 DEFAULT_MARKUP_TYPE = 'markdown'
38
38
39 NO_PARENT = -1
39 NO_PARENT = -1
40 NO_IP = '0.0.0.0'
40 NO_IP = '0.0.0.0'
41 UNKNOWN_UA = ''
41 UNKNOWN_UA = ''
42 ALL_PAGES = -1
42 ALL_PAGES = -1
43 IMAGES_DIRECTORY = 'images/'
43 IMAGES_DIRECTORY = 'images/'
44 FILE_EXTENSION_DELIMITER = '.'
44 FILE_EXTENSION_DELIMITER = '.'
45
45
46 SETTING_MODERATE = "moderate"
46 SETTING_MODERATE = "moderate"
47
47
48 REGEX_REPLY = re.compile('>>(\d+)')
48 REGEX_REPLY = re.compile('>>(\d+)')
49
49
50
50
51 class PostManager(models.Manager):
51 class PostManager(models.Manager):
52
52
53 def create_post(self, title, text, image=None, thread=None,
53 def create_post(self, title, text, image=None, thread=None,
54 ip=NO_IP, tags=None, user=None):
54 ip=NO_IP, tags=None, user=None):
55 """
55 """
56 Create new post
56 Create new post
57 """
57 """
58
58
59 posting_time = timezone.now()
59 posting_time = timezone.now()
60 if not thread:
60 if not thread:
61 thread = Thread.objects.create(bump_time=posting_time,
61 thread = Thread.objects.create(bump_time=posting_time,
62 last_edit_time=posting_time)
62 last_edit_time=posting_time)
63 else:
63 else:
64 thread.bump()
64 thread.bump()
65 thread.last_edit_time = posting_time
65 thread.last_edit_time = posting_time
66 thread.save()
66 thread.save()
67
67
68 post = self.create(title=title,
68 post = self.create(title=title,
69 text=text,
69 text=text,
70 pub_time=posting_time,
70 pub_time=posting_time,
71 thread_new=thread,
71 thread_new=thread,
72 image=image,
72 image=image,
73 poster_ip=ip,
73 poster_ip=ip,
74 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
74 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
75 # last!
75 # last!
76 last_edit_time=posting_time,
76 last_edit_time=posting_time,
77 user=user)
77 user=user)
78
78
79 thread.replies.add(post)
79 thread.replies.add(post)
80 if tags:
80 if tags:
81 linked_tags = []
81 linked_tags = []
82 for tag in tags:
82 for tag in tags:
83 tag_linked_tags = tag.get_linked_tags()
83 tag_linked_tags = tag.get_linked_tags()
84 if len(tag_linked_tags) > 0:
84 if len(tag_linked_tags) > 0:
85 linked_tags.extend(tag_linked_tags)
85 linked_tags.extend(tag_linked_tags)
86
86
87 tags.extend(linked_tags)
87 tags.extend(linked_tags)
88 map(thread.add_tag, tags)
88 map(thread.add_tag, tags)
89
89
90 self._delete_old_threads()
90 self._delete_old_threads()
91 self.connect_replies(post)
91 self.connect_replies(post)
92
92
93 return post
93 return post
94
94
95 def delete_post(self, post):
95 def delete_post(self, post):
96 """
96 """
97 Delete post and update or delete its thread
97 Delete post and update or delete its thread
98 """
98 """
99
99
100 thread = post.thread_new
100 thread = post.thread_new
101
101
102 if post.is_opening():
102 if post.is_opening():
103 thread.delete_with_posts()
103 thread.delete_with_posts()
104 else:
104 else:
105 thread.last_edit_time = timezone.now()
105 thread.last_edit_time = timezone.now()
106 thread.save()
106 thread.save()
107
107
108 post.delete()
108 post.delete()
109
109
110 def delete_posts_by_ip(self, ip):
110 def delete_posts_by_ip(self, ip):
111 """
111 """
112 Delete all posts of the author with same IP
112 Delete all posts of the author with same IP
113 """
113 """
114
114
115 posts = self.filter(poster_ip=ip)
115 posts = self.filter(poster_ip=ip)
116 map(self.delete_post, posts)
116 map(self.delete_post, posts)
117
117
118 # TODO This method may not be needed any more, because django's paginator
118 # TODO This method may not be needed any more, because django's paginator
119 # is used
119 # is used
120 def get_threads(self, tag=None, page=ALL_PAGES,
120 def get_threads(self, tag=None, page=ALL_PAGES,
121 order_by='-bump_time', archived=False):
121 order_by='-bump_time', archived=False):
122 if tag:
122 if tag:
123 threads = tag.threads
123 threads = tag.threads
124
124
125 if not threads.exists():
125 if not threads.exists():
126 raise Http404
126 raise Http404
127 else:
127 else:
128 threads = Thread.objects.all()
128 threads = Thread.objects.all()
129
129
130 threads = threads.filter(archived=archived).order_by(order_by)
130 threads = threads.filter(archived=archived).order_by(order_by)
131
131
132 if page != ALL_PAGES:
132 if page != ALL_PAGES:
133 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
133 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
134 page).object_list
134 page).object_list
135
135
136 return threads
136 return threads
137
137
138 # TODO Move this method to thread manager
138 # TODO Move this method to thread manager
139 def _delete_old_threads(self):
139 def _delete_old_threads(self):
140 """
140 """
141 Preserves maximum thread count. If there are too many threads,
141 Preserves maximum thread count. If there are too many threads,
142 archive the old ones.
142 archive the old ones.
143 """
143 """
144
144
145 threads = self.get_threads()
145 threads = self.get_threads()
146 thread_count = threads.count()
146 thread_count = threads.count()
147
147
148 if thread_count > settings.MAX_THREAD_COUNT:
148 if thread_count > settings.MAX_THREAD_COUNT:
149 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
149 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
150 old_threads = threads[thread_count - num_threads_to_delete:]
150 old_threads = threads[thread_count - num_threads_to_delete:]
151
151
152 for thread in old_threads:
152 for thread in old_threads:
153 thread.archived = True
153 thread.archived = True
154 thread.last_edit_time = timezone.now()
154 thread.last_edit_time = timezone.now()
155 thread.save()
155 thread.save()
156
156
157 def connect_replies(self, post):
157 def connect_replies(self, post):
158 """
158 """
159 Connect replies to a post to show them as a reflink map
159 Connect replies to a post to show them as a reflink map
160 """
160 """
161
161
162 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
162 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
163 post_id = reply_number.group(1)
163 post_id = reply_number.group(1)
164 ref_post = self.filter(id=post_id)
164 ref_post = self.filter(id=post_id)
165 if ref_post.count() > 0:
165 if ref_post.count() > 0:
166 referenced_post = ref_post[0]
166 referenced_post = ref_post[0]
167 referenced_post.referenced_posts.add(post)
167 referenced_post.referenced_posts.add(post)
168 referenced_post.last_edit_time = post.pub_time
168 referenced_post.last_edit_time = post.pub_time
169 referenced_post.save()
169 referenced_post.save()
170
170
171 referenced_thread = referenced_post.thread_new
171 referenced_thread = referenced_post.thread_new
172 referenced_thread.last_edit_time = post.pub_time
172 referenced_thread.last_edit_time = post.pub_time
173 referenced_thread.save()
173 referenced_thread.save()
174
174
175 def get_posts_per_day(self):
175 def get_posts_per_day(self):
176 """
176 """
177 Get average count of posts per day for the last 7 days
177 Get average count of posts per day for the last 7 days
178 """
178 """
179
179
180 today = date.today()
180 today = date.today()
181 ppd = cache.get(CACHE_KEY_PPD + str(today))
181 ppd = cache.get(CACHE_KEY_PPD + str(today))
182 if ppd:
182 if ppd:
183 return ppd
183 return ppd
184
184
185 posts_per_days = []
185 posts_per_days = []
186 for i in POSTS_PER_DAY_RANGE:
186 for i in POSTS_PER_DAY_RANGE:
187 day_end = today - timedelta(i + 1)
187 day_end = today - timedelta(i + 1)
188 day_start = today - timedelta(i + 2)
188 day_start = today - timedelta(i + 2)
189
189
190 day_time_start = timezone.make_aware(datetime.combine(day_start,
190 day_time_start = timezone.make_aware(datetime.combine(day_start,
191 dtime()), timezone.get_current_timezone())
191 dtime()), timezone.get_current_timezone())
192 day_time_end = timezone.make_aware(datetime.combine(day_end,
192 day_time_end = timezone.make_aware(datetime.combine(day_end,
193 dtime()), timezone.get_current_timezone())
193 dtime()), timezone.get_current_timezone())
194
194
195 posts_per_days.append(float(self.filter(
195 posts_per_days.append(float(self.filter(
196 pub_time__lte=day_time_end,
196 pub_time__lte=day_time_end,
197 pub_time__gte=day_time_start).count()))
197 pub_time__gte=day_time_start).count()))
198
198
199 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
199 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
200 len(posts_per_days))
200 len(posts_per_days))
201 cache.set(CACHE_KEY_PPD + str(today), ppd)
201 cache.set(CACHE_KEY_PPD + str(today), ppd)
202 return ppd
202 return ppd
203
203
204
204
205 class Post(models.Model):
205 class Post(models.Model):
206 """A post is a message."""
206 """A post is a message."""
207
207
208 objects = PostManager()
208 objects = PostManager()
209
209
210 class Meta:
210 class Meta:
211 app_label = APP_LABEL_BOARDS
211 app_label = APP_LABEL_BOARDS
212
212
213 # TODO Save original file name to some field
213 # TODO Save original file name to some field
214 def _update_image_filename(self, filename):
214 def _update_image_filename(self, filename):
215 """Get unique image filename"""
215 """Get unique image filename"""
216
216
217 path = IMAGES_DIRECTORY
217 path = IMAGES_DIRECTORY
218 new_name = str(int(time.mktime(time.gmtime())))
218 new_name = str(int(time.mktime(time.gmtime())))
219 new_name += str(int(random() * 1000))
219 new_name += str(int(random() * 1000))
220 new_name += FILE_EXTENSION_DELIMITER
220 new_name += FILE_EXTENSION_DELIMITER
221 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
221 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
222
222
223 return os.path.join(path, new_name)
223 return os.path.join(path, new_name)
224
224
225 title = models.CharField(max_length=TITLE_MAX_LENGTH)
225 title = models.CharField(max_length=TITLE_MAX_LENGTH)
226 pub_time = models.DateTimeField()
226 pub_time = models.DateTimeField()
227 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
227 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
228 escape_html=False)
228 escape_html=False)
229
229
230 image_width = models.IntegerField(default=0)
230 image_width = models.IntegerField(default=0)
231 image_height = models.IntegerField(default=0)
231 image_height = models.IntegerField(default=0)
232
232
233 image_pre_width = models.IntegerField(default=0)
233 image_pre_width = models.IntegerField(default=0)
234 image_pre_height = models.IntegerField(default=0)
234 image_pre_height = models.IntegerField(default=0)
235
235
236 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
236 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
237 blank=True, sizes=(IMAGE_THUMB_SIZE,),
237 blank=True, sizes=(IMAGE_THUMB_SIZE,),
238 width_field='image_width',
238 width_field='image_width',
239 height_field='image_height',
239 height_field='image_height',
240 preview_width_field='image_pre_width',
240 preview_width_field='image_pre_width',
241 preview_height_field='image_pre_height')
241 preview_height_field='image_pre_height')
242 image_hash = models.CharField(max_length=36)
242 image_hash = models.CharField(max_length=36)
243
243
244 poster_ip = models.GenericIPAddressField()
244 poster_ip = models.GenericIPAddressField()
245 poster_user_agent = models.TextField()
245 poster_user_agent = models.TextField()
246
246
247 thread = models.ForeignKey('Post', null=True, default=None)
247 thread = models.ForeignKey('Post', null=True, default=None)
248 thread_new = models.ForeignKey('Thread', null=True, default=None)
248 thread_new = models.ForeignKey('Thread', null=True, default=None)
249 last_edit_time = models.DateTimeField()
249 last_edit_time = models.DateTimeField()
250 user = models.ForeignKey('User', null=True, default=None)
250 user = models.ForeignKey('User', null=True, default=None)
251
251
252 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
252 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
253 null=True,
253 null=True,
254 blank=True, related_name='rfp+')
254 blank=True, related_name='rfp+')
255
255
256 def __unicode__(self):
256 def __unicode__(self):
257 return '#' + str(self.id) + ' ' + self.title + ' (' + \
257 return '#' + str(self.id) + ' ' + self.title + ' (' + \
258 self.text.raw[:50] + ')'
258 self.text.raw[:50] + ')'
259
259
260 def get_title(self):
260 def get_title(self):
261 title = self.title
261 title = self.title
262 if len(title) == 0:
262 if len(title) == 0:
263 title = self.text.rendered
263 title = self.text.rendered
264
264
265 return title
265 return title
266
266
267 def get_sorted_referenced_posts(self):
267 def get_sorted_referenced_posts(self):
268 return self.referenced_posts.order_by('id')
268 return self.referenced_posts.order_by('id')
269
269
270 def is_referenced(self):
270 def is_referenced(self):
271 return self.referenced_posts.all().exists()
271 return self.referenced_posts.all().exists()
272
272
273 def is_opening(self):
273 def is_opening(self):
274 return self.thread_new.get_replies()[0] == self
274 return self.thread_new.get_replies()[0] == self
275
275
276 def save(self, *args, **kwargs):
276 def save(self, *args, **kwargs):
277 """
277 """
278 Save the model and compute the image hash
278 Save the model and compute the image hash
279 """
279 """
280
280
281 if not self.pk and self.image:
281 if not self.pk and self.image:
282 md5 = hashlib.md5()
282 md5 = hashlib.md5()
283 for chunk in self.image.chunks():
283 for chunk in self.image.chunks():
284 md5.update(chunk)
284 md5.update(chunk)
285 self.image_hash = md5.hexdigest()
285 self.image_hash = md5.hexdigest()
286 super(Post, self).save(*args, **kwargs)
286 super(Post, self).save(*args, **kwargs)
287
287
288 @transaction.atomic
288 @transaction.atomic
289 def add_tag(self, tag):
289 def add_tag(self, tag):
290 edit_time = timezone.now()
290 edit_time = timezone.now()
291
291
292 thread = self.thread_new
292 thread = self.thread_new
293 thread.add_tag(tag)
293 thread.add_tag(tag)
294 self.last_edit_time = edit_time
294 self.last_edit_time = edit_time
295 self.save()
295 self.save()
296
296
297 thread.last_edit_time = edit_time
297 thread.last_edit_time = edit_time
298 thread.save()
298 thread.save()
299
299
300 @transaction.atomic
300 @transaction.atomic
301 def remove_tag(self, tag):
301 def remove_tag(self, tag):
302 edit_time = timezone.now()
302 edit_time = timezone.now()
303
303
304 thread = self.thread_new
304 thread = self.thread_new
305 thread.remove_tag(tag)
305 thread.remove_tag(tag)
306 self.last_edit_time = edit_time
306 self.last_edit_time = edit_time
307 self.save()
307 self.save()
308
308
309 thread.last_edit_time = edit_time
309 thread.last_edit_time = edit_time
310 thread.save()
310 thread.save()
311
311
312 def get_url(self):
312 def get_url(self):
313 """
313 """
314 Get full url to this post
314 Get full url to this post
315 """
315 """
316
316
317 cache_key = CACHE_KEY_POST_URL + str(self.id)
317 cache_key = CACHE_KEY_POST_URL + str(self.id)
318 link = cache.get(cache_key)
318 link = cache.get(cache_key)
319
319
320 if not link:
320 if not link:
321 if not self.is_opening():
321 opening_post = self.thread_new.get_opening_post()
322 link = reverse('thread', kwargs={
322 if self == opening_post:
323 'post_id': self.thread_new.get_opening_post().id}) + '#' + str(
323 link = reverse('thread',
324 self.id)
324 kwargs={'post_id': opening_post.id}) + '#' + str(
325 self.id)
325 else:
326 else:
326 link = reverse('thread', kwargs={'post_id': self.id})
327 link = reverse('thread', kwargs={'post_id': self.id})
327
328
328 cache.set(cache_key, link)
329 cache.set(cache_key, link)
329
330
330 return link
331 return link
331
332
332
333
333 class Thread(models.Model):
334 class Thread(models.Model):
334
335
335 class Meta:
336 class Meta:
336 app_label = APP_LABEL_BOARDS
337 app_label = APP_LABEL_BOARDS
337
338
338 tags = models.ManyToManyField('Tag')
339 tags = models.ManyToManyField('Tag')
339 bump_time = models.DateTimeField()
340 bump_time = models.DateTimeField()
340 last_edit_time = models.DateTimeField()
341 last_edit_time = models.DateTimeField()
341 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
342 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
342 blank=True, related_name='tre+')
343 blank=True, related_name='tre+')
343 archived = models.BooleanField(default=False)
344 archived = models.BooleanField(default=False)
344
345
345 def get_tags(self):
346 def get_tags(self):
346 """
347 """
347 Get a sorted tag list
348 Get a sorted tag list
348 """
349 """
349
350
350 return self.tags.order_by('name')
351 return self.tags.order_by('name')
351
352
352 def bump(self):
353 def bump(self):
353 """
354 """
354 Bump (move to up) thread
355 Bump (move to up) thread
355 """
356 """
356
357
357 if self.can_bump():
358 if self.can_bump():
358 self.bump_time = timezone.now()
359 self.bump_time = timezone.now()
359
360
360 def get_reply_count(self):
361 def get_reply_count(self):
361 return self.replies.count()
362 return self.replies.count()
362
363
363 def get_images_count(self):
364 def get_images_count(self):
364 return self.replies.filter(image_width__gt=0).count()
365 return self.replies.filter(image_width__gt=0).count()
365
366
366 def can_bump(self):
367 def can_bump(self):
367 """
368 """
368 Check if the thread can be bumped by replying
369 Check if the thread can be bumped by replying
369 """
370 """
370
371
371 if self.archived:
372 if self.archived:
372 return False
373 return False
373
374
374 post_count = self.get_reply_count()
375 post_count = self.get_reply_count()
375
376
376 return post_count < settings.MAX_POSTS_PER_THREAD
377 return post_count < settings.MAX_POSTS_PER_THREAD
377
378
378 def delete_with_posts(self):
379 def delete_with_posts(self):
379 """
380 """
380 Completely delete thread and all its posts
381 Completely delete thread and all its posts
381 """
382 """
382
383
383 if self.replies.count() > 0:
384 if self.replies.count() > 0:
384 self.replies.all().delete()
385 self.replies.all().delete()
385
386
386 self.delete()
387 self.delete()
387
388
388 def get_last_replies(self):
389 def get_last_replies(self):
389 """
390 """
390 Get last replies, not including opening post
391 Get last replies, not including opening post
391 """
392 """
392
393
393 if settings.LAST_REPLIES_COUNT > 0:
394 if settings.LAST_REPLIES_COUNT > 0:
394 reply_count = self.get_reply_count()
395 reply_count = self.get_reply_count()
395
396
396 if reply_count > 0:
397 if reply_count > 0:
397 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
398 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
398 reply_count - 1)
399 reply_count - 1)
399 last_replies = self.replies.all().order_by('pub_time')[
400 last_replies = self.replies.all().order_by('pub_time')[
400 reply_count - reply_count_to_show:]
401 reply_count - reply_count_to_show:]
401
402
402 return last_replies
403 return last_replies
403
404
404 def get_skipped_replies_count(self):
405 def get_skipped_replies_count(self):
405 last_replies = self.get_last_replies()
406 last_replies = self.get_last_replies()
406 return self.get_reply_count() - len(last_replies) - 1
407 return self.get_reply_count() - len(last_replies) - 1
407
408
408 def get_replies(self):
409 def get_replies(self):
409 """
410 """
410 Get sorted thread posts
411 Get sorted thread posts
411 """
412 """
412
413
413 return self.replies.all().order_by('pub_time')
414 return self.replies.all().order_by('pub_time')
414
415
415 def add_tag(self, tag):
416 def add_tag(self, tag):
416 """
417 """
417 Connect thread to a tag and tag to a thread
418 Connect thread to a tag and tag to a thread
418 """
419 """
419
420
420 self.tags.add(tag)
421 self.tags.add(tag)
421 tag.threads.add(self)
422 tag.threads.add(self)
422
423
423 def remove_tag(self, tag):
424 def remove_tag(self, tag):
424 self.tags.remove(tag)
425 self.tags.remove(tag)
425 tag.threads.remove(self)
426 tag.threads.remove(self)
426
427
427 def get_opening_post(self):
428 def get_opening_post(self):
428 """
429 """
429 Get first post of the thread
430 Get first post of the thread
430 """
431 """
431
432
432 return self.get_replies()[0]
433 return self.get_replies()[0]
433
434
434 def __unicode__(self):
435 def __unicode__(self):
435 return str(self.id)
436 return str(self.id)
436
437
437 def get_pub_time(self):
438 def get_pub_time(self):
438 """
439 """
439 Thread does not have its own pub time, so we need to get it from
440 Thread does not have its own pub time, so we need to get it from
440 the opening post
441 the opening post
441 """
442 """
442
443
443 return self.get_opening_post().pub_time
444 return self.get_opening_post().pub_time
General Comments 0
You need to be logged in to leave comments. Login now