##// END OF EJS Templates
Removed null=True from some text fields
neko259 -
r1751:3a810c6e default
parent child Browse files
Show More
@@ -0,0 +1,25 b''
1 # -*- coding: utf-8 -*-
2 # Generated by Django 1.10.4 on 2016-12-29 09:32
3 from __future__ import unicode_literals
4
5 from django.db import migrations, models
6
7
8 class Migration(migrations.Migration):
9
10 dependencies = [
11 ('boards', '0054_auto_20161228_2244'),
12 ]
13
14 operations = [
15 migrations.AlterField(
16 model_name='post',
17 name='text',
18 field=models.TextField(blank=True, default=''),
19 ),
20 migrations.AlterField(
21 model_name='post',
22 name='title',
23 field=models.CharField(blank=True, default='', max_length=200),
24 ),
25 ]
@@ -1,388 +1,388 b''
1 1 import uuid
2 2 import hashlib
3 3 import re
4 4
5 5 from boards import settings
6 6 from boards.abstracts.tripcode import Tripcode
7 7 from boards.models import Attachment, KeyPair, GlobalId
8 8 from boards.models.attachment import FILE_TYPES_IMAGE
9 9 from boards.models.base import Viewable
10 10 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
11 11 from boards.models.post.manager import PostManager, NO_IP
12 12 from boards.utils import datetime_to_epoch
13 13 from django.core.exceptions import ObjectDoesNotExist
14 14 from django.core.urlresolvers import reverse
15 15 from django.db import models
16 16 from django.db.models import TextField, QuerySet, F
17 17 from django.template.defaultfilters import truncatewords, striptags
18 18 from django.template.loader import render_to_string
19 19
20 20 CSS_CLS_HIDDEN_POST = 'hidden_post'
21 21 CSS_CLS_DEAD_POST = 'dead_post'
22 22 CSS_CLS_ARCHIVE_POST = 'archive_post'
23 23 CSS_CLS_POST = 'post'
24 24 CSS_CLS_MONOCHROME = 'monochrome'
25 25
26 26 TITLE_MAX_WORDS = 10
27 27
28 28 APP_LABEL_BOARDS = 'boards'
29 29
30 30 BAN_REASON_AUTO = 'Auto'
31 31
32 32 TITLE_MAX_LENGTH = 200
33 33
34 34 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
36 36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
37 37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38 38
39 39 PARAMETER_TRUNCATED = 'truncated'
40 40 PARAMETER_TAG = 'tag'
41 41 PARAMETER_OFFSET = 'offset'
42 42 PARAMETER_DIFF_TYPE = 'type'
43 43 PARAMETER_CSS_CLASS = 'css_class'
44 44 PARAMETER_THREAD = 'thread'
45 45 PARAMETER_IS_OPENING = 'is_opening'
46 46 PARAMETER_POST = 'post'
47 47 PARAMETER_OP_ID = 'opening_post_id'
48 48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 49 PARAMETER_REPLY_LINK = 'reply_link'
50 50 PARAMETER_NEED_OP_DATA = 'need_op_data'
51 51
52 52 POST_VIEW_PARAMS = (
53 53 'need_op_data',
54 54 'reply_link',
55 55 'need_open_link',
56 56 'truncated',
57 57 'mode_tree',
58 58 'perms',
59 59 'tree_depth',
60 60 )
61 61
62 62
63 63 class Post(models.Model, Viewable):
64 64 """A post is a message."""
65 65
66 66 objects = PostManager()
67 67
68 68 class Meta:
69 69 app_label = APP_LABEL_BOARDS
70 70 ordering = ('id',)
71 71
72 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
72 title = models.CharField(max_length=TITLE_MAX_LENGTH, blank=True, default='')
73 73 pub_time = models.DateTimeField(db_index=True)
74 text = TextField(blank=True, null=True)
74 text = TextField(blank=True, default='')
75 75 _text_rendered = TextField(blank=True, null=True, editable=False)
76 76
77 77 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
78 78 related_name='attachment_posts')
79 79
80 80 poster_ip = models.GenericIPAddressField()
81 81
82 82 # Used for cache and threads updating
83 83 last_edit_time = models.DateTimeField()
84 84
85 85 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
86 86 null=True,
87 87 blank=True, related_name='refposts',
88 88 db_index=True)
89 89 refmap = models.TextField(null=True, blank=True)
90 90 thread = models.ForeignKey('Thread', db_index=True, related_name='replies')
91 91
92 92 url = models.TextField()
93 93 uid = models.TextField(db_index=True)
94 94
95 95 # Global ID with author key. If the message was downloaded from another
96 96 # server, this indicates the server.
97 97 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
98 98 on_delete=models.CASCADE)
99 99
100 100 tripcode = models.CharField(max_length=50, blank=True, default='')
101 101 opening = models.BooleanField(db_index=True)
102 102 hidden = models.BooleanField(default=False)
103 103 version = models.IntegerField(default=1)
104 104
105 105 def __str__(self):
106 106 return 'P#{}/{}'.format(self.id, self.get_title())
107 107
108 108 def get_title(self) -> str:
109 109 return self.title
110 110
111 111 def get_title_or_text(self):
112 112 title = self.get_title()
113 113 if not title:
114 114 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
115 115
116 116 return title
117 117
118 118 def build_refmap(self, excluded_ids=None) -> None:
119 119 """
120 120 Builds a replies map string from replies list. This is a cache to stop
121 121 the server from recalculating the map on every post show.
122 122 """
123 123
124 124 replies = self.referenced_posts
125 125 if excluded_ids is not None:
126 126 replies = replies.exclude(id__in=excluded_ids)
127 127 else:
128 128 replies = replies.all()
129 129
130 130 post_urls = [refpost.get_link_view() for refpost in replies]
131 131
132 132 self.refmap = ', '.join(post_urls)
133 133
134 134 def is_referenced(self) -> bool:
135 135 return self.refmap and len(self.refmap) > 0
136 136
137 137 def is_opening(self) -> bool:
138 138 """
139 139 Checks if this is an opening post or just a reply.
140 140 """
141 141
142 142 return self.opening
143 143
144 144 def get_absolute_url(self, thread=None):
145 145 # Url is cached only for the "main" thread. When getting url
146 146 # for other threads, do it manually.
147 147 return self.url
148 148
149 149 def get_thread(self):
150 150 return self.thread
151 151
152 152 def get_thread_id(self):
153 153 return self.thread_id
154 154
155 155 def _get_cache_key(self):
156 156 return [datetime_to_epoch(self.last_edit_time)]
157 157
158 158 def get_view_params(self, *args, **kwargs):
159 159 """
160 160 Gets the parameters required for viewing the post based on the arguments
161 161 given and the post itself.
162 162 """
163 163 thread = kwargs.get('thread') or self.get_thread()
164 164
165 165 css_classes = [CSS_CLS_POST]
166 166 if thread.is_archived():
167 167 css_classes.append(CSS_CLS_ARCHIVE_POST)
168 168 elif not thread.can_bump():
169 169 css_classes.append(CSS_CLS_DEAD_POST)
170 170 if self.is_hidden():
171 171 css_classes.append(CSS_CLS_HIDDEN_POST)
172 172 if thread.is_monochrome():
173 173 css_classes.append(CSS_CLS_MONOCHROME)
174 174
175 175 params = dict()
176 176 for param in POST_VIEW_PARAMS:
177 177 if param in kwargs:
178 178 params[param] = kwargs[param]
179 179
180 180 params.update({
181 181 PARAMETER_POST: self,
182 182 PARAMETER_IS_OPENING: self.is_opening(),
183 183 PARAMETER_THREAD: thread,
184 184 PARAMETER_CSS_CLASS: ' '.join(css_classes),
185 185 })
186 186
187 187 return params
188 188
189 189 def get_view(self, *args, **kwargs) -> str:
190 190 """
191 191 Renders post's HTML view. Some of the post params can be passed over
192 192 kwargs for the means of caching (if we view the thread, some params
193 193 are same for every post and don't need to be computed over and over.
194 194 """
195 195 params = self.get_view_params(*args, **kwargs)
196 196
197 197 return render_to_string('boards/post.html', params)
198 198
199 199 def get_first_image(self) -> Attachment:
200 200 image = None
201 201 try:
202 202 image = self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE).earliest('id')
203 203 except Attachment.DoesNotExist:
204 204 pass
205 205 return image
206 206
207 207 def set_global_id(self, key_pair=None):
208 208 """
209 209 Sets global id based on the given key pair. If no key pair is given,
210 210 default one is used.
211 211 """
212 212
213 213 if key_pair:
214 214 key = key_pair
215 215 else:
216 216 try:
217 217 key = KeyPair.objects.get(primary=True)
218 218 except KeyPair.DoesNotExist:
219 219 # Do not update the global id because there is no key defined
220 220 return
221 221 global_id = GlobalId(key_type=key.key_type,
222 222 key=key.public_key,
223 223 local_id=self.id)
224 224 global_id.save()
225 225
226 226 self.global_id = global_id
227 227
228 228 self.save(update_fields=['global_id'])
229 229
230 230 def get_pub_time_str(self):
231 231 return str(self.pub_time)
232 232
233 233 def get_replied_ids(self):
234 234 """
235 235 Gets ID list of the posts that this post replies.
236 236 """
237 237
238 238 raw_text = self.get_raw_text()
239 239
240 240 local_replied = REGEX_REPLY.findall(raw_text)
241 241 global_replied = []
242 242 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
243 243 key_type = match[0]
244 244 key = match[1]
245 245 local_id = match[2]
246 246
247 247 try:
248 248 global_id = GlobalId.objects.get(key_type=key_type,
249 249 key=key, local_id=local_id)
250 250 for post in Post.objects.filter(global_id=global_id).only('id'):
251 251 global_replied.append(post.id)
252 252 except GlobalId.DoesNotExist:
253 253 pass
254 254 return local_replied + global_replied
255 255
256 256 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
257 257 include_last_update=False) -> str:
258 258 """
259 259 Gets post HTML or JSON data that can be rendered on a page or used by
260 260 API.
261 261 """
262 262
263 263 return get_exporter(format_type).export(self, request,
264 264 include_last_update)
265 265
266 266 def notify_clients(self, recursive=True):
267 267 """
268 268 Sends post HTML data to the thread web socket.
269 269 """
270 270
271 271 if not settings.get_bool('External', 'WebsocketsEnabled'):
272 272 return
273 273
274 274 thread_ids = list()
275 275 self.get_thread().notify_clients()
276 276
277 277 if recursive:
278 278 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
279 279 post_id = reply_number.group(1)
280 280
281 281 try:
282 282 ref_post = Post.objects.get(id=post_id)
283 283
284 284 if ref_post.get_thread().id not in thread_ids:
285 285 # If post is in this thread, its thread was already notified.
286 286 # Otherwise, notify its thread separately.
287 287 ref_post.notify_clients(recursive=False)
288 288 except ObjectDoesNotExist:
289 289 pass
290 290
291 291 def _build_url(self):
292 292 opening = self.is_opening()
293 293 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
294 294 url = reverse('thread', kwargs={'post_id': opening_id})
295 295 if not opening:
296 296 url += '#' + str(self.id)
297 297
298 298 return url
299 299
300 300 def save(self, force_insert=False, force_update=False, using=None,
301 301 update_fields=None):
302 302 new_post = self.id is None
303 303
304 304 self.uid = str(uuid.uuid4())
305 305 if update_fields is not None and 'uid' not in update_fields:
306 306 update_fields += ['uid']
307 307
308 308 if not new_post:
309 309 thread = self.get_thread()
310 310 if thread:
311 311 thread.last_edit_time = self.last_edit_time
312 312 thread.save(update_fields=['last_edit_time', 'status'])
313 313
314 314 super().save(force_insert, force_update, using, update_fields)
315 315
316 316 if new_post:
317 317 self.url = self._build_url()
318 318 super().save(update_fields=['url'])
319 319
320 320 def get_text(self) -> str:
321 321 return self._text_rendered
322 322
323 323 def get_raw_text(self) -> str:
324 324 return self.text
325 325
326 326 def get_sync_text(self) -> str:
327 327 """
328 328 Returns text applicable for sync. It has absolute post reflinks.
329 329 """
330 330
331 331 replacements = dict()
332 332 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
333 333 try:
334 334 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
335 335 replacements[post_id] = absolute_post_id
336 336 except Post.DoesNotExist:
337 337 pass
338 338
339 339 text = self.get_raw_text() or ''
340 340 for key in replacements:
341 341 text = text.replace('[post]{}[/post]'.format(key),
342 342 '[post]{}[/post]'.format(replacements[key]))
343 343 text = text.replace('\r\n', '\n').replace('\r', '\n')
344 344
345 345 return text
346 346
347 347 def get_tripcode(self):
348 348 if self.tripcode:
349 349 return Tripcode(self.tripcode)
350 350
351 351 def get_link_view(self):
352 352 """
353 353 Gets view of a reflink to the post.
354 354 """
355 355 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
356 356 self.id)
357 357 if self.is_opening():
358 358 result = '<b>{}</b>'.format(result)
359 359
360 360 return result
361 361
362 362 def is_hidden(self) -> bool:
363 363 return self.hidden
364 364
365 365 def set_hidden(self, hidden):
366 366 self.hidden = hidden
367 367
368 368 def increment_version(self):
369 369 self.version = F('version') + 1
370 370
371 371 def clear_cache(self):
372 372 """
373 373 Clears sync data (content cache, signatures etc).
374 374 """
375 375 global_id = self.global_id
376 376 if global_id is not None and global_id.is_local()\
377 377 and global_id.content is not None:
378 378 global_id.clear_cache()
379 379
380 380 def get_tags(self):
381 381 return self.get_thread().get_tags()
382 382
383 383 def get_ip_color(self):
384 384 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
385 385
386 386 def has_ip(self):
387 387 return self.poster_ip != NO_IP
388 388
General Comments 0
You need to be logged in to leave comments. Login now