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