##// END OF EJS Templates
Don't check new posts in the archived threads
neko259 -
r1344:efdda723 default
parent child Browse files
Show More
@@ -1,166 +1,173 b''
1 1 from boards.models import Tag
2 2
3 3 MAX_TRIPCODE_COLLISIONS = 50
4 4
5 5 __author__ = 'neko259'
6 6
7 7 SESSION_SETTING = 'setting'
8 8
9 9 # Remove this, it is not used any more cause there is a user's permission
10 10 PERMISSION_MODERATE = 'moderator'
11 11
12 12 SETTING_THEME = 'theme'
13 13 SETTING_FAVORITE_TAGS = 'favorite_tags'
14 14 SETTING_FAVORITE_THREADS = 'favorite_threads'
15 15 SETTING_HIDDEN_TAGS = 'hidden_tags'
16 16 SETTING_PERMISSIONS = 'permissions'
17 17 SETTING_USERNAME = 'username'
18 18 SETTING_LAST_NOTIFICATION_ID = 'last_notification'
19 19 SETTING_IMAGE_VIEWER = 'image_viewer'
20 20 SETTING_TRIPCODE = 'tripcode'
21 21
22 FAV_THREAD_NO_UPDATES = -1
23
22 24 DEFAULT_THEME = 'md'
23 25
24 26
25 27 class SettingsManager:
26 28 """
27 29 Base settings manager class. get_setting and set_setting methods should
28 30 be overriden.
29 31 """
30 32 def __init__(self):
31 33 pass
32 34
33 35 def get_theme(self) -> str:
34 36 theme = self.get_setting(SETTING_THEME)
35 37 if not theme:
36 38 theme = DEFAULT_THEME
37 39 self.set_setting(SETTING_THEME, theme)
38 40
39 41 return theme
40 42
41 43 def set_theme(self, theme):
42 44 self.set_setting(SETTING_THEME, theme)
43 45
44 46 def has_permission(self, permission):
45 47 permissions = self.get_setting(SETTING_PERMISSIONS)
46 48 if permissions:
47 49 return permission in permissions
48 50 else:
49 51 return False
50 52
51 53 def get_setting(self, setting, default=None):
52 54 pass
53 55
54 56 def set_setting(self, setting, value):
55 57 pass
56 58
57 59 def add_permission(self, permission):
58 60 permissions = self.get_setting(SETTING_PERMISSIONS)
59 61 if not permissions:
60 62 permissions = [permission]
61 63 else:
62 64 permissions.append(permission)
63 65 self.set_setting(SETTING_PERMISSIONS, permissions)
64 66
65 67 def del_permission(self, permission):
66 68 permissions = self.get_setting(SETTING_PERMISSIONS)
67 69 if not permissions:
68 70 permissions = []
69 71 else:
70 72 permissions.remove(permission)
71 73 self.set_setting(SETTING_PERMISSIONS, permissions)
72 74
73 75 def get_fav_tags(self) -> list:
74 76 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
75 77 tags = []
76 78 if tag_names:
77 79 tags = list(Tag.objects.filter(name__in=tag_names))
78 80 return tags
79 81
80 82 def add_fav_tag(self, tag):
81 83 tags = self.get_setting(SETTING_FAVORITE_TAGS)
82 84 if not tags:
83 85 tags = [tag.name]
84 86 else:
85 87 if not tag.name in tags:
86 88 tags.append(tag.name)
87 89
88 90 tags.sort()
89 91 self.set_setting(SETTING_FAVORITE_TAGS, tags)
90 92
91 93 def del_fav_tag(self, tag):
92 94 tags = self.get_setting(SETTING_FAVORITE_TAGS)
93 95 if tag.name in tags:
94 96 tags.remove(tag.name)
95 97 self.set_setting(SETTING_FAVORITE_TAGS, tags)
96 98
97 99 def get_hidden_tags(self) -> list:
98 100 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
99 101 tags = []
100 102 if tag_names:
101 103 tags = list(Tag.objects.filter(name__in=tag_names))
102 104
103 105 return tags
104 106
105 107 def add_hidden_tag(self, tag):
106 108 tags = self.get_setting(SETTING_HIDDEN_TAGS)
107 109 if not tags:
108 110 tags = [tag.name]
109 111 else:
110 112 if not tag.name in tags:
111 113 tags.append(tag.name)
112 114
113 115 tags.sort()
114 116 self.set_setting(SETTING_HIDDEN_TAGS, tags)
115 117
116 118 def del_hidden_tag(self, tag):
117 119 tags = self.get_setting(SETTING_HIDDEN_TAGS)
118 120 if tag.name in tags:
119 121 tags.remove(tag.name)
120 122 self.set_setting(SETTING_HIDDEN_TAGS, tags)
121 123
122 124 def get_fav_threads(self) -> dict:
123 125 return self.get_setting(SETTING_FAVORITE_THREADS, default=dict())
124 126
125 127 def add_or_read_fav_thread(self, opening_post):
126 128 threads = self.get_fav_threads()
127 threads[str(opening_post.id)] = opening_post.get_thread().get_replies()\
128 .last().id
129 thread = opening_post.get_thread()
130 # Don't check for new posts if the thread is archived already
131 if thread.is_archived():
132 last_id = FAV_THREAD_NO_UPDATES
133 else:
134 last_id = thread.get_replies().last().id
135 threads[str(opening_post.id)] = last_id
129 136 self.set_setting(SETTING_FAVORITE_THREADS, threads)
130 137
131 138 def del_fav_thread(self, opening_post):
132 139 threads = self.get_fav_threads()
133 140 if self.thread_is_fav(opening_post):
134 141 del threads[str(opening_post.id)]
135 142 self.set_setting(SETTING_FAVORITE_THREADS, threads)
136 143
137 144 def thread_is_fav(self, opening_post):
138 145 return str(opening_post.id) in self.get_fav_threads()
139 146
140 147 class SessionSettingsManager(SettingsManager):
141 148 """
142 149 Session-based settings manager. All settings are saved to the user's
143 150 session.
144 151 """
145 152 def __init__(self, session):
146 153 SettingsManager.__init__(self)
147 154 self.session = session
148 155
149 156 def get_setting(self, setting, default=None):
150 157 if setting in self.session:
151 158 return self.session[setting]
152 159 else:
153 160 self.set_setting(setting, default)
154 161 return default
155 162
156 163 def set_setting(self, setting, value):
157 164 self.session[setting] = value
158 165
159 166
160 167 def get_settings_manager(request) -> SettingsManager:
161 168 """
162 169 Get settings manager based on the request object. Currently only
163 170 session-based manager is supported. In the future, cookie-based or
164 171 database-based managers could be implemented.
165 172 """
166 173 return SessionSettingsManager(request.session)
@@ -1,235 +1,238 b''
1 1 import logging
2 2 from adjacent import Client
3 3
4 4 from django.db.models import Count, Sum, QuerySet
5 5 from django.utils import timezone
6 6 from django.db import models
7 7
8 8 from boards import settings
9 9 import boards
10 10 from boards.utils import cached_result, datetime_to_epoch
11 11 from boards.models.post import Post
12 12 from boards.models.tag import Tag
13 13
14 14
15 15 __author__ = 'neko259'
16 16
17 17
18 18 logger = logging.getLogger(__name__)
19 19
20 20
21 21 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
22 22 WS_NOTIFICATION_TYPE = 'notification_type'
23 23
24 24 WS_CHANNEL_THREAD = "thread:"
25 25
26 26
27 27 class ThreadManager(models.Manager):
28 28 def process_oldest_threads(self):
29 29 """
30 30 Preserves maximum thread count. If there are too many threads,
31 31 archive or delete the old ones.
32 32 """
33 33
34 34 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
35 35 thread_count = threads.count()
36 36
37 37 max_thread_count = settings.get_int('Messages', 'MaxThreadCount')
38 38 if thread_count > max_thread_count:
39 39 num_threads_to_delete = thread_count - max_thread_count
40 40 old_threads = threads[thread_count - num_threads_to_delete:]
41 41
42 42 for thread in old_threads:
43 43 if settings.get_bool('Storage', 'ArchiveThreads'):
44 44 self._archive_thread(thread)
45 45 else:
46 46 thread.delete()
47 47
48 48 logger.info('Processed %d old threads' % num_threads_to_delete)
49 49
50 50 def _archive_thread(self, thread):
51 51 thread.archived = True
52 52 thread.bumpable = False
53 53 thread.last_edit_time = timezone.now()
54 54 thread.update_posts_time()
55 55 thread.save(update_fields=['archived', 'last_edit_time', 'bumpable'])
56 56
57 57
58 58 def get_thread_max_posts():
59 59 return settings.get_int('Messages', 'MaxPostsPerThread')
60 60
61 61
62 62 class Thread(models.Model):
63 63 objects = ThreadManager()
64 64
65 65 class Meta:
66 66 app_label = 'boards'
67 67
68 68 tags = models.ManyToManyField('Tag', related_name='thread_tags')
69 69 bump_time = models.DateTimeField(db_index=True)
70 70 last_edit_time = models.DateTimeField()
71 71 archived = models.BooleanField(default=False)
72 72 bumpable = models.BooleanField(default=True)
73 73 max_posts = models.IntegerField(default=get_thread_max_posts)
74 74
75 75 def get_tags(self) -> QuerySet:
76 76 """
77 77 Gets a sorted tag list.
78 78 """
79 79
80 80 return self.tags.order_by('name')
81 81
82 82 def bump(self):
83 83 """
84 84 Bumps (moves to up) thread if possible.
85 85 """
86 86
87 87 if self.can_bump():
88 88 self.bump_time = self.last_edit_time
89 89
90 90 self.update_bump_status()
91 91
92 92 logger.info('Bumped thread %d' % self.id)
93 93
94 94 def has_post_limit(self) -> bool:
95 95 return self.max_posts > 0
96 96
97 97 def update_bump_status(self, exclude_posts=None):
98 98 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
99 99 self.bumpable = False
100 100 self.update_posts_time(exclude_posts=exclude_posts)
101 101
102 102 def _get_cache_key(self):
103 103 return [datetime_to_epoch(self.last_edit_time)]
104 104
105 105 @cached_result(key_method=_get_cache_key)
106 106 def get_reply_count(self) -> int:
107 107 return self.get_replies().count()
108 108
109 109 @cached_result(key_method=_get_cache_key)
110 110 def get_images_count(self) -> int:
111 111 return self.get_replies().annotate(images_count=Count(
112 112 'images')).aggregate(Sum('images_count'))['images_count__sum']
113 113
114 114 def can_bump(self) -> bool:
115 115 """
116 116 Checks if the thread can be bumped by replying to it.
117 117 """
118 118
119 return self.bumpable and not self.archived
119 return self.bumpable and not self.is_archived()
120 120
121 121 def get_last_replies(self) -> QuerySet:
122 122 """
123 123 Gets several last replies, not including opening post
124 124 """
125 125
126 126 last_replies_count = settings.get_int('View', 'LastRepliesCount')
127 127
128 128 if last_replies_count > 0:
129 129 reply_count = self.get_reply_count()
130 130
131 131 if reply_count > 0:
132 132 reply_count_to_show = min(last_replies_count,
133 133 reply_count - 1)
134 134 replies = self.get_replies()
135 135 last_replies = replies[reply_count - reply_count_to_show:]
136 136
137 137 return last_replies
138 138
139 139 def get_skipped_replies_count(self) -> int:
140 140 """
141 141 Gets number of posts between opening post and last replies.
142 142 """
143 143 reply_count = self.get_reply_count()
144 144 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
145 145 reply_count - 1)
146 146 return reply_count - last_replies_count - 1
147 147
148 148 def get_replies(self, view_fields_only=False) -> QuerySet:
149 149 """
150 150 Gets sorted thread posts
151 151 """
152 152
153 153 query = Post.objects.filter(threads__in=[self])
154 154 query = query.order_by('pub_time').prefetch_related(
155 155 'images', 'thread', 'threads', 'attachments')
156 156 if view_fields_only:
157 157 query = query.defer('poster_ip')
158 158 return query.all()
159 159
160 160 def get_top_level_replies(self) -> QuerySet:
161 161 return self.get_replies().exclude(refposts__threads__in=[self])
162 162
163 163 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
164 164 """
165 165 Gets replies that have at least one image attached
166 166 """
167 167
168 168 return self.get_replies(view_fields_only).annotate(images_count=Count(
169 169 'images')).filter(images_count__gt=0)
170 170
171 171 def get_opening_post(self, only_id=False) -> Post:
172 172 """
173 173 Gets the first post of the thread
174 174 """
175 175
176 176 query = self.get_replies().order_by('pub_time')
177 177 if only_id:
178 178 query = query.only('id')
179 179 opening_post = query.first()
180 180
181 181 return opening_post
182 182
183 183 @cached_result()
184 184 def get_opening_post_id(self) -> int:
185 185 """
186 186 Gets ID of the first thread post.
187 187 """
188 188
189 189 return self.get_opening_post(only_id=True).id
190 190
191 191 def get_pub_time(self):
192 192 """
193 193 Gets opening post's pub time because thread does not have its own one.
194 194 """
195 195
196 196 return self.get_opening_post().pub_time
197 197
198 198 def __str__(self):
199 199 return 'T#{}/{}'.format(self.id, self.get_opening_post_id())
200 200
201 201 def get_tag_url_list(self) -> list:
202 202 return boards.models.Tag.objects.get_tag_url_list(self.get_tags())
203 203
204 204 def update_posts_time(self, exclude_posts=None):
205 205 last_edit_time = self.last_edit_time
206 206
207 207 for post in self.post_set.all():
208 208 if exclude_posts is None or post not in exclude_posts:
209 209 # Manual update is required because uids are generated on save
210 210 post.last_edit_time = last_edit_time
211 211 post.save(update_fields=['last_edit_time'])
212 212
213 213 post.get_threads().update(last_edit_time=last_edit_time)
214 214
215 215 def notify_clients(self):
216 216 if not settings.get_bool('External', 'WebsocketsEnabled'):
217 217 return
218 218
219 219 client = Client()
220 220
221 221 channel_name = WS_CHANNEL_THREAD + str(self.get_opening_post_id())
222 222 client.publish(channel_name, {
223 223 WS_NOTIFICATION_TYPE: WS_NOTIFICATION_TYPE_NEW_POST,
224 224 })
225 225 client.send()
226 226
227 227 def get_absolute_url(self):
228 228 return self.get_opening_post().get_absolute_url()
229 229
230 230 def get_required_tags(self):
231 231 return self.get_tags().filter(required=True)
232 232
233 233 def get_replies_newer(self, post_id):
234 234 return self.get_replies().filter(id__gt=post_id)
235 235
236 def is_archived(self):
237 return self.archived
238
@@ -1,274 +1,280 b''
1 1 from collections import OrderedDict
2 2 import json
3 3 import logging
4 4
5 5 from django.db import transaction
6 6 from django.http import HttpResponse
7 7 from django.shortcuts import get_object_or_404
8 8 from django.core import serializers
9 from boards.abstracts.settingsmanager import get_settings_manager
9 from boards.abstracts.settingsmanager import get_settings_manager,\
10 FAV_THREAD_NO_UPDATES
10 11
11 12 from boards.forms import PostForm, PlainErrorList
12 13 from boards.models import Post, Thread, Tag
13 14 from boards.utils import datetime_to_epoch
14 15 from boards.views.thread import ThreadView
15 16 from boards.models.user import Notification
16 17 from boards.mdx_neboard import Parser
17 18
18 19
19 20 __author__ = 'neko259'
20 21
21 22 PARAMETER_TRUNCATED = 'truncated'
22 23 PARAMETER_TAG = 'tag'
23 24 PARAMETER_OFFSET = 'offset'
24 25 PARAMETER_DIFF_TYPE = 'type'
25 26 PARAMETER_POST = 'post'
26 27 PARAMETER_UPDATED = 'updated'
27 28 PARAMETER_LAST_UPDATE = 'last_update'
28 29 PARAMETER_THREAD = 'thread'
29 30 PARAMETER_UIDS = 'uids'
30 31
31 32 DIFF_TYPE_HTML = 'html'
32 33 DIFF_TYPE_JSON = 'json'
33 34
34 35 STATUS_OK = 'ok'
35 36 STATUS_ERROR = 'error'
36 37
37 38 logger = logging.getLogger(__name__)
38 39
39 40
40 41 @transaction.atomic
41 42 def api_get_threaddiff(request):
42 43 """
43 44 Gets posts that were changed or added since time
44 45 """
45 46
46 47 thread_id = request.POST.get(PARAMETER_THREAD)
47 48 uids_str = request.POST.get(PARAMETER_UIDS).strip()
48 49 uids = uids_str.split(' ')
49 50
50 51 opening_post = get_object_or_404(Post, id=thread_id)
51 52 thread = opening_post.get_thread()
52 53
53 54 json_data = {
54 55 PARAMETER_UPDATED: [],
55 56 PARAMETER_LAST_UPDATE: None, # TODO Maybe this can be removed already?
56 57 }
57 58 posts = Post.objects.filter(threads__in=[thread]).exclude(uid__in=uids)
58 59
59 60 diff_type = request.GET.get(PARAMETER_DIFF_TYPE, DIFF_TYPE_HTML)
60 61
61 62 for post in posts:
62 63 json_data[PARAMETER_UPDATED].append(post.get_post_data(
63 64 format_type=diff_type, request=request))
64 65 json_data[PARAMETER_LAST_UPDATE] = str(thread.last_edit_time)
65 66
66 67 # If the tag is favorite, update the counter
67 68 settings_manager = get_settings_manager(request)
68 69 favorite = settings_manager.thread_is_fav(opening_post)
69 70 if favorite:
70 71 settings_manager.add_or_read_fav_thread(opening_post)
71 72
72 73 return HttpResponse(content=json.dumps(json_data))
73 74
74 75
75 76 def api_add_post(request, opening_post_id):
76 77 """
77 78 Adds a post and return the JSON response for it
78 79 """
79 80
80 81 opening_post = get_object_or_404(Post, id=opening_post_id)
81 82
82 83 logger.info('Adding post via api...')
83 84
84 85 status = STATUS_OK
85 86 errors = []
86 87
87 88 if request.method == 'POST':
88 89 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
89 90 form.session = request.session
90 91
91 92 if form.need_to_ban:
92 93 # Ban user because he is suspected to be a bot
93 94 # _ban_current_user(request)
94 95 status = STATUS_ERROR
95 96 if form.is_valid():
96 97 post = ThreadView().new_post(request, form, opening_post,
97 98 html_response=False)
98 99 if not post:
99 100 status = STATUS_ERROR
100 101 else:
101 102 logger.info('Added post #%d via api.' % post.id)
102 103 else:
103 104 status = STATUS_ERROR
104 105 errors = form.as_json_errors()
105 106
106 107 response = {
107 108 'status': status,
108 109 'errors': errors,
109 110 }
110 111
111 112 return HttpResponse(content=json.dumps(response))
112 113
113 114
114 115 def get_post(request, post_id):
115 116 """
116 117 Gets the html of a post. Used for popups. Post can be truncated if used
117 118 in threads list with 'truncated' get parameter.
118 119 """
119 120
120 121 post = get_object_or_404(Post, id=post_id)
121 122 truncated = PARAMETER_TRUNCATED in request.GET
122 123
123 124 return HttpResponse(content=post.get_view(truncated=truncated))
124 125
125 126
126 127 def api_get_threads(request, count):
127 128 """
128 129 Gets the JSON thread opening posts list.
129 130 Parameters that can be used for filtering:
130 131 tag, offset (from which thread to get results)
131 132 """
132 133
133 134 if PARAMETER_TAG in request.GET:
134 135 tag_name = request.GET[PARAMETER_TAG]
135 136 if tag_name is not None:
136 137 tag = get_object_or_404(Tag, name=tag_name)
137 138 threads = tag.get_threads().filter(archived=False)
138 139 else:
139 140 threads = Thread.objects.filter(archived=False)
140 141
141 142 if PARAMETER_OFFSET in request.GET:
142 143 offset = request.GET[PARAMETER_OFFSET]
143 144 offset = int(offset) if offset is not None else 0
144 145 else:
145 146 offset = 0
146 147
147 148 threads = threads.order_by('-bump_time')
148 149 threads = threads[offset:offset + int(count)]
149 150
150 151 opening_posts = []
151 152 for thread in threads:
152 153 opening_post = thread.get_opening_post()
153 154
154 155 # TODO Add tags, replies and images count
155 156 post_data = opening_post.get_post_data(include_last_update=True)
156 157 post_data['bumpable'] = thread.can_bump()
157 158 post_data['archived'] = thread.archived
158 159
159 160 opening_posts.append(post_data)
160 161
161 162 return HttpResponse(content=json.dumps(opening_posts))
162 163
163 164
164 165 # TODO Test this
165 166 def api_get_tags(request):
166 167 """
167 168 Gets all tags or user tags.
168 169 """
169 170
170 171 # TODO Get favorite tags for the given user ID
171 172
172 173 tags = Tag.objects.get_not_empty_tags()
173 174
174 175 term = request.GET.get('term')
175 176 if term is not None:
176 177 tags = tags.filter(name__contains=term)
177 178
178 179 tag_names = [tag.name for tag in tags]
179 180
180 181 return HttpResponse(content=json.dumps(tag_names))
181 182
182 183
183 184 # TODO The result can be cached by the thread last update time
184 185 # TODO Test this
185 186 def api_get_thread_posts(request, opening_post_id):
186 187 """
187 188 Gets the JSON array of thread posts
188 189 """
189 190
190 191 opening_post = get_object_or_404(Post, id=opening_post_id)
191 192 thread = opening_post.get_thread()
192 193 posts = thread.get_replies()
193 194
194 195 json_data = {
195 196 'posts': [],
196 197 'last_update': None,
197 198 }
198 199 json_post_list = []
199 200
200 201 for post in posts:
201 202 json_post_list.append(post.get_post_data())
202 203 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
203 204 json_data['posts'] = json_post_list
204 205
205 206 return HttpResponse(content=json.dumps(json_data))
206 207
207 208
208 209 def api_get_notifications(request, username):
209 210 last_notification_id_str = request.GET.get('last', None)
210 211 last_id = int(last_notification_id_str) if last_notification_id_str is not None else None
211 212
212 213 posts = Notification.objects.get_notification_posts(username=username,
213 214 last=last_id)
214 215
215 216 json_post_list = []
216 217 for post in posts:
217 218 json_post_list.append(post.get_post_data())
218 219 return HttpResponse(content=json.dumps(json_post_list))
219 220
220 221
221 222 def api_get_post(request, post_id):
222 223 """
223 224 Gets the JSON of a post. This can be
224 225 used as and API for external clients.
225 226 """
226 227
227 228 post = get_object_or_404(Post, id=post_id)
228 229
229 230 json = serializers.serialize("json", [post], fields=(
230 231 "pub_time", "_text_rendered", "title", "text", "image",
231 232 "image_width", "image_height", "replies", "tags"
232 233 ))
233 234
234 235 return HttpResponse(content=json)
235 236
236 237
237 238 def api_get_preview(request):
238 239 raw_text = request.POST['raw_text']
239 240
240 241 parser = Parser()
241 242 return HttpResponse(content=parser.parse(parser.preparse(raw_text)))
242 243
243 244
244 245 def api_get_new_posts(request):
245 246 """
246 247 Gets favorite threads and unread posts count.
247 248 """
248 249 posts = list()
249 250
250 251 include_posts = 'include_posts' in request.GET
251 252
252 253 settings_manager = get_settings_manager(request)
253 254 fav_threads = settings_manager.get_fav_threads()
254 255 fav_thread_ops = Post.objects.filter(id__in=fav_threads.keys())\
255 256 .order_by('-pub_time').prefetch_related('thread')
256 257
257 258 for op in fav_thread_ops:
258 259 last_read_post = fav_threads[str(op.id)]
260
261 if last_read_post == FAV_THREAD_NO_UPDATES:
262 new_post_count = 0
263 else:
259 264 new_posts = op.get_thread().get_replies_newer(last_read_post)
260 265 new_post_count = new_posts.count()
266
261 267 fav_thread_dict = dict()
262 268 fav_thread_dict['id'] = op.id
263 269 fav_thread_dict['new_post_count'] = new_post_count
264 270
265 271 if include_posts:
266 272 fav_thread_dict['post_url'] = op.get_link_view()
267 273 fav_thread_dict['title'] = op.title
268 274 if new_post_count > 0:
269 275 fav_thread_dict['newest_post_link'] = new_posts.first()\
270 276 .get_absolute_url()
271 277
272 278 posts.append(fav_thread_dict)
273 279
274 280 return HttpResponse(content=json.dumps(posts))
General Comments 0
You need to be logged in to leave comments. Login now