##// END OF EJS Templates
Remove websocket support. JS internal auto-update works fine enough
neko259 -
r1760:641fa167 default
parent child Browse files
Show More
@@ -1,46 +1,42 b''
1 [Version]
1 [Version]
2 Version = 3.5.0 Dolores
2 Version = 3.5.0 Dolores
3 SiteName = Neboard DEV
3 SiteName = Neboard DEV
4
4
5 [Cache]
5 [Cache]
6 # Timeout for caching, if cache is used
6 # Timeout for caching, if cache is used
7 CacheTimeout = 600
7 CacheTimeout = 600
8
8
9 [Forms]
9 [Forms]
10 # Max post length in characters
10 # Max post length in characters
11 MaxTextLength = 30000
11 MaxTextLength = 30000
12 MaxFileSize = 8000000
12 MaxFileSize = 8000000
13 LimitFirstPosting = true
13 LimitFirstPosting = true
14 LimitPostingSpeed = false
14 LimitPostingSpeed = false
15 PowDifficulty = 0
15 PowDifficulty = 0
16 # Delay in seconds
16 # Delay in seconds
17 PostingDelay = 30
17 PostingDelay = 30
18 Autoban = false
18 Autoban = false
19 DefaultTag = test
19 DefaultTag = test
20
20
21 [Messages]
21 [Messages]
22 # Thread bumplimit
22 # Thread bumplimit
23 MaxPostsPerThread = 10
23 MaxPostsPerThread = 10
24 ThreadArchiveDays = 300
24 ThreadArchiveDays = 300
25 AnonymousMode = false
25 AnonymousMode = false
26
26
27 [View]
27 [View]
28 DefaultTheme = md
28 DefaultTheme = md
29 DefaultImageViewer = simple
29 DefaultImageViewer = simple
30 LastRepliesCount = 3
30 LastRepliesCount = 3
31 ThreadsPerPage = 3
31 ThreadsPerPage = 3
32 PostsPerPage = 10
32 PostsPerPage = 10
33 ImagesPerPageGallery = 20
33 ImagesPerPageGallery = 20
34 MaxFavoriteThreads = 20
34 MaxFavoriteThreads = 20
35 MaxLandingThreads = 20
35 MaxLandingThreads = 20
36
36
37 [Storage]
37 [Storage]
38 # Enable archiving threads instead of deletion when the thread limit is reached
38 # Enable archiving threads instead of deletion when the thread limit is reached
39 ArchiveThreads = true
39 ArchiveThreads = true
40
40
41 [External]
42 # Thread update
43 WebsocketsEnabled = false
44
45 [RSS]
41 [RSS]
46 MaxItems = 20
42 MaxItems = 20
@@ -1,89 +1,85 b''
1 from boards.abstracts.settingsmanager import get_settings_manager, \
1 from boards.abstracts.settingsmanager import get_settings_manager, \
2 SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER, SETTING_ONLY_FAVORITES
2 SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER, SETTING_ONLY_FAVORITES
3 from boards.models import Banner
3 from boards.models.user import Notification
4 from boards.models.user import Notification
4 from boards.models import Banner
5
6 __author__ = 'neko259'
7
8 import neboard
9 from boards import settings
5 from boards import settings
10 from boards.models import Post, Tag, Thread
6 from boards.models import Post, Tag, Thread
11
7
12 CONTEXT_SITE_NAME = 'site_name'
8 CONTEXT_SITE_NAME = 'site_name'
13 CONTEXT_VERSION = 'version'
9 CONTEXT_VERSION = 'version'
14 CONTEXT_THEME_CSS = 'theme_css'
10 CONTEXT_THEME_CSS = 'theme_css'
15 CONTEXT_THEME = 'theme'
11 CONTEXT_THEME = 'theme'
16 CONTEXT_PPD = 'posts_per_day'
12 CONTEXT_PPD = 'posts_per_day'
17 CONTEXT_USER = 'user'
13 CONTEXT_USER = 'user'
18 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
14 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
19 CONTEXT_USERNAMES = 'usernames'
15 CONTEXT_USERNAMES = 'usernames'
20 CONTEXT_TAGS_STR = 'tags_str'
16 CONTEXT_TAGS_STR = 'tags_str'
21 CONTEXT_IMAGE_VIEWER = 'image_viewer'
17 CONTEXT_IMAGE_VIEWER = 'image_viewer'
22 CONTEXT_HAS_FAV_THREADS = 'has_fav_threads'
18 CONTEXT_HAS_FAV_THREADS = 'has_fav_threads'
23 CONTEXT_POW_DIFFICULTY = 'pow_difficulty'
19 CONTEXT_POW_DIFFICULTY = 'pow_difficulty'
24 CONTEXT_NEW_POST_COUNT = 'new_post_count'
20 CONTEXT_NEW_POST_COUNT = 'new_post_count'
25 CONTEXT_BANNERS = 'banners'
21 CONTEXT_BANNERS = 'banners'
26 CONTEXT_ONLY_FAVORITES = 'only_favorites'
22 CONTEXT_ONLY_FAVORITES = 'only_favorites'
27
23
28
24
29 def get_notifications(context, settings_manager):
25 def get_notifications(context, settings_manager):
30 usernames = settings_manager.get_notification_usernames()
26 usernames = settings_manager.get_notification_usernames()
31 new_notifications_count = 0
27 new_notifications_count = 0
32 if usernames:
28 if usernames:
33 last_notification_id = settings_manager.get_setting(
29 last_notification_id = settings_manager.get_setting(
34 SETTING_LAST_NOTIFICATION_ID)
30 SETTING_LAST_NOTIFICATION_ID)
35
31
36 new_notifications_count = Notification.objects.get_notification_posts(
32 new_notifications_count = Notification.objects.get_notification_posts(
37 usernames=usernames, last=last_notification_id).only('id').count()
33 usernames=usernames, last=last_notification_id).only('id').count()
38 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
34 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
39 context[CONTEXT_USERNAMES] = usernames
35 context[CONTEXT_USERNAMES] = usernames
40
36
41
37
42 def get_new_post_count(context, settings_manager):
38 def get_new_post_count(context, settings_manager):
43 fav_threads = settings_manager.get_fav_threads()
39 fav_threads = settings_manager.get_fav_threads()
44 if fav_threads:
40 if fav_threads:
45 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys()) \
41 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys()) \
46 .order_by('-pub_time').only('thread_id', 'pub_time')
42 .order_by('-pub_time').only('thread_id', 'pub_time')
47 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
43 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
48 count = Thread.objects.get_new_post_count(ops)
44 count = Thread.objects.get_new_post_count(ops)
49 if count > 0:
45 if count > 0:
50 context[CONTEXT_NEW_POST_COUNT] = '(+{})'.format(count)
46 context[CONTEXT_NEW_POST_COUNT] = '(+{})'.format(count)
51
47
52
48
53 def user_and_ui_processor(request):
49 def user_and_ui_processor(request):
54 context = dict()
50 context = dict()
55
51
56 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
52 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
57
53
58 settings_manager = get_settings_manager(request)
54 settings_manager = get_settings_manager(request)
59 fav_tags = settings_manager.get_fav_tags()
55 fav_tags = settings_manager.get_fav_tags()
60
56
61 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
57 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
62 theme = settings_manager.get_theme()
58 theme = settings_manager.get_theme()
63 context[CONTEXT_THEME] = theme
59 context[CONTEXT_THEME] = theme
64
60
65 # TODO Use static here
61 # TODO Use static here
66 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
62 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
67
63
68 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
64 context[CONTEXT_VERSION] = settings.get('Version', 'Version')
69 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
65 context[CONTEXT_SITE_NAME] = settings.get('Version', 'SiteName')
70
66
71 if (settings.get_bool('Forms', 'LimitFirstPosting') and not settings_manager.get_setting('confirmed_user'))\
67 if (settings.get_bool('Forms', 'LimitFirstPosting') and not settings_manager.get_setting('confirmed_user'))\
72 or settings.get_bool('Forms', 'LimitPostingSpeed'):
68 or settings.get_bool('Forms', 'LimitPostingSpeed'):
73 context[CONTEXT_POW_DIFFICULTY] = settings.get_int('Forms', 'PowDifficulty')
69 context[CONTEXT_POW_DIFFICULTY] = settings.get_int('Forms', 'PowDifficulty')
74
70
75 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
71 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
76 SETTING_IMAGE_VIEWER,
72 SETTING_IMAGE_VIEWER,
77 default=settings.get('View', 'DefaultImageViewer'))
73 default=settings.get('View', 'DefaultImageViewer'))
78
74
79 context[CONTEXT_HAS_FAV_THREADS] =\
75 context[CONTEXT_HAS_FAV_THREADS] =\
80 len(settings_manager.get_fav_threads()) > 0
76 len(settings_manager.get_fav_threads()) > 0
81
77
82 context[CONTEXT_BANNERS] = Banner.objects.order_by('-id')
78 context[CONTEXT_BANNERS] = Banner.objects.order_by('-id')
83 context[CONTEXT_ONLY_FAVORITES] = settings_manager.get_setting(
79 context[CONTEXT_ONLY_FAVORITES] = settings_manager.get_setting(
84 SETTING_ONLY_FAVORITES, False)
80 SETTING_ONLY_FAVORITES, False)
85
81
86 get_notifications(context, settings_manager)
82 get_notifications(context, settings_manager)
87 get_new_post_count(context, settings_manager)
83 get_new_post_count(context, settings_manager)
88
84
89 return context
85 return context
@@ -1,389 +1,364 b''
1 import uuid
1 import uuid
2 import hashlib
2 import hashlib
3 import re
3 import re
4
4
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.models import Attachment, KeyPair, GlobalId
7 from boards.models import Attachment, KeyPair, GlobalId
8 from boards.models.attachment import FILE_TYPES_IMAGE
8 from boards.models.attachment import FILE_TYPES_IMAGE
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, NO_IP
11 from boards.models.post.manager import PostManager, NO_IP
12 from boards.utils import datetime_to_epoch
12 from boards.utils import datetime_to_epoch
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, F
16 from django.db.models import TextField, QuerySet, F
17 from django.template.defaultfilters import truncatewords, striptags
17 from django.template.defaultfilters import truncatewords, striptags
18 from django.template.loader import render_to_string
18 from django.template.loader import render_to_string
19
19
20 CSS_CLS_HIDDEN_POST = 'hidden_post'
20 CSS_CLS_HIDDEN_POST = 'hidden_post'
21 CSS_CLS_DEAD_POST = 'dead_post'
21 CSS_CLS_DEAD_POST = 'dead_post'
22 CSS_CLS_ARCHIVE_POST = 'archive_post'
22 CSS_CLS_ARCHIVE_POST = 'archive_post'
23 CSS_CLS_POST = 'post'
23 CSS_CLS_POST = 'post'
24 CSS_CLS_MONOCHROME = 'monochrome'
24 CSS_CLS_MONOCHROME = 'monochrome'
25
25
26 TITLE_MAX_WORDS = 10
26 TITLE_MAX_WORDS = 10
27
27
28 APP_LABEL_BOARDS = 'boards'
28 APP_LABEL_BOARDS = 'boards'
29
29
30 BAN_REASON_AUTO = 'Auto'
30 BAN_REASON_AUTO = 'Auto'
31
31
32 TITLE_MAX_LENGTH = 200
32 TITLE_MAX_LENGTH = 200
33
33
34 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
34 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
35 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
36 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
37 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
38
38
39 PARAMETER_TRUNCATED = 'truncated'
39 PARAMETER_TRUNCATED = 'truncated'
40 PARAMETER_TAG = 'tag'
40 PARAMETER_TAG = 'tag'
41 PARAMETER_OFFSET = 'offset'
41 PARAMETER_OFFSET = 'offset'
42 PARAMETER_DIFF_TYPE = 'type'
42 PARAMETER_DIFF_TYPE = 'type'
43 PARAMETER_CSS_CLASS = 'css_class'
43 PARAMETER_CSS_CLASS = 'css_class'
44 PARAMETER_THREAD = 'thread'
44 PARAMETER_THREAD = 'thread'
45 PARAMETER_IS_OPENING = 'is_opening'
45 PARAMETER_IS_OPENING = 'is_opening'
46 PARAMETER_POST = 'post'
46 PARAMETER_POST = 'post'
47 PARAMETER_OP_ID = 'opening_post_id'
47 PARAMETER_OP_ID = 'opening_post_id'
48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
48 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
49 PARAMETER_REPLY_LINK = 'reply_link'
49 PARAMETER_REPLY_LINK = 'reply_link'
50 PARAMETER_NEED_OP_DATA = 'need_op_data'
50 PARAMETER_NEED_OP_DATA = 'need_op_data'
51
51
52 POST_VIEW_PARAMS = (
52 POST_VIEW_PARAMS = (
53 'need_op_data',
53 'need_op_data',
54 'reply_link',
54 'reply_link',
55 'need_open_link',
55 'need_open_link',
56 'truncated',
56 'truncated',
57 'mode_tree',
57 'mode_tree',
58 'perms',
58 'perms',
59 'tree_depth',
59 'tree_depth',
60 )
60 )
61
61
62
62
63 class Post(models.Model, Viewable):
63 class Post(models.Model, Viewable):
64 """A post is a message."""
64 """A post is a message."""
65
65
66 objects = PostManager()
66 objects = PostManager()
67
67
68 class Meta:
68 class Meta:
69 app_label = APP_LABEL_BOARDS
69 app_label = APP_LABEL_BOARDS
70 ordering = ('id',)
70 ordering = ('id',)
71
71
72 title = models.CharField(max_length=TITLE_MAX_LENGTH, blank=True, default='')
72 title = models.CharField(max_length=TITLE_MAX_LENGTH, blank=True, default='')
73 pub_time = models.DateTimeField(db_index=True)
73 pub_time = models.DateTimeField(db_index=True)
74 text = TextField(blank=True, default='')
74 text = TextField(blank=True, default='')
75 _text_rendered = TextField(blank=True, null=True, editable=False)
75 _text_rendered = TextField(blank=True, null=True, editable=False)
76
76
77 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
77 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
78 related_name='attachment_posts')
78 related_name='attachment_posts')
79
79
80 poster_ip = models.GenericIPAddressField()
80 poster_ip = models.GenericIPAddressField()
81
81
82 # Used for cache and threads updating
82 # Used for cache and threads updating
83 last_edit_time = models.DateTimeField()
83 last_edit_time = models.DateTimeField()
84
84
85 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
85 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
86 null=True,
86 null=True,
87 blank=True, related_name='refposts',
87 blank=True, related_name='refposts',
88 db_index=True)
88 db_index=True)
89 refmap = models.TextField(null=True, blank=True)
89 refmap = models.TextField(null=True, blank=True)
90 thread = models.ForeignKey('Thread', db_index=True, related_name='replies')
90 thread = models.ForeignKey('Thread', db_index=True, related_name='replies')
91
91
92 url = models.TextField()
92 url = models.TextField()
93 uid = models.TextField(db_index=True)
93 uid = models.TextField(db_index=True)
94
94
95 # Global ID with author key. If the message was downloaded from another
95 # Global ID with author key. If the message was downloaded from another
96 # server, this indicates the server.
96 # server, this indicates the server.
97 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
97 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
98 on_delete=models.CASCADE)
98 on_delete=models.CASCADE)
99
99
100 tripcode = models.CharField(max_length=50, blank=True, default='')
100 tripcode = models.CharField(max_length=50, blank=True, default='')
101 opening = models.BooleanField(db_index=True)
101 opening = models.BooleanField(db_index=True)
102 hidden = models.BooleanField(default=False)
102 hidden = models.BooleanField(default=False)
103 version = models.IntegerField(default=1)
103 version = models.IntegerField(default=1)
104
104
105 def __str__(self):
105 def __str__(self):
106 return 'P#{}/{}'.format(self.id, self.get_title())
106 return 'P#{}/{}'.format(self.id, self.get_title())
107
107
108 def get_title(self) -> str:
108 def get_title(self) -> str:
109 return self.title
109 return self.title
110
110
111 def get_title_or_text(self):
111 def get_title_or_text(self):
112 title = self.get_title()
112 title = self.get_title()
113 if not title:
113 if not title:
114 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
114 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
115
115
116 return title
116 return title
117
117
118 def build_refmap(self, excluded_ids=None) -> None:
118 def build_refmap(self, excluded_ids=None) -> None:
119 """
119 """
120 Builds a replies map string from replies list. This is a cache to stop
120 Builds a replies map string from replies list. This is a cache to stop
121 the server from recalculating the map on every post show.
121 the server from recalculating the map on every post show.
122 """
122 """
123
123
124 replies = self.referenced_posts
124 replies = self.referenced_posts
125 if excluded_ids is not None:
125 if excluded_ids is not None:
126 replies = replies.exclude(id__in=excluded_ids)
126 replies = replies.exclude(id__in=excluded_ids)
127 else:
127 else:
128 replies = replies.all()
128 replies = replies.all()
129
129
130 post_urls = [refpost.get_link_view() for refpost in replies]
130 post_urls = [refpost.get_link_view() for refpost in replies]
131
131
132 self.refmap = ', '.join(post_urls)
132 self.refmap = ', '.join(post_urls)
133
133
134 def is_referenced(self) -> bool:
134 def is_referenced(self) -> bool:
135 return self.refmap and len(self.refmap) > 0
135 return self.refmap and len(self.refmap) > 0
136
136
137 def is_opening(self) -> bool:
137 def is_opening(self) -> bool:
138 """
138 """
139 Checks if this is an opening post or just a reply.
139 Checks if this is an opening post or just a reply.
140 """
140 """
141
141
142 return self.opening
142 return self.opening
143
143
144 def get_absolute_url(self, thread=None):
144 def get_absolute_url(self, thread=None):
145 # Url is cached only for the "main" thread. When getting url
145 # Url is cached only for the "main" thread. When getting url
146 # for other threads, do it manually.
146 # for other threads, do it manually.
147 return self.url
147 return self.url
148
148
149 def get_thread(self):
149 def get_thread(self):
150 return self.thread
150 return self.thread
151
151
152 def get_thread_id(self):
152 def get_thread_id(self):
153 return self.thread_id
153 return self.thread_id
154
154
155 def _get_cache_key(self):
155 def _get_cache_key(self):
156 return [datetime_to_epoch(self.last_edit_time)]
156 return [datetime_to_epoch(self.last_edit_time)]
157
157
158 def get_view_params(self, *args, **kwargs):
158 def get_view_params(self, *args, **kwargs):
159 """
159 """
160 Gets the parameters required for viewing the post based on the arguments
160 Gets the parameters required for viewing the post based on the arguments
161 given and the post itself.
161 given and the post itself.
162 """
162 """
163 thread = kwargs.get('thread') or self.get_thread()
163 thread = kwargs.get('thread') or self.get_thread()
164
164
165 css_classes = [CSS_CLS_POST]
165 css_classes = [CSS_CLS_POST]
166 if thread.is_archived():
166 if thread.is_archived():
167 css_classes.append(CSS_CLS_ARCHIVE_POST)
167 css_classes.append(CSS_CLS_ARCHIVE_POST)
168 elif not thread.can_bump():
168 elif not thread.can_bump():
169 css_classes.append(CSS_CLS_DEAD_POST)
169 css_classes.append(CSS_CLS_DEAD_POST)
170 if self.is_hidden():
170 if self.is_hidden():
171 css_classes.append(CSS_CLS_HIDDEN_POST)
171 css_classes.append(CSS_CLS_HIDDEN_POST)
172 if thread.is_monochrome():
172 if thread.is_monochrome():
173 css_classes.append(CSS_CLS_MONOCHROME)
173 css_classes.append(CSS_CLS_MONOCHROME)
174
174
175 params = dict()
175 params = dict()
176 for param in POST_VIEW_PARAMS:
176 for param in POST_VIEW_PARAMS:
177 if param in kwargs:
177 if param in kwargs:
178 params[param] = kwargs[param]
178 params[param] = kwargs[param]
179
179
180 params.update({
180 params.update({
181 PARAMETER_POST: self,
181 PARAMETER_POST: self,
182 PARAMETER_IS_OPENING: self.is_opening(),
182 PARAMETER_IS_OPENING: self.is_opening(),
183 PARAMETER_THREAD: thread,
183 PARAMETER_THREAD: thread,
184 PARAMETER_CSS_CLASS: ' '.join(css_classes),
184 PARAMETER_CSS_CLASS: ' '.join(css_classes),
185 })
185 })
186
186
187 return params
187 return params
188
188
189 def get_view(self, *args, **kwargs) -> str:
189 def get_view(self, *args, **kwargs) -> str:
190 """
190 """
191 Renders post's HTML view. Some of the post params can be passed over
191 Renders post's HTML view. Some of the post params can be passed over
192 kwargs for the means of caching (if we view the thread, some params
192 kwargs for the means of caching (if we view the thread, some params
193 are same for every post and don't need to be computed over and over.
193 are same for every post and don't need to be computed over and over.
194 """
194 """
195 params = self.get_view_params(*args, **kwargs)
195 params = self.get_view_params(*args, **kwargs)
196
196
197 return render_to_string('boards/post.html', params)
197 return render_to_string('boards/post.html', params)
198
198
199 def get_images(self) -> Attachment:
199 def get_images(self) -> Attachment:
200 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
200 return self.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
201
201
202 def get_first_image(self) -> Attachment:
202 def get_first_image(self) -> Attachment:
203 try:
203 try:
204 return self.get_images().earliest('-id')
204 return self.get_images().earliest('-id')
205 except Attachment.DoesNotExist:
205 except Attachment.DoesNotExist:
206 return None
206 return None
207
207
208 def set_global_id(self, key_pair=None):
208 def set_global_id(self, key_pair=None):
209 """
209 """
210 Sets global id based on the given key pair. If no key pair is given,
210 Sets global id based on the given key pair. If no key pair is given,
211 default one is used.
211 default one is used.
212 """
212 """
213
213
214 if key_pair:
214 if key_pair:
215 key = key_pair
215 key = key_pair
216 else:
216 else:
217 try:
217 try:
218 key = KeyPair.objects.get(primary=True)
218 key = KeyPair.objects.get(primary=True)
219 except KeyPair.DoesNotExist:
219 except KeyPair.DoesNotExist:
220 # Do not update the global id because there is no key defined
220 # Do not update the global id because there is no key defined
221 return
221 return
222 global_id = GlobalId(key_type=key.key_type,
222 global_id = GlobalId(key_type=key.key_type,
223 key=key.public_key,
223 key=key.public_key,
224 local_id=self.id)
224 local_id=self.id)
225 global_id.save()
225 global_id.save()
226
226
227 self.global_id = global_id
227 self.global_id = global_id
228
228
229 self.save(update_fields=['global_id'])
229 self.save(update_fields=['global_id'])
230
230
231 def get_pub_time_str(self):
231 def get_pub_time_str(self):
232 return str(self.pub_time)
232 return str(self.pub_time)
233
233
234 def get_replied_ids(self):
234 def get_replied_ids(self):
235 """
235 """
236 Gets ID list of the posts that this post replies.
236 Gets ID list of the posts that this post replies.
237 """
237 """
238
238
239 raw_text = self.get_raw_text()
239 raw_text = self.get_raw_text()
240
240
241 local_replied = REGEX_REPLY.findall(raw_text)
241 local_replied = REGEX_REPLY.findall(raw_text)
242 global_replied = []
242 global_replied = []
243 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
243 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
244 key_type = match[0]
244 key_type = match[0]
245 key = match[1]
245 key = match[1]
246 local_id = match[2]
246 local_id = match[2]
247
247
248 try:
248 try:
249 global_id = GlobalId.objects.get(key_type=key_type,
249 global_id = GlobalId.objects.get(key_type=key_type,
250 key=key, local_id=local_id)
250 key=key, local_id=local_id)
251 for post in Post.objects.filter(global_id=global_id).only('id'):
251 for post in Post.objects.filter(global_id=global_id).only('id'):
252 global_replied.append(post.id)
252 global_replied.append(post.id)
253 except GlobalId.DoesNotExist:
253 except GlobalId.DoesNotExist:
254 pass
254 pass
255 return local_replied + global_replied
255 return local_replied + global_replied
256
256
257 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
257 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
258 include_last_update=False) -> str:
258 include_last_update=False) -> str:
259 """
259 """
260 Gets post HTML or JSON data that can be rendered on a page or used by
260 Gets post HTML or JSON data that can be rendered on a page or used by
261 API.
261 API.
262 """
262 """
263
263
264 return get_exporter(format_type).export(self, request,
264 return get_exporter(format_type).export(self, request,
265 include_last_update)
265 include_last_update)
266
266
267 def notify_clients(self, recursive=True):
268 """
269 Sends post HTML data to the thread web socket.
270 """
271
272 if not settings.get_bool('External', 'WebsocketsEnabled'):
273 return
274
275 thread_ids = list()
276 self.get_thread().notify_clients()
277
278 if recursive:
279 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
280 post_id = reply_number.group(1)
281
282 try:
283 ref_post = Post.objects.get(id=post_id)
284
285 if ref_post.get_thread().id not in thread_ids:
286 # If post is in this thread, its thread was already notified.
287 # Otherwise, notify its thread separately.
288 ref_post.notify_clients(recursive=False)
289 except ObjectDoesNotExist:
290 pass
291
292 def _build_url(self):
267 def _build_url(self):
293 opening = self.is_opening()
268 opening = self.is_opening()
294 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
269 opening_id = self.id if opening else self.get_thread().get_opening_post_id()
295 url = reverse('thread', kwargs={'post_id': opening_id})
270 url = reverse('thread', kwargs={'post_id': opening_id})
296 if not opening:
271 if not opening:
297 url += '#' + str(self.id)
272 url += '#' + str(self.id)
298
273
299 return url
274 return url
300
275
301 def save(self, force_insert=False, force_update=False, using=None,
276 def save(self, force_insert=False, force_update=False, using=None,
302 update_fields=None):
277 update_fields=None):
303 new_post = self.id is None
278 new_post = self.id is None
304
279
305 self.uid = str(uuid.uuid4())
280 self.uid = str(uuid.uuid4())
306 if update_fields is not None and 'uid' not in update_fields:
281 if update_fields is not None and 'uid' not in update_fields:
307 update_fields += ['uid']
282 update_fields += ['uid']
308
283
309 if not new_post:
284 if not new_post:
310 thread = self.get_thread()
285 thread = self.get_thread()
311 if thread:
286 if thread:
312 thread.last_edit_time = self.last_edit_time
287 thread.last_edit_time = self.last_edit_time
313 thread.save(update_fields=['last_edit_time', 'status'])
288 thread.save(update_fields=['last_edit_time', 'status'])
314
289
315 super().save(force_insert, force_update, using, update_fields)
290 super().save(force_insert, force_update, using, update_fields)
316
291
317 if new_post:
292 if new_post:
318 self.url = self._build_url()
293 self.url = self._build_url()
319 super().save(update_fields=['url'])
294 super().save(update_fields=['url'])
320
295
321 def get_text(self) -> str:
296 def get_text(self) -> str:
322 return self._text_rendered
297 return self._text_rendered
323
298
324 def get_raw_text(self) -> str:
299 def get_raw_text(self) -> str:
325 return self.text
300 return self.text
326
301
327 def get_sync_text(self) -> str:
302 def get_sync_text(self) -> str:
328 """
303 """
329 Returns text applicable for sync. It has absolute post reflinks.
304 Returns text applicable for sync. It has absolute post reflinks.
330 """
305 """
331
306
332 replacements = dict()
307 replacements = dict()
333 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
308 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
334 try:
309 try:
335 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
310 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
336 replacements[post_id] = absolute_post_id
311 replacements[post_id] = absolute_post_id
337 except Post.DoesNotExist:
312 except Post.DoesNotExist:
338 pass
313 pass
339
314
340 text = self.get_raw_text() or ''
315 text = self.get_raw_text() or ''
341 for key in replacements:
316 for key in replacements:
342 text = text.replace('[post]{}[/post]'.format(key),
317 text = text.replace('[post]{}[/post]'.format(key),
343 '[post]{}[/post]'.format(replacements[key]))
318 '[post]{}[/post]'.format(replacements[key]))
344 text = text.replace('\r\n', '\n').replace('\r', '\n')
319 text = text.replace('\r\n', '\n').replace('\r', '\n')
345
320
346 return text
321 return text
347
322
348 def get_tripcode(self):
323 def get_tripcode(self):
349 if self.tripcode:
324 if self.tripcode:
350 return Tripcode(self.tripcode)
325 return Tripcode(self.tripcode)
351
326
352 def get_link_view(self):
327 def get_link_view(self):
353 """
328 """
354 Gets view of a reflink to the post.
329 Gets view of a reflink to the post.
355 """
330 """
356 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
331 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
357 self.id)
332 self.id)
358 if self.is_opening():
333 if self.is_opening():
359 result = '<b>{}</b>'.format(result)
334 result = '<b>{}</b>'.format(result)
360
335
361 return result
336 return result
362
337
363 def is_hidden(self) -> bool:
338 def is_hidden(self) -> bool:
364 return self.hidden
339 return self.hidden
365
340
366 def set_hidden(self, hidden):
341 def set_hidden(self, hidden):
367 self.hidden = hidden
342 self.hidden = hidden
368
343
369 def increment_version(self):
344 def increment_version(self):
370 self.version = F('version') + 1
345 self.version = F('version') + 1
371
346
372 def clear_cache(self):
347 def clear_cache(self):
373 """
348 """
374 Clears sync data (content cache, signatures etc).
349 Clears sync data (content cache, signatures etc).
375 """
350 """
376 global_id = self.global_id
351 global_id = self.global_id
377 if global_id is not None and global_id.is_local()\
352 if global_id is not None and global_id.is_local()\
378 and global_id.content is not None:
353 and global_id.content is not None:
379 global_id.clear_cache()
354 global_id.clear_cache()
380
355
381 def get_tags(self):
356 def get_tags(self):
382 return self.get_thread().get_tags()
357 return self.get_thread().get_tags()
383
358
384 def get_ip_color(self):
359 def get_ip_color(self):
385 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
360 return hashlib.md5(self.poster_ip.encode()).hexdigest()[:6]
386
361
387 def has_ip(self):
362 def has_ip(self):
388 return self.poster_ip != NO_IP
363 return self.poster_ip != NO_IP
389
364
@@ -1,328 +1,313 b''
1 import logging
1 import logging
2 from adjacent import Client
3 from datetime import timedelta
2 from datetime import timedelta
4
3
5
4 from django.db import models, transaction
6 from django.db.models import Count, Sum, QuerySet, Q
5 from django.db.models import Count, Sum, QuerySet, Q
7 from django.utils import timezone
6 from django.utils import timezone
8 from django.db import models, transaction
9
7
10 from boards.models.attachment import FILE_TYPES_IMAGE
8 import boards
9 from boards import settings
11 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
10 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
12
11 from boards.models.attachment import FILE_TYPES_IMAGE
13 from boards import settings
14 import boards
15 from boards.utils import cached_result, datetime_to_epoch
16 from boards.models.post import Post
12 from boards.models.post import Post
17 from boards.models.tag import Tag
13 from boards.models.tag import Tag
14 from boards.utils import cached_result, datetime_to_epoch
18
15
19 FAV_THREAD_NO_UPDATES = -1
16 FAV_THREAD_NO_UPDATES = -1
20
17
21
18
22 __author__ = 'neko259'
19 __author__ = 'neko259'
23
20
24
21
25 logger = logging.getLogger(__name__)
22 logger = logging.getLogger(__name__)
26
23
27
24
28 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
25 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
29 WS_NOTIFICATION_TYPE = 'notification_type'
26 WS_NOTIFICATION_TYPE = 'notification_type'
30
27
31 WS_CHANNEL_THREAD = "thread:"
28 WS_CHANNEL_THREAD = "thread:"
32
29
33 STATUS_CHOICES = (
30 STATUS_CHOICES = (
34 (STATUS_ACTIVE, STATUS_ACTIVE),
31 (STATUS_ACTIVE, STATUS_ACTIVE),
35 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
32 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
36 (STATUS_ARCHIVE, STATUS_ARCHIVE),
33 (STATUS_ARCHIVE, STATUS_ARCHIVE),
37 )
34 )
38
35
39
36
40 class ThreadManager(models.Manager):
37 class ThreadManager(models.Manager):
41 def process_old_threads(self):
38 def process_old_threads(self):
42 """
39 """
43 Preserves maximum thread count. If there are too many threads,
40 Preserves maximum thread count. If there are too many threads,
44 archive or delete the old ones.
41 archive or delete the old ones.
45 """
42 """
46 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
43 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
47 old_time = timezone.now() - timedelta(days=old_time_delta)
44 old_time = timezone.now() - timedelta(days=old_time_delta)
48 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
45 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
49
46
50 for op in old_ops:
47 for op in old_ops:
51 thread = op.get_thread()
48 thread = op.get_thread()
52 if settings.get_bool('Storage', 'ArchiveThreads'):
49 if settings.get_bool('Storage', 'ArchiveThreads'):
53 self._archive_thread(thread)
50 self._archive_thread(thread)
54 else:
51 else:
55 thread.delete()
52 thread.delete()
56 logger.info('Processed old thread {}'.format(thread))
53 logger.info('Processed old thread {}'.format(thread))
57
54
58
55
59 def _archive_thread(self, thread):
56 def _archive_thread(self, thread):
60 thread.status = STATUS_ARCHIVE
57 thread.status = STATUS_ARCHIVE
61 thread.last_edit_time = timezone.now()
58 thread.last_edit_time = timezone.now()
62 thread.update_posts_time()
59 thread.update_posts_time()
63 thread.save(update_fields=['last_edit_time', 'status'])
60 thread.save(update_fields=['last_edit_time', 'status'])
64
61
65 def get_new_posts(self, datas):
62 def get_new_posts(self, datas):
66 query = None
63 query = None
67 # TODO Use classes instead of dicts
64 # TODO Use classes instead of dicts
68 for data in datas:
65 for data in datas:
69 if data['last_id'] != FAV_THREAD_NO_UPDATES:
66 if data['last_id'] != FAV_THREAD_NO_UPDATES:
70 q = (Q(id=data['op'].get_thread_id())
67 q = (Q(id=data['op'].get_thread_id())
71 & Q(replies__id__gt=data['last_id']))
68 & Q(replies__id__gt=data['last_id']))
72 if query is None:
69 if query is None:
73 query = q
70 query = q
74 else:
71 else:
75 query = query | q
72 query = query | q
76 if query is not None:
73 if query is not None:
77 return self.filter(query).annotate(
74 return self.filter(query).annotate(
78 new_post_count=Count('replies'))
75 new_post_count=Count('replies'))
79
76
80 def get_new_post_count(self, datas):
77 def get_new_post_count(self, datas):
81 new_posts = self.get_new_posts(datas)
78 new_posts = self.get_new_posts(datas)
82 return new_posts.aggregate(total_count=Count('replies'))\
79 return new_posts.aggregate(total_count=Count('replies'))\
83 ['total_count'] if new_posts else 0
80 ['total_count'] if new_posts else 0
84
81
85
82
86 def get_thread_max_posts():
83 def get_thread_max_posts():
87 return settings.get_int('Messages', 'MaxPostsPerThread')
84 return settings.get_int('Messages', 'MaxPostsPerThread')
88
85
89
86
90 class Thread(models.Model):
87 class Thread(models.Model):
91 objects = ThreadManager()
88 objects = ThreadManager()
92
89
93 class Meta:
90 class Meta:
94 app_label = 'boards'
91 app_label = 'boards'
95
92
96 tags = models.ManyToManyField('Tag', related_name='thread_tags')
93 tags = models.ManyToManyField('Tag', related_name='thread_tags')
97 bump_time = models.DateTimeField(db_index=True)
94 bump_time = models.DateTimeField(db_index=True)
98 last_edit_time = models.DateTimeField()
95 last_edit_time = models.DateTimeField()
99 max_posts = models.IntegerField(default=get_thread_max_posts)
96 max_posts = models.IntegerField(default=get_thread_max_posts)
100 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
97 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
101 choices=STATUS_CHOICES, db_index=True)
98 choices=STATUS_CHOICES, db_index=True)
102 monochrome = models.BooleanField(default=False)
99 monochrome = models.BooleanField(default=False)
103
100
104 def get_tags(self) -> QuerySet:
101 def get_tags(self) -> QuerySet:
105 """
102 """
106 Gets a sorted tag list.
103 Gets a sorted tag list.
107 """
104 """
108
105
109 return self.tags.order_by('name')
106 return self.tags.order_by('name')
110
107
111 def bump(self):
108 def bump(self):
112 """
109 """
113 Bumps (moves to up) thread if possible.
110 Bumps (moves to up) thread if possible.
114 """
111 """
115
112
116 if self.can_bump():
113 if self.can_bump():
117 self.bump_time = self.last_edit_time
114 self.bump_time = self.last_edit_time
118
115
119 self.update_bump_status()
116 self.update_bump_status()
120
117
121 logger.info('Bumped thread %d' % self.id)
118 logger.info('Bumped thread %d' % self.id)
122
119
123 def has_post_limit(self) -> bool:
120 def has_post_limit(self) -> bool:
124 return self.max_posts > 0
121 return self.max_posts > 0
125
122
126 def update_bump_status(self, exclude_posts=None):
123 def update_bump_status(self, exclude_posts=None):
127 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
124 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
128 self.status = STATUS_BUMPLIMIT
125 self.status = STATUS_BUMPLIMIT
129 self.update_posts_time(exclude_posts=exclude_posts)
126 self.update_posts_time(exclude_posts=exclude_posts)
130
127
131 def _get_cache_key(self):
128 def _get_cache_key(self):
132 return [datetime_to_epoch(self.last_edit_time)]
129 return [datetime_to_epoch(self.last_edit_time)]
133
130
134 @cached_result(key_method=_get_cache_key)
131 @cached_result(key_method=_get_cache_key)
135 def get_reply_count(self) -> int:
132 def get_reply_count(self) -> int:
136 return self.get_replies().count()
133 return self.get_replies().count()
137
134
138 @cached_result(key_method=_get_cache_key)
135 @cached_result(key_method=_get_cache_key)
139 def get_images_count(self) -> int:
136 def get_images_count(self) -> int:
140 return self.get_replies().filter(
137 return self.get_replies().filter(
141 attachments__mimetype__in=FILE_TYPES_IMAGE)\
138 attachments__mimetype__in=FILE_TYPES_IMAGE)\
142 .annotate(images_count=Count(
139 .annotate(images_count=Count(
143 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
140 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
144
141
145 def can_bump(self) -> bool:
142 def can_bump(self) -> bool:
146 """
143 """
147 Checks if the thread can be bumped by replying to it.
144 Checks if the thread can be bumped by replying to it.
148 """
145 """
149
146
150 return self.get_status() == STATUS_ACTIVE
147 return self.get_status() == STATUS_ACTIVE
151
148
152 def get_last_replies(self) -> QuerySet:
149 def get_last_replies(self) -> QuerySet:
153 """
150 """
154 Gets several last replies, not including opening post
151 Gets several last replies, not including opening post
155 """
152 """
156
153
157 last_replies_count = settings.get_int('View', 'LastRepliesCount')
154 last_replies_count = settings.get_int('View', 'LastRepliesCount')
158
155
159 if last_replies_count > 0:
156 if last_replies_count > 0:
160 reply_count = self.get_reply_count()
157 reply_count = self.get_reply_count()
161
158
162 if reply_count > 0:
159 if reply_count > 0:
163 reply_count_to_show = min(last_replies_count,
160 reply_count_to_show = min(last_replies_count,
164 reply_count - 1)
161 reply_count - 1)
165 replies = self.get_replies()
162 replies = self.get_replies()
166 last_replies = replies[reply_count - reply_count_to_show:]
163 last_replies = replies[reply_count - reply_count_to_show:]
167
164
168 return last_replies
165 return last_replies
169
166
170 def get_skipped_replies_count(self) -> int:
167 def get_skipped_replies_count(self) -> int:
171 """
168 """
172 Gets number of posts between opening post and last replies.
169 Gets number of posts between opening post and last replies.
173 """
170 """
174 reply_count = self.get_reply_count()
171 reply_count = self.get_reply_count()
175 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
172 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
176 reply_count - 1)
173 reply_count - 1)
177 return reply_count - last_replies_count - 1
174 return reply_count - last_replies_count - 1
178
175
179 # TODO Remove argument, it is not used
176 # TODO Remove argument, it is not used
180 def get_replies(self, view_fields_only=True) -> QuerySet:
177 def get_replies(self, view_fields_only=True) -> QuerySet:
181 """
178 """
182 Gets sorted thread posts
179 Gets sorted thread posts
183 """
180 """
184 query = self.replies.order_by('pub_time').prefetch_related(
181 query = self.replies.order_by('pub_time').prefetch_related(
185 'attachments')
182 'attachments')
186 return query
183 return query
187
184
188 def get_viewable_replies(self) -> QuerySet:
185 def get_viewable_replies(self) -> QuerySet:
189 """
186 """
190 Gets replies with only fields that are used for viewing.
187 Gets replies with only fields that are used for viewing.
191 """
188 """
192 return self.get_replies().defer('text', 'last_edit_time', 'version')
189 return self.get_replies().defer('text', 'last_edit_time', 'version')
193
190
194 def get_top_level_replies(self) -> QuerySet:
191 def get_top_level_replies(self) -> QuerySet:
195 return self.get_replies().exclude(refposts__threads__in=[self])
192 return self.get_replies().exclude(refposts__threads__in=[self])
196
193
197 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
194 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
198 """
195 """
199 Gets replies that have at least one image attached
196 Gets replies that have at least one image attached
200 """
197 """
201 return self.get_replies(view_fields_only).filter(
198 return self.get_replies(view_fields_only).filter(
202 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
199 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
203 'attachments')).filter(images_count__gt=0)
200 'attachments')).filter(images_count__gt=0)
204
201
205 def get_opening_post(self, only_id=False) -> Post:
202 def get_opening_post(self, only_id=False) -> Post:
206 """
203 """
207 Gets the first post of the thread
204 Gets the first post of the thread
208 """
205 """
209
206
210 query = self.get_replies().filter(opening=True)
207 query = self.get_replies().filter(opening=True)
211 if only_id:
208 if only_id:
212 query = query.only('id')
209 query = query.only('id')
213 opening_post = query.first()
210 opening_post = query.first()
214
211
215 return opening_post
212 return opening_post
216
213
217 @cached_result()
214 @cached_result()
218 def get_opening_post_id(self) -> int:
215 def get_opening_post_id(self) -> int:
219 """
216 """
220 Gets ID of the first thread post.
217 Gets ID of the first thread post.
221 """
218 """
222
219
223 return self.get_opening_post(only_id=True).id
220 return self.get_opening_post(only_id=True).id
224
221
225 def get_pub_time(self):
222 def get_pub_time(self):
226 """
223 """
227 Gets opening post's pub time because thread does not have its own one.
224 Gets opening post's pub time because thread does not have its own one.
228 """
225 """
229
226
230 return self.get_opening_post().pub_time
227 return self.get_opening_post().pub_time
231
228
232 def __str__(self):
229 def __str__(self):
233 return 'T#{}'.format(self.id)
230 return 'T#{}'.format(self.id)
234
231
235 def get_tag_url_list(self) -> list:
232 def get_tag_url_list(self) -> list:
236 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
233 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
237
234
238 def update_posts_time(self, exclude_posts=None):
235 def update_posts_time(self, exclude_posts=None):
239 last_edit_time = self.last_edit_time
236 last_edit_time = self.last_edit_time
240
237
241 for post in self.replies.all():
238 for post in self.replies.all():
242 if exclude_posts is None or post not in exclude_posts:
239 if exclude_posts is None or post not in exclude_posts:
243 # Manual update is required because uids are generated on save
240 # Manual update is required because uids are generated on save
244 post.last_edit_time = last_edit_time
241 post.last_edit_time = last_edit_time
245 post.save(update_fields=['last_edit_time'])
242 post.save(update_fields=['last_edit_time'])
246
243
247 def notify_clients(self):
248 if not settings.get_bool('External', 'WebsocketsEnabled'):
249 return
250
251 client = Client()
252
253 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
254 client.publish(channel_name, {
255 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
256 })
257 client.send()
258
259 def get_absolute_url(self):
244 def get_absolute_url(self):
260 return self.get_opening_post().get_absolute_url()
245 return self.get_opening_post().get_absolute_url()
261
246
262 def get_required_tags(self):
247 def get_required_tags(self):
263 return self.get_tags().filter(required=True)
248 return self.get_tags().filter(required=True)
264
249
265 def get_sections_str(self):
250 def get_sections_str(self):
266 return Tag.objects.get_tag_url_list(self.get_required_tags())
251 return Tag.objects.get_tag_url_list(self.get_required_tags())
267
252
268 def get_replies_newer(self, post_id):
253 def get_replies_newer(self, post_id):
269 return self.get_replies().filter(id__gt=post_id)
254 return self.get_replies().filter(id__gt=post_id)
270
255
271 def is_archived(self):
256 def is_archived(self):
272 return self.get_status() == STATUS_ARCHIVE
257 return self.get_status() == STATUS_ARCHIVE
273
258
274 def get_status(self):
259 def get_status(self):
275 return self.status
260 return self.status
276
261
277 def is_monochrome(self):
262 def is_monochrome(self):
278 return self.monochrome
263 return self.monochrome
279
264
280 # If tags have parent, add them to the tag list
265 # If tags have parent, add them to the tag list
281 @transaction.atomic
266 @transaction.atomic
282 def refresh_tags(self):
267 def refresh_tags(self):
283 for tag in self.get_tags().all():
268 for tag in self.get_tags().all():
284 parents = tag.get_all_parents()
269 parents = tag.get_all_parents()
285 if len(parents) > 0:
270 if len(parents) > 0:
286 self.tags.add(*parents)
271 self.tags.add(*parents)
287
272
288 def get_reply_tree(self):
273 def get_reply_tree(self):
289 replies = self.get_replies().prefetch_related('refposts')
274 replies = self.get_replies().prefetch_related('refposts')
290 tree = []
275 tree = []
291 for reply in replies:
276 for reply in replies:
292 parents = reply.refposts.all()
277 parents = reply.refposts.all()
293
278
294 found_parent = False
279 found_parent = False
295 searching_for_index = False
280 searching_for_index = False
296
281
297 if len(parents) > 0:
282 if len(parents) > 0:
298 index = 0
283 index = 0
299 parent_depth = 0
284 parent_depth = 0
300
285
301 indexes_to_insert = []
286 indexes_to_insert = []
302
287
303 for depth, element in tree:
288 for depth, element in tree:
304 index += 1
289 index += 1
305
290
306 # If this element is next after parent on the same level,
291 # If this element is next after parent on the same level,
307 # insert child before it
292 # insert child before it
308 if searching_for_index and depth <= parent_depth:
293 if searching_for_index and depth <= parent_depth:
309 indexes_to_insert.append((index - 1, parent_depth))
294 indexes_to_insert.append((index - 1, parent_depth))
310 searching_for_index = False
295 searching_for_index = False
311
296
312 if element in parents:
297 if element in parents:
313 found_parent = True
298 found_parent = True
314 searching_for_index = True
299 searching_for_index = True
315 parent_depth = depth
300 parent_depth = depth
316
301
317 if not found_parent:
302 if not found_parent:
318 tree.append((0, reply))
303 tree.append((0, reply))
319 else:
304 else:
320 if searching_for_index:
305 if searching_for_index:
321 tree.append((parent_depth + 1, reply))
306 tree.append((parent_depth + 1, reply))
322
307
323 offset = 0
308 offset = 0
324 for last_index, parent_depth in indexes_to_insert:
309 for last_index, parent_depth in indexes_to_insert:
325 tree.insert(last_index + offset, (parent_depth + 1, reply))
310 tree.insert(last_index + offset, (parent_depth + 1, reply))
326 offset += 1
311 offset += 1
327
312
328 return tree
313 return tree
@@ -1,462 +1,402 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013-2014 neko259
6 Copyright (C) 2013-2014 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var CLASS_POST = '.post';
26 var CLASS_POST = '.post';
27
27
28 var POST_ADDED = 0;
28 var POST_ADDED = 0;
29 var POST_UPDATED = 1;
29 var POST_UPDATED = 1;
30
30
31 // TODO These need to be syncronized with board settings.
31 // TODO These need to be syncronized with board settings.
32 var JS_AUTOUPDATE_PERIOD = 20000;
32 var JS_AUTOUPDATE_PERIOD = 20000;
33 // TODO This needs to be the same for attachment download time limit.
33 // TODO This needs to be the same for attachment download time limit.
34 var POST_AJAX_TIMEOUT = 30000;
34 var POST_AJAX_TIMEOUT = 30000;
35 var BLINK_SPEED = 500;
35 var BLINK_SPEED = 500;
36
36
37 var ALLOWED_FOR_PARTIAL_UPDATE = [
37 var ALLOWED_FOR_PARTIAL_UPDATE = [
38 'refmap',
38 'refmap',
39 'post-info'
39 'post-info'
40 ];
40 ];
41
41
42 var ATTR_CLASS = 'class';
42 var ATTR_CLASS = 'class';
43 var ATTR_UID = 'data-uid';
43 var ATTR_UID = 'data-uid';
44
44
45 var wsUser = '';
46
47 var unreadPosts = 0;
45 var unreadPosts = 0;
48 var documentOriginalTitle = '';
46 var documentOriginalTitle = '';
49
47
50 // Thread ID does not change, can be stored one time
48 // Thread ID does not change, can be stored one time
51 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
49 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
52 var blinkColor = $('<div class="post-blink"></div>').css('background-color');
50 var blinkColor = $('<div class="post-blink"></div>').css('background-color');
53
51
54 /**
52 /**
55 * Connect to websocket server and subscribe to thread updates. On any update we
56 * request a thread diff.
57 *
58 * @returns {boolean} true if connected, false otherwise
59 */
60 function connectWebsocket() {
61 var metapanel = $('.metapanel')[0];
62
63 var wsHost = metapanel.getAttribute('data-ws-host');
64 var wsPort = metapanel.getAttribute('data-ws-port');
65
66 if (wsHost.length > 0 && wsPort.length > 0) {
67 var centrifuge = new Centrifuge({
68 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
69 "project": metapanel.getAttribute('data-ws-project'),
70 "user": wsUser,
71 "timestamp": metapanel.getAttribute('data-ws-token-time'),
72 "token": metapanel.getAttribute('data-ws-token'),
73 "debug": false
74 });
75
76 centrifuge.on('error', function(error_message) {
77 console.log("Error connecting to websocket server.");
78 console.log(error_message);
79 console.log("Using javascript update instead.");
80
81 // If websockets don't work, enable JS update instead
82 enableJsUpdate()
83 });
84
85 centrifuge.on('connect', function() {
86 var channelName = 'thread:' + threadId;
87 centrifuge.subscribe(channelName, function(message) {
88 getThreadDiff();
89 });
90
91 // For the case we closed the browser and missed some updates
92 getThreadDiff();
93 $('#autoupdate').hide();
94 });
95
96 centrifuge.connect();
97
98 return true;
99 } else {
100 return false;
101 }
102 }
103
104 /**
105 * Get diff of the posts from the current thread timestamp.
53 * Get diff of the posts from the current thread timestamp.
106 * This is required if the browser was closed and some post updates were
54 * This is required if the browser was closed and some post updates were
107 * missed.
55 * missed.
108 */
56 */
109 function getThreadDiff() {
57 function getThreadDiff() {
110 var all_posts = $('.post');
58 var all_posts = $('.post');
111
59
112 var uids = '';
60 var uids = '';
113 var posts = all_posts;
61 var posts = all_posts;
114 for (var i = 0; i < posts.length; i++) {
62 for (var i = 0; i < posts.length; i++) {
115 uids += posts[i].getAttribute('data-uid') + ' ';
63 uids += posts[i].getAttribute('data-uid') + ' ';
116 }
64 }
117
65
118 var data = {
66 var data = {
119 uids: uids,
67 uids: uids,
120 thread: threadId
68 thread: threadId
121 };
69 };
122
70
123 var diffUrl = '/api/diff_thread/';
71 var diffUrl = '/api/diff_thread/';
124
72
125 $.post(diffUrl,
73 $.post(diffUrl,
126 data,
74 data,
127 function(data) {
75 function(data) {
128 var updatedPosts = data.updated;
76 var updatedPosts = data.updated;
129 var addedPostCount = 0;
77 var addedPostCount = 0;
130
78
131 for (var i = 0; i < updatedPosts.length; i++) {
79 for (var i = 0; i < updatedPosts.length; i++) {
132 var postText = updatedPosts[i];
80 var postText = updatedPosts[i];
133 var post = $(postText);
81 var post = $(postText);
134
82
135 if (updatePost(post) == POST_ADDED) {
83 if (updatePost(post) == POST_ADDED) {
136 addedPostCount++;
84 addedPostCount++;
137 }
85 }
138 }
86 }
139
87
140 var hasMetaUpdates = updatedPosts.length > 0;
88 var hasMetaUpdates = updatedPosts.length > 0;
141 if (hasMetaUpdates) {
89 if (hasMetaUpdates) {
142 updateMetadataPanel();
90 updateMetadataPanel();
143 }
91 }
144
92
145 if (addedPostCount > 0) {
93 if (addedPostCount > 0) {
146 updateBumplimitProgress(addedPostCount);
94 updateBumplimitProgress(addedPostCount);
147 }
95 }
148
96
149 if (updatedPosts.length > 0) {
97 if (updatedPosts.length > 0) {
150 showNewPostsTitle(addedPostCount);
98 showNewPostsTitle(addedPostCount);
151 }
99 }
152
100
153 // TODO Process removed posts if any
101 // TODO Process removed posts if any
154 $('.metapanel').attr('data-last-update', data.last_update);
102 $('.metapanel').attr('data-last-update', data.last_update);
155
103
156 if (data.subscribed == 'True') {
104 if (data.subscribed == 'True') {
157 var favButton = $('#thread-fav-button .not_fav');
105 var favButton = $('#thread-fav-button .not_fav');
158
106
159 if (favButton.length > 0) {
107 if (favButton.length > 0) {
160 favButton.attr('value', 'unsubscribe');
108 favButton.attr('value', 'unsubscribe');
161 favButton.removeClass('not_fav');
109 favButton.removeClass('not_fav');
162 favButton.addClass('fav');
110 favButton.addClass('fav');
163 }
111 }
164 }
112 }
165 },
113 },
166 'json'
114 'json'
167 )
115 )
168 }
116 }
169
117
170 /**
118 /**
171 * Add or update the post on html page.
119 * Add or update the post on html page.
172 */
120 */
173 function updatePost(postHtml) {
121 function updatePost(postHtml) {
174 // This needs to be set on start because the page is scrolled after posts
122 // This needs to be set on start because the page is scrolled after posts
175 // are added or updated
123 // are added or updated
176 var bottom = isPageBottom();
124 var bottom = isPageBottom();
177
125
178 var post = $(postHtml);
126 var post = $(postHtml);
179
127
180 var threadBlock = $('div.thread');
128 var threadBlock = $('div.thread');
181
129
182 var postId = post.attr('id');
130 var postId = post.attr('id');
183
131
184 // If the post already exists, replace it. Otherwise add as a new one.
132 // If the post already exists, replace it. Otherwise add as a new one.
185 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
133 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
186
134
187 var type;
135 var type;
188
136
189 if (existingPosts.size() > 0) {
137 if (existingPosts.size() > 0) {
190 replacePartial(existingPosts.first(), post, false);
138 replacePartial(existingPosts.first(), post, false);
191 post = existingPosts.first();
139 post = existingPosts.first();
192
140
193 type = POST_UPDATED;
141 type = POST_UPDATED;
194 } else {
142 } else {
195 post.appendTo(threadBlock);
143 post.appendTo(threadBlock);
196
144
197 if (bottom) {
145 if (bottom) {
198 scrollToBottom();
146 scrollToBottom();
199 }
147 }
200
148
201 type = POST_ADDED;
149 type = POST_ADDED;
202 }
150 }
203
151
204 processNewPost(post);
152 processNewPost(post);
205
153
206 return type;
154 return type;
207 }
155 }
208
156
209 /**
157 /**
210 * Initiate a blinking animation on a node to show it was updated.
158 * Initiate a blinking animation on a node to show it was updated.
211 */
159 */
212 function blink(node) {
160 function blink(node) {
213 node.effect('highlight', { color: blinkColor }, BLINK_SPEED);
161 node.effect('highlight', { color: blinkColor }, BLINK_SPEED);
214 }
162 }
215
163
216 function isPageBottom() {
164 function isPageBottom() {
217 var scroll = $(window).scrollTop() / ($(document).height()
165 var scroll = $(window).scrollTop() / ($(document).height()
218 - $(window).height());
166 - $(window).height());
219
167
220 return scroll == 1
168 return scroll == 1
221 }
169 }
222
170
223 function enableJsUpdate() {
171 function enableJsUpdate() {
224 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
172 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
225 return true;
173 return true;
226 }
174 }
227
175
228 function initAutoupdate() {
176 function initAutoupdate() {
229 if (location.protocol === 'https:') {
177 return enableJsUpdate();
230 return enableJsUpdate();
231 } else {
232 if (connectWebsocket()) {
233 return true;
234 } else {
235 return enableJsUpdate();
236 }
237 }
238 }
178 }
239
179
240 function getReplyCount() {
180 function getReplyCount() {
241 return $('.thread').children(CLASS_POST).length
181 return $('.thread').children(CLASS_POST).length
242 }
182 }
243
183
244 function getImageCount() {
184 function getImageCount() {
245 return $('.thread').find('img').length
185 return $('.thread').find('img').length
246 }
186 }
247
187
248 /**
188 /**
249 * Update post count, images count and last update time in the metadata
189 * Update post count, images count and last update time in the metadata
250 * panel.
190 * panel.
251 */
191 */
252 function updateMetadataPanel() {
192 function updateMetadataPanel() {
253 var replyCountField = $('#reply-count');
193 var replyCountField = $('#reply-count');
254 var imageCountField = $('#image-count');
194 var imageCountField = $('#image-count');
255
195
256 var replyCount = getReplyCount();
196 var replyCount = getReplyCount();
257 replyCountField.text(replyCount);
197 replyCountField.text(replyCount);
258 var imageCount = getImageCount();
198 var imageCount = getImageCount();
259 imageCountField.text(imageCount);
199 imageCountField.text(imageCount);
260
200
261 var lastUpdate = $('.post:last').children('.post-info').first()
201 var lastUpdate = $('.post:last').children('.post-info').first()
262 .children('.pub_time').first().html();
202 .children('.pub_time').first().html();
263 if (lastUpdate !== '') {
203 if (lastUpdate !== '') {
264 var lastUpdateField = $('#last-update');
204 var lastUpdateField = $('#last-update');
265 lastUpdateField.html(lastUpdate);
205 lastUpdateField.html(lastUpdate);
266 blink(lastUpdateField);
206 blink(lastUpdateField);
267 }
207 }
268
208
269 blink(replyCountField);
209 blink(replyCountField);
270 blink(imageCountField);
210 blink(imageCountField);
271
211
272 $('#message-count-text').text(ngettext('message', 'messages', replyCount));
212 $('#message-count-text').text(ngettext('message', 'messages', replyCount));
273 $('#image-count-text').text(ngettext('image', 'images', imageCount));
213 $('#image-count-text').text(ngettext('image', 'images', imageCount));
274 }
214 }
275
215
276 /**
216 /**
277 * Update bumplimit progress bar
217 * Update bumplimit progress bar
278 */
218 */
279 function updateBumplimitProgress(postDelta) {
219 function updateBumplimitProgress(postDelta) {
280 var progressBar = $('#bumplimit_progress');
220 var progressBar = $('#bumplimit_progress');
281 if (progressBar) {
221 if (progressBar) {
282 var postsToLimitElement = $('#left_to_limit');
222 var postsToLimitElement = $('#left_to_limit');
283
223
284 var oldPostsToLimit = parseInt(postsToLimitElement.text());
224 var oldPostsToLimit = parseInt(postsToLimitElement.text());
285 var postCount = getReplyCount();
225 var postCount = getReplyCount();
286 var bumplimit = postCount - postDelta + oldPostsToLimit;
226 var bumplimit = postCount - postDelta + oldPostsToLimit;
287
227
288 var newPostsToLimit = bumplimit - postCount;
228 var newPostsToLimit = bumplimit - postCount;
289 if (newPostsToLimit <= 0) {
229 if (newPostsToLimit <= 0) {
290 $('.bar-bg').remove();
230 $('.bar-bg').remove();
291 } else {
231 } else {
292 postsToLimitElement.text(newPostsToLimit);
232 postsToLimitElement.text(newPostsToLimit);
293 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
233 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
294 }
234 }
295 }
235 }
296 }
236 }
297
237
298 /**
238 /**
299 * Show 'new posts' text in the title if the document is not visible to a user
239 * Show 'new posts' text in the title if the document is not visible to a user
300 */
240 */
301 function showNewPostsTitle(newPostCount) {
241 function showNewPostsTitle(newPostCount) {
302 if (document.hidden) {
242 if (document.hidden) {
303 if (documentOriginalTitle === '') {
243 if (documentOriginalTitle === '') {
304 documentOriginalTitle = document.title;
244 documentOriginalTitle = document.title;
305 }
245 }
306 unreadPosts = unreadPosts + newPostCount;
246 unreadPosts = unreadPosts + newPostCount;
307
247
308 var newTitle = null;
248 var newTitle = null;
309 if (unreadPosts > 0) {
249 if (unreadPosts > 0) {
310 newTitle = '[' + unreadPosts + '] ';
250 newTitle = '[' + unreadPosts + '] ';
311 } else {
251 } else {
312 newTitle = '* ';
252 newTitle = '* ';
313 }
253 }
314 newTitle += documentOriginalTitle;
254 newTitle += documentOriginalTitle;
315
255
316 document.title = newTitle;
256 document.title = newTitle;
317
257
318 document.addEventListener('visibilitychange', function() {
258 document.addEventListener('visibilitychange', function() {
319 if (documentOriginalTitle !== '') {
259 if (documentOriginalTitle !== '') {
320 document.title = documentOriginalTitle;
260 document.title = documentOriginalTitle;
321 documentOriginalTitle = '';
261 documentOriginalTitle = '';
322 unreadPosts = 0;
262 unreadPosts = 0;
323 }
263 }
324
264
325 document.removeEventListener('visibilitychange', null);
265 document.removeEventListener('visibilitychange', null);
326 });
266 });
327 }
267 }
328 }
268 }
329
269
330 /**
270 /**
331 * Clear all entered values in the form fields
271 * Clear all entered values in the form fields
332 */
272 */
333 function resetForm(form) {
273 function resetForm(form) {
334 form.find('input:text, input:password, input:file, select, textarea').val('');
274 form.find('input:text, input:password, input:file, select, textarea').val('');
335 form.find('input:radio, input:checkbox')
275 form.find('input:radio, input:checkbox')
336 .removeAttr('checked').removeAttr('selected');
276 .removeAttr('checked').removeAttr('selected');
337 $('.file_wrap').find('.file-thumb').remove();
277 $('.file_wrap').find('.file-thumb').remove();
338 $('#preview-text').hide();
278 $('#preview-text').hide();
339 }
279 }
340
280
341 /**
281 /**
342 * When the form is posted, this method will be run as a callback
282 * When the form is posted, this method will be run as a callback
343 */
283 */
344 function updateOnPost(response, statusText, xhr, form) {
284 function updateOnPost(response, statusText, xhr, form) {
345 var json = $.parseJSON(response);
285 var json = $.parseJSON(response);
346 var status = json.status;
286 var status = json.status;
347
287
348 showAsErrors(form, '');
288 showAsErrors(form, '');
349 $('.post-form-w').unblock();
289 $('.post-form-w').unblock();
350
290
351 if (status === 'ok') {
291 if (status === 'ok') {
352 resetFormPosition();
292 resetFormPosition();
353 resetForm(form);
293 resetForm(form);
354 getThreadDiff();
294 getThreadDiff();
355 scrollToBottom();
295 scrollToBottom();
356 } else {
296 } else {
357 var errors = json.errors;
297 var errors = json.errors;
358 for (var i = 0; i < errors.length; i++) {
298 for (var i = 0; i < errors.length; i++) {
359 var fieldErrors = errors[i];
299 var fieldErrors = errors[i];
360
300
361 var error = fieldErrors.errors;
301 var error = fieldErrors.errors;
362
302
363 showAsErrors(form, error);
303 showAsErrors(form, error);
364 }
304 }
365 }
305 }
366 }
306 }
367
307
368
308
369 /**
309 /**
370 * Run js methods that are usually run on the document, on the new post
310 * Run js methods that are usually run on the document, on the new post
371 */
311 */
372 function processNewPost(post) {
312 function processNewPost(post) {
373 addScriptsToPost(post);
313 addScriptsToPost(post);
374 blink(post);
314 blink(post);
375 }
315 }
376
316
377 function replacePartial(oldNode, newNode, recursive) {
317 function replacePartial(oldNode, newNode, recursive) {
378 if (!equalNodes(oldNode, newNode)) {
318 if (!equalNodes(oldNode, newNode)) {
379 // Update parent node attributes
319 // Update parent node attributes
380 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
320 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
381 updateNodeAttr(oldNode, newNode, ATTR_UID);
321 updateNodeAttr(oldNode, newNode, ATTR_UID);
382
322
383 // Replace children
323 // Replace children
384 var children = oldNode.children();
324 var children = oldNode.children();
385 if (children.length == 0) {
325 if (children.length == 0) {
386 oldNode.replaceWith(newNode);
326 oldNode.replaceWith(newNode);
387 } else {
327 } else {
388 var newChildren = newNode.children();
328 var newChildren = newNode.children();
389 newChildren.each(function(i) {
329 newChildren.each(function(i) {
390 var newChild = newChildren.eq(i);
330 var newChild = newChildren.eq(i);
391 var newChildClass = newChild.attr(ATTR_CLASS);
331 var newChildClass = newChild.attr(ATTR_CLASS);
392
332
393 // Update only certain allowed blocks (e.g. not images)
333 // Update only certain allowed blocks (e.g. not images)
394 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
334 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
395 var oldChild = oldNode.children('.' + newChildClass);
335 var oldChild = oldNode.children('.' + newChildClass);
396
336
397 if (oldChild.length == 0) {
337 if (oldChild.length == 0) {
398 oldNode.append(newChild);
338 oldNode.append(newChild);
399 } else {
339 } else {
400 if (!equalNodes(oldChild, newChild)) {
340 if (!equalNodes(oldChild, newChild)) {
401 if (recursive) {
341 if (recursive) {
402 replacePartial(oldChild, newChild, false);
342 replacePartial(oldChild, newChild, false);
403 } else {
343 } else {
404 oldChild.replaceWith(newChild);
344 oldChild.replaceWith(newChild);
405 }
345 }
406 }
346 }
407 }
347 }
408 }
348 }
409 });
349 });
410 }
350 }
411 }
351 }
412 }
352 }
413
353
414 /**
354 /**
415 * Compare nodes by content
355 * Compare nodes by content
416 */
356 */
417 function equalNodes(node1, node2) {
357 function equalNodes(node1, node2) {
418 return node1[0].outerHTML == node2[0].outerHTML;
358 return node1[0].outerHTML == node2[0].outerHTML;
419 }
359 }
420
360
421 /**
361 /**
422 * Update attribute of a node if it has changed
362 * Update attribute of a node if it has changed
423 */
363 */
424 function updateNodeAttr(oldNode, newNode, attrName) {
364 function updateNodeAttr(oldNode, newNode, attrName) {
425 var oldAttr = oldNode.attr(attrName);
365 var oldAttr = oldNode.attr(attrName);
426 var newAttr = newNode.attr(attrName);
366 var newAttr = newNode.attr(attrName);
427 if (oldAttr != newAttr) {
367 if (oldAttr != newAttr) {
428 oldNode.attr(attrName, newAttr);
368 oldNode.attr(attrName, newAttr);
429 }
369 }
430 }
370 }
431
371
432 $(document).ready(function() {
372 $(document).ready(function() {
433 if (initAutoupdate()) {
373 if (initAutoupdate()) {
434 // Post form data over AJAX
374 // Post form data over AJAX
435 var threadId = $('div.thread').children('.post').first().attr('id');
375 var threadId = $('div.thread').children('.post').first().attr('id');
436
376
437 var form = $('#form');
377 var form = $('#form');
438
378
439 if (form.length > 0) {
379 if (form.length > 0) {
440 var options = {
380 var options = {
441 beforeSubmit: function(arr, form, options) {
381 beforeSubmit: function(arr, form, options) {
442 $('.post-form-w').block({ message: gettext('Sending message...') });
382 $('.post-form-w').block({ message: gettext('Sending message...') });
443 },
383 },
444 success: updateOnPost,
384 success: updateOnPost,
445 error: function(xhr, textStatus, errorString) {
385 error: function(xhr, textStatus, errorString) {
446 var errorText = gettext('Server error: ') + textStatus;
386 var errorText = gettext('Server error: ') + textStatus;
447 if (errorString) {
387 if (errorString) {
448 errorText += ' / ' + errorString;
388 errorText += ' / ' + errorString;
449 }
389 }
450 showAsErrors(form, errorText);
390 showAsErrors(form, errorText);
451 $('.post-form-w').unblock();
391 $('.post-form-w').unblock();
452 },
392 },
453 url: '/api/add_post/' + threadId + '/',
393 url: '/api/add_post/' + threadId + '/',
454 timeout: POST_AJAX_TIMEOUT
394 timeout: POST_AJAX_TIMEOUT
455 };
395 };
456
396
457 form.ajaxForm(options);
397 form.ajaxForm(options);
458
398
459 resetForm(form);
399 resetForm(form);
460 }
400 }
461 }
401 }
462 });
402 });
@@ -1,42 +1,38 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5 {% load board %}
5 {% load board %}
6 {% load tz %}
6 {% load tz %}
7
7
8 {% block head %}
8 {% block head %}
9 <title>{{ opening_post.get_title_or_text }} - {{ site_name }}</title>
9 <title>{{ opening_post.get_title_or_text }} - {{ site_name }}</title>
10 {% endblock %}
10 {% endblock %}
11
11
12 {% block content %}
12 {% block content %}
13 <div class="image-mode-tab">
13 <div class="image-mode-tab">
14 <a {% ifequal mode 'normal' %}class="current_mode"{% endifequal %} href="{% url 'thread' opening_post.id %}">{% trans 'Normal' %}</a>,
14 <a {% ifequal mode 'normal' %}class="current_mode"{% endifequal %} href="{% url 'thread' opening_post.id %}">{% trans 'Normal' %}</a>,
15 <a {% ifequal mode 'gallery' %}class="current_mode"{% endifequal %} href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery' %}</a>,
15 <a {% ifequal mode 'gallery' %}class="current_mode"{% endifequal %} href="{% url 'thread_gallery' opening_post.id %}">{% trans 'Gallery' %}</a>,
16 <a {% ifequal mode 'tree' %}class="current_mode"{% endifequal %} href="{% url 'thread_tree' opening_post.id %}">{% trans 'Tree' %}</a>
16 <a {% ifequal mode 'tree' %}class="current_mode"{% endifequal %} href="{% url 'thread_tree' opening_post.id %}">{% trans 'Tree' %}</a>
17 </div>
17 </div>
18
18
19 {% block thread_content %}
19 {% block thread_content %}
20 {% endblock %}
20 {% endblock %}
21 {% endblock %}
21 {% endblock %}
22
22
23 {% block metapanel %}
23 {% block metapanel %}
24
24
25 <span class="metapanel"
25 <span class="metapanel"
26 data-last-update="{{ last_update }}"
26 data-last-update="{{ last_update }}"
27 data-ws-token-time="{{ ws_token_time }}"
27 data-ws-token-time="{{ ws_token_time }}">
28 data-ws-token="{{ ws_token }}"
29 data-ws-project="{{ ws_project }}"
30 data-ws-host="{{ ws_host }}"
31 data-ws-port="{{ ws_port }}">
32
28
33 {% with replies_count=thread.get_reply_count%}
29 {% with replies_count=thread.get_reply_count%}
34 <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %}
30 <span id="reply-count">{{ thread.get_reply_count }}</span>{% if thread.has_post_limit %}/{{ thread.max_posts }}{% endif %}
35 {% endwith %}
31 {% endwith %}
36 {% with images_count=thread.get_images_count%}
32 {% with images_count=thread.get_images_count%}
37 <span id="image-count">{{ images_count }}</span> <span id="image-count-text">
33 <span id="image-count">{{ images_count }}</span> <span id="image-count-text">
38 {% endwith %}
34 {% endwith %}
39 {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span>
35 {% trans 'Last update: ' %}<span id="last-update"><time datetime="{{ thread.last_edit_time|date:'c' }}">{{ thread.last_edit_time }}</time></span>
40 </span>
36 </span>
41
37
42 {% endblock %}
38 {% endblock %}
@@ -1,191 +1,187 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.core.files import File
2 from django.core.files import File
3 from django.core.files.temp import NamedTemporaryFile
3 from django.core.files.temp import NamedTemporaryFile
4 from django.core.paginator import EmptyPage
4 from django.core.paginator import EmptyPage
5 from django.db import transaction
5 from django.db import transaction
6 from django.http import Http404
6 from django.http import Http404
7 from django.shortcuts import render, redirect
7 from django.shortcuts import render, redirect
8 from django.utils.decorators import method_decorator
8 from django.utils.decorators import method_decorator
9 from django.views.decorators.csrf import csrf_protect
9 from django.views.decorators.csrf import csrf_protect
10
10
11 from boards import utils, settings
11 from boards import utils, settings
12 from boards.abstracts.paginator import get_paginator
12 from boards.abstracts.paginator import get_paginator
13 from boards.abstracts.settingsmanager import get_settings_manager,\
13 from boards.abstracts.settingsmanager import get_settings_manager,\
14 SETTING_ONLY_FAVORITES
14 SETTING_ONLY_FAVORITES
15 from boards.forms import ThreadForm, PlainErrorList
15 from boards.forms import ThreadForm, PlainErrorList
16 from boards.models import Post, Thread, Ban
16 from boards.models import Post, Thread, Ban
17 from boards.views.banned import BannedView
17 from boards.views.banned import BannedView
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
19 from boards.views.posting_mixin import PostMixin
19 from boards.views.posting_mixin import PostMixin
20 from boards.views.mixins import FileUploadMixin, PaginatedMixin,\
20 from boards.views.mixins import FileUploadMixin, PaginatedMixin,\
21 DispatcherMixin, PARAMETER_METHOD
21 DispatcherMixin, PARAMETER_METHOD
22
22
23 FORM_TAGS = 'tags'
23 FORM_TAGS = 'tags'
24 FORM_TEXT = 'text'
24 FORM_TEXT = 'text'
25 FORM_TITLE = 'title'
25 FORM_TITLE = 'title'
26 FORM_IMAGE = 'image'
26 FORM_IMAGE = 'image'
27 FORM_THREADS = 'threads'
27 FORM_THREADS = 'threads'
28
28
29 TAG_DELIMITER = ' '
29 TAG_DELIMITER = ' '
30
30
31 PARAMETER_CURRENT_PAGE = 'current_page'
31 PARAMETER_CURRENT_PAGE = 'current_page'
32 PARAMETER_PAGINATOR = 'paginator'
32 PARAMETER_PAGINATOR = 'paginator'
33 PARAMETER_THREADS = 'threads'
33 PARAMETER_THREADS = 'threads'
34 PARAMETER_ADDITIONAL = 'additional_params'
34 PARAMETER_ADDITIONAL = 'additional_params'
35 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
35 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
36 PARAMETER_RSS_URL = 'rss_url'
36 PARAMETER_RSS_URL = 'rss_url'
37
37
38 TEMPLATE = 'boards/all_threads.html'
38 TEMPLATE = 'boards/all_threads.html'
39 DEFAULT_PAGE = 1
39 DEFAULT_PAGE = 1
40
40
41 FORM_TAGS = 'tags'
41 FORM_TAGS = 'tags'
42
42
43
43
44 class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin, DispatcherMixin):
44 class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin, DispatcherMixin):
45
45
46 tag_name = ''
46 tag_name = ''
47
47
48 def __init__(self):
48 def __init__(self):
49 self.settings_manager = None
49 self.settings_manager = None
50 super(AllThreadsView, self).__init__()
50 super(AllThreadsView, self).__init__()
51
51
52 @method_decorator(csrf_protect)
52 @method_decorator(csrf_protect)
53 def get(self, request, form: ThreadForm=None):
53 def get(self, request, form: ThreadForm=None):
54 page = request.GET.get('page', DEFAULT_PAGE)
54 page = request.GET.get('page', DEFAULT_PAGE)
55
55
56 params = self.get_context_data(request=request)
56 params = self.get_context_data(request=request)
57
57
58 if not form:
58 if not form:
59 form = ThreadForm(error_class=PlainErrorList,
59 form = ThreadForm(error_class=PlainErrorList,
60 initial={FORM_TAGS: self.tag_name})
60 initial={FORM_TAGS: self.tag_name})
61
61
62 self.settings_manager = get_settings_manager(request)
62 self.settings_manager = get_settings_manager(request)
63
63
64 threads = self.get_threads()
64 threads = self.get_threads()
65
65
66 order = request.GET.get('order', 'bump')
66 order = request.GET.get('order', 'bump')
67 if order == 'bump':
67 if order == 'bump':
68 threads = threads.order_by('-bump_time')
68 threads = threads.order_by('-bump_time')
69 else:
69 else:
70 threads = threads.filter(replies__opening=True)\
70 threads = threads.filter(replies__opening=True)\
71 .order_by('-replies__pub_time')
71 .order_by('-replies__pub_time')
72 filter = request.GET.get('filter')
72 filter = request.GET.get('filter')
73 threads = threads.distinct()
73 threads = threads.distinct()
74
74
75 paginator = get_paginator(threads,
75 paginator = get_paginator(threads,
76 settings.get_int('View', 'ThreadsPerPage'))
76 settings.get_int('View', 'ThreadsPerPage'))
77 paginator.current_page = int(page)
77 paginator.current_page = int(page)
78
78
79 try:
79 try:
80 threads = paginator.page(page).object_list
80 threads = paginator.page(page).object_list
81 except EmptyPage:
81 except EmptyPage:
82 raise Http404()
82 raise Http404()
83
83
84 params[PARAMETER_THREADS] = threads
84 params[PARAMETER_THREADS] = threads
85 params[CONTEXT_FORM] = form
85 params[CONTEXT_FORM] = form
86 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
86 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
87 params[PARAMETER_RSS_URL] = self.get_rss_url()
87 params[PARAMETER_RSS_URL] = self.get_rss_url()
88
88
89 paginator.set_url(self.get_reverse_url(), request.GET.dict())
89 paginator.set_url(self.get_reverse_url(), request.GET.dict())
90 self.get_page_context(paginator, params, page)
90 self.get_page_context(paginator, params, page)
91
91
92 return render(request, TEMPLATE, params)
92 return render(request, TEMPLATE, params)
93
93
94 @method_decorator(csrf_protect)
94 @method_decorator(csrf_protect)
95 def post(self, request):
95 def post(self, request):
96 if PARAMETER_METHOD in request.POST:
96 if PARAMETER_METHOD in request.POST:
97 self.dispatch_method(request)
97 self.dispatch_method(request)
98
98
99 return redirect('index') # FIXME Different for different modes
99 return redirect('index') # FIXME Different for different modes
100
100
101 form = ThreadForm(request.POST, request.FILES,
101 form = ThreadForm(request.POST, request.FILES,
102 error_class=PlainErrorList)
102 error_class=PlainErrorList)
103 form.session = request.session
103 form.session = request.session
104
104
105 if form.is_valid():
105 if form.is_valid():
106 return self.create_thread(request, form)
106 return self.create_thread(request, form)
107 if form.need_to_ban:
107 if form.need_to_ban:
108 # Ban user because he is suspected to be a bot
108 # Ban user because he is suspected to be a bot
109 self._ban_current_user(request)
109 self._ban_current_user(request)
110
110
111 return self.get(request, form)
111 return self.get(request, form)
112
112
113 def get_page_context(self, paginator, params, page):
113 def get_page_context(self, paginator, params, page):
114 """
114 """
115 Get pagination context variables
115 Get pagination context variables
116 """
116 """
117
117
118 params[PARAMETER_PAGINATOR] = paginator
118 params[PARAMETER_PAGINATOR] = paginator
119 current_page = paginator.page(int(page))
119 current_page = paginator.page(int(page))
120 params[PARAMETER_CURRENT_PAGE] = current_page
120 params[PARAMETER_CURRENT_PAGE] = current_page
121 self.set_page_urls(paginator, params)
121 self.set_page_urls(paginator, params)
122
122
123 def get_reverse_url(self):
123 def get_reverse_url(self):
124 return reverse('index')
124 return reverse('index')
125
125
126 @transaction.atomic
126 @transaction.atomic
127 def create_thread(self, request, form: ThreadForm, html_response=True):
127 def create_thread(self, request, form: ThreadForm, html_response=True):
128 """
128 """
129 Creates a new thread with an opening post.
129 Creates a new thread with an opening post.
130 """
130 """
131
131
132 ip = utils.get_client_ip(request)
132 ip = utils.get_client_ip(request)
133 is_banned = Ban.objects.filter(ip=ip).exists()
133 is_banned = Ban.objects.filter(ip=ip).exists()
134
134
135 if is_banned:
135 if is_banned:
136 if html_response:
136 if html_response:
137 return redirect(BannedView().as_view())
137 return redirect(BannedView().as_view())
138 else:
138 else:
139 return
139 return
140
140
141 data = form.cleaned_data
141 data = form.cleaned_data
142
142
143 title = form.get_title()
143 title = form.get_title()
144 text = data[FORM_TEXT]
144 text = data[FORM_TEXT]
145 files = form.get_files()
145 files = form.get_files()
146 file_urls = form.get_file_urls()
146 file_urls = form.get_file_urls()
147 images = form.get_images()
147 images = form.get_images()
148
148
149 text = self._remove_invalid_links(text)
149 text = self._remove_invalid_links(text)
150
150
151 tags = data[FORM_TAGS]
151 tags = data[FORM_TAGS]
152 monochrome = form.is_monochrome()
152 monochrome = form.is_monochrome()
153
153
154 post = Post.objects.create_post(title=title, text=text, files=files,
154 post = Post.objects.create_post(title=title, text=text, files=files,
155 ip=ip, tags=tags,
155 ip=ip, tags=tags,
156 tripcode=form.get_tripcode(),
156 tripcode=form.get_tripcode(),
157 monochrome=monochrome, images=images,
157 monochrome=monochrome, images=images,
158 file_urls = file_urls)
158 file_urls=file_urls)
159
160 # This is required to update the threads to which posts we have replied
161 # when creating this one
162 post.notify_clients()
163
159
164 if form.is_subscribe():
160 if form.is_subscribe():
165 settings_manager = get_settings_manager(request)
161 settings_manager = get_settings_manager(request)
166 settings_manager.add_or_read_fav_thread(post)
162 settings_manager.add_or_read_fav_thread(post)
167
163
168 if html_response:
164 if html_response:
169 return redirect(post.get_absolute_url())
165 return redirect(post.get_absolute_url())
170
166
171 def get_threads(self):
167 def get_threads(self):
172 """
168 """
173 Gets list of threads that will be shown on a page.
169 Gets list of threads that will be shown on a page.
174 """
170 """
175
171
176 threads = Thread.objects\
172 threads = Thread.objects\
177 .exclude(tags__in=self.settings_manager.get_hidden_tags())
173 .exclude(tags__in=self.settings_manager.get_hidden_tags())
178 if self.settings_manager.get_setting(SETTING_ONLY_FAVORITES):
174 if self.settings_manager.get_setting(SETTING_ONLY_FAVORITES):
179 fav_tags = self.settings_manager.get_fav_tags()
175 fav_tags = self.settings_manager.get_fav_tags()
180 if len(fav_tags) > 0:
176 if len(fav_tags) > 0:
181 threads = threads.filter(tags__in=fav_tags)
177 threads = threads.filter(tags__in=fav_tags)
182
178
183 return threads
179 return threads
184
180
185 def get_rss_url(self):
181 def get_rss_url(self):
186 return self.get_reverse_url() + 'rss/'
182 return self.get_reverse_url() + 'rss/'
187
183
188 def toggle_fav(self, request):
184 def toggle_fav(self, request):
189 settings_manager = get_settings_manager(request)
185 settings_manager = get_settings_manager(request)
190 settings_manager.set_setting(SETTING_ONLY_FAVORITES,
186 settings_manager.set_setting(SETTING_ONLY_FAVORITES,
191 not settings_manager.get_setting(SETTING_ONLY_FAVORITES, False))
187 not settings_manager.get_setting(SETTING_ONLY_FAVORITES, False))
@@ -1,181 +1,165 b''
1 from django.contrib.auth.decorators import permission_required
1 from django.contrib.auth.decorators import permission_required
2
2
3 from django.core.exceptions import ObjectDoesNotExist
3 from django.core.exceptions import ObjectDoesNotExist
4 from django.core.urlresolvers import reverse
4 from django.core.urlresolvers import reverse
5 from django.http import Http404
5 from django.http import Http404
6 from django.shortcuts import get_object_or_404, render, redirect
6 from django.shortcuts import get_object_or_404, render, redirect
7 from django.template.context_processors import csrf
7 from django.template.context_processors import csrf
8 from django.utils.decorators import method_decorator
8 from django.utils.decorators import method_decorator
9 from django.views.decorators.csrf import csrf_protect
9 from django.views.decorators.csrf import csrf_protect
10 from django.views.generic.edit import FormMixin
10 from django.views.generic.edit import FormMixin
11 from django.utils import timezone
11 from django.utils import timezone
12 from django.utils.dateformat import format
12 from django.utils.dateformat import format
13
13
14 from boards import utils, settings
14 from boards import utils, settings
15 from boards.abstracts.settingsmanager import get_settings_manager
15 from boards.abstracts.settingsmanager import get_settings_manager
16 from boards.forms import PostForm, PlainErrorList
16 from boards.forms import PostForm, PlainErrorList
17 from boards.models import Post
17 from boards.models import Post
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
19 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
19 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
20 from boards.views.posting_mixin import PostMixin
20 from boards.views.posting_mixin import PostMixin
21 import neboard
21 import neboard
22
22
23 REQ_POST_ID = 'post_id'
23 REQ_POST_ID = 'post_id'
24
24
25 CONTEXT_LASTUPDATE = "last_update"
25 CONTEXT_LASTUPDATE = "last_update"
26 CONTEXT_THREAD = 'thread'
26 CONTEXT_THREAD = 'thread'
27 CONTEXT_WS_TOKEN = 'ws_token'
28 CONTEXT_WS_PROJECT = 'ws_project'
29 CONTEXT_WS_HOST = 'ws_host'
30 CONTEXT_WS_PORT = 'ws_port'
31 CONTEXT_WS_TIME = 'ws_token_time'
32 CONTEXT_MODE = 'mode'
27 CONTEXT_MODE = 'mode'
33 CONTEXT_OP = 'opening_post'
28 CONTEXT_OP = 'opening_post'
34 CONTEXT_FAVORITE = 'is_favorite'
29 CONTEXT_FAVORITE = 'is_favorite'
35 CONTEXT_RSS_URL = 'rss_url'
30 CONTEXT_RSS_URL = 'rss_url'
36
31
37 FORM_TITLE = 'title'
32 FORM_TITLE = 'title'
38 FORM_TEXT = 'text'
33 FORM_TEXT = 'text'
39 FORM_IMAGE = 'image'
34 FORM_IMAGE = 'image'
40 FORM_THREADS = 'threads'
35 FORM_THREADS = 'threads'
41
36
42
37
43 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
38 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
44
39
45 @method_decorator(csrf_protect)
40 @method_decorator(csrf_protect)
46 def get(self, request, post_id, form: PostForm=None):
41 def get(self, request, post_id, form: PostForm=None):
47 try:
42 try:
48 opening_post = Post.objects.get(id=post_id)
43 opening_post = Post.objects.get(id=post_id)
49 except ObjectDoesNotExist:
44 except ObjectDoesNotExist:
50 raise Http404
45 raise Http404
51
46
52 # If the tag is favorite, update the counter
47 # If the tag is favorite, update the counter
53 settings_manager = get_settings_manager(request)
48 settings_manager = get_settings_manager(request)
54 favorite = settings_manager.thread_is_fav(opening_post)
49 favorite = settings_manager.thread_is_fav(opening_post)
55 if favorite:
50 if favorite:
56 settings_manager.add_or_read_fav_thread(opening_post)
51 settings_manager.add_or_read_fav_thread(opening_post)
57
52
58 # If this is not OP, don't show it as it is
53 # If this is not OP, don't show it as it is
59 if not opening_post.is_opening():
54 if not opening_post.is_opening():
60 return redirect(opening_post.get_thread().get_opening_post()
55 return redirect(opening_post.get_thread().get_opening_post()
61 .get_absolute_url())
56 .get_absolute_url())
62
57
63 if not form:
58 if not form:
64 form = PostForm(error_class=PlainErrorList)
59 form = PostForm(error_class=PlainErrorList)
65
60
66 thread_to_show = opening_post.get_thread()
61 thread_to_show = opening_post.get_thread()
67
62
68 params = dict()
63 params = dict()
69
64
70 params[CONTEXT_FORM] = form
65 params[CONTEXT_FORM] = form
71 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
66 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
72 params[CONTEXT_THREAD] = thread_to_show
67 params[CONTEXT_THREAD] = thread_to_show
73 params[CONTEXT_MODE] = self.get_mode()
68 params[CONTEXT_MODE] = self.get_mode()
74 params[CONTEXT_OP] = opening_post
69 params[CONTEXT_OP] = opening_post
75 params[CONTEXT_FAVORITE] = favorite
70 params[CONTEXT_FAVORITE] = favorite
76 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
71 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
77
72
78 if settings.get_bool('External', 'WebsocketsEnabled'):
79 token_time = format(timezone.now(), u'U')
80
81 params[CONTEXT_WS_TIME] = token_time
82 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
83 timestamp=token_time)
84 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
85 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
86 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
87
88 params.update(self.get_data(thread_to_show))
73 params.update(self.get_data(thread_to_show))
89
74
90 return render(request, self.get_template(), params)
75 return render(request, self.get_template(), params)
91
76
92 @method_decorator(csrf_protect)
77 @method_decorator(csrf_protect)
93 def post(self, request, post_id):
78 def post(self, request, post_id):
94 opening_post = get_object_or_404(Post, id=post_id)
79 opening_post = get_object_or_404(Post, id=post_id)
95
80
96 # If this is not OP, don't show it as it is
81 # If this is not OP, don't show it as it is
97 if not opening_post.is_opening():
82 if not opening_post.is_opening():
98 raise Http404
83 raise Http404
99
84
100 if PARAMETER_METHOD in request.POST:
85 if PARAMETER_METHOD in request.POST:
101 self.dispatch_method(request, opening_post)
86 self.dispatch_method(request, opening_post)
102
87
103 return redirect('thread', post_id) # FIXME Different for different modes
88 return redirect('thread', post_id) # FIXME Different for different modes
104
89
105 if not opening_post.get_thread().is_archived():
90 if not opening_post.get_thread().is_archived():
106 form = PostForm(request.POST, request.FILES,
91 form = PostForm(request.POST, request.FILES,
107 error_class=PlainErrorList)
92 error_class=PlainErrorList)
108 form.session = request.session
93 form.session = request.session
109
94
110 if form.is_valid():
95 if form.is_valid():
111 return self.new_post(request, form, opening_post)
96 return self.new_post(request, form, opening_post)
112 if form.need_to_ban:
97 if form.need_to_ban:
113 # Ban user because he is suspected to be a bot
98 # Ban user because he is suspected to be a bot
114 self._ban_current_user(request)
99 self._ban_current_user(request)
115
100
116 return self.get(request, post_id, form)
101 return self.get(request, post_id, form)
117
102
118 def new_post(self, request, form: PostForm, opening_post: Post=None,
103 def new_post(self, request, form: PostForm, opening_post: Post=None,
119 html_response=True):
104 html_response=True):
120 """
105 """
121 Adds a new post (in thread or as a reply).
106 Adds a new post (in thread or as a reply).
122 """
107 """
123
108
124 ip = utils.get_client_ip(request)
109 ip = utils.get_client_ip(request)
125
110
126 data = form.cleaned_data
111 data = form.cleaned_data
127
112
128 title = form.get_title()
113 title = form.get_title()
129 text = data[FORM_TEXT]
114 text = data[FORM_TEXT]
130 files = form.get_files()
115 files = form.get_files()
131 file_urls = form.get_file_urls()
116 file_urls = form.get_file_urls()
132 images = form.get_images()
117 images = form.get_images()
133
118
134 text = self._remove_invalid_links(text)
119 text = self._remove_invalid_links(text)
135
120
136 post_thread = opening_post.get_thread()
121 post_thread = opening_post.get_thread()
137
122
138 post = Post.objects.create_post(title=title, text=text, files=files,
123 post = Post.objects.create_post(title=title, text=text, files=files,
139 thread=post_thread, ip=ip,
124 thread=post_thread, ip=ip,
140 tripcode=form.get_tripcode(),
125 tripcode=form.get_tripcode(),
141 images=images, file_urls=file_urls)
126 images=images, file_urls=file_urls)
142 post.notify_clients()
143
127
144 if form.is_subscribe():
128 if form.is_subscribe():
145 settings_manager = get_settings_manager(request)
129 settings_manager = get_settings_manager(request)
146 settings_manager.add_or_read_fav_thread(
130 settings_manager.add_or_read_fav_thread(
147 post_thread.get_opening_post())
131 post_thread.get_opening_post())
148
132
149 if html_response:
133 if html_response:
150 if opening_post:
134 if opening_post:
151 return redirect(post.get_absolute_url())
135 return redirect(post.get_absolute_url())
152 else:
136 else:
153 return post
137 return post
154
138
155 def get_data(self, thread) -> dict:
139 def get_data(self, thread) -> dict:
156 """
140 """
157 Returns context params for the view.
141 Returns context params for the view.
158 """
142 """
159
143
160 return dict()
144 return dict()
161
145
162 def get_template(self) -> str:
146 def get_template(self) -> str:
163 """
147 """
164 Gets template to show the thread mode on.
148 Gets template to show the thread mode on.
165 """
149 """
166
150
167 pass
151 pass
168
152
169 def get_mode(self) -> str:
153 def get_mode(self) -> str:
170 pass
154 pass
171
155
172 def subscribe(self, request, opening_post):
156 def subscribe(self, request, opening_post):
173 settings_manager = get_settings_manager(request)
157 settings_manager = get_settings_manager(request)
174 settings_manager.add_or_read_fav_thread(opening_post)
158 settings_manager.add_or_read_fav_thread(opening_post)
175
159
176 def unsubscribe(self, request, opening_post):
160 def unsubscribe(self, request, opening_post):
177 settings_manager = get_settings_manager(request)
161 settings_manager = get_settings_manager(request)
178 settings_manager.del_fav_thread(opening_post)
162 settings_manager.del_fav_thread(opening_post)
179
163
180 def get_rss_url(self, opening_id):
164 def get_rss_url(self, opening_id):
181 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
165 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
@@ -1,219 +1,210 b''
1 # Django settings for neboard project.
1 # Django settings for neboard project.
2 import os
2 import os
3
3
4 DEBUG = True
4 DEBUG = True
5
5
6 ADMINS = (
6 ADMINS = (
7 # ('Your Name', 'your_email@example.com'),
7 # ('Your Name', 'your_email@example.com'),
8 ('admin', 'admin@example.com')
8 ('admin', 'admin@example.com')
9 )
9 )
10
10
11 MANAGERS = ADMINS
11 MANAGERS = ADMINS
12
12
13 DATABASES = {
13 DATABASES = {
14 'default': {
14 'default': {
15 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
15 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
16 'NAME': 'database.db', # Or path to database file if using sqlite3.
16 'NAME': 'database.db', # Or path to database file if using sqlite3.
17 'USER': '', # Not used with sqlite3.
17 'USER': '', # Not used with sqlite3.
18 'PASSWORD': '', # Not used with sqlite3.
18 'PASSWORD': '', # Not used with sqlite3.
19 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
19 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
20 'PORT': '', # Set to empty string for default. Not used with sqlite3.
20 'PORT': '', # Set to empty string for default. Not used with sqlite3.
21 'CONN_MAX_AGE': None,
21 'CONN_MAX_AGE': None,
22 }
22 }
23 }
23 }
24
24
25 # Local time zone for this installation. Choices can be found here:
25 # Local time zone for this installation. Choices can be found here:
26 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
26 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
27 # although not all choices may be available on all operating systems.
27 # although not all choices may be available on all operating systems.
28 # In a Windows environment this must be set to your system time zone.
28 # In a Windows environment this must be set to your system time zone.
29 TIME_ZONE = 'Europe/Kiev'
29 TIME_ZONE = 'Europe/Kiev'
30
30
31 # Language code for this installation. All choices can be found here:
31 # Language code for this installation. All choices can be found here:
32 # http://www.i18nguy.com/unicode/language-identifiers.html
32 # http://www.i18nguy.com/unicode/language-identifiers.html
33 LANGUAGE_CODE = 'en'
33 LANGUAGE_CODE = 'en'
34
34
35 SITE_ID = 1
35 SITE_ID = 1
36
36
37 # If you set this to False, Django will make some optimizations so as not
37 # If you set this to False, Django will make some optimizations so as not
38 # to load the internationalization machinery.
38 # to load the internationalization machinery.
39 USE_I18N = True
39 USE_I18N = True
40
40
41 # If you set this to False, Django will not format dates, numbers and
41 # If you set this to False, Django will not format dates, numbers and
42 # calendars according to the current locale.
42 # calendars according to the current locale.
43 USE_L10N = True
43 USE_L10N = True
44
44
45 # If you set this to False, Django will not use timezone-aware datetimes.
45 # If you set this to False, Django will not use timezone-aware datetimes.
46 USE_TZ = True
46 USE_TZ = True
47
47
48 USE_ETAGS = True
48 USE_ETAGS = True
49
49
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 # Example: "/home/media/media.lawrence.com/media/"
51 # Example: "/home/media/media.lawrence.com/media/"
52 MEDIA_ROOT = './media/'
52 MEDIA_ROOT = './media/'
53
53
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 # trailing slash.
55 # trailing slash.
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 MEDIA_URL = '/media/'
57 MEDIA_URL = '/media/'
58
58
59 # Absolute path to the directory static files should be collected to.
59 # Absolute path to the directory static files should be collected to.
60 # Don't put anything in this directory yourself; store your static files
60 # Don't put anything in this directory yourself; store your static files
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 # Example: "/home/media/media.lawrence.com/static/"
62 # Example: "/home/media/media.lawrence.com/static/"
63 STATIC_ROOT = ''
63 STATIC_ROOT = ''
64
64
65 # URL prefix for static files.
65 # URL prefix for static files.
66 # Example: "http://media.lawrence.com/static/"
66 # Example: "http://media.lawrence.com/static/"
67 STATIC_URL = '/static/'
67 STATIC_URL = '/static/'
68
68
69 STATICFILES_DIRS = []
69 STATICFILES_DIRS = []
70
70
71 # List of finder classes that know how to find static files in
71 # List of finder classes that know how to find static files in
72 # various locations.
72 # various locations.
73 STATICFILES_FINDERS = (
73 STATICFILES_FINDERS = (
74 'django.contrib.staticfiles.finders.FileSystemFinder',
74 'django.contrib.staticfiles.finders.FileSystemFinder',
75 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
75 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
76 )
76 )
77
77
78 if DEBUG:
78 if DEBUG:
79 STATICFILES_STORAGE = \
79 STATICFILES_STORAGE = \
80 'django.contrib.staticfiles.storage.StaticFilesStorage'
80 'django.contrib.staticfiles.storage.StaticFilesStorage'
81 else:
81 else:
82 STATICFILES_STORAGE = \
82 STATICFILES_STORAGE = \
83 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
83 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
84
84
85 # Make this unique, and don't share it with anybody.
85 # Make this unique, and don't share it with anybody.
86 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
86 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
87
87
88 TEMPLATES = [{
88 TEMPLATES = [{
89 'BACKEND': 'django.template.backends.django.DjangoTemplates',
89 'BACKEND': 'django.template.backends.django.DjangoTemplates',
90 'DIRS': ['templates'],
90 'DIRS': ['templates'],
91 'OPTIONS': {
91 'OPTIONS': {
92 'loaders': [
92 'loaders': [
93 ('django.template.loaders.cached.Loader', [
93 ('django.template.loaders.cached.Loader', [
94 'django.template.loaders.filesystem.Loader',
94 'django.template.loaders.filesystem.Loader',
95 'django.template.loaders.app_directories.Loader',
95 'django.template.loaders.app_directories.Loader',
96 ]),
96 ]),
97 ],
97 ],
98 'context_processors': [
98 'context_processors': [
99 'django.template.context_processors.csrf',
99 'django.template.context_processors.csrf',
100 'django.contrib.auth.context_processors.auth',
100 'django.contrib.auth.context_processors.auth',
101 'boards.context_processors.user_and_ui_processor',
101 'boards.context_processors.user_and_ui_processor',
102 ],
102 ],
103 },
103 },
104 }]
104 }]
105
105
106
106
107 MIDDLEWARE_CLASSES = [
107 MIDDLEWARE_CLASSES = [
108 'django.middleware.http.ConditionalGetMiddleware',
108 'django.middleware.http.ConditionalGetMiddleware',
109 'django.contrib.sessions.middleware.SessionMiddleware',
109 'django.contrib.sessions.middleware.SessionMiddleware',
110 'django.middleware.locale.LocaleMiddleware',
110 'django.middleware.locale.LocaleMiddleware',
111 'django.middleware.common.CommonMiddleware',
111 'django.middleware.common.CommonMiddleware',
112 'django.contrib.auth.middleware.AuthenticationMiddleware',
112 'django.contrib.auth.middleware.AuthenticationMiddleware',
113 'django.contrib.messages.middleware.MessageMiddleware',
113 'django.contrib.messages.middleware.MessageMiddleware',
114 'boards.middlewares.BanMiddleware',
114 'boards.middlewares.BanMiddleware',
115 'boards.middlewares.TimezoneMiddleware',
115 'boards.middlewares.TimezoneMiddleware',
116 ]
116 ]
117
117
118 ROOT_URLCONF = 'neboard.urls'
118 ROOT_URLCONF = 'neboard.urls'
119
119
120 # Python dotted path to the WSGI application used by Django's runserver.
120 # Python dotted path to the WSGI application used by Django's runserver.
121 WSGI_APPLICATION = 'neboard.wsgi.application'
121 WSGI_APPLICATION = 'neboard.wsgi.application'
122
122
123 INSTALLED_APPS = (
123 INSTALLED_APPS = (
124 'django.contrib.auth',
124 'django.contrib.auth',
125 'django.contrib.contenttypes',
125 'django.contrib.contenttypes',
126 'django.contrib.sessions',
126 'django.contrib.sessions',
127 'django.contrib.staticfiles',
127 'django.contrib.staticfiles',
128 # Uncomment the next line to enable the admin:
128 # Uncomment the next line to enable the admin:
129 'django.contrib.admin',
129 'django.contrib.admin',
130 # Uncomment the next line to enable admin documentation:
130 # Uncomment the next line to enable admin documentation:
131 # 'django.contrib.admindocs',
131 # 'django.contrib.admindocs',
132 'django.contrib.messages',
132 'django.contrib.messages',
133
133
134 'debug_toolbar',
134 'debug_toolbar',
135
135
136 'boards',
136 'boards',
137 )
137 )
138
138
139 # A sample logging configuration. The only tangible logging
139 # A sample logging configuration. The only tangible logging
140 # performed by this configuration is to send an email to
140 # performed by this configuration is to send an email to
141 # the site admins on every HTTP 500 error when DEBUG=False.
141 # the site admins on every HTTP 500 error when DEBUG=False.
142 # See http://docs.djangoproject.com/en/dev/topics/logging for
142 # See http://docs.djangoproject.com/en/dev/topics/logging for
143 # more details on how to customize your logging configuration.
143 # more details on how to customize your logging configuration.
144 LOGGING = {
144 LOGGING = {
145 'version': 1,
145 'version': 1,
146 'disable_existing_loggers': False,
146 'disable_existing_loggers': False,
147 'formatters': {
147 'formatters': {
148 'verbose': {
148 'verbose': {
149 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
149 'format': '%(levelname)s %(asctime)s %(name)s %(process)d %(thread)d %(message)s'
150 },
150 },
151 'simple': {
151 'simple': {
152 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
152 'format': '%(levelname)s %(asctime)s [%(name)s] %(message)s'
153 },
153 },
154 },
154 },
155 'filters': {
155 'filters': {
156 'require_debug_false': {
156 'require_debug_false': {
157 '()': 'django.utils.log.RequireDebugFalse'
157 '()': 'django.utils.log.RequireDebugFalse'
158 }
158 }
159 },
159 },
160 'handlers': {
160 'handlers': {
161 'console': {
161 'console': {
162 'level': 'DEBUG',
162 'level': 'DEBUG',
163 'class': 'logging.StreamHandler',
163 'class': 'logging.StreamHandler',
164 'formatter': 'simple'
164 'formatter': 'simple'
165 },
165 },
166 },
166 },
167 'loggers': {
167 'loggers': {
168 'boards': {
168 'boards': {
169 'handlers': ['console'],
169 'handlers': ['console'],
170 'level': 'DEBUG',
170 'level': 'DEBUG',
171 }
171 }
172 },
172 },
173 }
173 }
174
174
175 THEMES = [
175 THEMES = [
176 ('md', 'Mystic Dark'),
176 ('md', 'Mystic Dark'),
177 ('md_centered', 'Mystic Dark (centered)'),
177 ('md_centered', 'Mystic Dark (centered)'),
178 ('sw', 'Snow White'),
178 ('sw', 'Snow White'),
179 ('pg', 'Photon Gray'),
179 ('pg', 'Photon Gray'),
180 ]
180 ]
181
181
182 IMAGE_VIEWERS = [
182 IMAGE_VIEWERS = [
183 ('simple', 'Simple'),
183 ('simple', 'Simple'),
184 ('popup', 'Popup'),
184 ('popup', 'Popup'),
185 ]
185 ]
186
186
187 ALLOWED_HOSTS = ['*']
187 ALLOWED_HOSTS = ['*']
188
188
189 POSTING_DELAY = 20 # seconds
189 POSTING_DELAY = 20 # seconds
190
190
191 # Websocket settins
192 CENTRIFUGE_HOST = 'localhost'
193 CENTRIFUGE_PORT = '9090'
194
195 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
196 CENTRIFUGE_PROJECT_ID = '<project id here>'
197 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
198 CENTRIFUGE_TIMEOUT = 5
199
200 SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
191 SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
201
192
202 # Debug middlewares
193 # Debug middlewares
203 MIDDLEWARE_CLASSES += [
194 MIDDLEWARE_CLASSES += [
204 'debug_toolbar.middleware.DebugToolbarMiddleware',
195 'debug_toolbar.middleware.DebugToolbarMiddleware',
205 ]
196 ]
206
197
207
198
208 def custom_show_toolbar(request):
199 def custom_show_toolbar(request):
209 return request.user.has_perm('admin.debug')
200 return request.user.has_perm('admin.debug')
210
201
211 DEBUG_TOOLBAR_CONFIG = {
202 DEBUG_TOOLBAR_CONFIG = {
212 'ENABLE_STACKTRACES': True,
203 'ENABLE_STACKTRACES': True,
213 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
204 'SHOW_TOOLBAR_CALLBACK': 'neboard.settings.custom_show_toolbar',
214 }
205 }
215
206
216 # FIXME Uncommenting this fails somehow. Need to investigate this
207 # FIXME Uncommenting this fails somehow. Need to investigate this
217 #DEBUG_TOOLBAR_PANELS += (
208 #DEBUG_TOOLBAR_PANELS += (
218 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
209 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
219 #)
210 #)
@@ -1,12 +1,11 b''
1 python-magic
1 python-magic
2 httplib2
2 httplib2
3 simplejson
3 simplejson
4 pytube
4 pytube
5 requests
5 requests
6 adjacent
7 pillow
6 pillow
8 django>=1.8
7 django>=1.8
9 bbcode
8 bbcode
10 django-debug-toolbar
9 django-debug-toolbar
11 pytz
10 pytz
12 ecdsa
11 ecdsa
General Comments 0
You need to be logged in to leave comments. Login now