##// END OF EJS Templates
New backend for fav threads. Now only last post ids are saved, no thread ids
neko259 -
r2044:227641ed default
parent child Browse files
Show More
@@ -1,240 +1,246
1 from boards import settings
1 import boards
2 2 from boards.models import Tag, TagAlias, Attachment
3 3 from boards.models.attachment import AttachmentSticker
4 from boards.models.thread import FAV_THREAD_NO_UPDATES
5 4 from boards.models.user import UserSettings
6 from boards.settings import SECTION_VIEW
7 5
8 6 MAX_TRIPCODE_COLLISIONS = 50
9 7
10 8 __author__ = 'neko259'
11 9
12 10 SESSION_SETTING = 'setting'
13 11
14 12 SETTING_THEME = 'theme'
15 13 SETTING_FAVORITE_TAGS = 'favorite_tags'
16 14 SETTING_FAVORITE_THREADS = 'favorite_threads'
17 15 SETTING_HIDDEN_TAGS = 'hidden_tags'
18 16 SETTING_USERNAME = 'username'
19 17 SETTING_LAST_NOTIFICATION_ID = 'last_notification'
20 18 SETTING_IMAGE_VIEWER = 'image_viewer'
21 19 SETTING_IMAGES = 'images_aliases'
22 20 SETTING_ONLY_FAVORITES = 'only_favorites'
21 SETTING_LAST_POSTS = 'last_posts'
23 22
24 23 DEFAULT_THEME = 'md'
25 24
26 25
27 26 class SettingsManager:
28 27 """
29 28 Base settings manager class. get_setting and set_setting methods should
30 29 be overriden.
31 30 """
32 31 def __init__(self):
33 32 pass
34 33
35 34 def get_theme(self) -> str:
36 35 theme = self.get_setting(SETTING_THEME)
37 36 if not theme:
38 37 theme = DEFAULT_THEME
39 38 self.set_setting(SETTING_THEME, theme)
40 39
41 40 return theme
42 41
43 42 def set_theme(self, theme):
44 43 self.set_setting(SETTING_THEME, theme)
45 44
46 45 def get_setting(self, setting, default=None):
47 46 pass
48 47
49 48 def set_setting(self, setting, value):
50 49 pass
51 50
52 51 def get_fav_tags(self) -> list:
53 52 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
54 53 tags = []
55 54 if tag_names:
56 55 tags = list(Tag.objects.filter(aliases__in=TagAlias.objects
57 56 .filter_localized(parent__aliases__name__in=tag_names))
58 57 .order_by('aliases__name'))
59 58 return tags
60 59
61 60 def add_fav_tag(self, tag):
62 61 tags = self.get_setting(SETTING_FAVORITE_TAGS)
63 62 if not tags:
64 63 tags = [tag.get_name()]
65 64 else:
66 65 if not tag.get_name() in tags:
67 66 tags.append(tag.get_name())
68 67
69 68 tags.sort()
70 69 self.set_setting(SETTING_FAVORITE_TAGS, tags)
71 70
72 71 def del_fav_tag(self, tag):
73 72 tags = self.get_setting(SETTING_FAVORITE_TAGS)
74 73 if tag.get_name() in tags:
75 74 tags.remove(tag.get_name())
76 75 self.set_setting(SETTING_FAVORITE_TAGS, tags)
77 76
78 77 def get_hidden_tags(self) -> list:
79 78 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
80 79 tags = []
81 80 if tag_names:
82 81 tags = list(Tag.objects.filter(aliases__in=TagAlias.objects
83 82 .filter_localized(parent__aliases__name__in=tag_names))
84 83 .order_by('aliases__name'))
85 84
86 85 return tags
87 86
88 87 def add_hidden_tag(self, tag):
89 88 tags = self.get_setting(SETTING_HIDDEN_TAGS)
90 89 if not tags:
91 90 tags = [tag.get_name()]
92 91 else:
93 92 if not tag.get_name() in tags:
94 93 tags.append(tag.get_name())
95 94
96 95 tags.sort()
97 96 self.set_setting(SETTING_HIDDEN_TAGS, tags)
98 97
99 98 def del_hidden_tag(self, tag):
100 99 tags = self.get_setting(SETTING_HIDDEN_TAGS)
101 100 if tag.get_name() in tags:
102 101 tags.remove(tag.get_name())
103 102 self.set_setting(SETTING_HIDDEN_TAGS, tags)
104 103
105 def get_fav_threads(self) -> dict:
106 return self.get_setting(SETTING_FAVORITE_THREADS, default=dict())
107
108 104 def add_or_read_fav_thread(self, opening_post):
109 threads = self.get_fav_threads()
105 last_post_ids = self.get_setting(SETTING_LAST_POSTS)
106 if not last_post_ids:
107 last_post_ids = []
110 108
111 max_fav_threads = settings.get_int(SECTION_VIEW, 'MaxFavoriteThreads')
112 if (str(opening_post.id) in threads) or (len(threads) < max_fav_threads):
113 thread = opening_post.get_thread()
114 # Don't check for new posts if the thread is archived already
115 if thread.is_archived():
116 last_id = FAV_THREAD_NO_UPDATES
117 else:
118 last_id = thread.get_replies().last().id
119 threads[str(opening_post.id)] = last_id
120 self.set_setting(SETTING_FAVORITE_THREADS, threads)
109 self.del_fav_thread(opening_post)
110
111 last_post_id = opening_post.get_thread().get_replies().last().id
112 last_post_ids.append(last_post_id)
113
114 self.set_setting(SETTING_LAST_POSTS, last_post_ids)
121 115
122 116 def del_fav_thread(self, opening_post):
123 threads = self.get_fav_threads()
124 if self.thread_is_fav(opening_post):
125 del threads[str(opening_post.id)]
126 self.set_setting(SETTING_FAVORITE_THREADS, threads)
117 last_posts_ids = self.get_setting(SETTING_LAST_POSTS)
118
119 for post in self.get_last_posts():
120 if post.get_thread() == opening_post.get_thread():
121 last_posts_ids.remove(post.id)
122
123 self.set_setting(SETTING_LAST_POSTS, last_posts_ids)
127 124
128 125 def thread_is_fav(self, opening_post):
129 return str(opening_post.id) in self.get_fav_threads()
126 for post in self.get_last_posts():
127 if post.get_thread() == opening_post.get_thread():
128 return True
129 return False
130 130
131 131 def get_notification_usernames(self):
132 132 names = set()
133 133 name_list = self.get_setting(SETTING_USERNAME)
134 134 if name_list is not None:
135 135 name_list = name_list.strip()
136 136 if len(name_list) > 0:
137 137 names = name_list.lower().split(',')
138 138 names = set(name.strip() for name in names)
139 139 return names
140 140
141 141 def get_attachment_by_alias(self, alias):
142 142 images = self.get_setting(SETTING_IMAGES)
143 143 if images and alias in images:
144 144 try:
145 145 return Attachment.objects.get(id=images.get(alias))
146 146 except Attachment.DoesNotExist:
147 147 self.remove_attachment_alias(alias)
148 148
149 149 def add_attachment_alias(self, alias, attachment):
150 150 images = self.get_setting(SETTING_IMAGES)
151 151 if images is None:
152 152 images = dict()
153 153 images[alias] = attachment.id
154 154 self.set_setting(SETTING_IMAGES, images)
155 155
156 156 def remove_attachment_alias(self, alias):
157 157 images = self.get_setting(SETTING_IMAGES)
158 158 del images[alias]
159 159 self.set_setting(SETTING_IMAGES, images)
160 160
161 161 def get_stickers(self):
162 162 images = self.get_setting(SETTING_IMAGES)
163 163 stickers = []
164 164 if images:
165 165 for key, value in images.items():
166 166 try:
167 167 attachment = Attachment.objects.get(id=value)
168 168 stickers.append(AttachmentSticker(name=key, attachment=attachment))
169 169 except Attachment.DoesNotExist:
170 170 self.remove_attachment_alias(key)
171 171 return stickers
172 172
173 173 def tag_is_fav(self, tag):
174 174 fav_tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
175 175 return fav_tag_names is not None and tag.get_name() in fav_tag_names
176 176
177 177 def tag_is_hidden(self, tag):
178 178 hidden_tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
179 179 return hidden_tag_names is not None and tag.get_name() in hidden_tag_names
180 180
181 def get_last_posts(self):
182 post_ids = self.get_setting(SETTING_LAST_POSTS) or []
183 return [boards.models.Post.objects.get(id=post_id) for post_id in post_ids]
184
181 185
182 186 class SessionSettingsManager(SettingsManager):
183 187 """
184 188 Session-based settings manager. All settings are saved to the user's
185 189 session.
186 190 """
187 191 def __init__(self, session):
188 192 SettingsManager.__init__(self)
189 193 self.session = session
190 194
191 195 def get_setting(self, setting, default=None):
192 196 if setting in self.session:
193 197 return self.session[setting]
194 198 else:
195 199 self.set_setting(setting, default)
196 200 return default
197 201
198 202 def set_setting(self, setting, value):
199 203 self.session[setting] = value
200 204
201 205
202 206 class DatabaseSettingsManager(SessionSettingsManager):
203 207 def __init__(self, session):
204 208 super().__init__(session)
209 if not session.session_key:
210 session.save()
205 211 self.settings, created = UserSettings.objects.get_or_create(session_key=session.session_key)
206 212
207 213 def add_fav_tag(self, tag):
208 214 self.settings.fav_tags.add(tag)
209 215
210 216 def del_fav_tag(self, tag):
211 217 self.settings.fav_tags.remove(tag)
212 218
213 219 def get_fav_tags(self) -> list:
214 220 return self.settings.fav_tags.filter(
215 221 aliases__in=TagAlias.objects.filter_localized())\
216 222 .order_by('aliases__name')
217 223
218 224 def get_hidden_tags(self) -> list:
219 225 return self.settings.hidden_tags.all()
220 226
221 227 def add_hidden_tag(self, tag):
222 228 self.settings.hidden_tags.add(tag)
223 229
224 230 def del_hidden_tag(self, tag):
225 231 self.settings.hidden_tags.remove(tag)
226 232
227 233 def tag_is_fav(self, tag):
228 234 return self.settings.fav_tags.filter(id=tag.id).exists()
229 235
230 236 def tag_is_hidden(self, tag):
231 237 return self.settings.hidden_tags.filter(id=tag.id).exists()
232 238
233 239
234 240 def get_settings_manager(request) -> SettingsManager:
235 241 """
236 242 Get settings manager based on the request object. Currently only
237 243 session-based manager is supported. In the future, cookie-based or
238 244 database-based managers could be implemented.
239 245 """
240 246 return DatabaseSettingsManager(request.session)
@@ -1,87 +1,83
1 1 from boards.abstracts.settingsmanager import get_settings_manager, \
2 2 SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER, SETTING_ONLY_FAVORITES
3 3 from boards.models import Banner
4 4 from boards.models.user import Notification
5 5 from boards import settings
6 6 from boards.models import Post, Tag, Thread
7 7 from boards.settings import SECTION_FORMS, SECTION_VIEW, SECTION_VERSION
8 8
9 9 THEME_CSS = 'css/{}/base_page.css'
10 10
11 11 CONTEXT_SITE_NAME = 'site_name'
12 12 CONTEXT_VERSION = 'version'
13 13 CONTEXT_THEME_CSS = 'theme_css'
14 14 CONTEXT_THEME = 'theme'
15 15 CONTEXT_PPD = 'posts_per_day'
16 16 CONTEXT_USER = 'user'
17 17 CONTEXT_NEW_NOTIFICATIONS_COUNT = 'new_notifications_count'
18 18 CONTEXT_USERNAMES = 'usernames'
19 19 CONTEXT_TAGS_STR = 'tags_str'
20 20 CONTEXT_IMAGE_VIEWER = 'image_viewer'
21 21 CONTEXT_HAS_FAV_THREADS = 'has_fav_threads'
22 22 CONTEXT_POW_DIFFICULTY = 'pow_difficulty'
23 23 CONTEXT_NEW_POST_COUNT = 'new_post_count'
24 24 CONTEXT_BANNERS = 'banners'
25 25 CONTEXT_ONLY_FAVORITES = 'only_favorites'
26 26
27 27
28 28 def get_notifications(context, settings_manager):
29 29 usernames = settings_manager.get_notification_usernames()
30 30 new_notifications_count = 0
31 31 if usernames:
32 32 last_notification_id = settings_manager.get_setting(
33 33 SETTING_LAST_NOTIFICATION_ID)
34 34
35 35 new_notifications_count = Notification.objects.get_notification_posts(
36 36 usernames=usernames, last=last_notification_id).only('id').count()
37 37 context[CONTEXT_NEW_NOTIFICATIONS_COUNT] = new_notifications_count
38 38 context[CONTEXT_USERNAMES] = usernames
39 39
40 40
41 41 def get_new_post_count(context, settings_manager):
42 fav_threads = settings_manager.get_fav_threads()
43 if fav_threads:
44 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys()) \
45 .order_by('-pub_time').only('thread_id', 'pub_time')
46 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
47 count = Thread.objects.get_new_post_count(ops)
42 last_posts = settings_manager.get_last_posts()
43 count = Thread.objects.get_new_post_count(last_posts)
48 44 if count > 0:
49 45 context[CONTEXT_NEW_POST_COUNT] = '(+{})'.format(count)
50 46
51 47
52 48 def user_and_ui_processor(request):
53 49 context = dict()
54 50
55 51 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
56 52
57 53 settings_manager = get_settings_manager(request)
58 54 fav_tags = settings_manager.get_fav_tags()
59 55
60 56 context[CONTEXT_TAGS_STR] = Tag.objects.get_tag_url_list(fav_tags)
61 57 theme = settings_manager.get_theme()
62 58 context[CONTEXT_THEME] = theme
63 59
64 60 # TODO Use static here
65 61 context[CONTEXT_THEME_CSS] = THEME_CSS.format(theme)
66 62
67 63 context[CONTEXT_VERSION] = settings.get(SECTION_VERSION, 'Version')
68 64 context[CONTEXT_SITE_NAME] = settings.get(SECTION_VERSION, 'SiteName')
69 65
70 66 if settings.get_bool(SECTION_FORMS, 'LimitFirstPosting'):
71 67 context[CONTEXT_POW_DIFFICULTY] = settings.get_int(SECTION_FORMS, 'PowDifficulty')
72 68
73 69 context[CONTEXT_IMAGE_VIEWER] = settings_manager.get_setting(
74 70 SETTING_IMAGE_VIEWER,
75 71 default=settings.get(SECTION_VIEW, 'DefaultImageViewer'))
76 72
77 73 context[CONTEXT_HAS_FAV_THREADS] =\
78 len(settings_manager.get_fav_threads()) > 0
74 len(settings_manager.get_last_posts()) > 0
79 75
80 76 context[CONTEXT_BANNERS] = Banner.objects.order_by('-id')
81 77 context[CONTEXT_ONLY_FAVORITES] = settings_manager.get_setting(
82 78 SETTING_ONLY_FAVORITES, default=False)
83 79
84 80 get_notifications(context, settings_manager)
85 81 get_new_post_count(context, settings_manager)
86 82
87 83 return context
@@ -1,326 +1,324
1 1 import logging
2 2 from datetime import timedelta
3 3
4 4 from django.db import models, transaction
5 5 from django.db.models import Count, Sum, QuerySet, Q
6 6 from django.utils import timezone
7 7
8 8 import boards
9 9 from boards import settings
10 10 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
11 11 from boards.models.attachment import FILE_TYPES_IMAGE
12 12 from boards.models.post import Post
13 13 from boards.models.tag import Tag, TagAlias
14 14 from boards.settings import SECTION_VIEW
15 15 from boards.utils import cached_result, datetime_to_epoch
16 16
17 17 FAV_THREAD_NO_UPDATES = -1
18 18
19 19
20 20 __author__ = 'neko259'
21 21
22 22
23 23 logger = logging.getLogger(__name__)
24 24
25 25
26 26 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
27 27 WS_NOTIFICATION_TYPE = 'notification_type'
28 28
29 29 WS_CHANNEL_THREAD = "thread:"
30 30
31 31 STATUS_CHOICES = (
32 32 (STATUS_ACTIVE, STATUS_ACTIVE),
33 33 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
34 34 (STATUS_ARCHIVE, STATUS_ARCHIVE),
35 35 )
36 36
37 37
38 38 class ThreadManager(models.Manager):
39 39 def process_old_threads(self):
40 40 """
41 41 Preserves maximum thread count. If there are too many threads,
42 42 archive or delete the old ones.
43 43 """
44 44 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
45 45 old_time = timezone.now() - timedelta(days=old_time_delta)
46 46 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
47 47
48 48 for op in old_ops:
49 49 thread = op.get_thread()
50 50 if settings.get_bool('Storage', 'ArchiveThreads'):
51 51 self._archive_thread(thread)
52 52 else:
53 53 thread.delete()
54 54 logger.info('Processed old thread {}'.format(thread))
55 55
56 56
57 57 def _archive_thread(self, thread):
58 58 thread.status = STATUS_ARCHIVE
59 59 thread.last_edit_time = timezone.now()
60 60 thread.update_posts_time()
61 61 thread.save(update_fields=['last_edit_time', 'status'])
62 62
63 def get_new_posts(self, datas):
63 def get_new_posts(self, last_posts):
64 64 query = None
65 # TODO Use classes instead of dicts
66 for data in datas:
67 if data['last_id'] != FAV_THREAD_NO_UPDATES:
68 q = (Q(id=data['op'].get_thread_id())
69 & Q(replies__id__gt=data['last_id']))
65 for post in last_posts:
66 if not post.get_thread().is_archived():
67 q = Q(id=post.thread_id) & Q(replies__id__gt=post.id)
70 68 if query is None:
71 69 query = q
72 70 else:
73 71 query = query | q
74 72 if query is not None:
75 73 return self.filter(query).annotate(
76 74 new_post_count=Count('replies'))
77 75
78 def get_new_post_count(self, datas):
79 new_posts = self.get_new_posts(datas)
76 def get_new_post_count(self, last_posts):
77 new_posts = self.get_new_posts(last_posts)
80 78 return new_posts.aggregate(total_count=Count('replies'))\
81 79 ['total_count'] if new_posts else 0
82 80
83 81
84 82 def get_thread_max_posts():
85 83 return settings.get_int('Messages', 'MaxPostsPerThread')
86 84
87 85
88 86 class Thread(models.Model):
89 87 objects = ThreadManager()
90 88
91 89 class Meta:
92 90 app_label = 'boards'
93 91
94 92 tags = models.ManyToManyField('Tag', related_name='thread_tags')
95 93 bump_time = models.DateTimeField(db_index=True)
96 94 last_edit_time = models.DateTimeField()
97 95 max_posts = models.IntegerField(default=get_thread_max_posts)
98 96 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
99 97 choices=STATUS_CHOICES, db_index=True)
100 98 monochrome = models.BooleanField(default=False)
101 99 stickerpack = models.BooleanField(default=False)
102 100
103 101 def get_tags(self) -> QuerySet:
104 102 """
105 103 Gets a sorted tag list.
106 104 """
107 105
108 106 return self.tags.filter(aliases__in=TagAlias.objects.filter_localized(parent__thread_tags=self)).order_by('aliases__name')
109 107
110 108 def bump(self):
111 109 """
112 110 Bumps (moves to up) thread if possible.
113 111 """
114 112
115 113 if self.can_bump():
116 114 self.bump_time = self.last_edit_time
117 115
118 116 self.update_bump_status()
119 117
120 118 logger.info('Bumped thread %d' % self.id)
121 119
122 120 def has_post_limit(self) -> bool:
123 121 return self.max_posts > 0
124 122
125 123 def update_bump_status(self, exclude_posts=None):
126 124 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
127 125 self.status = STATUS_BUMPLIMIT
128 126 self.update_posts_time(exclude_posts=exclude_posts)
129 127
130 128 def _get_cache_key(self):
131 129 return [datetime_to_epoch(self.last_edit_time)]
132 130
133 131 @cached_result(key_method=_get_cache_key)
134 132 def get_reply_count(self) -> int:
135 133 return self.get_replies().count()
136 134
137 135 @cached_result(key_method=_get_cache_key)
138 136 def get_images_count(self) -> int:
139 137 return self.get_replies().filter(
140 138 attachments__mimetype__in=FILE_TYPES_IMAGE)\
141 139 .annotate(images_count=Count(
142 140 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
143 141
144 142 @cached_result(key_method=_get_cache_key)
145 143 def get_attachment_count(self) -> int:
146 144 return self.get_replies().annotate(attachment_count=Count('attachments'))\
147 145 .aggregate(Sum('attachment_count'))['attachment_count__sum'] or 0
148 146
149 147 def can_bump(self) -> bool:
150 148 """
151 149 Checks if the thread can be bumped by replying to it.
152 150 """
153 151
154 152 return self.get_status() == STATUS_ACTIVE
155 153
156 154 def get_last_replies(self) -> QuerySet:
157 155 """
158 156 Gets several last replies, not including opening post
159 157 """
160 158
161 159 last_replies_count = settings.get_int(SECTION_VIEW, 'LastRepliesCount')
162 160
163 161 if last_replies_count > 0:
164 162 reply_count = self.get_reply_count()
165 163
166 164 if reply_count > 0:
167 165 reply_count_to_show = min(last_replies_count,
168 166 reply_count - 1)
169 167 replies = self.get_replies()
170 168 last_replies = replies[reply_count - reply_count_to_show:]
171 169
172 170 return last_replies
173 171
174 172 def get_skipped_replies_count(self) -> int:
175 173 """
176 174 Gets number of posts between opening post and last replies.
177 175 """
178 176 reply_count = self.get_reply_count()
179 177 last_replies_count = min(settings.get_int(SECTION_VIEW, 'LastRepliesCount'),
180 178 reply_count - 1)
181 179 return reply_count - last_replies_count - 1
182 180
183 181 # TODO Remove argument, it is not used
184 182 def get_replies(self, view_fields_only=True) -> QuerySet:
185 183 """
186 184 Gets sorted thread posts
187 185 """
188 186 query = self.replies.order_by('pub_time').prefetch_related(
189 187 'attachments')
190 188 return query
191 189
192 190 def get_viewable_replies(self) -> QuerySet:
193 191 """
194 192 Gets replies with only fields that are used for viewing.
195 193 """
196 194 return self.get_replies().defer('text', 'last_edit_time')
197 195
198 196 def get_top_level_replies(self) -> QuerySet:
199 197 return self.get_replies().exclude(refposts__threads__in=[self])
200 198
201 199 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
202 200 """
203 201 Gets replies that have at least one image attached
204 202 """
205 203 return self.get_replies(view_fields_only).filter(
206 204 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
207 205 'attachments')).filter(images_count__gt=0)
208 206
209 207 def get_opening_post(self, only_id=False) -> Post:
210 208 """
211 209 Gets the first post of the thread
212 210 """
213 211
214 212 query = self.get_replies().filter(opening=True)
215 213 if only_id:
216 214 query = query.only('id')
217 215 opening_post = query.first()
218 216
219 217 return opening_post
220 218
221 219 @cached_result()
222 220 def get_opening_post_id(self) -> int:
223 221 """
224 222 Gets ID of the first thread post.
225 223 """
226 224
227 225 return self.get_opening_post(only_id=True).id
228 226
229 227 def get_pub_time(self):
230 228 """
231 229 Gets opening post's pub time because thread does not have its own one.
232 230 """
233 231
234 232 return self.get_opening_post().pub_time
235 233
236 234 def __str__(self):
237 235 return 'T#{}/{}'.format(self.id, self.get_opening_post())
238 236
239 237 def get_tag_url_list(self) -> list:
240 238 return boards.models.Tag.objects.get_tag_url_list(self.get_tags().all())
241 239
242 240 def update_posts_time(self, exclude_posts=None):
243 241 last_edit_time = self.last_edit_time
244 242
245 243 for post in self.replies.all():
246 244 if exclude_posts is None or post not in exclude_posts:
247 245 # Manual update is required because uids are generated on save
248 246 post.last_edit_time = last_edit_time
249 247 post.save(update_fields=['last_edit_time'])
250 248
251 249 def get_absolute_url(self):
252 250 return self.get_opening_post().get_absolute_url()
253 251
254 252 def get_required_tags(self):
255 253 return self.get_tags().filter(required=True)
256 254
257 255 def get_sections_str(self):
258 256 return Tag.objects.get_tag_url_list(self.get_required_tags())
259 257
260 258 def get_replies_newer(self, post_id):
261 259 return self.get_replies().filter(id__gt=post_id)
262 260
263 261 def is_archived(self):
264 262 return self.get_status() == STATUS_ARCHIVE
265 263
266 264 def is_bumplimit(self):
267 265 return self.get_status() == STATUS_BUMPLIMIT
268 266
269 267 def get_status(self):
270 268 return self.status
271 269
272 270 def is_monochrome(self):
273 271 return self.monochrome
274 272
275 273 def is_stickerpack(self):
276 274 return self.stickerpack
277 275
278 276 # If tags have parent, add them to the tag list
279 277 @transaction.atomic
280 278 def refresh_tags(self):
281 279 for tag in self.get_tags().all():
282 280 parents = tag.get_all_parents()
283 281 if len(parents) > 0:
284 282 self.tags.add(*parents)
285 283
286 284 def get_reply_tree(self):
287 285 replies = self.get_replies().prefetch_related('refposts')
288 286 tree = []
289 287 for reply in replies:
290 288 parents = reply.refposts.all()
291 289
292 290 found_parent = False
293 291 searching_for_index = False
294 292
295 293 if len(parents) > 0:
296 294 index = 0
297 295 parent_depth = 0
298 296
299 297 indexes_to_insert = []
300 298
301 299 for depth, element in tree:
302 300 index += 1
303 301
304 302 # If this element is next after parent on the same level,
305 303 # insert child before it
306 304 if searching_for_index and depth <= parent_depth:
307 305 indexes_to_insert.append((index - 1, parent_depth))
308 306 searching_for_index = False
309 307
310 308 if element in parents:
311 309 found_parent = True
312 310 searching_for_index = True
313 311 parent_depth = depth
314 312
315 313 if not found_parent:
316 314 tree.append((0, reply))
317 315 else:
318 316 if searching_for_index:
319 317 tree.append((parent_depth + 1, reply))
320 318
321 319 offset = 0
322 320 for last_index, parent_depth in indexes_to_insert:
323 321 tree.insert(last_index + offset, (parent_depth + 1, reply))
324 322 offset += 1
325 323
326 324 return tree
@@ -1,331 +1,329
1 1 import json
2 2 import logging
3 3
4 4 from django.core import serializers
5 5 from django.db import transaction
6 6 from django.db.models import Q
7 7 from django.http import HttpResponse, HttpResponseBadRequest
8 8 from django.shortcuts import get_object_or_404
9 9 from django.views.decorators.csrf import csrf_protect
10 10
11 11 from boards.abstracts.settingsmanager import get_settings_manager
12 12 from boards.forms import PostForm, PlainErrorList, ThreadForm
13 13 from boards.mdx_neboard import Parser
14 14 from boards.models import Post, Thread, Tag, TagAlias
15 15 from boards.models.attachment import AttachmentSticker
16 16 from boards.models.thread import STATUS_ARCHIVE
17 17 from boards.models.user import Notification
18 18 from boards.utils import datetime_to_epoch
19 19
20 20 __author__ = 'neko259'
21 21
22 22 PARAMETER_TRUNCATED = 'truncated'
23 23 PARAMETER_TAG = 'tag'
24 24 PARAMETER_OFFSET = 'offset'
25 25 PARAMETER_DIFF_TYPE = 'type'
26 26 PARAMETER_POST = 'post'
27 27 PARAMETER_UPDATED = 'updated'
28 28 PARAMETER_LAST_UPDATE = 'last_update'
29 29 PARAMETER_THREAD = 'thread'
30 30 PARAMETER_UIDS = 'uids'
31 31 PARAMETER_SUBSCRIBED = 'subscribed'
32 32
33 33 DIFF_TYPE_HTML = 'html'
34 34 DIFF_TYPE_JSON = 'json'
35 35
36 36 STATUS_OK = 'ok'
37 37 STATUS_ERROR = 'error'
38 38
39 39 logger = logging.getLogger(__name__)
40 40
41 41
42 42 @transaction.atomic
43 43 def api_get_threaddiff(request):
44 44 """
45 45 Gets posts that were changed or added since time
46 46 """
47 47
48 48 thread_id = request.POST.get(PARAMETER_THREAD)
49 49 uids_str = request.POST.get(PARAMETER_UIDS)
50 50
51 51 if not thread_id or not uids_str:
52 52 return HttpResponse(content='Invalid request.')
53 53
54 54 uids = uids_str.strip().split(' ')
55 55
56 56 opening_post = get_object_or_404(Post, id=thread_id)
57 57 thread = opening_post.get_thread()
58 58
59 59 json_data = {
60 60 PARAMETER_UPDATED: [],
61 61 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
62 62 }
63 63 posts = Post.objects.filter(thread=thread).exclude(uid__in=uids)
64 64
65 65 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
66 66
67 67 for post in posts:
68 68 json_data[PARAMETER_UPDATED].append(post.get_post_data(
69 69 format_type=diff_type, request=request))
70 70 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
71 71
72 72 settings_manager = get_settings_manager(request)
73 73 json_data[PARAMETER_SUBSCRIBED] = str(settings_manager.thread_is_fav(opening_post))
74 74
75 75 # If the tag is favorite, update the counter
76 76 settings_manager = get_settings_manager(request)
77 77 favorite = settings_manager.thread_is_fav(opening_post)
78 78 if favorite:
79 79 settings_manager.add_or_read_fav_thread(opening_post)
80 80
81 81 return HttpResponse(content=json.dumps(json_data))
82 82
83 83
84 84 @csrf_protect
85 85 def api_add_post(request, opening_post_id=None):
86 86 """
87 87 Adds a post and return the JSON response for it
88 88 """
89 89
90 90 if opening_post_id:
91 91 opening_post = get_object_or_404(Post, id=opening_post_id)
92 92 else:
93 93 opening_post = None
94 94
95 95 status = STATUS_OK
96 96 errors = []
97 97
98 98 post = None
99 99 if request.method == 'POST':
100 100 if opening_post:
101 101 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
102 102 else:
103 103 form = ThreadForm(request.POST, request.FILES, error_class=PlainErrorList)
104 104
105 105 form.session = request.session
106 106
107 107 if form.need_to_ban:
108 108 # Ban user because he is suspected to be a bot
109 109 # _ban_current_user(request)
110 110 status = STATUS_ERROR
111 111 if form.is_valid():
112 112 post = Post.objects.create_from_form(request, form, opening_post,
113 113 html_response=False)
114 114 if not post:
115 115 status = STATUS_ERROR
116 116 else:
117 117 logger.info('Added post #%d via api.' % post.id)
118 118 else:
119 119 status = STATUS_ERROR
120 120 errors = form.as_json_errors()
121 121 else:
122 122 status = STATUS_ERROR
123 123
124 124 response = {
125 125 'status': status,
126 126 'errors': errors,
127 127 }
128 128
129 129 if post:
130 130 response['post_id'] = post.id
131 131 if not opening_post:
132 132 # FIXME For now we include URL only for threads to navigate to them.
133 133 # This needs to become something universal, just not yet sure how.
134 134 response['url'] = post.get_absolute_url()
135 135
136 136 return HttpResponse(content=json.dumps(response))
137 137
138 138
139 139 def get_post(request, post_id):
140 140 """
141 141 Gets the html of a post. Used for popups. Post can be truncated if used
142 142 in threads list with 'truncated' get parameter.
143 143 """
144 144
145 145 post = get_object_or_404(Post, id=post_id)
146 146 truncated = PARAMETER_TRUNCATED in request.GET
147 147
148 148 return HttpResponse(content=post.get_view(truncated=truncated, need_op_data=True))
149 149
150 150
151 151 def api_get_threads(request, count):
152 152 """
153 153 Gets the JSON thread opening posts list.
154 154 Parameters that can be used for filtering:
155 155 tag, offset (from which thread to get results)
156 156 """
157 157
158 158 if PARAMETER_TAG in request.GET:
159 159 tag_name = request.GET[PARAMETER_TAG]
160 160 if tag_name is not None:
161 161 tag = get_object_or_404(Tag, name=tag_name)
162 162 threads = tag.get_threads().exclude(status=STATUS_ARCHIVE)
163 163 else:
164 164 threads = Thread.objects.exclude(status=STATUS_ARCHIVE)
165 165
166 166 if PARAMETER_OFFSET in request.GET:
167 167 offset = request.GET[PARAMETER_OFFSET]
168 168 offset = int(offset) if offset is not None else 0
169 169 else:
170 170 offset = 0
171 171
172 172 threads = threads.order_by('-bump_time')
173 173 threads = threads[offset:offset + int(count)]
174 174
175 175 opening_posts = []
176 176 for thread in threads:
177 177 opening_post = thread.get_opening_post()
178 178
179 179 # TODO Add tags, replies and images count
180 180 post_data = opening_post.get_post_data(include_last_update=True)
181 181 post_data['status'] = thread.get_status()
182 182
183 183 opening_posts.append(post_data)
184 184
185 185 return HttpResponse(content=json.dumps(opening_posts))
186 186
187 187
188 188 # TODO Test this
189 189 def api_get_tags(request):
190 190 """
191 191 Gets all tags or user tags.
192 192 """
193 193
194 194 # TODO Get favorite tags for the given user ID
195 195
196 196 tags = TagAlias.objects.all()
197 197
198 198 term = request.GET.get('term')
199 199 if term is not None:
200 200 tags = tags.filter(name__contains=term)
201 201
202 202 tag_names = [tag.name for tag in tags]
203 203
204 204 return HttpResponse(content=json.dumps(tag_names))
205 205
206 206
207 207 def api_get_stickers(request):
208 208 term = request.GET.get('term')
209 209 if not term:
210 210 return HttpResponseBadRequest()
211 211
212 212 global_stickers = AttachmentSticker.objects.filter(Q(name__icontains=term) | Q(stickerpack__name__icontains=term))
213 213 local_stickers = [sticker for sticker in get_settings_manager(request).get_stickers() if term in sticker.name]
214 214 stickers = list(global_stickers) + local_stickers
215 215
216 216 image_dict = [{'thumb': sticker.attachment.get_thumb_url(),
217 217 'alias': str(sticker)}
218 218 for sticker in stickers]
219 219
220 220 return HttpResponse(content=json.dumps(image_dict))
221 221
222 222
223 223 # TODO The result can be cached by the thread last update time
224 224 # TODO Test this
225 225 def api_get_thread_posts(request, opening_post_id):
226 226 """
227 227 Gets the JSON array of thread posts
228 228 """
229 229
230 230 opening_post = get_object_or_404(Post, id=opening_post_id)
231 231 thread = opening_post.get_thread()
232 232 posts = thread.get_replies()
233 233
234 234 json_data = {
235 235 'posts': [],
236 236 'last_update': None,
237 237 }
238 238 json_post_list = []
239 239
240 240 for post in posts:
241 241 json_post_list.append(post.get_post_data())
242 242 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
243 243 json_data['posts'] = json_post_list
244 244
245 245 return HttpResponse(content=json.dumps(json_data))
246 246
247 247
248 248 def api_get_notifications(request, username):
249 249 last_notification_id_str = request.GET.get('last', None)
250 250 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
251 251
252 252 posts = Notification.objects.get_notification_posts(usernames=[username],
253 253 last=last_id)
254 254
255 255 json_post_list = []
256 256 for post in posts:
257 257 json_post_list.append(post.get_post_data())
258 258 return HttpResponse(content=json.dumps(json_post_list))
259 259
260 260
261 261 def api_get_post(request, post_id):
262 262 """
263 263 Gets the JSON of a post. This can be
264 264 used as and API for external clients.
265 265 """
266 266
267 267 post = get_object_or_404(Post, id=post_id)
268 268
269 269 json = serializers.serialize("json", [post], fields=(
270 270 "pub_time", "_text_rendered", "title", "text", "image",
271 271 "image_width", "image_height", "replies", "tags"
272 272 ))
273 273
274 274 return HttpResponse(content=json)
275 275
276 276
277 277 def api_get_preview(request):
278 278 raw_text = request.POST['raw_text']
279 279
280 280 parser = Parser()
281 281 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
282 282
283 283
284 284 def api_get_new_posts(request):
285 285 """
286 286 Gets favorite threads and unread posts count.
287 287 """
288 288 posts = list()
289 289
290 290 include_posts = 'include_posts' in request.GET
291 291
292 292 settings_manager = get_settings_manager(request)
293 fav_threads = settings_manager.get_fav_threads()
294 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
295 .order_by('-pub_time').prefetch_related('thread')
296 293
297 ops = [{'op': op, 'last_id': fav_threads[str(op.id)]} for op in fav_thread_ops]
294 last_posts = settings_manager.get_last_posts()
298 295 if include_posts:
299 new_post_threads = Thread.objects.get_new_posts(ops)
296 new_post_threads = Thread.objects.get_new_posts(last_posts)
300 297 if new_post_threads:
301 298 thread_ids = {thread.id: thread for thread in new_post_threads}
302 299 else:
303 300 thread_ids = dict()
304 301
305 for op in fav_thread_ops:
302 for post in last_posts:
306 303 fav_thread_dict = dict()
307 304
308 op_thread = op.get_thread()
309 if op_thread.id in thread_ids:
310 thread = thread_ids[op_thread.id]
305 thread = post.get_thread()
306 op = thread.get_opening_post()
307 if thread.id in thread_ids:
308 thread = thread_ids[thread.id]
311 309 new_post_count = thread.new_post_count
312 310 fav_thread_dict['newest_post_link'] = thread.get_replies()\
313 .filter(id__gt=fav_threads[str(op.id)])\
311 .filter(id__gt=post.id)\
314 312 .first().get_absolute_url(thread=thread)
315 313 else:
316 314 new_post_count = 0
317 315 fav_thread_dict['new_post_count'] = new_post_count
318 316
319 317 fav_thread_dict['id'] = op.id
320 318
321 319 fav_thread_dict['post_url'] = op.get_link_view()
322 320 fav_thread_dict['title'] = op.title
323 321
324 322 posts.append(fav_thread_dict)
325 323 else:
326 324 fav_thread_dict = dict()
327 325 fav_thread_dict['new_post_count'] = \
328 Thread.objects.get_new_post_count(ops)
326 Thread.objects.get_new_post_count(last_posts)
329 327 posts.append(fav_thread_dict)
330 328
331 329 return HttpResponse(content=json.dumps(posts))
@@ -1,132 +1,132
1 1 from django.urls import reverse
2 2 from django.shortcuts import render
3 3
4 4 from boards import settings
5 5 from boards.abstracts.constants import PARAM_PAGE
6 6 from boards.abstracts.paginator import get_paginator
7 7 from boards.abstracts.settingsmanager import get_settings_manager
8 8 from boards.models import Post
9 9 from boards.settings import SECTION_VIEW
10 10 from boards.views.base import BaseBoardView
11 11 from boards.views.mixins import PaginatedMixin
12 12
13 13 POSTS_PER_PAGE = settings.get_int(SECTION_VIEW, 'PostsPerPage')
14 14
15 15 PARAMETER_POSTS = 'posts'
16 16 PARAMETER_QUERIES = 'queries'
17 17
18 18 TEMPLATE = 'boards/feed.html'
19 19 DEFAULT_PAGE = 1
20 20
21 21
22 22 class FeedFilter:
23 23 @staticmethod
24 24 def get_filtered_posts(request, posts):
25 25 return posts
26 26
27 27 @staticmethod
28 28 def get_query(request):
29 29 return None
30 30
31 31
32 32 class TripcodeFilter(FeedFilter):
33 33 @staticmethod
34 34 def get_filtered_posts(request, posts):
35 35 filtered_posts = posts
36 36 tripcode = request.GET.get('tripcode', None)
37 37 if tripcode:
38 38 filtered_posts = filtered_posts.filter(tripcode=tripcode)
39 39 return filtered_posts
40 40
41 41 @staticmethod
42 42 def get_query(request):
43 43 tripcode = request.GET.get('tripcode', None)
44 44 if tripcode:
45 45 return 'Tripcode: {}'.format(tripcode)
46 46
47 47
48 48 class FavoritesFilter(FeedFilter):
49 49 @staticmethod
50 50 def get_filtered_posts(request, posts):
51 51 filtered_posts = posts
52 52
53 53 favorites = 'favorites' in request.GET
54 54 if favorites:
55 55 settings_manager = get_settings_manager(request)
56 fav_thread_ops = Post.objects.filter(id__in=settings_manager.get_fav_threads().keys())
57 fav_threads = [op.get_thread() for op in fav_thread_ops]
58 filtered_posts = filtered_posts.filter(thread__in=fav_threads)
56 last_posts = settings_manager.get_last_posts()
57 threads = [post.get_thread() for post in last_posts]
58 filtered_posts = filtered_posts.filter(thread__in=threads)
59 59 return filtered_posts
60 60
61 61
62 62 class IpFilter(FeedFilter):
63 63 @staticmethod
64 64 def get_filtered_posts(request, posts):
65 65 filtered_posts = posts
66 66
67 67 ip = request.GET.get('ip', None)
68 68 if ip and request.user.has_perm('post_delete'):
69 69 filtered_posts = filtered_posts.filter(poster_ip=ip)
70 70 return filtered_posts
71 71
72 72 @staticmethod
73 73 def get_query(request):
74 74 ip = request.GET.get('ip', None)
75 75 if ip:
76 76 return 'IP: {}'.format(ip)
77 77
78 78
79 79 class ImageFilter(FeedFilter):
80 80 @staticmethod
81 81 def get_filtered_posts(request, posts):
82 82 filtered_posts = posts
83 83
84 84 image = request.GET.get('image', None)
85 85 if image:
86 86 filtered_posts = filtered_posts.filter(attachments__file=image)
87 87 return filtered_posts
88 88
89 89 @staticmethod
90 90 def get_query(request):
91 91 image = request.GET.get('image', None)
92 92 if image:
93 93 return 'File: {}'.format(image)
94 94
95 95
96 96 class FeedView(PaginatedMixin, BaseBoardView):
97 97 filters = (
98 98 TripcodeFilter,
99 99 FavoritesFilter,
100 100 IpFilter,
101 101 ImageFilter,
102 102 )
103 103
104 104 def get(self, request):
105 105 page = request.GET.get(PARAM_PAGE, DEFAULT_PAGE)
106 106
107 107 params = self.get_context_data(request=request)
108 108
109 109 settings_manager = get_settings_manager(request)
110 110
111 111 posts = Post.objects.exclude(
112 112 thread__tags__in=settings_manager.get_hidden_tags()).order_by(
113 113 '-pub_time').prefetch_related('attachments', 'thread')
114 114 queries = []
115 115 for filter in self.filters:
116 116 posts = filter.get_filtered_posts(request, posts)
117 117 query = filter.get_query(request)
118 118 if query:
119 119 queries.append(query)
120 120 params[PARAMETER_QUERIES] = queries
121 121
122 122 paginator = get_paginator(posts, POSTS_PER_PAGE)
123 123 paginator.current_page = int(page)
124 124
125 125 params[PARAMETER_POSTS] = paginator.page(page).object_list
126 126
127 127 paginator.set_url(reverse('feed'), request.GET.dict())
128 128
129 129 params.update(self.get_page_context(paginator, page))
130 130
131 131 return render(request, TEMPLATE, params)
132 132
General Comments 0
You need to be logged in to leave comments. Login now