##// END OF EJS Templates
Added 'bumpable' field. Disable thread bump once and for all, not counting it...
neko259 -
r863:0dc734ff default
parent child Browse files
Show More
@@ -0,0 +1,74 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import SchemaMigration
5 from django.db import models
6
7
8 class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding field 'Thread.bumpable'
12 db.add_column('boards_thread', 'bumpable',
13 self.gf('django.db.models.fields.BooleanField')(default=True),
14 keep_default=False)
15
16
17 def backwards(self, orm):
18 # Deleting field 'Thread.bumpable'
19 db.delete_column('boards_thread', 'bumpable')
20
21
22 models = {
23 'boards.ban': {
24 'Meta': {'object_name': 'Ban'},
25 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
26 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
27 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
28 'reason': ('django.db.models.fields.CharField', [], {'max_length': '200', 'default': "'Auto'"})
29 },
30 'boards.post': {
31 'Meta': {'object_name': 'Post', 'ordering': "('id',)"},
32 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
33 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
34 'images': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'null': 'True', 'db_index': 'True', 'blank': 'True', 'related_name': "'ip+'", 'to': "orm['boards.PostImage']"}),
35 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
36 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
37 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
38 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
39 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'null': 'True', 'db_index': 'True', 'blank': 'True', 'related_name': "'rfp+'", 'to': "orm['boards.Post']"}),
40 'refmap': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
41 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
42 'text_markup_type': ('django.db.models.fields.CharField', [], {'max_length': '30', 'default': "'bbcode'"}),
43 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'null': 'True', 'to': "orm['boards.Thread']"}),
44 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'})
45 },
46 'boards.postimage': {
47 'Meta': {'object_name': 'PostImage', 'ordering': "('id',)"},
48 'hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
49 'height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
50 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
51 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
52 'pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
53 'pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
54 'width': ('django.db.models.fields.IntegerField', [], {'default': '0'})
55 },
56 'boards.tag': {
57 'Meta': {'object_name': 'Tag', 'ordering': "('name',)"},
58 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
59 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '100'}),
60 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'null': 'True', 'symmetrical': 'False', 'blank': 'True', 'related_name': "'tag+'", 'to': "orm['boards.Thread']"})
61 },
62 'boards.thread': {
63 'Meta': {'object_name': 'Thread'},
64 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
65 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
66 'bumpable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
67 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
68 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
69 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'null': 'True', 'symmetrical': 'False', 'blank': 'True', 'related_name': "'tre+'", 'to': "orm['boards.Post']"}),
70 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']"})
71 }
72 }
73
74 complete_apps = ['boards']
@@ -0,0 +1,81 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import DataMigration
5 from django.db import models
6 import boards
7
8 class Migration(DataMigration):
9
10 def forwards(self, orm):
11 changed_count = 0
12
13 for thread in orm['boards.Thread'].objects.all():
14 if thread.replies.count() >= boards.settings.MAX_POSTS_PER_THREAD:
15 thread.bumpable = False
16 print('Disabled bump on thread {}'.format(thread.id))
17 changed_count += 1
18 else:
19 thread.bumpable = True
20 thread.save(update_fields=['bumpable'])
21
22
23 print('Changed {} threads.'.format(changed_count))
24
25 def backwards(self, orm):
26 "Write your backwards methods here."
27
28 models = {
29 'boards.ban': {
30 'Meta': {'object_name': 'Ban'},
31 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
32 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
33 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
34 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
35 },
36 'boards.post': {
37 'Meta': {'object_name': 'Post', 'ordering': "('id',)"},
38 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
39 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
40 'images': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'ip+'", 'null': 'True', 'to': "orm['boards.PostImage']", 'db_index': 'True', 'blank': 'True'}),
41 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
42 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
43 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
44 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
45 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'rfp+'", 'null': 'True', 'to': "orm['boards.Post']", 'db_index': 'True', 'blank': 'True'}),
46 'refmap': ('django.db.models.fields.TextField', [], {'blank': 'True', 'null': 'True'}),
47 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
48 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'bbcode'", 'max_length': '30'}),
49 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
50 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'})
51 },
52 'boards.postimage': {
53 'Meta': {'object_name': 'PostImage', 'ordering': "('id',)"},
54 'hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
55 'height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
56 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
57 'image': ('boards.thumbs.ImageWithThumbsField', [], {'blank': 'True', 'max_length': '100'}),
58 'pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
59 'pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
60 'width': ('django.db.models.fields.IntegerField', [], {'default': '0'})
61 },
62 'boards.tag': {
63 'Meta': {'object_name': 'Tag', 'ordering': "('name',)"},
64 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
65 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '100'}),
66 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'null': 'True', 'to': "orm['boards.Thread']", 'related_name': "'tag+'", 'blank': 'True'})
67 },
68 'boards.thread': {
69 'Meta': {'object_name': 'Thread'},
70 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
71 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
72 'bumpable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
73 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
74 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
75 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'null': 'True', 'to': "orm['boards.Post']", 'related_name': "'tre+'", 'blank': 'True'}),
76 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']"})
77 }
78 }
79
80 complete_apps = ['boards']
81 symmetrical = True
@@ -1,417 +1,421 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 from adjacent import Client
3 from adjacent import Client
4 import logging
4 import logging
5 import re
5 import re
6
6
7 from django.core.cache import cache
7 from django.core.cache import cache
8 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
9 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.shortcuts import get_object_or_404
10 from django.shortcuts import get_object_or_404
11 from django.template import RequestContext
11 from django.template import RequestContext
12 from django.template.loader import render_to_string
12 from django.template.loader import render_to_string
13 from django.utils import timezone
13 from django.utils import timezone
14 from markupfield.fields import MarkupField
14 from markupfield.fields import MarkupField
15 from boards import settings
15 from boards import settings
16
16
17 from boards.models import PostImage
17 from boards.models import PostImage
18 from boards.models.base import Viewable
18 from boards.models.base import Viewable
19 from boards.models.thread import Thread
19 from boards.models.thread import Thread
20 from boards.utils import datetime_to_epoch
20 from boards.utils import datetime_to_epoch
21
21
22 WS_CHANNEL_THREAD = "thread:"
22 WS_CHANNEL_THREAD = "thread:"
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 = 7
29 POSTS_PER_DAY_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 = 200
35 TITLE_MAX_LENGTH = 200
36
36
37 DEFAULT_MARKUP_TYPE = 'bbcode'
37 DEFAULT_MARKUP_TYPE = 'bbcode'
38
38
39 # TODO This should be removed
39 # TODO This should be removed
40 NO_IP = '0.0.0.0'
40 NO_IP = '0.0.0.0'
41
41
42 # TODO Real user agent should be saved instead of this
42 # TODO Real user agent should be saved instead of this
43 UNKNOWN_UA = ''
43 UNKNOWN_UA = ''
44
44
45 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
45 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
46
46
47 PARAMETER_TRUNCATED = 'truncated'
47 PARAMETER_TRUNCATED = 'truncated'
48 PARAMETER_TAG = 'tag'
48 PARAMETER_TAG = 'tag'
49 PARAMETER_OFFSET = 'offset'
49 PARAMETER_OFFSET = 'offset'
50 PARAMETER_DIFF_TYPE = 'type'
50 PARAMETER_DIFF_TYPE = 'type'
51
51
52 DIFF_TYPE_HTML = 'html'
52 DIFF_TYPE_HTML = 'html'
53 DIFF_TYPE_JSON = 'json'
53 DIFF_TYPE_JSON = 'json'
54
54
55 logger = logging.getLogger(__name__)
55 logger = logging.getLogger(__name__)
56
56
57
57
58 class PostManager(models.Manager):
58 class PostManager(models.Manager):
59 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
59 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
60 tags=None):
60 tags=None):
61 """
61 """
62 Creates new post
62 Creates new post
63 """
63 """
64
64
65 if not tags:
65 if not tags:
66 tags = []
66 tags = []
67
67
68 posting_time = timezone.now()
68 posting_time = timezone.now()
69 if not thread:
69 if not thread:
70 thread = Thread.objects.create(bump_time=posting_time,
70 thread = Thread.objects.create(bump_time=posting_time,
71 last_edit_time=posting_time)
71 last_edit_time=posting_time)
72 new_thread = True
72 new_thread = True
73 else:
73 else:
74 thread.bump()
74 thread.bump()
75 thread.last_edit_time = posting_time
75 thread.last_edit_time = posting_time
76 thread.save()
76 if thread.can_bump() and (
77 thread.get_reply_count() >= settings.MAX_POSTS_PER_THREAD):
78 thread.bumpable = False
79 thread.save(update_fields=['last_edit_time', 'bumpable'])
77 new_thread = False
80 new_thread = False
78
81
79 post = self.create(title=title,
82 post = self.create(title=title,
80 text=text,
83 text=text,
81 pub_time=posting_time,
84 pub_time=posting_time,
82 thread_new=thread,
85 thread_new=thread,
83 poster_ip=ip,
86 poster_ip=ip,
84 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
87 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
85 # last!
88 # last!
86 last_edit_time=posting_time)
89 last_edit_time=posting_time)
87
90
88 logger.info('Created post #%d with title "%s"' % (post.id,
91 logger.info('Created post #%d with title "%s"' % (post.id,
89 post.title))
92 post.title))
90
93
91 if image:
94 if image:
92 post_image = PostImage.objects.create(image=image)
95 post_image = PostImage.objects.create(image=image)
93 post.images.add(post_image)
96 post.images.add(post_image)
94 logger.info('Created image #%d for post #%d' % (post_image.id,
97 logger.info('Created image #%d for post #%d' % (post_image.id,
95 post.id))
98 post.id))
96
99
97 thread.replies.add(post)
100 thread.replies.add(post)
98 list(map(thread.add_tag, tags))
101 list(map(thread.add_tag, tags))
99
102
100 if new_thread:
103 if new_thread:
101 Thread.objects.process_oldest_threads()
104 Thread.objects.process_oldest_threads()
102 self.connect_replies(post)
105 self.connect_replies(post)
103
106
107
104 return post
108 return post
105
109
106 def delete_post(self, post):
110 def delete_post(self, post):
107 """
111 """
108 Deletes post and update or delete its thread
112 Deletes post and update or delete its thread
109 """
113 """
110
114
111 post_id = post.id
115 post_id = post.id
112
116
113 thread = post.get_thread()
117 thread = post.get_thread()
114
118
115 if post.is_opening():
119 if post.is_opening():
116 thread.delete()
120 thread.delete()
117 else:
121 else:
118 thread.last_edit_time = timezone.now()
122 thread.last_edit_time = timezone.now()
119 thread.save()
123 thread.save()
120
124
121 post.delete()
125 post.delete()
122
126
123 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
127 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
124
128
125 def delete_posts_by_ip(self, ip):
129 def delete_posts_by_ip(self, ip):
126 """
130 """
127 Deletes all posts of the author with same IP
131 Deletes all posts of the author with same IP
128 """
132 """
129
133
130 posts = self.filter(poster_ip=ip)
134 posts = self.filter(poster_ip=ip)
131 for post in posts:
135 for post in posts:
132 self.delete_post(post)
136 self.delete_post(post)
133
137
134 def connect_replies(self, post):
138 def connect_replies(self, post):
135 """
139 """
136 Connects replies to a post to show them as a reflink map
140 Connects replies to a post to show them as a reflink map
137 """
141 """
138
142
139 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
143 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
140 post_id = reply_number.group(1)
144 post_id = reply_number.group(1)
141 ref_post = self.filter(id=post_id)
145 ref_post = self.filter(id=post_id)
142 if ref_post.count() > 0:
146 if ref_post.count() > 0:
143 referenced_post = ref_post[0]
147 referenced_post = ref_post[0]
144 referenced_post.referenced_posts.add(post)
148 referenced_post.referenced_posts.add(post)
145 referenced_post.last_edit_time = post.pub_time
149 referenced_post.last_edit_time = post.pub_time
146 referenced_post.build_refmap()
150 referenced_post.build_refmap()
147 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
151 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
148
152
149 referenced_thread = referenced_post.get_thread()
153 referenced_thread = referenced_post.get_thread()
150 referenced_thread.last_edit_time = post.pub_time
154 referenced_thread.last_edit_time = post.pub_time
151 referenced_thread.save(update_fields=['last_edit_time'])
155 referenced_thread.save(update_fields=['last_edit_time'])
152
156
153 def get_posts_per_day(self):
157 def get_posts_per_day(self):
154 """
158 """
155 Gets average count of posts per day for the last 7 days
159 Gets average count of posts per day for the last 7 days
156 """
160 """
157
161
158 day_end = date.today()
162 day_end = date.today()
159 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
163 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
160
164
161 cache_key = CACHE_KEY_PPD + str(day_end)
165 cache_key = CACHE_KEY_PPD + str(day_end)
162 ppd = cache.get(cache_key)
166 ppd = cache.get(cache_key)
163 if ppd:
167 if ppd:
164 return ppd
168 return ppd
165
169
166 day_time_start = timezone.make_aware(datetime.combine(
170 day_time_start = timezone.make_aware(datetime.combine(
167 day_start, dtime()), timezone.get_current_timezone())
171 day_start, dtime()), timezone.get_current_timezone())
168 day_time_end = timezone.make_aware(datetime.combine(
172 day_time_end = timezone.make_aware(datetime.combine(
169 day_end, dtime()), timezone.get_current_timezone())
173 day_end, dtime()), timezone.get_current_timezone())
170
174
171 posts_per_period = float(self.filter(
175 posts_per_period = float(self.filter(
172 pub_time__lte=day_time_end,
176 pub_time__lte=day_time_end,
173 pub_time__gte=day_time_start).count())
177 pub_time__gte=day_time_start).count())
174
178
175 ppd = posts_per_period / POSTS_PER_DAY_RANGE
179 ppd = posts_per_period / POSTS_PER_DAY_RANGE
176
180
177 cache.set(cache_key, ppd)
181 cache.set(cache_key, ppd)
178 return ppd
182 return ppd
179
183
180
184
181 class Post(models.Model, Viewable):
185 class Post(models.Model, Viewable):
182 """A post is a message."""
186 """A post is a message."""
183
187
184 objects = PostManager()
188 objects = PostManager()
185
189
186 class Meta:
190 class Meta:
187 app_label = APP_LABEL_BOARDS
191 app_label = APP_LABEL_BOARDS
188 ordering = ('id',)
192 ordering = ('id',)
189
193
190 title = models.CharField(max_length=TITLE_MAX_LENGTH)
194 title = models.CharField(max_length=TITLE_MAX_LENGTH)
191 pub_time = models.DateTimeField()
195 pub_time = models.DateTimeField()
192 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
196 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
193 escape_html=False)
197 escape_html=False)
194
198
195 images = models.ManyToManyField(PostImage, null=True, blank=True,
199 images = models.ManyToManyField(PostImage, null=True, blank=True,
196 related_name='ip+', db_index=True)
200 related_name='ip+', db_index=True)
197
201
198 poster_ip = models.GenericIPAddressField()
202 poster_ip = models.GenericIPAddressField()
199 poster_user_agent = models.TextField()
203 poster_user_agent = models.TextField()
200
204
201 thread_new = models.ForeignKey('Thread', null=True, default=None,
205 thread_new = models.ForeignKey('Thread', null=True, default=None,
202 db_index=True)
206 db_index=True)
203 last_edit_time = models.DateTimeField()
207 last_edit_time = models.DateTimeField()
204
208
205 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
209 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
206 null=True,
210 null=True,
207 blank=True, related_name='rfp+',
211 blank=True, related_name='rfp+',
208 db_index=True)
212 db_index=True)
209 refmap = models.TextField(null=True, blank=True)
213 refmap = models.TextField(null=True, blank=True)
210
214
211 def __unicode__(self):
215 def __unicode__(self):
212 return '#' + str(self.id) + ' ' + self.title + ' (' + \
216 return '#' + str(self.id) + ' ' + self.title + ' (' + \
213 self.text.raw[:50] + ')'
217 self.text.raw[:50] + ')'
214
218
215 def get_title(self):
219 def get_title(self):
216 """
220 """
217 Gets original post title or part of its text.
221 Gets original post title or part of its text.
218 """
222 """
219
223
220 title = self.title
224 title = self.title
221 if not title:
225 if not title:
222 title = self.text.rendered
226 title = self.text.rendered
223
227
224 return title
228 return title
225
229
226 def build_refmap(self):
230 def build_refmap(self):
227 """
231 """
228 Builds a replies map string from replies list. This is a cache to stop
232 Builds a replies map string from replies list. This is a cache to stop
229 the server from recalculating the map on every post show.
233 the server from recalculating the map on every post show.
230 """
234 """
231 map_string = ''
235 map_string = ''
232
236
233 first = True
237 first = True
234 for refpost in self.referenced_posts.all():
238 for refpost in self.referenced_posts.all():
235 if not first:
239 if not first:
236 map_string += ', '
240 map_string += ', '
237 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
241 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
238 refpost.id)
242 refpost.id)
239 first = False
243 first = False
240
244
241 self.refmap = map_string
245 self.refmap = map_string
242
246
243 def get_sorted_referenced_posts(self):
247 def get_sorted_referenced_posts(self):
244 return self.refmap
248 return self.refmap
245
249
246 def is_referenced(self):
250 def is_referenced(self):
247 return len(self.refmap) > 0
251 return len(self.refmap) > 0
248
252
249 def is_opening(self):
253 def is_opening(self):
250 """
254 """
251 Checks if this is an opening post or just a reply.
255 Checks if this is an opening post or just a reply.
252 """
256 """
253
257
254 return self.get_thread().get_opening_post_id() == self.id
258 return self.get_thread().get_opening_post_id() == self.id
255
259
256 @transaction.atomic
260 @transaction.atomic
257 def add_tag(self, tag):
261 def add_tag(self, tag):
258 edit_time = timezone.now()
262 edit_time = timezone.now()
259
263
260 thread = self.get_thread()
264 thread = self.get_thread()
261 thread.add_tag(tag)
265 thread.add_tag(tag)
262 self.last_edit_time = edit_time
266 self.last_edit_time = edit_time
263 self.save(update_fields=['last_edit_time'])
267 self.save(update_fields=['last_edit_time'])
264
268
265 thread.last_edit_time = edit_time
269 thread.last_edit_time = edit_time
266 thread.save(update_fields=['last_edit_time'])
270 thread.save(update_fields=['last_edit_time'])
267
271
268 @transaction.atomic
272 @transaction.atomic
269 def remove_tag(self, tag):
273 def remove_tag(self, tag):
270 edit_time = timezone.now()
274 edit_time = timezone.now()
271
275
272 thread = self.get_thread()
276 thread = self.get_thread()
273 thread.remove_tag(tag)
277 thread.remove_tag(tag)
274 self.last_edit_time = edit_time
278 self.last_edit_time = edit_time
275 self.save(update_fields=['last_edit_time'])
279 self.save(update_fields=['last_edit_time'])
276
280
277 thread.last_edit_time = edit_time
281 thread.last_edit_time = edit_time
278 thread.save(update_fields=['last_edit_time'])
282 thread.save(update_fields=['last_edit_time'])
279
283
280 def get_url(self, thread=None):
284 def get_url(self, thread=None):
281 """
285 """
282 Gets full url to the post.
286 Gets full url to the post.
283 """
287 """
284
288
285 cache_key = CACHE_KEY_POST_URL + str(self.id)
289 cache_key = CACHE_KEY_POST_URL + str(self.id)
286 link = cache.get(cache_key)
290 link = cache.get(cache_key)
287
291
288 if not link:
292 if not link:
289 if not thread:
293 if not thread:
290 thread = self.get_thread()
294 thread = self.get_thread()
291
295
292 opening_id = thread.get_opening_post_id()
296 opening_id = thread.get_opening_post_id()
293
297
294 if self.id != opening_id:
298 if self.id != opening_id:
295 link = reverse('thread', kwargs={
299 link = reverse('thread', kwargs={
296 'post_id': opening_id}) + '#' + str(self.id)
300 'post_id': opening_id}) + '#' + str(self.id)
297 else:
301 else:
298 link = reverse('thread', kwargs={'post_id': self.id})
302 link = reverse('thread', kwargs={'post_id': self.id})
299
303
300 cache.set(cache_key, link)
304 cache.set(cache_key, link)
301
305
302 return link
306 return link
303
307
304 def get_thread(self):
308 def get_thread(self):
305 """
309 """
306 Gets post's thread.
310 Gets post's thread.
307 """
311 """
308
312
309 return self.thread_new
313 return self.thread_new
310
314
311 def get_referenced_posts(self):
315 def get_referenced_posts(self):
312 return self.referenced_posts.only('id', 'thread_new')
316 return self.referenced_posts.only('id', 'thread_new')
313
317
314 def get_text(self):
318 def get_text(self):
315 return self.text
319 return self.text
316
320
317 def get_view(self, moderator=False, need_open_link=False,
321 def get_view(self, moderator=False, need_open_link=False,
318 truncated=False, *args, **kwargs):
322 truncated=False, *args, **kwargs):
319 if 'is_opening' in kwargs:
323 if 'is_opening' in kwargs:
320 is_opening = kwargs['is_opening']
324 is_opening = kwargs['is_opening']
321 else:
325 else:
322 is_opening = self.is_opening()
326 is_opening = self.is_opening()
323
327
324 if 'thread' in kwargs:
328 if 'thread' in kwargs:
325 thread = kwargs['thread']
329 thread = kwargs['thread']
326 else:
330 else:
327 thread = self.get_thread()
331 thread = self.get_thread()
328
332
329 if 'can_bump' in kwargs:
333 if 'can_bump' in kwargs:
330 can_bump = kwargs['can_bump']
334 can_bump = kwargs['can_bump']
331 else:
335 else:
332 can_bump = thread.can_bump()
336 can_bump = thread.can_bump()
333
337
334 if is_opening:
338 if is_opening:
335 opening_post_id = self.id
339 opening_post_id = self.id
336 else:
340 else:
337 opening_post_id = thread.get_opening_post_id()
341 opening_post_id = thread.get_opening_post_id()
338
342
339 return render_to_string('boards/post.html', {
343 return render_to_string('boards/post.html', {
340 'post': self,
344 'post': self,
341 'moderator': moderator,
345 'moderator': moderator,
342 'is_opening': is_opening,
346 'is_opening': is_opening,
343 'thread': thread,
347 'thread': thread,
344 'bumpable': can_bump,
348 'bumpable': can_bump,
345 'need_open_link': need_open_link,
349 'need_open_link': need_open_link,
346 'truncated': truncated,
350 'truncated': truncated,
347 'opening_post_id': opening_post_id,
351 'opening_post_id': opening_post_id,
348 })
352 })
349
353
350 def get_first_image(self):
354 def get_first_image(self):
351 return self.images.earliest('id')
355 return self.images.earliest('id')
352
356
353 def delete(self, using=None):
357 def delete(self, using=None):
354 """
358 """
355 Deletes all post images and the post itself.
359 Deletes all post images and the post itself.
356 """
360 """
357
361
358 self.images.all().delete()
362 self.images.all().delete()
359
363
360 super(Post, self).delete(using)
364 super(Post, self).delete(using)
361
365
362 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
366 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
363 include_last_update=False):
367 include_last_update=False):
364 """
368 """
365 Gets post HTML or JSON data that can be rendered on a page or used by
369 Gets post HTML or JSON data that can be rendered on a page or used by
366 API.
370 API.
367 """
371 """
368
372
369 if format_type == DIFF_TYPE_HTML:
373 if format_type == DIFF_TYPE_HTML:
370 context = RequestContext(request)
374 context = RequestContext(request)
371 context['post'] = self
375 context['post'] = self
372 if PARAMETER_TRUNCATED in request.GET:
376 if PARAMETER_TRUNCATED in request.GET:
373 context[PARAMETER_TRUNCATED] = True
377 context[PARAMETER_TRUNCATED] = True
374
378
375 return render_to_string('boards/api_post.html', context)
379 return render_to_string('boards/api_post.html', context)
376 elif format_type == DIFF_TYPE_JSON:
380 elif format_type == DIFF_TYPE_JSON:
377 post_json = {
381 post_json = {
378 'id': self.id,
382 'id': self.id,
379 'title': self.title,
383 'title': self.title,
380 'text': self.text.rendered,
384 'text': self.text.rendered,
381 }
385 }
382 if self.images.exists():
386 if self.images.exists():
383 post_image = self.get_first_image()
387 post_image = self.get_first_image()
384 post_json['image'] = post_image.image.url
388 post_json['image'] = post_image.image.url
385 post_json['image_preview'] = post_image.image.url_200x150
389 post_json['image_preview'] = post_image.image.url_200x150
386 if include_last_update:
390 if include_last_update:
387 post_json['bump_time'] = datetime_to_epoch(
391 post_json['bump_time'] = datetime_to_epoch(
388 self.thread_new.bump_time)
392 self.thread_new.bump_time)
389 return post_json
393 return post_json
390
394
391 def send_to_websocket(self, request, recursive=True):
395 def send_to_websocket(self, request, recursive=True):
392 """
396 """
393 Sends post HTML data to the thread web socket.
397 Sends post HTML data to the thread web socket.
394 """
398 """
395
399
396 if not settings.WEBSOCKETS_ENABLED:
400 if not settings.WEBSOCKETS_ENABLED:
397 return
401 return
398
402
399 client = Client()
403 client = Client()
400
404
401 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
405 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
402 client.publish(channel_name, {
406 client.publish(channel_name, {
403 'html': self.get_post_data(
407 'html': self.get_post_data(
404 format_type=DIFF_TYPE_HTML,
408 format_type=DIFF_TYPE_HTML,
405 request=request),
409 request=request),
406 'diff_type': 'added' if recursive else 'updated',
410 'diff_type': 'added' if recursive else 'updated',
407 })
411 })
408 client.send()
412 client.send()
409
413
410 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
414 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
411
415
412 if recursive:
416 if recursive:
413 for reply_number in re.finditer(REGEX_REPLY, self.text.raw):
417 for reply_number in re.finditer(REGEX_REPLY, self.text.raw):
414 post_id = reply_number.group(1)
418 post_id = reply_number.group(1)
415 ref_post = Post.objects.filter(id=post_id)[0]
419 ref_post = Post.objects.filter(id=post_id)[0]
416
420
417 ref_post.send_to_websocket(request, recursive=False) No newline at end of file
421 ref_post.send_to_websocket(request, recursive=False)
@@ -1,188 +1,185 b''
1 import logging
1 import logging
2 from django.db.models import Count
2 from django.db.models import Count
3 from django.utils import timezone
3 from django.utils import timezone
4 from django.core.cache import cache
4 from django.core.cache import cache
5 from django.db import models
5 from django.db import models
6 from boards import settings
6 from boards import settings
7
7
8 __author__ = 'neko259'
8 __author__ = 'neko259'
9
9
10
10
11 logger = logging.getLogger(__name__)
11 logger = logging.getLogger(__name__)
12
12
13
13
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
15
15
16
16
17 class ThreadManager(models.Manager):
17 class ThreadManager(models.Manager):
18 def process_oldest_threads(self):
18 def process_oldest_threads(self):
19 """
19 """
20 Preserves maximum thread count. If there are too many threads,
20 Preserves maximum thread count. If there are too many threads,
21 archive or delete the old ones.
21 archive or delete the old ones.
22 """
22 """
23
23
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
25 thread_count = threads.count()
25 thread_count = threads.count()
26
26
27 if thread_count > settings.MAX_THREAD_COUNT:
27 if thread_count > settings.MAX_THREAD_COUNT:
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
28 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
29 old_threads = threads[thread_count - num_threads_to_delete:]
29 old_threads = threads[thread_count - num_threads_to_delete:]
30
30
31 for thread in old_threads:
31 for thread in old_threads:
32 if settings.ARCHIVE_THREADS:
32 if settings.ARCHIVE_THREADS:
33 self._archive_thread(thread)
33 self._archive_thread(thread)
34 else:
34 else:
35 thread.delete()
35 thread.delete()
36
36
37 logger.info('Processed %d old threads' % num_threads_to_delete)
37 logger.info('Processed %d old threads' % num_threads_to_delete)
38
38
39 def _archive_thread(self, thread):
39 def _archive_thread(self, thread):
40 thread.archived = True
40 thread.archived = True
41 thread.bumpable = False
41 thread.last_edit_time = timezone.now()
42 thread.last_edit_time = timezone.now()
42 thread.save(update_fields=['archived', 'last_edit_time'])
43 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
43
44
44
45
45 class Thread(models.Model):
46 class Thread(models.Model):
46 objects = ThreadManager()
47 objects = ThreadManager()
47
48
48 class Meta:
49 class Meta:
49 app_label = 'boards'
50 app_label = 'boards'
50
51
51 tags = models.ManyToManyField('Tag')
52 tags = models.ManyToManyField('Tag')
52 bump_time = models.DateTimeField()
53 bump_time = models.DateTimeField()
53 last_edit_time = models.DateTimeField()
54 last_edit_time = models.DateTimeField()
54 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
55 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
55 blank=True, related_name='tre+')
56 blank=True, related_name='tre+')
56 archived = models.BooleanField(default=False)
57 archived = models.BooleanField(default=False)
58 bumpable = models.BooleanField(default=True)
57
59
58 def get_tags(self):
60 def get_tags(self):
59 """
61 """
60 Gets a sorted tag list.
62 Gets a sorted tag list.
61 """
63 """
62
64
63 return self.tags.order_by('name')
65 return self.tags.order_by('name')
64
66
65 def bump(self):
67 def bump(self):
66 """
68 """
67 Bumps (moves to up) thread if possible.
69 Bumps (moves to up) thread if possible.
68 """
70 """
69
71
70 if self.can_bump():
72 if self.can_bump():
71 self.bump_time = timezone.now()
73 self.bump_time = timezone.now()
72
74
73 logger.info('Bumped thread %d' % self.id)
75 logger.info('Bumped thread %d' % self.id)
74
76
75 def get_reply_count(self):
77 def get_reply_count(self):
76 return self.replies.count()
78 return self.replies.count()
77
79
78 def get_images_count(self):
80 def get_images_count(self):
79 # TODO Use sum
81 # TODO Use sum
80 total_count = 0
82 total_count = 0
81 for post_with_image in self.replies.annotate(images_count=Count(
83 for post_with_image in self.replies.annotate(images_count=Count(
82 'images')):
84 'images')):
83 total_count += post_with_image.images_count
85 total_count += post_with_image.images_count
84 return total_count
86 return total_count
85
87
86 def can_bump(self):
88 def can_bump(self):
87 """
89 """
88 Checks if the thread can be bumped by replying to it.
90 Checks if the thread can be bumped by replying to it.
89 """
91 """
90
92
91 if self.archived:
93 return self.bumpable
92 return False
93
94 post_count = self.get_reply_count()
95
96 return post_count < settings.MAX_POSTS_PER_THREAD
97
94
98 def get_last_replies(self):
95 def get_last_replies(self):
99 """
96 """
100 Gets several last replies, not including opening post
97 Gets several last replies, not including opening post
101 """
98 """
102
99
103 if settings.LAST_REPLIES_COUNT > 0:
100 if settings.LAST_REPLIES_COUNT > 0:
104 reply_count = self.get_reply_count()
101 reply_count = self.get_reply_count()
105
102
106 if reply_count > 0:
103 if reply_count > 0:
107 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
104 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
108 reply_count - 1)
105 reply_count - 1)
109 replies = self.get_replies()
106 replies = self.get_replies()
110 last_replies = replies[reply_count - reply_count_to_show:]
107 last_replies = replies[reply_count - reply_count_to_show:]
111
108
112 return last_replies
109 return last_replies
113
110
114 def get_skipped_replies_count(self):
111 def get_skipped_replies_count(self):
115 """
112 """
116 Gets number of posts between opening post and last replies.
113 Gets number of posts between opening post and last replies.
117 """
114 """
118 reply_count = self.get_reply_count()
115 reply_count = self.get_reply_count()
119 last_replies_count = min(settings.LAST_REPLIES_COUNT,
116 last_replies_count = min(settings.LAST_REPLIES_COUNT,
120 reply_count - 1)
117 reply_count - 1)
121 return reply_count - last_replies_count - 1
118 return reply_count - last_replies_count - 1
122
119
123 def get_replies(self, view_fields_only=False):
120 def get_replies(self, view_fields_only=False):
124 """
121 """
125 Gets sorted thread posts
122 Gets sorted thread posts
126 """
123 """
127
124
128 query = self.replies.order_by('pub_time').prefetch_related('images')
125 query = self.replies.order_by('pub_time').prefetch_related('images')
129 if view_fields_only:
126 if view_fields_only:
130 query = query.defer('poster_user_agent', 'text_markup_type')
127 query = query.defer('poster_user_agent', 'text_markup_type')
131 return query.all()
128 return query.all()
132
129
133 def get_replies_with_images(self, view_fields_only=False):
130 def get_replies_with_images(self, view_fields_only=False):
134 return self.get_replies(view_fields_only).annotate(images_count=Count(
131 return self.get_replies(view_fields_only).annotate(images_count=Count(
135 'images')).filter(images_count__gt=0)
132 'images')).filter(images_count__gt=0)
136
133
137 def add_tag(self, tag):
134 def add_tag(self, tag):
138 """
135 """
139 Connects thread to a tag and tag to a thread
136 Connects thread to a tag and tag to a thread
140 """
137 """
141
138
142 self.tags.add(tag)
139 self.tags.add(tag)
143 tag.threads.add(self)
140 tag.threads.add(self)
144
141
145 def remove_tag(self, tag):
142 def remove_tag(self, tag):
146 self.tags.remove(tag)
143 self.tags.remove(tag)
147 tag.threads.remove(self)
144 tag.threads.remove(self)
148
145
149 def get_opening_post(self, only_id=False):
146 def get_opening_post(self, only_id=False):
150 """
147 """
151 Gets the first post of the thread
148 Gets the first post of the thread
152 """
149 """
153
150
154 query = self.replies.order_by('pub_time')
151 query = self.replies.order_by('pub_time')
155 if only_id:
152 if only_id:
156 query = query.only('id')
153 query = query.only('id')
157 opening_post = query.first()
154 opening_post = query.first()
158
155
159 return opening_post
156 return opening_post
160
157
161 def get_opening_post_id(self):
158 def get_opening_post_id(self):
162 """
159 """
163 Gets ID of the first thread post.
160 Gets ID of the first thread post.
164 """
161 """
165
162
166 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
163 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
167 opening_post_id = cache.get(cache_key)
164 opening_post_id = cache.get(cache_key)
168 if not opening_post_id:
165 if not opening_post_id:
169 opening_post_id = self.get_opening_post(only_id=True).id
166 opening_post_id = self.get_opening_post(only_id=True).id
170 cache.set(cache_key, opening_post_id)
167 cache.set(cache_key, opening_post_id)
171
168
172 return opening_post_id
169 return opening_post_id
173
170
174 def __unicode__(self):
171 def __unicode__(self):
175 return str(self.id)
172 return str(self.id)
176
173
177 def get_pub_time(self):
174 def get_pub_time(self):
178 """
175 """
179 Gets opening post's pub time because thread does not have its own one.
176 Gets opening post's pub time because thread does not have its own one.
180 """
177 """
181
178
182 return self.get_opening_post().pub_time
179 return self.get_opening_post().pub_time
183
180
184 def delete(self, using=None):
181 def delete(self, using=None):
185 if self.replies.exists():
182 if self.replies.exists():
186 self.replies.all().delete()
183 self.replies.all().delete()
187
184
188 super(Thread, self).delete(using) No newline at end of file
185 super(Thread, self).delete(using)
@@ -1,282 +1,285 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var wsUrl = 'ws://localhost:9090/connection/websocket';
26 var wsUrl = 'ws://localhost:9090/connection/websocket';
27 var wsUser = '';
27 var wsUser = '';
28
28
29 var loading = false;
29 var loading = false;
30 var lastUpdateTime = null;
30 var lastUpdateTime = null;
31 var unreadPosts = 0;
31 var unreadPosts = 0;
32
32
33 // Thread ID does not change, can be stored one time
33 // Thread ID does not change, can be stored one time
34 var threadId = $('div.thread').children('.post').first().attr('id');
34 var threadId = $('div.thread').children('.post').first().attr('id');
35
35
36 function connectWebsocket() {
36 function connectWebsocket() {
37 var metapanel = $('.metapanel')[0];
37 var metapanel = $('.metapanel')[0];
38
38
39 var wsHost = metapanel.getAttribute('data-ws-host');
39 var wsHost = metapanel.getAttribute('data-ws-host');
40 var wsPort = metapanel.getAttribute('data-ws-port');
40 var wsPort = metapanel.getAttribute('data-ws-port');
41
41
42 if (wsHost.length > 0 && wsPort.length > 0)
42 if (wsHost.length > 0 && wsPort.length > 0)
43 var centrifuge = new Centrifuge({
43 var centrifuge = new Centrifuge({
44 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
44 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
45 "project": metapanel.getAttribute('data-ws-project'),
45 "project": metapanel.getAttribute('data-ws-project'),
46 "user": wsUser,
46 "user": wsUser,
47 "timestamp": metapanel.getAttribute('data-last-update'),
47 "timestamp": metapanel.getAttribute('data-last-update'),
48 "token": metapanel.getAttribute('data-ws-token'),
48 "token": metapanel.getAttribute('data-ws-token'),
49 "debug": false
49 "debug": false
50 });
50 });
51
51
52 centrifuge.on('error', function(error_message) {
52 centrifuge.on('error', function(error_message) {
53 alert("Error connecting to websocket server.");
53 console.log("Error connecting to websocket server.");
54 return false;
54 });
55 });
55
56
56 centrifuge.on('connect', function() {
57 centrifuge.on('connect', function() {
57 var channelName = 'thread:' + threadId;
58 var channelName = 'thread:' + threadId;
58 centrifuge.subscribe(channelName, function(message) {
59 centrifuge.subscribe(channelName, function(message) {
59 var postHtml = message.data['html'];
60 var postHtml = message.data['html'];
60 var isAdded = (message.data['diff_type'] === 'added');
61 var isAdded = (message.data['diff_type'] === 'added');
61
62
62 if (postHtml) {
63 if (postHtml) {
63 updatePost(postHtml, isAdded);
64 updatePost(postHtml, isAdded);
64 }
65 }
65 });
66 });
66
67
67 $('#autoupdate').text('[+]');
68 $('#autoupdate').text('[+]');
68 });
69 });
69
70
70 centrifuge.connect();
71 centrifuge.connect();
72
73 return true;
71 }
74 }
72
75
73 function updatePost(postHtml, isAdded) {
76 function updatePost(postHtml, isAdded) {
74 // This needs to be set on start because the page is scrolled after posts
77 // This needs to be set on start because the page is scrolled after posts
75 // are added or updated
78 // are added or updated
76 var bottom = isPageBottom();
79 var bottom = isPageBottom();
77
80
78 var post = $(postHtml);
81 var post = $(postHtml);
79
82
80 var threadPosts = $('div.thread').children('.post');
83 var threadPosts = $('div.thread').children('.post');
81
84
82 var lastUpdate = '';
85 var lastUpdate = '';
83
86
84 if (isAdded) {
87 if (isAdded) {
85 var lastPost = threadPosts.last();
88 var lastPost = threadPosts.last();
86
89
87 post.appendTo(lastPost.parent());
90 post.appendTo(lastPost.parent());
88
91
89 updateBumplimitProgress(1);
92 updateBumplimitProgress(1);
90 showNewPostsTitle(1);
93 showNewPostsTitle(1);
91
94
92 lastUpdate = post.children('.post-info').first()
95 lastUpdate = post.children('.post-info').first()
93 .children('.pub_time').first().text();
96 .children('.pub_time').first().text();
94
97
95 if (bottom) {
98 if (bottom) {
96 scrollToBottom();
99 scrollToBottom();
97 }
100 }
98 } else {
101 } else {
99 var postId = post.attr('id');
102 var postId = post.attr('id');
100
103
101 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
104 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
102
105
103 oldPost.replaceWith(post);
106 oldPost.replaceWith(post);
104 }
107 }
105
108
106 processNewPost(post);
109 processNewPost(post);
107 updateMetadataPanel(lastUpdate)
110 updateMetadataPanel(lastUpdate)
108 }
111 }
109
112
110 function blink(node) {
113 function blink(node) {
111 var blinkCount = 2;
114 var blinkCount = 2;
112
115
113 var nodeToAnimate = node;
116 var nodeToAnimate = node;
114 for (var i = 0; i < blinkCount; i++) {
117 for (var i = 0; i < blinkCount; i++) {
115 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
118 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
116 }
119 }
117 }
120 }
118
121
119 function isPageBottom() {
122 function isPageBottom() {
120 var scroll = $(window).scrollTop() / ($(document).height()
123 var scroll = $(window).scrollTop() / ($(document).height()
121 - $(window).height());
124 - $(window).height());
122
125
123 return scroll == 1
126 return scroll == 1
124 }
127 }
125
128
126 function initAutoupdate() {
129 function initAutoupdate() {
127 connectWebsocket();
130 connectWebsocket();
128 }
131 }
129
132
130 function getReplyCount() {
133 function getReplyCount() {
131 return $('.thread').children('.post').length
134 return $('.thread').children('.post').length
132 }
135 }
133
136
134 function getImageCount() {
137 function getImageCount() {
135 return $('.thread').find('img').length
138 return $('.thread').find('img').length
136 }
139 }
137
140
138 function updateMetadataPanel(lastUpdate) {
141 function updateMetadataPanel(lastUpdate) {
139 var replyCountField = $('#reply-count');
142 var replyCountField = $('#reply-count');
140 var imageCountField = $('#image-count');
143 var imageCountField = $('#image-count');
141
144
142 replyCountField.text(getReplyCount());
145 replyCountField.text(getReplyCount());
143 imageCountField.text(getImageCount());
146 imageCountField.text(getImageCount());
144
147
145 if (lastUpdate !== '') {
148 if (lastUpdate !== '') {
146 var lastUpdateField = $('#last-update');
149 var lastUpdateField = $('#last-update');
147 lastUpdateField.text(lastUpdate);
150 lastUpdateField.text(lastUpdate);
148 blink(lastUpdateField);
151 blink(lastUpdateField);
149 }
152 }
150
153
151 blink(replyCountField);
154 blink(replyCountField);
152 blink(imageCountField);
155 blink(imageCountField);
153 }
156 }
154
157
155 /**
158 /**
156 * Update bumplimit progress bar
159 * Update bumplimit progress bar
157 */
160 */
158 function updateBumplimitProgress(postDelta) {
161 function updateBumplimitProgress(postDelta) {
159 var progressBar = $('#bumplimit_progress');
162 var progressBar = $('#bumplimit_progress');
160 if (progressBar) {
163 if (progressBar) {
161 var postsToLimitElement = $('#left_to_limit');
164 var postsToLimitElement = $('#left_to_limit');
162
165
163 var oldPostsToLimit = parseInt(postsToLimitElement.text());
166 var oldPostsToLimit = parseInt(postsToLimitElement.text());
164 var postCount = getReplyCount();
167 var postCount = getReplyCount();
165 var bumplimit = postCount - postDelta + oldPostsToLimit;
168 var bumplimit = postCount - postDelta + oldPostsToLimit;
166
169
167 var newPostsToLimit = bumplimit - postCount;
170 var newPostsToLimit = bumplimit - postCount;
168 if (newPostsToLimit <= 0) {
171 if (newPostsToLimit <= 0) {
169 $('.bar-bg').remove();
172 $('.bar-bg').remove();
170 $('.thread').children('.post').addClass('dead_post');
173 $('.thread').children('.post').addClass('dead_post');
171 } else {
174 } else {
172 postsToLimitElement.text(newPostsToLimit);
175 postsToLimitElement.text(newPostsToLimit);
173 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
176 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
174 }
177 }
175 }
178 }
176 }
179 }
177
180
178 var documentOriginalTitle = '';
181 var documentOriginalTitle = '';
179 /**
182 /**
180 * Show 'new posts' text in the title if the document is not visible to a user
183 * Show 'new posts' text in the title if the document is not visible to a user
181 */
184 */
182 function showNewPostsTitle(newPostCount) {
185 function showNewPostsTitle(newPostCount) {
183 if (document.hidden) {
186 if (document.hidden) {
184 if (documentOriginalTitle === '') {
187 if (documentOriginalTitle === '') {
185 documentOriginalTitle = document.title;
188 documentOriginalTitle = document.title;
186 }
189 }
187 unreadPosts = unreadPosts + newPostCount;
190 unreadPosts = unreadPosts + newPostCount;
188 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
191 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
189
192
190 document.addEventListener('visibilitychange', function() {
193 document.addEventListener('visibilitychange', function() {
191 if (documentOriginalTitle !== '') {
194 if (documentOriginalTitle !== '') {
192 document.title = documentOriginalTitle;
195 document.title = documentOriginalTitle;
193 documentOriginalTitle = '';
196 documentOriginalTitle = '';
194 unreadPosts = 0;
197 unreadPosts = 0;
195 }
198 }
196
199
197 document.removeEventListener('visibilitychange', null);
200 document.removeEventListener('visibilitychange', null);
198 });
201 });
199 }
202 }
200 }
203 }
201
204
202 /**
205 /**
203 * Clear all entered values in the form fields
206 * Clear all entered values in the form fields
204 */
207 */
205 function resetForm(form) {
208 function resetForm(form) {
206 form.find('input:text, input:password, input:file, select, textarea').val('');
209 form.find('input:text, input:password, input:file, select, textarea').val('');
207 form.find('input:radio, input:checkbox')
210 form.find('input:radio, input:checkbox')
208 .removeAttr('checked').removeAttr('selected');
211 .removeAttr('checked').removeAttr('selected');
209 $('.file_wrap').find('.file-thumb').remove();
212 $('.file_wrap').find('.file-thumb').remove();
210 }
213 }
211
214
212 /**
215 /**
213 * When the form is posted, this method will be run as a callback
216 * When the form is posted, this method will be run as a callback
214 */
217 */
215 function updateOnPost(response, statusText, xhr, form) {
218 function updateOnPost(response, statusText, xhr, form) {
216 var json = $.parseJSON(response);
219 var json = $.parseJSON(response);
217 var status = json.status;
220 var status = json.status;
218
221
219 showAsErrors(form, '');
222 showAsErrors(form, '');
220
223
221 if (status === 'ok') {
224 if (status === 'ok') {
222 resetForm(form);
225 resetForm(form);
223 } else {
226 } else {
224 var errors = json.errors;
227 var errors = json.errors;
225 for (var i = 0; i < errors.length; i++) {
228 for (var i = 0; i < errors.length; i++) {
226 var fieldErrors = errors[i];
229 var fieldErrors = errors[i];
227
230
228 var error = fieldErrors.errors;
231 var error = fieldErrors.errors;
229
232
230 showAsErrors(form, error);
233 showAsErrors(form, error);
231 }
234 }
232 }
235 }
233
236
234 scrollToBottom();
237 scrollToBottom();
235 }
238 }
236
239
237 /**
240 /**
238 * Show text in the errors row of the form.
241 * Show text in the errors row of the form.
239 * @param form
242 * @param form
240 * @param text
243 * @param text
241 */
244 */
242 function showAsErrors(form, text) {
245 function showAsErrors(form, text) {
243 form.children('.form-errors').remove();
246 form.children('.form-errors').remove();
244
247
245 if (text.length > 0) {
248 if (text.length > 0) {
246 var errorList = $('<div class="form-errors">' + text
249 var errorList = $('<div class="form-errors">' + text
247 + '<div>');
250 + '<div>');
248 errorList.appendTo(form);
251 errorList.appendTo(form);
249 }
252 }
250 }
253 }
251
254
252 /**
255 /**
253 * Run js methods that are usually run on the document, on the new post
256 * Run js methods that are usually run on the document, on the new post
254 */
257 */
255 function processNewPost(post) {
258 function processNewPost(post) {
256 addRefLinkPreview(post[0]);
259 addRefLinkPreview(post[0]);
257 highlightCode(post);
260 highlightCode(post);
258 blink(post);
261 blink(post);
259 }
262 }
260
263
261 $(document).ready(function(){
264 $(document).ready(function(){
262 if ('WebSocket' in window) {
265 if ('WebSocket' in window) {
263 initAutoupdate();
266 if (initAutoupdate()) {
264
265 // Post form data over AJAX
267 // Post form data over AJAX
266 var threadId = $('div.thread').children('.post').first().attr('id');
268 var threadId = $('div.thread').children('.post').first().attr('id');
267
269
268 var form = $('#form');
270 var form = $('#form');
269
271
270 var options = {
272 var options = {
271 beforeSubmit: function(arr, $form, options) {
273 beforeSubmit: function(arr, $form, options) {
272 showAsErrors($('form'), gettext('Sending message...'));
274 showAsErrors($('form'), gettext('Sending message...'));
273 },
275 },
274 success: updateOnPost,
276 success: updateOnPost,
275 url: '/api/add_post/' + threadId + '/'
277 url: '/api/add_post/' + threadId + '/'
276 };
278 };
277
279
278 form.ajaxForm(options);
280 form.ajaxForm(options);
279
281
280 resetForm(form);
282 resetForm(form);
281 }
283 }
284 }
282 });
285 });
General Comments 0
You need to be logged in to leave comments. Login now