##// END OF EJS Templates
Code cleanup. Update only edited fields while performing thread archiving or post editing. Remove image when post is removed
neko259 -
r715:056b308f default
parent child Browse files
Show More
@@ -1,75 +1,73 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 import datetime
3 from south.db import db
2 from south.db import db
4 from south.v2 import SchemaMigration
3 from south.v2 import SchemaMigration
5 from django.db import models
4 from django.db import models
6 from boards.models.post import Post, NO_PARENT
7
5
8
6
9 class Migration(SchemaMigration):
7 class Migration(SchemaMigration):
10
8
11 def forwards(self, orm):
9 def forwards(self, orm):
12 # Adding M2M table for field replies on 'Post'
10 # Adding M2M table for field replies on 'Post'
13 m2m_table_name = db.shorten_name(u'boards_post_replies')
11 m2m_table_name = db.shorten_name(u'boards_post_replies')
14 db.create_table(m2m_table_name, (
12 db.create_table(m2m_table_name, (
15 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
13 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
16 ('from_post', models.ForeignKey(orm[u'boards.post'], null=False)),
14 ('from_post', models.ForeignKey(orm[u'boards.post'], null=False)),
17 ('to_post', models.ForeignKey(orm[u'boards.post'], null=False))
15 ('to_post', models.ForeignKey(orm[u'boards.post'], null=False))
18 ))
16 ))
19 db.create_unique(m2m_table_name, ['from_post_id', 'to_post_id'])
17 db.create_unique(m2m_table_name, ['from_post_id', 'to_post_id'])
20
18
21 def backwards(self, orm):
19 def backwards(self, orm):
22 # Removing M2M table for field replies on 'Post'
20 # Removing M2M table for field replies on 'Post'
23 db.delete_table(db.shorten_name(u'boards_post_replies'))
21 db.delete_table(db.shorten_name(u'boards_post_replies'))
24
22
25
23
26 models = {
24 models = {
27 u'boards.ban': {
25 u'boards.ban': {
28 'Meta': {'object_name': 'Ban'},
26 'Meta': {'object_name': 'Ban'},
29 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
27 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
30 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
28 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
31 },
29 },
32 u'boards.post': {
30 u'boards.post': {
33 'Meta': {'object_name': 'Post'},
31 'Meta': {'object_name': 'Post'},
34 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
32 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
35 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
33 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
36 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
34 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
37 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
35 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
38 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
36 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
39 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
37 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
40 'parent': ('django.db.models.fields.BigIntegerField', [], {}),
38 'parent': ('django.db.models.fields.BigIntegerField', [], {}),
41 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
39 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
42 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
40 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
43 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
41 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
44 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['boards.Post']"}),
42 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'re+'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['boards.Post']"}),
45 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}),
43 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}),
46 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
44 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
47 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
45 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
48 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
46 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
49 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['boards.User']", 'null': 'True'})
47 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['boards.User']", 'null': 'True'})
50 },
48 },
51 u'boards.setting': {
49 u'boards.setting': {
52 'Meta': {'object_name': 'Setting'},
50 'Meta': {'object_name': 'Setting'},
53 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
51 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
54 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
52 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
55 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['boards.User']"}),
53 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['boards.User']"}),
56 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
54 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
57 },
55 },
58 u'boards.tag': {
56 u'boards.tag': {
59 'Meta': {'object_name': 'Tag'},
57 'Meta': {'object_name': 'Tag'},
60 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
58 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
61 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
59 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
62 },
60 },
63 u'boards.user': {
61 u'boards.user': {
64 'Meta': {'object_name': 'User'},
62 'Meta': {'object_name': 'User'},
65 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
63 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
66 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['boards.Post']"}),
64 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['boards.Post']"}),
67 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
65 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
68 'last_access_time': ('django.db.models.fields.DateTimeField', [], {}),
66 'last_access_time': ('django.db.models.fields.DateTimeField', [], {}),
69 'rank': ('django.db.models.fields.IntegerField', [], {}),
67 'rank': ('django.db.models.fields.IntegerField', [], {}),
70 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
68 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
71 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
69 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
72 }
70 }
73 }
71 }
74
72
75 complete_apps = ['boards'] No newline at end of file
73 complete_apps = ['boards']
@@ -1,381 +1,354 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 logging
3 import logging
4 import os
4 import os
5 from random import random
5 from random import random
6 import time
6 import time
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.urlresolvers import reverse
11 from django.core.urlresolvers import reverse
12 from django.db import models, transaction
12 from django.db import models, transaction
13 from django.template.loader import render_to_string
13 from django.template.loader import render_to_string
14 from django.utils import timezone
14 from django.utils import timezone
15 from markupfield.fields import MarkupField
15 from markupfield.fields import MarkupField
16 from boards.models import PostImage
16 from boards.models import PostImage
17 from boards.models.base import Viewable
17 from boards.models.base import Viewable
18
18
19 from boards.models.thread import Thread
19 from boards.models.thread import Thread
20 from neboard import settings
20 from neboard import settings
21 from boards import thumbs
21 from boards import thumbs
22
22
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 = 200
35 TITLE_MAX_LENGTH = 200
36
36
37 DEFAULT_MARKUP_TYPE = 'markdown'
37 DEFAULT_MARKUP_TYPE = 'markdown'
38
38
39 # TODO This should be removed when no code relies on it because thread id field
40 # was removed a long time ago
41 NO_PARENT = -1
42
43 # TODO This should be removed
39 # TODO This should be removed
44 NO_IP = '0.0.0.0'
40 NO_IP = '0.0.0.0'
45
41
46 # TODO Real user agent should be saved instead of this
42 # TODO Real user agent should be saved instead of this
47 UNKNOWN_UA = ''
43 UNKNOWN_UA = ''
48
44
49 # TODO This should be checked for usage and removed because a nativa
50 # paginator is used now
51 ALL_PAGES = -1
52
53 IMAGES_DIRECTORY = 'images/'
54 FILE_EXTENSION_DELIMITER = '.'
55
56 SETTING_MODERATE = "moderate"
45 SETTING_MODERATE = "moderate"
57
46
58 REGEX_REPLY = re.compile('>>(\d+)')
47 REGEX_REPLY = re.compile('>>(\d+)')
59
48
60 logger = logging.getLogger(__name__)
49 logger = logging.getLogger(__name__)
61
50
62
51
63 class PostManager(models.Manager):
52 class PostManager(models.Manager):
64
53
65 def create_post(self, title, text, image=None, thread=None,
54 def create_post(self, title, text, image=None, thread=None,
66 ip=NO_IP, tags=None, user=None):
55 ip=NO_IP, tags=None, user=None):
67 """
56 """
68 Creates new post
57 Creates new post
69 """
58 """
70
59
71 posting_time = timezone.now()
60 posting_time = timezone.now()
72 if not thread:
61 if not thread:
73 thread = Thread.objects.create(bump_time=posting_time,
62 thread = Thread.objects.create(bump_time=posting_time,
74 last_edit_time=posting_time)
63 last_edit_time=posting_time)
75 new_thread = True
64 new_thread = True
76 else:
65 else:
77 thread.bump()
66 thread.bump()
78 thread.last_edit_time = posting_time
67 thread.last_edit_time = posting_time
79 thread.save()
68 thread.save()
80 new_thread = False
69 new_thread = False
81
70
82 post = self.create(title=title,
71 post = self.create(title=title,
83 text=text,
72 text=text,
84 pub_time=posting_time,
73 pub_time=posting_time,
85 thread_new=thread,
74 thread_new=thread,
86 poster_ip=ip,
75 poster_ip=ip,
87 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
76 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
88 # last!
77 # last!
89 last_edit_time=posting_time,
78 last_edit_time=posting_time,
90 user=user)
79 user=user)
91
80
92 if image:
81 if image:
93 post_image = PostImage.objects.create(image=image)
82 post_image = PostImage.objects.create(image=image)
94 post.images.add(post_image)
83 post.images.add(post_image)
95 logger.info('Created image #%d for post #%d' % (post_image.id,
84 logger.info('Created image #%d for post #%d' % (post_image.id,
96 post.id))
85 post.id))
97
86
98 thread.replies.add(post)
87 thread.replies.add(post)
99 if tags:
88 if tags:
100 linked_tags = []
89 linked_tags = []
101 for tag in tags:
90 for tag in tags:
102 tag_linked_tags = tag.get_linked_tags()
91 tag_linked_tags = tag.get_linked_tags()
103 if len(tag_linked_tags) > 0:
92 if len(tag_linked_tags) > 0:
104 linked_tags.extend(tag_linked_tags)
93 linked_tags.extend(tag_linked_tags)
105
94
106 tags.extend(linked_tags)
95 tags.extend(linked_tags)
107 map(thread.add_tag, tags)
96 map(thread.add_tag, tags)
108
97
109 if new_thread:
98 if new_thread:
110 self._delete_old_threads()
99 Thread.objects.archive_oldest_threads()
111 self.connect_replies(post)
100 self.connect_replies(post)
112
101
113 logger.info('Created post #%d' % post.id)
102 logger.info('Created post #%d' % post.id)
114
103
115 return post
104 return post
116
105
117 def delete_post(self, post):
106 def delete_post(self, post):
118 """
107 """
119 Deletes post and update or delete its thread
108 Deletes post and update or delete its thread
120 """
109 """
121
110
122 post_id = post.id
111 post_id = post.id
123
112
124 thread = post.get_thread()
113 thread = post.get_thread()
125
114
126 if post.is_opening():
115 if post.is_opening():
127 thread.delete_with_posts()
116 thread.delete_with_posts()
128 else:
117 else:
129 thread.last_edit_time = timezone.now()
118 thread.last_edit_time = timezone.now()
130 thread.save()
119 thread.save()
131
120
132 post.delete()
121 post.delete()
133
122
134 logger.info('Deleted post #%d' % post_id)
123 logger.info('Deleted post #%d' % post_id)
135
124
136 def delete_posts_by_ip(self, ip):
125 def delete_posts_by_ip(self, ip):
137 """
126 """
138 Deletes all posts of the author with same IP
127 Deletes all posts of the author with same IP
139 """
128 """
140
129
141 posts = self.filter(poster_ip=ip)
130 posts = self.filter(poster_ip=ip)
142 map(self.delete_post, posts)
131 map(self.delete_post, posts)
143
132
144 # TODO Move this method to thread manager
145 # TODO Rename it, because the threads are archived instead of plain
146 # removal. Split the delete and archive methods and make a setting to
147 # enable or disable archiving.
148 def _delete_old_threads(self):
149 """
150 Preserves maximum thread count. If there are too many threads,
151 archive the old ones.
152 """
153
154 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
155 thread_count = threads.count()
156
157 if thread_count > settings.MAX_THREAD_COUNT:
158 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
159 old_threads = threads[thread_count - num_threads_to_delete:]
160
161 for thread in old_threads:
162 thread.archived = True
163 thread.last_edit_time = timezone.now()
164 thread.save()
165
166 logger.info('Archived %d old threads' % num_threads_to_delete)
167
168 def connect_replies(self, post):
133 def connect_replies(self, post):
169 """
134 """
170 Connects replies to a post to show them as a reflink map
135 Connects replies to a post to show them as a reflink map
171 """
136 """
172
137
173 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
138 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
174 post_id = reply_number.group(1)
139 post_id = reply_number.group(1)
175 ref_post = self.filter(id=post_id)
140 ref_post = self.filter(id=post_id)
176 if ref_post.count() > 0:
141 if ref_post.count() > 0:
177 referenced_post = ref_post[0]
142 referenced_post = ref_post[0]
178 referenced_post.referenced_posts.add(post)
143 referenced_post.referenced_posts.add(post)
179 referenced_post.last_edit_time = post.pub_time
144 referenced_post.last_edit_time = post.pub_time
180 referenced_post.build_refmap()
145 referenced_post.build_refmap()
181 referenced_post.save()
146 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
182
147
183 referenced_thread = referenced_post.get_thread()
148 referenced_thread = referenced_post.get_thread()
184 referenced_thread.last_edit_time = post.pub_time
149 referenced_thread.last_edit_time = post.pub_time
185 referenced_thread.save()
150 referenced_thread.save(update_fields=['last_edit_time'])
186
151
187 def get_posts_per_day(self):
152 def get_posts_per_day(self):
188 """
153 """
189 Gets average count of posts per day for the last 7 days
154 Gets average count of posts per day for the last 7 days
190 """
155 """
191
156
192 today = date.today()
157 today = date.today()
193 ppd = cache.get(CACHE_KEY_PPD + str(today))
158 ppd = cache.get(CACHE_KEY_PPD + str(today))
194 if ppd:
159 if ppd:
195 return ppd
160 return ppd
196
161
197 posts_per_days = []
162 posts_per_days = []
198 for i in POSTS_PER_DAY_RANGE:
163 for i in POSTS_PER_DAY_RANGE:
199 day_end = today - timedelta(i + 1)
164 day_end = today - timedelta(i + 1)
200 day_start = today - timedelta(i + 2)
165 day_start = today - timedelta(i + 2)
201
166
202 day_time_start = timezone.make_aware(datetime.combine(
167 day_time_start = timezone.make_aware(datetime.combine(
203 day_start, dtime()), timezone.get_current_timezone())
168 day_start, dtime()), timezone.get_current_timezone())
204 day_time_end = timezone.make_aware(datetime.combine(
169 day_time_end = timezone.make_aware(datetime.combine(
205 day_end, dtime()), timezone.get_current_timezone())
170 day_end, dtime()), timezone.get_current_timezone())
206
171
207 posts_per_days.append(float(self.filter(
172 posts_per_days.append(float(self.filter(
208 pub_time__lte=day_time_end,
173 pub_time__lte=day_time_end,
209 pub_time__gte=day_time_start).count()))
174 pub_time__gte=day_time_start).count()))
210
175
211 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
176 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
212 len(posts_per_days))
177 len(posts_per_days))
213 cache.set(CACHE_KEY_PPD + str(today), ppd)
178 cache.set(CACHE_KEY_PPD + str(today), ppd)
214 return ppd
179 return ppd
215
180
216
181
217 class Post(models.Model, Viewable):
182 class Post(models.Model, Viewable):
218 """A post is a message."""
183 """A post is a message."""
219
184
220 objects = PostManager()
185 objects = PostManager()
221
186
222 class Meta:
187 class Meta:
223 app_label = APP_LABEL_BOARDS
188 app_label = APP_LABEL_BOARDS
224 ordering = ('id',)
189 ordering = ('id',)
225
190
226 title = models.CharField(max_length=TITLE_MAX_LENGTH)
191 title = models.CharField(max_length=TITLE_MAX_LENGTH)
227 pub_time = models.DateTimeField()
192 pub_time = models.DateTimeField()
228 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
193 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
229 escape_html=False)
194 escape_html=False)
230
195
231 images = models.ManyToManyField(PostImage, null=True, blank=True,
196 images = models.ManyToManyField(PostImage, null=True, blank=True,
232 related_name='ip+', db_index=True)
197 related_name='ip+', db_index=True)
233
198
234 poster_ip = models.GenericIPAddressField()
199 poster_ip = models.GenericIPAddressField()
235 poster_user_agent = models.TextField()
200 poster_user_agent = models.TextField()
236
201
237 thread_new = models.ForeignKey('Thread', null=True, default=None,
202 thread_new = models.ForeignKey('Thread', null=True, default=None,
238 db_index=True)
203 db_index=True)
239 last_edit_time = models.DateTimeField()
204 last_edit_time = models.DateTimeField()
240 user = models.ForeignKey('User', null=True, default=None, db_index=True)
205 user = models.ForeignKey('User', null=True, default=None, db_index=True)
241
206
242 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
207 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
243 null=True,
208 null=True,
244 blank=True, related_name='rfp+',
209 blank=True, related_name='rfp+',
245 db_index=True)
210 db_index=True)
246 refmap = models.TextField(null=True, blank=True)
211 refmap = models.TextField(null=True, blank=True)
247
212
248 def __unicode__(self):
213 def __unicode__(self):
249 return '#' + str(self.id) + ' ' + self.title + ' (' + \
214 return '#' + str(self.id) + ' ' + self.title + ' (' + \
250 self.text.raw[:50] + ')'
215 self.text.raw[:50] + ')'
251
216
252 def get_title(self):
217 def get_title(self):
253 """
218 """
254 Gets original post title or part of its text.
219 Gets original post title or part of its text.
255 """
220 """
256
221
257 title = self.title
222 title = self.title
258 if not title:
223 if not title:
259 title = self.text.rendered
224 title = self.text.rendered
260
225
261 return title
226 return title
262
227
263 def build_refmap(self):
228 def build_refmap(self):
264 map_string = ''
229 map_string = ''
265
230
266 first = True
231 first = True
267 for refpost in self.referenced_posts.all():
232 for refpost in self.referenced_posts.all():
268 if not first:
233 if not first:
269 map_string += ', '
234 map_string += ', '
270 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
235 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
271 first = False
236 first = False
272
237
273 self.refmap = map_string
238 self.refmap = map_string
274
239
275 def get_sorted_referenced_posts(self):
240 def get_sorted_referenced_posts(self):
276 return self.refmap
241 return self.refmap
277
242
278 def is_referenced(self):
243 def is_referenced(self):
279 return len(self.refmap) > 0
244 return len(self.refmap) > 0
280
245
281 def is_opening(self):
246 def is_opening(self):
282 """
247 """
283 Checks if this is an opening post or just a reply.
248 Checks if this is an opening post or just a reply.
284 """
249 """
285
250
286 return self.get_thread().get_opening_post_id() == self.id
251 return self.get_thread().get_opening_post_id() == self.id
287
252
288 @transaction.atomic
253 @transaction.atomic
289 def add_tag(self, tag):
254 def add_tag(self, tag):
290 edit_time = timezone.now()
255 edit_time = timezone.now()
291
256
292 thread = self.get_thread()
257 thread = self.get_thread()
293 thread.add_tag(tag)
258 thread.add_tag(tag)
294 self.last_edit_time = edit_time
259 self.last_edit_time = edit_time
295 self.save()
260 self.save()
296
261
297 thread.last_edit_time = edit_time
262 thread.last_edit_time = edit_time
298 thread.save()
263 thread.save()
299
264
300 @transaction.atomic
265 @transaction.atomic
301 def remove_tag(self, tag):
266 def remove_tag(self, tag):
302 edit_time = timezone.now()
267 edit_time = timezone.now()
303
268
304 thread = self.get_thread()
269 thread = self.get_thread()
305 thread.remove_tag(tag)
270 thread.remove_tag(tag)
306 self.last_edit_time = edit_time
271 self.last_edit_time = edit_time
307 self.save()
272 self.save()
308
273
309 thread.last_edit_time = edit_time
274 thread.last_edit_time = edit_time
310 thread.save()
275 thread.save()
311
276
312 def get_url(self, thread=None):
277 def get_url(self, thread=None):
313 """
278 """
314 Gets full url to the post.
279 Gets full url to the post.
315 """
280 """
316
281
317 cache_key = CACHE_KEY_POST_URL + str(self.id)
282 cache_key = CACHE_KEY_POST_URL + str(self.id)
318 link = cache.get(cache_key)
283 link = cache.get(cache_key)
319
284
320 if not link:
285 if not link:
321 if not thread:
286 if not thread:
322 thread = self.get_thread()
287 thread = self.get_thread()
323
288
324 opening_id = thread.get_opening_post_id()
289 opening_id = thread.get_opening_post_id()
325
290
326 if self.id != opening_id:
291 if self.id != opening_id:
327 link = reverse('thread', kwargs={
292 link = reverse('thread', kwargs={
328 'post_id': opening_id}) + '#' + str(self.id)
293 'post_id': opening_id}) + '#' + str(self.id)
329 else:
294 else:
330 link = reverse('thread', kwargs={'post_id': self.id})
295 link = reverse('thread', kwargs={'post_id': self.id})
331
296
332 cache.set(cache_key, link)
297 cache.set(cache_key, link)
333
298
334 return link
299 return link
335
300
336 def get_thread(self):
301 def get_thread(self):
337 """
302 """
338 Gets post's thread.
303 Gets post's thread.
339 """
304 """
340
305
341 return self.thread_new
306 return self.thread_new
342
307
343 def get_referenced_posts(self):
308 def get_referenced_posts(self):
344 return self.referenced_posts.only('id', 'thread_new')
309 return self.referenced_posts.only('id', 'thread_new')
345
310
346 def get_text(self):
311 def get_text(self):
347 return self.text
312 return self.text
348
313
349 def get_view(self, moderator=False, need_open_link=False,
314 def get_view(self, moderator=False, need_open_link=False,
350 truncated=False, *args, **kwargs):
315 truncated=False, *args, **kwargs):
351 if 'is_opening' in kwargs:
316 if 'is_opening' in kwargs:
352 is_opening = kwargs['is_opening']
317 is_opening = kwargs['is_opening']
353 else:
318 else:
354 is_opening = self.is_opening()
319 is_opening = self.is_opening()
355
320
356 if 'thread' in kwargs:
321 if 'thread' in kwargs:
357 thread = kwargs['thread']
322 thread = kwargs['thread']
358 else:
323 else:
359 thread = self.get_thread()
324 thread = self.get_thread()
360
325
361 if 'can_bump' in kwargs:
326 if 'can_bump' in kwargs:
362 can_bump = kwargs['can_bump']
327 can_bump = kwargs['can_bump']
363 else:
328 else:
364 can_bump = thread.can_bump()
329 can_bump = thread.can_bump()
365
330
366 opening_post_id = thread.get_opening_post_id()
331 opening_post_id = thread.get_opening_post_id()
367
332
368 return render_to_string('boards/post.html', {
333 return render_to_string('boards/post.html', {
369 'post': self,
334 'post': self,
370 'moderator': moderator,
335 'moderator': moderator,
371 'is_opening': is_opening,
336 'is_opening': is_opening,
372 'thread': thread,
337 'thread': thread,
373 'bumpable': can_bump,
338 'bumpable': can_bump,
374 'need_open_link': need_open_link,
339 'need_open_link': need_open_link,
375 'truncated': truncated,
340 'truncated': truncated,
376 'opening_post_id': opening_post_id,
341 'opening_post_id': opening_post_id,
377 })
342 })
378
343
379 def get_first_image(self):
344 def get_first_image(self):
380 return self.images.earliest('id')
345 return self.images.earliest('id')
381
346
347 def delete(self, using=None):
348 """
349 Delete all post images and the post itself.
350 """
351
352 self.images.all().delete()
353
354 super(Post, self).delete(using) No newline at end of file
@@ -1,141 +1,100 b''
1 from django.template.loader import render_to_string
1 from django.template.loader import render_to_string
2 from boards.models import Thread, Post
2 from boards.models import Thread, Post
3 from django.db import models
3 from django.db import models
4 from django.db.models import Count, Sum
4 from django.db.models import Count, Sum
5 from django.core.urlresolvers import reverse
5 from django.core.urlresolvers import reverse
6 from boards.models.base import Viewable
6 from boards.models.base import Viewable
7
7
8 __author__ = 'neko259'
8 __author__ = 'neko259'
9
9
10 # TODO Tag popularity ratings are not used any more, remove all of this
11 MAX_TAG_FONT = 1
12 MIN_TAG_FONT = 0.2
13
14 TAG_POPULARITY_MULTIPLIER = 20
15
16 ARCHIVE_POPULARITY_MODIFIER = 0.5
17
18
10
19 class TagManager(models.Manager):
11 class TagManager(models.Manager):
20
12
21 def get_not_empty_tags(self):
13 def get_not_empty_tags(self):
22 """
14 """
23 Gets tags that have non-archived threads.
15 Gets tags that have non-archived threads.
24 """
16 """
25
17
26 tags = self.annotate(Count('threads')) \
18 tags = self.annotate(Count('threads')) \
27 .filter(threads__count__gt=0).order_by('name')
19 .filter(threads__count__gt=0).order_by('name')
28
20
29 return tags
21 return tags
30
22
31
23
32 class Tag(models.Model, Viewable):
24 class Tag(models.Model, Viewable):
33 """
25 """
34 A tag is a text node assigned to the thread. The tag serves as a board
26 A tag is a text node assigned to the thread. The tag serves as a board
35 section. There can be multiple tags for each thread
27 section. There can be multiple tags for each thread
36 """
28 """
37
29
38 objects = TagManager()
30 objects = TagManager()
39
31
40 class Meta:
32 class Meta:
41 app_label = 'boards'
33 app_label = 'boards'
42 ordering = ('name',)
34 ordering = ('name',)
43
35
44 name = models.CharField(max_length=100, db_index=True)
36 name = models.CharField(max_length=100, db_index=True)
45 threads = models.ManyToManyField(Thread, null=True,
37 threads = models.ManyToManyField(Thread, null=True,
46 blank=True, related_name='tag+')
38 blank=True, related_name='tag+')
47 linked = models.ForeignKey('Tag', null=True, blank=True)
39 linked = models.ForeignKey('Tag', null=True, blank=True)
48
40
49 def __unicode__(self):
41 def __unicode__(self):
50 return self.name
42 return self.name
51
43
52 def is_empty(self):
44 def is_empty(self):
53 """
45 """
54 Checks if the tag has some threads.
46 Checks if the tag has some threads.
55 """
47 """
56
48
57 return self.get_thread_count() == 0
49 return self.get_thread_count() == 0
58
50
59 def get_thread_count(self):
51 def get_thread_count(self):
60 return self.threads.count()
52 return self.threads.count()
61
53
62 # TODO Remove, not used any more
63 def get_popularity(self):
64 """
65 Gets tag's popularity value as a percentage of overall board post
66 count.
67 """
68
69 all_post_count = Post.objects.count()
70
71 tag_reply_count = 0.0
72
73 tag_reply_count += self.get_post_count()
74 tag_reply_count +=\
75 self.get_post_count(archived=True) * ARCHIVE_POPULARITY_MODIFIER
76
77 popularity = tag_reply_count / all_post_count
78
79 return popularity
80
81 def get_linked_tags(self):
54 def get_linked_tags(self):
82 """
55 """
83 Gets tags linked to the current one.
56 Gets tags linked to the current one.
84 """
57 """
85
58
86 tag_list = []
59 tag_list = []
87 self.get_linked_tags_list(tag_list)
60 self.get_linked_tags_list(tag_list)
88
61
89 return tag_list
62 return tag_list
90
63
91 def get_linked_tags_list(self, tag_list=[]):
64 def get_linked_tags_list(self, tag_list=[]):
92 """
65 """
93 Returns the list of tags linked to current. The list can be got
66 Returns the list of tags linked to current. The list can be got
94 through returned value or tag_list parameter
67 through returned value or tag_list parameter
95 """
68 """
96
69
97 linked_tag = self.linked
70 linked_tag = self.linked
98
71
99 if linked_tag and not (linked_tag in tag_list):
72 if linked_tag and not (linked_tag in tag_list):
100 tag_list.append(linked_tag)
73 tag_list.append(linked_tag)
101
74
102 linked_tag.get_linked_tags_list(tag_list)
75 linked_tag.get_linked_tags_list(tag_list)
103
76
104 # TODO Remove
105 def get_font_value(self):
106 """
107 Gets tag font value to differ most popular tags in the list
108 """
109
110 popularity = self.get_popularity()
111
112 font_value = popularity * Tag.objects.get_not_empty_tags().count()
113 font_value = max(font_value, MIN_TAG_FONT)
114 font_value = min(font_value, MAX_TAG_FONT)
115
116 return str(font_value)
117
118 def get_post_count(self, archived=False):
77 def get_post_count(self, archived=False):
119 """
78 """
120 Gets posts count for the tag's threads.
79 Gets posts count for the tag's threads.
121 """
80 """
122
81
123 posts_count = 0
82 posts_count = 0
124
83
125 threads = self.threads.filter(archived=archived)
84 threads = self.threads.filter(archived=archived)
126 if threads.exists():
85 if threads.exists():
127 posts_count = threads.annotate(posts_count=Count('replies')).aggregate(
86 posts_count = threads.annotate(posts_count=Count('replies')) \
128 posts_sum=Sum('posts_count'))['posts_sum']
87 .aggregate(posts_sum=Sum('posts_count'))['posts_sum']
129
88
130 if not posts_count:
89 if not posts_count:
131 posts_count = 0
90 posts_count = 0
132
91
133 return posts_count
92 return posts_count
134
93
135 def get_url(self):
94 def get_url(self):
136 return reverse('tag', kwargs={'tag_name': self.name})
95 return reverse('tag', kwargs={'tag_name': self.name})
137
96
138 def get_view(self, *args, **kwargs):
97 def get_view(self, *args, **kwargs):
139 return render_to_string('boards/tag.html', {
98 return render_to_string('boards/tag.html', {
140 'tag': self,
99 'tag': self,
141 })
100 })
@@ -1,163 +1,186 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 neboard import settings
6 from neboard 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):
18 def archive_oldest_threads(self):
19 """
20 Preserves maximum thread count. If there are too many threads,
21 archive the old ones.
22 """
23
24 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
25 thread_count = threads.count()
26
27 if 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:]
30
31 for thread in old_threads:
32 thread.archived = True
33 thread.last_edit_time = timezone.now()
34 thread.save(update_fields=['archived', 'last_edit_time'])
35
36 logger.info('Archived %d old threads' % num_threads_to_delete)
37
38
17 class Thread(models.Model):
39 class Thread(models.Model):
40 objects = ThreadManager()
18
41
19 class Meta:
42 class Meta:
20 app_label = 'boards'
43 app_label = 'boards'
21
44
22 tags = models.ManyToManyField('Tag')
45 tags = models.ManyToManyField('Tag')
23 bump_time = models.DateTimeField()
46 bump_time = models.DateTimeField()
24 last_edit_time = models.DateTimeField()
47 last_edit_time = models.DateTimeField()
25 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
48 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
26 blank=True, related_name='tre+')
49 blank=True, related_name='tre+')
27 archived = models.BooleanField(default=False)
50 archived = models.BooleanField(default=False)
28
51
29 def get_tags(self):
52 def get_tags(self):
30 """
53 """
31 Gets a sorted tag list.
54 Gets a sorted tag list.
32 """
55 """
33
56
34 return self.tags.order_by('name')
57 return self.tags.order_by('name')
35
58
36 def bump(self):
59 def bump(self):
37 """
60 """
38 Bumps (moves to up) thread if possible.
61 Bumps (moves to up) thread if possible.
39 """
62 """
40
63
41 if self.can_bump():
64 if self.can_bump():
42 self.bump_time = timezone.now()
65 self.bump_time = timezone.now()
43
66
44 logger.info('Bumped thread %d' % self.id)
67 logger.info('Bumped thread %d' % self.id)
45
68
46 def get_reply_count(self):
69 def get_reply_count(self):
47 return self.replies.count()
70 return self.replies.count()
48
71
49 def get_images_count(self):
72 def get_images_count(self):
50 # TODO Use sum
73 # TODO Use sum
51 total_count = 0
74 total_count = 0
52 for post_with_image in self.replies.annotate(images_count=Count(
75 for post_with_image in self.replies.annotate(images_count=Count(
53 'images')):
76 'images')):
54 total_count += post_with_image.images_count
77 total_count += post_with_image.images_count
55 return total_count
78 return total_count
56
79
57 def can_bump(self):
80 def can_bump(self):
58 """
81 """
59 Checks if the thread can be bumped by replying to it.
82 Checks if the thread can be bumped by replying to it.
60 """
83 """
61
84
62 if self.archived:
85 if self.archived:
63 return False
86 return False
64
87
65 post_count = self.get_reply_count()
88 post_count = self.get_reply_count()
66
89
67 return post_count < settings.MAX_POSTS_PER_THREAD
90 return post_count < settings.MAX_POSTS_PER_THREAD
68
91
69 def delete_with_posts(self):
92 def delete_with_posts(self):
70 """
93 """
71 Completely deletes thread and all its posts
94 Completely deletes thread and all its posts
72 """
95 """
73
96
74 if self.replies.exists():
97 if self.replies.exists():
75 self.replies.all().delete()
98 self.replies.all().delete()
76
99
77 self.delete()
100 self.delete()
78
101
79 def get_last_replies(self):
102 def get_last_replies(self):
80 """
103 """
81 Gets several last replies, not including opening post
104 Gets several last replies, not including opening post
82 """
105 """
83
106
84 if settings.LAST_REPLIES_COUNT > 0:
107 if settings.LAST_REPLIES_COUNT > 0:
85 reply_count = self.get_reply_count()
108 reply_count = self.get_reply_count()
86
109
87 if reply_count > 0:
110 if reply_count > 0:
88 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
111 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
89 reply_count - 1)
112 reply_count - 1)
90 replies = self.get_replies()
113 replies = self.get_replies()
91 last_replies = replies[reply_count - reply_count_to_show:]
114 last_replies = replies[reply_count - reply_count_to_show:]
92
115
93 return last_replies
116 return last_replies
94
117
95 def get_skipped_replies_count(self):
118 def get_skipped_replies_count(self):
96 """
119 """
97 Gets number of posts between opening post and last replies.
120 Gets number of posts between opening post and last replies.
98 """
121 """
99 reply_count = self.get_reply_count()
122 reply_count = self.get_reply_count()
100 last_replies_count = min(settings.LAST_REPLIES_COUNT,
123 last_replies_count = min(settings.LAST_REPLIES_COUNT,
101 reply_count - 1)
124 reply_count - 1)
102 return reply_count - last_replies_count - 1
125 return reply_count - last_replies_count - 1
103
126
104 def get_replies(self, view_fields_only=False):
127 def get_replies(self, view_fields_only=False):
105 """
128 """
106 Gets sorted thread posts
129 Gets sorted thread posts
107 """
130 """
108
131
109 query = self.replies.order_by('pub_time').prefetch_related('images')
132 query = self.replies.order_by('pub_time').prefetch_related('images')
110 if view_fields_only:
133 if view_fields_only:
111 query = query.defer('poster_user_agent', 'text_markup_type')
134 query = query.defer('poster_user_agent', 'text_markup_type')
112 return query.all()
135 return query.all()
113
136
114 def get_replies_with_images(self, view_fields_only=False):
137 def get_replies_with_images(self, view_fields_only=False):
115 return self.get_replies(view_fields_only).annotate(images_count=Count(
138 return self.get_replies(view_fields_only).annotate(images_count=Count(
116 'images')).filter(images_count__gt=0)
139 'images')).filter(images_count__gt=0)
117
140
118 def add_tag(self, tag):
141 def add_tag(self, tag):
119 """
142 """
120 Connects thread to a tag and tag to a thread
143 Connects thread to a tag and tag to a thread
121 """
144 """
122
145
123 self.tags.add(tag)
146 self.tags.add(tag)
124 tag.threads.add(self)
147 tag.threads.add(self)
125
148
126 def remove_tag(self, tag):
149 def remove_tag(self, tag):
127 self.tags.remove(tag)
150 self.tags.remove(tag)
128 tag.threads.remove(self)
151 tag.threads.remove(self)
129
152
130 def get_opening_post(self, only_id=False):
153 def get_opening_post(self, only_id=False):
131 """
154 """
132 Gets the first post of the thread
155 Gets the first post of the thread
133 """
156 """
134
157
135 query = self.replies.order_by('pub_time')
158 query = self.replies.order_by('pub_time')
136 if only_id:
159 if only_id:
137 query = query.only('id')
160 query = query.only('id')
138 opening_post = query.first()
161 opening_post = query.first()
139
162
140 return opening_post
163 return opening_post
141
164
142 def get_opening_post_id(self):
165 def get_opening_post_id(self):
143 """
166 """
144 Gets ID of the first thread post.
167 Gets ID of the first thread post.
145 """
168 """
146
169
147 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
170 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
148 opening_post_id = cache.get(cache_key)
171 opening_post_id = cache.get(cache_key)
149 if not opening_post_id:
172 if not opening_post_id:
150 opening_post_id = self.get_opening_post(only_id=True).id
173 opening_post_id = self.get_opening_post(only_id=True).id
151 cache.set(cache_key, opening_post_id)
174 cache.set(cache_key, opening_post_id)
152
175
153 return opening_post_id
176 return opening_post_id
154
177
155 def __unicode__(self):
178 def __unicode__(self):
156 return str(self.id)
179 return str(self.id)
157
180
158 def get_pub_time(self):
181 def get_pub_time(self):
159 """
182 """
160 Gets opening post's pub time because thread does not have its own one.
183 Gets opening post's pub time because thread does not have its own one.
161 """
184 """
162
185
163 return self.get_opening_post().pub_time
186 return self.get_opening_post().pub_time
@@ -1,84 +1,82 b''
1 from django.conf.urls import patterns, url, include
1 from django.conf.urls import patterns, url, include
2 from boards import views
2 from boards import views
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 from boards.views import api, tag_threads, all_threads, \
4 from boards.views import api, tag_threads, all_threads, \
5 login, settings, all_tags
5 login, settings, all_tags
6 from boards.views.authors import AuthorsView
6 from boards.views.authors import AuthorsView
7 from boards.views.delete_post import DeletePostView
7 from boards.views.delete_post import DeletePostView
8 from boards.views.ban import BanUserView
8 from boards.views.ban import BanUserView
9 from boards.views.static import StaticPageView
9 from boards.views.static import StaticPageView
10 from boards.views.post_admin import PostAdminView
10 from boards.views.post_admin import PostAdminView
11
11
12 js_info_dict = {
12 js_info_dict = {
13 'packages': ('boards',),
13 'packages': ('boards',),
14 }
14 }
15
15
16 urlpatterns = patterns('',
16 urlpatterns = patterns('',
17
17
18 # /boards/
18 # /boards/
19 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
19 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
20 # /boards/page/
20 # /boards/page/
21 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
21 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
22 name='index'),
22 name='index'),
23
23
24 # login page
24 # login page
25 url(r'^login/$', login.LoginView.as_view(), name='login'),
25 url(r'^login/$', login.LoginView.as_view(), name='login'),
26
26
27 # /boards/tag/tag_name/
27 # /boards/tag/tag_name/
28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
29 name='tag'),
29 name='tag'),
30 # /boards/tag/tag_id/page/
30 # /boards/tag/tag_id/page/
31 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
31 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
32 tag_threads.TagView.as_view(), name='tag'),
32 tag_threads.TagView.as_view(), name='tag'),
33
33
34 # /boards/thread/
34 # /boards/thread/
35 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
35 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
36 name='thread'),
36 name='thread'),
37 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
37 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
38 .as_view(), name='thread_mode'),
38 .as_view(), name='thread_mode'),
39
39
40 # /boards/post_admin/
40 # /boards/post_admin/
41 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
41 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
42 name='post_admin'),
42 name='post_admin'),
43
43
44 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
44 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
45 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
45 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
46 url(r'^captcha/', include('captcha.urls')),
46 url(r'^captcha/', include('captcha.urls')),
47 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
47 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
48 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
48 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
49 name='delete'),
49 name='delete'),
50 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
50 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
51
51
52 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
52 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
53 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
53 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
54 name='staticpage'),
54 name='staticpage'),
55
55
56 # RSS feeds
56 # RSS feeds
57 url(r'^rss/$', AllThreadsFeed()),
57 url(r'^rss/$', AllThreadsFeed()),
58 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
58 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
59 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
59 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
60 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
60 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
61 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
61 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
62
62
63 # i18n
63 # i18n
64 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
64 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
65 name='js_info_dict'),
65 name='js_info_dict'),
66
66
67 # API
67 # API
68 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
68 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
69 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
69 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
70 api.api_get_threaddiff, name="get_thread_diff"),
70 api.api_get_threaddiff, name="get_thread_diff"),
71 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
71 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
72 name='get_threads'),
72 name='get_threads'),
73 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
73 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
74 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
74 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
75 name='get_thread'),
75 name='get_thread'),
76 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
76 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
77 name='add_post'),
77 name='add_post'),
78 url(r'^api/get_tag_popularity/(?P<tag_name>\w+)$', api.get_tag_popularity,
79 name='get_tag_popularity'),
80
78
81 # Search
79 # Search
82 url(r'^search/', include('haystack.urls')),
80 url(r'^search/', include('haystack.urls')),
83
81
84 )
82 )
@@ -1,246 +1,237 b''
1 from datetime import datetime
1 from datetime import datetime
2 import json
2 import json
3 import logging
3 import logging
4 from django.db import transaction
4 from django.db import transaction
5 from django.http import HttpResponse
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, render
6 from django.shortcuts import get_object_or_404, render
7 from django.template import RequestContext
7 from django.template import RequestContext
8 from django.utils import timezone
8 from django.utils import timezone
9 from django.core import serializers
9 from django.core import serializers
10
10
11 from boards.forms import PostForm, PlainErrorList
11 from boards.forms import PostForm, PlainErrorList
12 from boards.models import Post, Thread, Tag
12 from boards.models import Post, Thread, Tag
13 from boards.utils import datetime_to_epoch
13 from boards.utils import datetime_to_epoch
14 from boards.views.thread import ThreadView
14 from boards.views.thread import ThreadView
15
15
16 __author__ = 'neko259'
16 __author__ = 'neko259'
17
17
18 PARAMETER_TRUNCATED = 'truncated'
18 PARAMETER_TRUNCATED = 'truncated'
19 PARAMETER_TAG = 'tag'
19 PARAMETER_TAG = 'tag'
20 PARAMETER_OFFSET = 'offset'
20 PARAMETER_OFFSET = 'offset'
21 PARAMETER_DIFF_TYPE = 'type'
21 PARAMETER_DIFF_TYPE = 'type'
22
22
23 DIFF_TYPE_HTML = 'html'
23 DIFF_TYPE_HTML = 'html'
24 DIFF_TYPE_JSON = 'json'
24 DIFF_TYPE_JSON = 'json'
25
25
26 STATUS_OK = 'ok'
26 STATUS_OK = 'ok'
27 STATUS_ERROR = 'error'
27 STATUS_ERROR = 'error'
28
28
29 logger = logging.getLogger(__name__)
29 logger = logging.getLogger(__name__)
30
30
31
31
32 @transaction.atomic
32 @transaction.atomic
33 def api_get_threaddiff(request, thread_id, last_update_time):
33 def api_get_threaddiff(request, thread_id, last_update_time):
34 """
34 """
35 Gets posts that were changed or added since time
35 Gets posts that were changed or added since time
36 """
36 """
37
37
38 thread = get_object_or_404(Post, id=thread_id).get_thread()
38 thread = get_object_or_404(Post, id=thread_id).get_thread()
39
39
40 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
40 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
41 timezone.get_current_timezone())
41 timezone.get_current_timezone())
42
42
43 json_data = {
43 json_data = {
44 'added': [],
44 'added': [],
45 'updated': [],
45 'updated': [],
46 'last_update': None,
46 'last_update': None,
47 }
47 }
48 added_posts = Post.objects.filter(thread_new=thread,
48 added_posts = Post.objects.filter(thread_new=thread,
49 pub_time__gt=filter_time) \
49 pub_time__gt=filter_time) \
50 .order_by('pub_time')
50 .order_by('pub_time')
51 updated_posts = Post.objects.filter(thread_new=thread,
51 updated_posts = Post.objects.filter(thread_new=thread,
52 pub_time__lte=filter_time,
52 pub_time__lte=filter_time,
53 last_edit_time__gt=filter_time)
53 last_edit_time__gt=filter_time)
54
54
55 diff_type = DIFF_TYPE_HTML
55 diff_type = DIFF_TYPE_HTML
56 if PARAMETER_DIFF_TYPE in request.GET:
56 if PARAMETER_DIFF_TYPE in request.GET:
57 diff_type = request.GET[PARAMETER_DIFF_TYPE]
57 diff_type = request.GET[PARAMETER_DIFF_TYPE]
58
58
59 for post in added_posts:
59 for post in added_posts:
60 json_data['added'].append(_get_post_data(post.id, diff_type, request))
60 json_data['added'].append(_get_post_data(post.id, diff_type, request))
61 for post in updated_posts:
61 for post in updated_posts:
62 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
62 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
63 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
63 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
64
64
65 return HttpResponse(content=json.dumps(json_data))
65 return HttpResponse(content=json.dumps(json_data))
66
66
67
67
68 def api_add_post(request, opening_post_id):
68 def api_add_post(request, opening_post_id):
69 """
69 """
70 Adds a post and return the JSON response for it
70 Adds a post and return the JSON response for it
71 """
71 """
72
72
73 opening_post = get_object_or_404(Post, id=opening_post_id)
73 opening_post = get_object_or_404(Post, id=opening_post_id)
74
74
75 logger.info('Adding post via api...')
75 logger.info('Adding post via api...')
76
76
77 status = STATUS_OK
77 status = STATUS_OK
78 errors = []
78 errors = []
79
79
80 if request.method == 'POST':
80 if request.method == 'POST':
81 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
81 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
82 form.session = request.session
82 form.session = request.session
83
83
84 if form.need_to_ban:
84 if form.need_to_ban:
85 # Ban user because he is suspected to be a bot
85 # Ban user because he is suspected to be a bot
86 # _ban_current_user(request)
86 # _ban_current_user(request)
87 status = STATUS_ERROR
87 status = STATUS_ERROR
88 if form.is_valid():
88 if form.is_valid():
89 post = ThreadView().new_post(request, form, opening_post,
89 post = ThreadView().new_post(request, form, opening_post,
90 html_response=False)
90 html_response=False)
91 if not post:
91 if not post:
92 status = STATUS_ERROR
92 status = STATUS_ERROR
93 else:
93 else:
94 logger.info('Added post #%d via api.' % post.id)
94 logger.info('Added post #%d via api.' % post.id)
95 else:
95 else:
96 status = STATUS_ERROR
96 status = STATUS_ERROR
97 errors = form.as_json_errors()
97 errors = form.as_json_errors()
98
98
99 response = {
99 response = {
100 'status': status,
100 'status': status,
101 'errors': errors,
101 'errors': errors,
102 }
102 }
103
103
104 return HttpResponse(content=json.dumps(response))
104 return HttpResponse(content=json.dumps(response))
105
105
106
106
107 def get_post(request, post_id):
107 def get_post(request, post_id):
108 """
108 """
109 Gets the html of a post. Used for popups. Post can be truncated if used
109 Gets the html of a post. Used for popups. Post can be truncated if used
110 in threads list with 'truncated' get parameter.
110 in threads list with 'truncated' get parameter.
111 """
111 """
112
112
113 logger.info('Getting post #%s' % post_id)
113 logger.info('Getting post #%s' % post_id)
114
114
115 post = get_object_or_404(Post, id=post_id)
115 post = get_object_or_404(Post, id=post_id)
116
116
117 context = RequestContext(request)
117 context = RequestContext(request)
118 context['post'] = post
118 context['post'] = post
119 if PARAMETER_TRUNCATED in request.GET:
119 if PARAMETER_TRUNCATED in request.GET:
120 context[PARAMETER_TRUNCATED] = True
120 context[PARAMETER_TRUNCATED] = True
121
121
122 return render(request, 'boards/api_post.html', context)
122 return render(request, 'boards/api_post.html', context)
123
123
124
124
125 # TODO Test this
125 # TODO Test this
126 def api_get_threads(request, count):
126 def api_get_threads(request, count):
127 """
127 """
128 Gets the JSON thread opening posts list.
128 Gets the JSON thread opening posts list.
129 Parameters that can be used for filtering:
129 Parameters that can be used for filtering:
130 tag, offset (from which thread to get results)
130 tag, offset (from which thread to get results)
131 """
131 """
132
132
133 if PARAMETER_TAG in request.GET:
133 if PARAMETER_TAG in request.GET:
134 tag_name = request.GET[PARAMETER_TAG]
134 tag_name = request.GET[PARAMETER_TAG]
135 if tag_name is not None:
135 if tag_name is not None:
136 tag = get_object_or_404(Tag, name=tag_name)
136 tag = get_object_or_404(Tag, name=tag_name)
137 threads = tag.threads.filter(archived=False)
137 threads = tag.threads.filter(archived=False)
138 else:
138 else:
139 threads = Thread.objects.filter(archived=False)
139 threads = Thread.objects.filter(archived=False)
140
140
141 if PARAMETER_OFFSET in request.GET:
141 if PARAMETER_OFFSET in request.GET:
142 offset = request.GET[PARAMETER_OFFSET]
142 offset = request.GET[PARAMETER_OFFSET]
143 offset = int(offset) if offset is not None else 0
143 offset = int(offset) if offset is not None else 0
144 else:
144 else:
145 offset = 0
145 offset = 0
146
146
147 threads = threads.order_by('-bump_time')
147 threads = threads.order_by('-bump_time')
148 threads = threads[offset:offset + int(count)]
148 threads = threads[offset:offset + int(count)]
149
149
150 opening_posts = []
150 opening_posts = []
151 for thread in threads:
151 for thread in threads:
152 opening_post = thread.get_opening_post()
152 opening_post = thread.get_opening_post()
153
153
154 # TODO Add tags, replies and images count
154 # TODO Add tags, replies and images count
155 opening_posts.append(_get_post_data(opening_post.id,
155 opening_posts.append(_get_post_data(opening_post.id,
156 include_last_update=True))
156 include_last_update=True))
157
157
158 return HttpResponse(content=json.dumps(opening_posts))
158 return HttpResponse(content=json.dumps(opening_posts))
159
159
160
160
161 # TODO Test this
161 # TODO Test this
162 def api_get_tags(request):
162 def api_get_tags(request):
163 """
163 """
164 Gets all tags or user tags.
164 Gets all tags or user tags.
165 """
165 """
166
166
167 # TODO Get favorite tags for the given user ID
167 # TODO Get favorite tags for the given user ID
168
168
169 tags = Tag.objects.get_not_empty_tags()
169 tags = Tag.objects.get_not_empty_tags()
170 tag_names = []
170 tag_names = []
171 for tag in tags:
171 for tag in tags:
172 tag_names.append(tag.name)
172 tag_names.append(tag.name)
173
173
174 return HttpResponse(content=json.dumps(tag_names))
174 return HttpResponse(content=json.dumps(tag_names))
175
175
176
176
177 # TODO The result can be cached by the thread last update time
177 # TODO The result can be cached by the thread last update time
178 # TODO Test this
178 # TODO Test this
179 def api_get_thread_posts(request, opening_post_id):
179 def api_get_thread_posts(request, opening_post_id):
180 """
180 """
181 Gets the JSON array of thread posts
181 Gets the JSON array of thread posts
182 """
182 """
183
183
184 opening_post = get_object_or_404(Post, id=opening_post_id)
184 opening_post = get_object_or_404(Post, id=opening_post_id)
185 thread = opening_post.get_thread()
185 thread = opening_post.get_thread()
186 posts = thread.get_replies()
186 posts = thread.get_replies()
187
187
188 json_data = {
188 json_data = {
189 'posts': [],
189 'posts': [],
190 'last_update': None,
190 'last_update': None,
191 }
191 }
192 json_post_list = []
192 json_post_list = []
193
193
194 for post in posts:
194 for post in posts:
195 json_post_list.append(_get_post_data(post.id))
195 json_post_list.append(_get_post_data(post.id))
196 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
196 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
197 json_data['posts'] = json_post_list
197 json_data['posts'] = json_post_list
198
198
199 return HttpResponse(content=json.dumps(json_data))
199 return HttpResponse(content=json.dumps(json_data))
200
200
201
201
202 def api_get_post(request, post_id):
202 def api_get_post(request, post_id):
203 """
203 """
204 Gets the JSON of a post. This can be
204 Gets the JSON of a post. This can be
205 used as and API for external clients.
205 used as and API for external clients.
206 """
206 """
207
207
208 post = get_object_or_404(Post, id=post_id)
208 post = get_object_or_404(Post, id=post_id)
209
209
210 json = serializers.serialize("json", [post], fields=(
210 json = serializers.serialize("json", [post], fields=(
211 "pub_time", "_text_rendered", "title", "text", "image",
211 "pub_time", "_text_rendered", "title", "text", "image",
212 "image_width", "image_height", "replies", "tags"
212 "image_width", "image_height", "replies", "tags"
213 ))
213 ))
214
214
215 return HttpResponse(content=json)
215 return HttpResponse(content=json)
216
216
217
217
218 def get_tag_popularity(request, tag_name):
219 tag = get_object_or_404(Tag, name=tag_name)
220
221 json_data = []
222 json_data['popularity'] = tag.get_popularity()
223
224 return HttpResponse(content=json.dumps(json_data))
225
226
227 # TODO Add pub time and replies
218 # TODO Add pub time and replies
228 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
219 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
229 include_last_update=False):
220 include_last_update=False):
230 if format_type == DIFF_TYPE_HTML:
221 if format_type == DIFF_TYPE_HTML:
231 return get_post(request, post_id).content.strip()
222 return get_post(request, post_id).content.strip()
232 elif format_type == DIFF_TYPE_JSON:
223 elif format_type == DIFF_TYPE_JSON:
233 post = get_object_or_404(Post, id=post_id)
224 post = get_object_or_404(Post, id=post_id)
234 post_json = {
225 post_json = {
235 'id': post.id,
226 'id': post.id,
236 'title': post.title,
227 'title': post.title,
237 'text': post.text.rendered,
228 'text': post.text.rendered,
238 }
229 }
239 if post.images.exists():
230 if post.images.exists():
240 post_image = post.get_first_image()
231 post_image = post.get_first_image()
241 post_json['image'] = post_image.image.url
232 post_json['image'] = post_image.image.url
242 post_json['image_preview'] = post_image.image.url_200x150
233 post_json['image_preview'] = post_image.image.url_200x150
243 if include_last_update:
234 if include_last_update:
244 post_json['bump_time'] = datetime_to_epoch(
235 post_json['bump_time'] = datetime_to_epoch(
245 post.thread_new.bump_time)
236 post.thread_new.bump_time)
246 return post_json
237 return post_json
General Comments 0
You need to be logged in to leave comments. Login now