|
@@
-1,453
+1,401
b''
|
|
1
|
import logging
|
|
1
|
import logging
|
|
2
|
import uuid
|
|
2
|
import uuid
|
|
3
|
|
|
3
|
|
|
4
|
import re
|
|
4
|
import re
|
|
5
|
from boards import settings
|
|
5
|
from boards import settings
|
|
6
|
from boards.abstracts.tripcode import Tripcode
|
|
6
|
from boards.abstracts.tripcode import Tripcode
|
|
7
|
from boards.mdx_neboard import get_parser
|
|
7
|
from boards.mdx_neboard import get_parser
|
|
8
|
from boards.models import PostImage, Attachment, KeyPair, GlobalId
|
|
8
|
from boards.models import PostImage, Attachment, KeyPair, GlobalId
|
|
9
|
from boards.models.base import Viewable
|
|
9
|
from boards.models.base import Viewable
|
|
10
|
from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
|
|
10
|
from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
|
|
11
|
from boards.models.post.manager import PostManager
|
|
11
|
from boards.models.post.manager import PostManager
|
|
12
|
from boards.models.user import Notification
|
|
12
|
from boards.models.user import Notification
|
|
13
|
from django.core.exceptions import ObjectDoesNotExist
|
|
13
|
from django.core.exceptions import ObjectDoesNotExist
|
|
14
|
from django.core.urlresolvers import reverse
|
|
14
|
from django.core.urlresolvers import reverse
|
|
15
|
from django.db import models
|
|
15
|
from django.db import models
|
|
16
|
from django.db.models import TextField, QuerySet
|
|
16
|
from django.db.models import TextField, QuerySet
|
|
17
|
from django.db.models.signals import pre_save, post_save, pre_delete, \
|
|
17
|
from django.db.models.signals import pre_save, post_save, pre_delete, \
|
|
18
|
post_delete
|
|
18
|
post_delete
|
|
19
|
from django.dispatch import receiver
|
|
19
|
from django.dispatch import receiver
|
|
20
|
from django.template.defaultfilters import truncatewords, striptags
|
|
20
|
from django.template.defaultfilters import truncatewords, striptags
|
|
21
|
from django.template.loader import render_to_string
|
|
21
|
from django.template.loader import render_to_string
|
|
22
|
from django.utils import timezone
|
|
22
|
from django.utils import timezone
|
|
23
|
|
|
23
|
|
|
24
|
CSS_CLS_HIDDEN_POST = 'hidden_post'
|
|
24
|
CSS_CLS_HIDDEN_POST = 'hidden_post'
|
|
25
|
CSS_CLS_DEAD_POST = 'dead_post'
|
|
25
|
CSS_CLS_DEAD_POST = 'dead_post'
|
|
26
|
CSS_CLS_ARCHIVE_POST = 'archive_post'
|
|
26
|
CSS_CLS_ARCHIVE_POST = 'archive_post'
|
|
27
|
CSS_CLS_POST = 'post'
|
|
27
|
CSS_CLS_POST = 'post'
|
|
28
|
CSS_CLS_MONOCHROME = 'monochrome'
|
|
28
|
CSS_CLS_MONOCHROME = 'monochrome'
|
|
29
|
|
|
29
|
|
|
30
|
TITLE_MAX_WORDS = 10
|
|
30
|
TITLE_MAX_WORDS = 10
|
|
31
|
|
|
31
|
|
|
32
|
APP_LABEL_BOARDS = 'boards'
|
|
32
|
APP_LABEL_BOARDS = 'boards'
|
|
33
|
|
|
33
|
|
|
34
|
BAN_REASON_AUTO = 'Auto'
|
|
34
|
BAN_REASON_AUTO = 'Auto'
|
|
35
|
|
|
35
|
|
|
36
|
IMAGE_THUMB_SIZE = (200, 150)
|
|
36
|
IMAGE_THUMB_SIZE = (200, 150)
|
|
37
|
|
|
37
|
|
|
38
|
TITLE_MAX_LENGTH = 200
|
|
38
|
TITLE_MAX_LENGTH = 200
|
|
39
|
|
|
39
|
|
|
40
|
REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
|
|
40
|
REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
|
|
41
|
REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
|
|
41
|
REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
|
|
42
|
REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
|
|
42
|
REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
|
|
43
|
REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
|
|
43
|
REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
|
|
44
|
|
|
44
|
|
|
45
|
PARAMETER_TRUNCATED = 'truncated'
|
|
45
|
PARAMETER_TRUNCATED = 'truncated'
|
|
46
|
PARAMETER_TAG = 'tag'
|
|
46
|
PARAMETER_TAG = 'tag'
|
|
47
|
PARAMETER_OFFSET = 'offset'
|
|
47
|
PARAMETER_OFFSET = 'offset'
|
|
48
|
PARAMETER_DIFF_TYPE = 'type'
|
|
48
|
PARAMETER_DIFF_TYPE = 'type'
|
|
49
|
PARAMETER_CSS_CLASS = 'css_class'
|
|
49
|
PARAMETER_CSS_CLASS = 'css_class'
|
|
50
|
PARAMETER_THREAD = 'thread'
|
|
50
|
PARAMETER_THREAD = 'thread'
|
|
51
|
PARAMETER_IS_OPENING = 'is_opening'
|
|
51
|
PARAMETER_IS_OPENING = 'is_opening'
|
|
52
|
PARAMETER_POST = 'post'
|
|
52
|
PARAMETER_POST = 'post'
|
|
53
|
PARAMETER_OP_ID = 'opening_post_id'
|
|
53
|
PARAMETER_OP_ID = 'opening_post_id'
|
|
54
|
PARAMETER_NEED_OPEN_LINK = 'need_open_link'
|
|
54
|
PARAMETER_NEED_OPEN_LINK = 'need_open_link'
|
|
55
|
PARAMETER_REPLY_LINK = 'reply_link'
|
|
55
|
PARAMETER_REPLY_LINK = 'reply_link'
|
|
56
|
PARAMETER_NEED_OP_DATA = 'need_op_data'
|
|
56
|
PARAMETER_NEED_OP_DATA = 'need_op_data'
|
|
57
|
|
|
57
|
|
|
58
|
POST_VIEW_PARAMS = (
|
|
58
|
POST_VIEW_PARAMS = (
|
|
59
|
'need_op_data',
|
|
59
|
'need_op_data',
|
|
60
|
'reply_link',
|
|
60
|
'reply_link',
|
|
61
|
'need_open_link',
|
|
61
|
'need_open_link',
|
|
62
|
'truncated',
|
|
62
|
'truncated',
|
|
63
|
'mode_tree',
|
|
63
|
'mode_tree',
|
|
64
|
'perms',
|
|
64
|
'perms',
|
|
65
|
'tree_depth',
|
|
65
|
'tree_depth',
|
|
66
|
)
|
|
66
|
)
|
|
67
|
|
|
67
|
|
|
68
|
|
|
68
|
|
|
69
|
class Post(models.Model, Viewable):
|
|
69
|
class Post(models.Model, Viewable):
|
|
70
|
"""A post is a message."""
|
|
70
|
"""A post is a message."""
|
|
71
|
|
|
71
|
|
|
72
|
objects = PostManager()
|
|
72
|
objects = PostManager()
|
|
73
|
|
|
73
|
|
|
74
|
class Meta:
|
|
74
|
class Meta:
|
|
75
|
app_label = APP_LABEL_BOARDS
|
|
75
|
app_label = APP_LABEL_BOARDS
|
|
76
|
ordering = ('id',)
|
|
76
|
ordering = ('id',)
|
|
77
|
|
|
77
|
|
|
78
|
title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
|
|
78
|
title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
|
|
79
|
pub_time = models.DateTimeField()
|
|
79
|
pub_time = models.DateTimeField()
|
|
80
|
text = TextField(blank=True, null=True)
|
|
80
|
text = TextField(blank=True, null=True)
|
|
81
|
_text_rendered = TextField(blank=True, null=True, editable=False)
|
|
81
|
_text_rendered = TextField(blank=True, null=True, editable=False)
|
|
82
|
|
|
82
|
|
|
83
|
images = models.ManyToManyField(PostImage, null=True, blank=True,
|
|
83
|
images = models.ManyToManyField(PostImage, null=True, blank=True,
|
|
84
|
related_name='post_images', db_index=True)
|
|
84
|
related_name='post_images', db_index=True)
|
|
85
|
attachments = models.ManyToManyField(Attachment, null=True, blank=True,
|
|
85
|
attachments = models.ManyToManyField(Attachment, null=True, blank=True,
|
|
86
|
related_name='attachment_posts')
|
|
86
|
related_name='attachment_posts')
|
|
87
|
|
|
87
|
|
|
88
|
poster_ip = models.GenericIPAddressField()
|
|
88
|
poster_ip = models.GenericIPAddressField()
|
|
89
|
|
|
89
|
|
|
90
|
# TODO This field can be removed cause UID is used for update now
|
|
90
|
# TODO This field can be removed cause UID is used for update now
|
|
91
|
last_edit_time = models.DateTimeField()
|
|
91
|
last_edit_time = models.DateTimeField()
|
|
92
|
|
|
92
|
|
|
93
|
referenced_posts = models.ManyToManyField('Post', symmetrical=False,
|
|
93
|
referenced_posts = models.ManyToManyField('Post', symmetrical=False,
|
|
94
|
null=True,
|
|
94
|
null=True,
|
|
95
|
blank=True, related_name='refposts',
|
|
95
|
blank=True, related_name='refposts',
|
|
96
|
db_index=True)
|
|
96
|
db_index=True)
|
|
97
|
refmap = models.TextField(null=True, blank=True)
|
|
97
|
refmap = models.TextField(null=True, blank=True)
|
|
98
|
threads = models.ManyToManyField('Thread', db_index=True,
|
|
98
|
threads = models.ManyToManyField('Thread', db_index=True,
|
|
99
|
related_name='multi_replies')
|
|
99
|
related_name='multi_replies')
|
|
100
|
thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
|
|
100
|
thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
|
|
101
|
|
|
101
|
|
|
102
|
url = models.TextField()
|
|
102
|
url = models.TextField()
|
|
103
|
uid = models.TextField(db_index=True)
|
|
103
|
uid = models.TextField(db_index=True)
|
|
104
|
|
|
104
|
|
|
105
|
# Global ID with author key. If the message was downloaded from another
|
|
105
|
# Global ID with author key. If the message was downloaded from another
|
|
106
|
# server, this indicates the server.
|
|
106
|
# server, this indicates the server.
|
|
107
|
global_id = models.OneToOneField(GlobalId, null=True, blank=True,
|
|
107
|
global_id = models.OneToOneField(GlobalId, null=True, blank=True,
|
|
108
|
on_delete=models.CASCADE)
|
|
108
|
on_delete=models.CASCADE)
|
|
109
|
|
|
109
|
|
|
110
|
tripcode = models.CharField(max_length=50, blank=True, default='')
|
|
110
|
tripcode = models.CharField(max_length=50, blank=True, default='')
|
|
111
|
opening = models.BooleanField(db_index=True)
|
|
111
|
opening = models.BooleanField(db_index=True)
|
|
112
|
hidden = models.BooleanField(default=False)
|
|
112
|
hidden = models.BooleanField(default=False)
|
|
113
|
|
|
113
|
|
|
114
|
def __str__(self):
|
|
114
|
def __str__(self):
|
|
115
|
return 'P#{}/{}'.format(self.id, self.get_title())
|
|
115
|
return 'P#{}/{}'.format(self.id, self.get_title())
|
|
116
|
|
|
116
|
|
|
117
|
def get_referenced_posts(self):
|
|
117
|
def get_referenced_posts(self):
|
|
118
|
threads = self.get_threads().all()
|
|
118
|
threads = self.get_threads().all()
|
|
119
|
return self.referenced_posts.filter(threads__in=threads)\
|
|
119
|
return self.referenced_posts.filter(threads__in=threads)\
|
|
120
|
.order_by('pub_time').distinct().all()
|
|
120
|
.order_by('pub_time').distinct().all()
|
|
121
|
|
|
121
|
|
|
122
|
def get_title(self) -> str:
|
|
122
|
def get_title(self) -> str:
|
|
123
|
return self.title
|
|
123
|
return self.title
|
|
124
|
|
|
124
|
|
|
125
|
def get_title_or_text(self):
|
|
125
|
def get_title_or_text(self):
|
|
126
|
title = self.get_title()
|
|
126
|
title = self.get_title()
|
|
127
|
if not title:
|
|
127
|
if not title:
|
|
128
|
title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
|
|
128
|
title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
|
|
129
|
|
|
129
|
|
|
130
|
return title
|
|
130
|
return title
|
|
131
|
|
|
131
|
|
|
132
|
def build_refmap(self) -> None:
|
|
132
|
def build_refmap(self) -> None:
|
|
133
|
"""
|
|
133
|
"""
|
|
134
|
Builds a replies map string from replies list. This is a cache to stop
|
|
134
|
Builds a replies map string from replies list. This is a cache to stop
|
|
135
|
the server from recalculating the map on every post show.
|
|
135
|
the server from recalculating the map on every post show.
|
|
136
|
"""
|
|
136
|
"""
|
|
137
|
|
|
137
|
|
|
138
|
post_urls = [refpost.get_link_view()
|
|
138
|
post_urls = [refpost.get_link_view()
|
|
139
|
for refpost in self.referenced_posts.all()]
|
|
139
|
for refpost in self.referenced_posts.all()]
|
|
140
|
|
|
140
|
|
|
141
|
self.refmap = ', '.join(post_urls)
|
|
141
|
self.refmap = ', '.join(post_urls)
|
|
142
|
|
|
142
|
|
|
143
|
def is_referenced(self) -> bool:
|
|
143
|
def is_referenced(self) -> bool:
|
|
144
|
return self.refmap and len(self.refmap) > 0
|
|
144
|
return self.refmap and len(self.refmap) > 0
|
|
145
|
|
|
145
|
|
|
146
|
def is_opening(self) -> bool:
|
|
146
|
def is_opening(self) -> bool:
|
|
147
|
"""
|
|
147
|
"""
|
|
148
|
Checks if this is an opening post or just a reply.
|
|
148
|
Checks if this is an opening post or just a reply.
|
|
149
|
"""
|
|
149
|
"""
|
|
150
|
|
|
150
|
|
|
151
|
return self.opening
|
|
151
|
return self.opening
|
|
152
|
|
|
152
|
|
|
153
|
def get_absolute_url(self, thread=None):
|
|
153
|
def get_absolute_url(self, thread=None):
|
|
154
|
url = None
|
|
154
|
url = None
|
|
155
|
|
|
155
|
|
|
156
|
if thread is None:
|
|
156
|
if thread is None:
|
|
157
|
thread = self.get_thread()
|
|
157
|
thread = self.get_thread()
|
|
158
|
|
|
158
|
|
|
159
|
# Url is cached only for the "main" thread. When getting url
|
|
159
|
# Url is cached only for the "main" thread. When getting url
|
|
160
|
# for other threads, do it manually.
|
|
160
|
# for other threads, do it manually.
|
|
161
|
if self.url:
|
|
161
|
if self.url:
|
|
162
|
url = self.url
|
|
162
|
url = self.url
|
|
163
|
|
|
163
|
|
|
164
|
if url is None:
|
|
164
|
if url is None:
|
|
165
|
opening = self.is_opening()
|
|
165
|
opening = self.is_opening()
|
|
166
|
opening_id = self.id if opening else thread.get_opening_post_id()
|
|
166
|
opening_id = self.id if opening else thread.get_opening_post_id()
|
|
167
|
url = reverse('thread', kwargs={'post_id': opening_id})
|
|
167
|
url = reverse('thread', kwargs={'post_id': opening_id})
|
|
168
|
if not opening:
|
|
168
|
if not opening:
|
|
169
|
url += '#' + str(self.id)
|
|
169
|
url += '#' + str(self.id)
|
|
170
|
|
|
170
|
|
|
171
|
return url
|
|
171
|
return url
|
|
172
|
|
|
172
|
|
|
173
|
def get_thread(self):
|
|
173
|
def get_thread(self):
|
|
174
|
return self.thread
|
|
174
|
return self.thread
|
|
175
|
|
|
175
|
|
|
176
|
def get_thread_id(self):
|
|
176
|
def get_thread_id(self):
|
|
177
|
return self.thread_id
|
|
177
|
return self.thread_id
|
|
178
|
|
|
178
|
|
|
179
|
def get_threads(self) -> QuerySet:
|
|
179
|
def get_threads(self) -> QuerySet:
|
|
180
|
"""
|
|
180
|
"""
|
|
181
|
Gets post's thread.
|
|
181
|
Gets post's thread.
|
|
182
|
"""
|
|
182
|
"""
|
|
183
|
|
|
183
|
|
|
184
|
return self.threads
|
|
184
|
return self.threads
|
|
185
|
|
|
185
|
|
|
186
|
def get_view(self, *args, **kwargs) -> str:
|
|
186
|
def get_view(self, *args, **kwargs) -> str:
|
|
187
|
"""
|
|
187
|
"""
|
|
188
|
Renders post's HTML view. Some of the post params can be passed over
|
|
188
|
Renders post's HTML view. Some of the post params can be passed over
|
|
189
|
kwargs for the means of caching (if we view the thread, some params
|
|
189
|
kwargs for the means of caching (if we view the thread, some params
|
|
190
|
are same for every post and don't need to be computed over and over.
|
|
190
|
are same for every post and don't need to be computed over and over.
|
|
191
|
"""
|
|
191
|
"""
|
|
192
|
|
|
192
|
|
|
193
|
thread = self.get_thread()
|
|
193
|
thread = self.get_thread()
|
|
194
|
|
|
194
|
|
|
195
|
css_classes = [CSS_CLS_POST]
|
|
195
|
css_classes = [CSS_CLS_POST]
|
|
196
|
if thread.is_archived():
|
|
196
|
if thread.is_archived():
|
|
197
|
css_classes.append(CSS_CLS_ARCHIVE_POST)
|
|
197
|
css_classes.append(CSS_CLS_ARCHIVE_POST)
|
|
198
|
elif not thread.can_bump():
|
|
198
|
elif not thread.can_bump():
|
|
199
|
css_classes.append(CSS_CLS_DEAD_POST)
|
|
199
|
css_classes.append(CSS_CLS_DEAD_POST)
|
|
200
|
if self.is_hidden():
|
|
200
|
if self.is_hidden():
|
|
201
|
css_classes.append(CSS_CLS_HIDDEN_POST)
|
|
201
|
css_classes.append(CSS_CLS_HIDDEN_POST)
|
|
202
|
if thread.is_monochrome():
|
|
202
|
if thread.is_monochrome():
|
|
203
|
css_classes.append(CSS_CLS_MONOCHROME)
|
|
203
|
css_classes.append(CSS_CLS_MONOCHROME)
|
|
204
|
|
|
204
|
|
|
205
|
params = dict()
|
|
205
|
params = dict()
|
|
206
|
for param in POST_VIEW_PARAMS:
|
|
206
|
for param in POST_VIEW_PARAMS:
|
|
207
|
if param in kwargs:
|
|
207
|
if param in kwargs:
|
|
208
|
params[param] = kwargs[param]
|
|
208
|
params[param] = kwargs[param]
|
|
209
|
|
|
209
|
|
|
210
|
params.update({
|
|
210
|
params.update({
|
|
211
|
PARAMETER_POST: self,
|
|
211
|
PARAMETER_POST: self,
|
|
212
|
PARAMETER_IS_OPENING: self.is_opening(),
|
|
212
|
PARAMETER_IS_OPENING: self.is_opening(),
|
|
213
|
PARAMETER_THREAD: thread,
|
|
213
|
PARAMETER_THREAD: thread,
|
|
214
|
PARAMETER_CSS_CLASS: ' '.join(css_classes),
|
|
214
|
PARAMETER_CSS_CLASS: ' '.join(css_classes),
|
|
215
|
})
|
|
215
|
})
|
|
216
|
|
|
216
|
|
|
217
|
return render_to_string('boards/post.html', params)
|
|
217
|
return render_to_string('boards/post.html', params)
|
|
218
|
|
|
218
|
|
|
219
|
def get_search_view(self, *args, **kwargs):
|
|
219
|
def get_search_view(self, *args, **kwargs):
|
|
220
|
return self.get_view(need_op_data=True, *args, **kwargs)
|
|
220
|
return self.get_view(need_op_data=True, *args, **kwargs)
|
|
221
|
|
|
221
|
|
|
222
|
def get_first_image(self) -> PostImage:
|
|
222
|
def get_first_image(self) -> PostImage:
|
|
223
|
return self.images.earliest('id')
|
|
223
|
return self.images.earliest('id')
|
|
224
|
|
|
224
|
|
|
225
|
def set_global_id(self, key_pair=None):
|
|
225
|
def set_global_id(self, key_pair=None):
|
|
226
|
"""
|
|
226
|
"""
|
|
227
|
Sets global id based on the given key pair. If no key pair is given,
|
|
227
|
Sets global id based on the given key pair. If no key pair is given,
|
|
228
|
default one is used.
|
|
228
|
default one is used.
|
|
229
|
"""
|
|
229
|
"""
|
|
230
|
|
|
230
|
|
|
231
|
if key_pair:
|
|
231
|
if key_pair:
|
|
232
|
key = key_pair
|
|
232
|
key = key_pair
|
|
233
|
else:
|
|
233
|
else:
|
|
234
|
try:
|
|
234
|
try:
|
|
235
|
key = KeyPair.objects.get(primary=True)
|
|
235
|
key = KeyPair.objects.get(primary=True)
|
|
236
|
except KeyPair.DoesNotExist:
|
|
236
|
except KeyPair.DoesNotExist:
|
|
237
|
# Do not update the global id because there is no key defined
|
|
237
|
# Do not update the global id because there is no key defined
|
|
238
|
return
|
|
238
|
return
|
|
239
|
global_id = GlobalId(key_type=key.key_type,
|
|
239
|
global_id = GlobalId(key_type=key.key_type,
|
|
240
|
key=key.public_key,
|
|
240
|
key=key.public_key,
|
|
241
|
local_id=self.id)
|
|
241
|
local_id=self.id)
|
|
242
|
global_id.save()
|
|
242
|
global_id.save()
|
|
243
|
|
|
243
|
|
|
244
|
self.global_id = global_id
|
|
244
|
self.global_id = global_id
|
|
245
|
|
|
245
|
|
|
246
|
self.save(update_fields=['global_id'])
|
|
246
|
self.save(update_fields=['global_id'])
|
|
247
|
|
|
247
|
|
|
248
|
def get_pub_time_str(self):
|
|
248
|
def get_pub_time_str(self):
|
|
249
|
return str(self.pub_time)
|
|
249
|
return str(self.pub_time)
|
|
250
|
|
|
250
|
|
|
251
|
def get_replied_ids(self):
|
|
251
|
def get_replied_ids(self):
|
|
252
|
"""
|
|
252
|
"""
|
|
253
|
Gets ID list of the posts that this post replies.
|
|
253
|
Gets ID list of the posts that this post replies.
|
|
254
|
"""
|
|
254
|
"""
|
|
255
|
|
|
255
|
|
|
256
|
raw_text = self.get_raw_text()
|
|
256
|
raw_text = self.get_raw_text()
|
|
257
|
|
|
257
|
|
|
258
|
local_replied = REGEX_REPLY.findall(raw_text)
|
|
258
|
local_replied = REGEX_REPLY.findall(raw_text)
|
|
259
|
global_replied = []
|
|
259
|
global_replied = []
|
|
260
|
for match in REGEX_GLOBAL_REPLY.findall(raw_text):
|
|
260
|
for match in REGEX_GLOBAL_REPLY.findall(raw_text):
|
|
261
|
key_type = match[0]
|
|
261
|
key_type = match[0]
|
|
262
|
key = match[1]
|
|
262
|
key = match[1]
|
|
263
|
local_id = match[2]
|
|
263
|
local_id = match[2]
|
|
264
|
|
|
264
|
|
|
265
|
try:
|
|
265
|
try:
|
|
266
|
global_id = GlobalId.objects.get(key_type=key_type,
|
|
266
|
global_id = GlobalId.objects.get(key_type=key_type,
|
|
267
|
key=key, local_id=local_id)
|
|
267
|
key=key, local_id=local_id)
|
|
268
|
for post in Post.objects.filter(global_id=global_id).only('id'):
|
|
268
|
for post in Post.objects.filter(global_id=global_id).only('id'):
|
|
269
|
global_replied.append(post.id)
|
|
269
|
global_replied.append(post.id)
|
|
270
|
except GlobalId.DoesNotExist:
|
|
270
|
except GlobalId.DoesNotExist:
|
|
271
|
pass
|
|
271
|
pass
|
|
272
|
return local_replied + global_replied
|
|
272
|
return local_replied + global_replied
|
|
273
|
|
|
273
|
|
|
274
|
def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
|
|
274
|
def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
|
|
275
|
include_last_update=False) -> str:
|
|
275
|
include_last_update=False) -> str:
|
|
276
|
"""
|
|
276
|
"""
|
|
277
|
Gets post HTML or JSON data that can be rendered on a page or used by
|
|
277
|
Gets post HTML or JSON data that can be rendered on a page or used by
|
|
278
|
API.
|
|
278
|
API.
|
|
279
|
"""
|
|
279
|
"""
|
|
280
|
|
|
280
|
|
|
281
|
return get_exporter(format_type).export(self, request,
|
|
281
|
return get_exporter(format_type).export(self, request,
|
|
282
|
include_last_update)
|
|
282
|
include_last_update)
|
|
283
|
|
|
283
|
|
|
284
|
def notify_clients(self, recursive=True):
|
|
284
|
def notify_clients(self, recursive=True):
|
|
285
|
"""
|
|
285
|
"""
|
|
286
|
Sends post HTML data to the thread web socket.
|
|
286
|
Sends post HTML data to the thread web socket.
|
|
287
|
"""
|
|
287
|
"""
|
|
288
|
|
|
288
|
|
|
289
|
if not settings.get_bool('External', 'WebsocketsEnabled'):
|
|
289
|
if not settings.get_bool('External', 'WebsocketsEnabled'):
|
|
290
|
return
|
|
290
|
return
|
|
291
|
|
|
291
|
|
|
292
|
thread_ids = list()
|
|
292
|
thread_ids = list()
|
|
293
|
for thread in self.get_threads().all():
|
|
293
|
for thread in self.get_threads().all():
|
|
294
|
thread_ids.append(thread.id)
|
|
294
|
thread_ids.append(thread.id)
|
|
295
|
|
|
295
|
|
|
296
|
thread.notify_clients()
|
|
296
|
thread.notify_clients()
|
|
297
|
|
|
297
|
|
|
298
|
if recursive:
|
|
298
|
if recursive:
|
|
299
|
for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
|
|
299
|
for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
|
|
300
|
post_id = reply_number.group(1)
|
|
300
|
post_id = reply_number.group(1)
|
|
301
|
|
|
301
|
|
|
302
|
try:
|
|
302
|
try:
|
|
303
|
ref_post = Post.objects.get(id=post_id)
|
|
303
|
ref_post = Post.objects.get(id=post_id)
|
|
304
|
|
|
304
|
|
|
305
|
if ref_post.get_threads().exclude(id__in=thread_ids).exists():
|
|
305
|
if ref_post.get_threads().exclude(id__in=thread_ids).exists():
|
|
306
|
# If post is in this thread, its thread was already notified.
|
|
306
|
# If post is in this thread, its thread was already notified.
|
|
307
|
# Otherwise, notify its thread separately.
|
|
307
|
# Otherwise, notify its thread separately.
|
|
308
|
ref_post.notify_clients(recursive=False)
|
|
308
|
ref_post.notify_clients(recursive=False)
|
|
309
|
except ObjectDoesNotExist:
|
|
309
|
except ObjectDoesNotExist:
|
|
310
|
pass
|
|
310
|
pass
|
|
311
|
|
|
311
|
|
|
312
|
def build_url(self):
|
|
312
|
def build_url(self):
|
|
313
|
self.url = self.get_absolute_url()
|
|
313
|
self.url = self.get_absolute_url()
|
|
314
|
self.save(update_fields=['url'])
|
|
314
|
self.save(update_fields=['url'])
|
|
315
|
|
|
315
|
|
|
316
|
def save(self, force_insert=False, force_update=False, using=None,
|
|
316
|
def save(self, force_insert=False, force_update=False, using=None,
|
|
317
|
update_fields=None):
|
|
317
|
update_fields=None):
|
|
318
|
new_post = self.id is None
|
|
318
|
new_post = self.id is None
|
|
319
|
|
|
319
|
|
|
320
|
self.uid = str(uuid.uuid4())
|
|
320
|
self.uid = str(uuid.uuid4())
|
|
321
|
if update_fields is not None and 'uid' not in update_fields:
|
|
321
|
if update_fields is not None and 'uid' not in update_fields:
|
|
322
|
update_fields += ['uid']
|
|
322
|
update_fields += ['uid']
|
|
323
|
|
|
323
|
|
|
324
|
if not new_post:
|
|
324
|
if not new_post:
|
|
325
|
for thread in self.get_threads().all():
|
|
325
|
for thread in self.get_threads().all():
|
|
326
|
thread.last_edit_time = self.last_edit_time
|
|
326
|
thread.last_edit_time = self.last_edit_time
|
|
327
|
|
|
327
|
|
|
328
|
thread.save(update_fields=['last_edit_time', 'status'])
|
|
328
|
thread.save(update_fields=['last_edit_time', 'status'])
|
|
329
|
|
|
329
|
|
|
330
|
super().save(force_insert, force_update, using, update_fields)
|
|
330
|
super().save(force_insert, force_update, using, update_fields)
|
|
331
|
|
|
331
|
|
|
332
|
if self.url is None:
|
|
332
|
if self.url is None:
|
|
333
|
self.build_url()
|
|
333
|
self.build_url()
|
|
334
|
|
|
334
|
|
|
335
|
def get_text(self) -> str:
|
|
335
|
def get_text(self) -> str:
|
|
336
|
return self._text_rendered
|
|
336
|
return self._text_rendered
|
|
337
|
|
|
337
|
|
|
338
|
def get_raw_text(self) -> str:
|
|
338
|
def get_raw_text(self) -> str:
|
|
339
|
return self.text
|
|
339
|
return self.text
|
|
340
|
|
|
340
|
|
|
341
|
def get_sync_text(self) -> str:
|
|
341
|
def get_sync_text(self) -> str:
|
|
342
|
"""
|
|
342
|
"""
|
|
343
|
Returns text applicable for sync. It has absolute post reflinks.
|
|
343
|
Returns text applicable for sync. It has absolute post reflinks.
|
|
344
|
"""
|
|
344
|
"""
|
|
345
|
|
|
345
|
|
|
346
|
replacements = dict()
|
|
346
|
replacements = dict()
|
|
347
|
for post_id in REGEX_REPLY.findall(self.get_raw_text()):
|
|
347
|
for post_id in REGEX_REPLY.findall(self.get_raw_text()):
|
|
348
|
absolute_post_id = str(Post.objects.get(id=post_id).global_id)
|
|
348
|
absolute_post_id = str(Post.objects.get(id=post_id).global_id)
|
|
349
|
replacements[post_id] = absolute_post_id
|
|
349
|
replacements[post_id] = absolute_post_id
|
|
350
|
|
|
350
|
|
|
351
|
text = self.get_raw_text() or ''
|
|
351
|
text = self.get_raw_text() or ''
|
|
352
|
for key in replacements:
|
|
352
|
for key in replacements:
|
|
353
|
text = text.replace('[post]{}[/post]'.format(key),
|
|
353
|
text = text.replace('[post]{}[/post]'.format(key),
|
|
354
|
'[post]{}[/post]'.format(replacements[key]))
|
|
354
|
'[post]{}[/post]'.format(replacements[key]))
|
|
355
|
text = text.replace('\r\n', '\n').replace('\r', '\n')
|
|
355
|
text = text.replace('\r\n', '\n').replace('\r', '\n')
|
|
356
|
|
|
356
|
|
|
357
|
return text
|
|
357
|
return text
|
|
358
|
|
|
358
|
|
|
359
|
def get_absolute_id(self) -> str:
|
|
359
|
def get_absolute_id(self) -> str:
|
|
360
|
"""
|
|
360
|
"""
|
|
361
|
If the post has many threads, shows its main thread OP id in the post
|
|
361
|
If the post has many threads, shows its main thread OP id in the post
|
|
362
|
ID.
|
|
362
|
ID.
|
|
363
|
"""
|
|
363
|
"""
|
|
364
|
|
|
364
|
|
|
365
|
if self.get_threads().count() > 1:
|
|
365
|
if self.get_threads().count() > 1:
|
|
366
|
return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
|
|
366
|
return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
|
|
367
|
else:
|
|
367
|
else:
|
|
368
|
return str(self.id)
|
|
368
|
return str(self.id)
|
|
369
|
|
|
369
|
|
|
370
|
|
|
370
|
|
|
371
|
def connect_threads(self, opening_posts):
|
|
371
|
def connect_threads(self, opening_posts):
|
|
372
|
for opening_post in opening_posts:
|
|
372
|
for opening_post in opening_posts:
|
|
373
|
threads = opening_post.get_threads().all()
|
|
373
|
threads = opening_post.get_threads().all()
|
|
374
|
for thread in threads:
|
|
374
|
for thread in threads:
|
|
375
|
if thread.can_bump():
|
|
375
|
if thread.can_bump():
|
|
376
|
thread.update_bump_status()
|
|
376
|
thread.update_bump_status()
|
|
377
|
|
|
377
|
|
|
378
|
thread.last_edit_time = self.last_edit_time
|
|
378
|
thread.last_edit_time = self.last_edit_time
|
|
379
|
thread.save(update_fields=['last_edit_time', 'status'])
|
|
379
|
thread.save(update_fields=['last_edit_time', 'status'])
|
|
380
|
self.threads.add(opening_post.get_thread())
|
|
380
|
self.threads.add(opening_post.get_thread())
|
|
381
|
|
|
381
|
|
|
382
|
def get_tripcode(self):
|
|
382
|
def get_tripcode(self):
|
|
383
|
if self.tripcode:
|
|
383
|
if self.tripcode:
|
|
384
|
return Tripcode(self.tripcode)
|
|
384
|
return Tripcode(self.tripcode)
|
|
385
|
|
|
385
|
|
|
386
|
def get_link_view(self):
|
|
386
|
def get_link_view(self):
|
|
387
|
"""
|
|
387
|
"""
|
|
388
|
Gets view of a reflink to the post.
|
|
388
|
Gets view of a reflink to the post.
|
|
389
|
"""
|
|
389
|
"""
|
|
390
|
result = '<a href="{}">>>{}</a>'.format(self.get_absolute_url(),
|
|
390
|
result = '<a href="{}">>>{}</a>'.format(self.get_absolute_url(),
|
|
391
|
self.id)
|
|
391
|
self.id)
|
|
392
|
if self.is_opening():
|
|
392
|
if self.is_opening():
|
|
393
|
result = '<b>{}</b>'.format(result)
|
|
393
|
result = '<b>{}</b>'.format(result)
|
|
394
|
|
|
394
|
|
|
395
|
return result
|
|
395
|
return result
|
|
396
|
|
|
396
|
|
|
397
|
def is_hidden(self) -> bool:
|
|
397
|
def is_hidden(self) -> bool:
|
|
398
|
return self.hidden
|
|
398
|
return self.hidden
|
|
399
|
|
|
399
|
|
|
400
|
def set_hidden(self, hidden):
|
|
400
|
def set_hidden(self, hidden):
|
|
401
|
self.hidden = hidden
|
|
401
|
self.hidden = hidden
|
|
402
|
|
|
|
|
|
403
|
|
|
|
|
|
404
|
# SIGNALS (Maybe move to other module?)
|
|
|
|
|
405
|
@receiver(post_save, sender=Post)
|
|
|
|
|
406
|
def connect_replies(instance, **kwargs):
|
|
|
|
|
407
|
for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
|
|
|
|
|
408
|
post_id = reply_number.group(1)
|
|
|
|
|
409
|
|
|
|
|
|
410
|
try:
|
|
|
|
|
411
|
referenced_post = Post.objects.get(id=post_id)
|
|
|
|
|
412
|
|
|
|
|
|
413
|
referenced_post.referenced_posts.add(instance)
|
|
|
|
|
414
|
referenced_post.last_edit_time = instance.pub_time
|
|
|
|
|
415
|
referenced_post.build_refmap()
|
|
|
|
|
416
|
referenced_post.save(update_fields=['refmap', 'last_edit_time'])
|
|
|
|
|
417
|
except ObjectDoesNotExist:
|
|
|
|
|
418
|
pass
|
|
|
|
|
419
|
|
|
|
|
|
420
|
|
|
|
|
|
421
|
@receiver(post_save, sender=Post)
|
|
|
|
|
422
|
def connect_notifications(instance, **kwargs):
|
|
|
|
|
423
|
for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
|
|
|
|
|
424
|
user_name = reply_number.group(1).lower()
|
|
|
|
|
425
|
Notification.objects.get_or_create(name=user_name, post=instance)
|
|
|
|
|
426
|
|
|
|
|
|
427
|
|
|
|
|
|
428
|
@receiver(pre_save, sender=Post)
|
|
|
|
|
429
|
def preparse_text(instance, **kwargs):
|
|
|
|
|
430
|
instance._text_rendered = get_parser().parse(instance.get_raw_text())
|
|
|
|
|
431
|
|
|
|
|
|
432
|
|
|
|
|
|
433
|
@receiver(pre_delete, sender=Post)
|
|
|
|
|
434
|
def delete_images(instance, **kwargs):
|
|
|
|
|
435
|
for image in instance.images.all():
|
|
|
|
|
436
|
image_refs_count = image.post_images.count()
|
|
|
|
|
437
|
if image_refs_count == 1:
|
|
|
|
|
438
|
image.delete()
|
|
|
|
|
439
|
|
|
|
|
|
440
|
|
|
|
|
|
441
|
@receiver(pre_delete, sender=Post)
|
|
|
|
|
442
|
def delete_attachments(instance, **kwargs):
|
|
|
|
|
443
|
for attachment in instance.attachments.all():
|
|
|
|
|
444
|
attachment_refs_count = attachment.attachment_posts.count()
|
|
|
|
|
445
|
if attachment_refs_count == 1:
|
|
|
|
|
446
|
attachment.delete()
|
|
|
|
|
447
|
|
|
|
|
|
448
|
|
|
|
|
|
449
|
@receiver(post_delete, sender=Post)
|
|
|
|
|
450
|
def update_thread_on_delete(instance, **kwargs):
|
|
|
|
|
451
|
thread = instance.get_thread()
|
|
|
|
|
452
|
thread.last_edit_time = timezone.now()
|
|
|
|
|
453
|
thread.save()
|
|
|
|