##// END OF EJS Templates
Fixed RSS
neko259 -
r402:f1273cae default
parent child Browse files
Show More
@@ -1,292 +1,295 b''
1 1 import os
2 2 from random import random
3 3 import time
4 4 import math
5 5 import re
6 6
7 7 from django.db import models
8 8 from django.http import Http404
9 9 from django.utils import timezone
10 10 from markupfield.fields import MarkupField
11 11
12 12 from neboard import settings
13 13 from boards import settings as boards_settings
14 14 from boards import thumbs
15 15
16 16 BAN_REASON_AUTO = 'Auto'
17 17
18 18 IMAGE_THUMB_SIZE = (200, 150)
19 19
20 20 TITLE_MAX_LENGTH = 50
21 21
22 22 DEFAULT_MARKUP_TYPE = 'markdown'
23 23
24 24 NO_PARENT = -1
25 25 NO_IP = '0.0.0.0'
26 26 UNKNOWN_UA = ''
27 27 ALL_PAGES = -1
28 28 IMAGES_DIRECTORY = 'images/'
29 29 FILE_EXTENSION_DELIMITER = '.'
30 30
31 31 SETTING_MODERATE = "moderate"
32 32
33 33 REGEX_REPLY = re.compile('>>(\d+)')
34 34
35 35
36 36 class PostManager(models.Manager):
37 37
38 38 def create_post(self, title, text, image=None, thread=None,
39 39 ip=NO_IP, tags=None, user=None):
40 40 posting_time = timezone.now()
41 41 if not thread:
42 42 thread = Thread.objects.create(bump_time=posting_time,
43 43 last_edit_time=posting_time)
44 44 else:
45 45 thread.bump()
46 46 thread.last_edit_time = posting_time
47 47 thread.save()
48 48
49 49 post = self.create(title=title,
50 50 text=text,
51 51 pub_time=posting_time,
52 52 thread_new=thread,
53 53 image=image,
54 54 poster_ip=ip,
55 55 poster_user_agent=UNKNOWN_UA,
56 56 last_edit_time=posting_time,
57 57 user=user)
58 58
59 59 thread.replies.add(post)
60 60 if tags:
61 61 linked_tags = []
62 62 for tag in tags:
63 63 tag_linked_tags = tag.get_linked_tags()
64 64 if len(tag_linked_tags) > 0:
65 65 linked_tags.extend(tag_linked_tags)
66 66
67 67 tags.extend(linked_tags)
68 68 map(thread.add_tag, tags)
69 69
70 70 self._delete_old_threads()
71 71 self.connect_replies(post)
72 72
73 73 return post
74 74
75 75 def delete_post(self, post):
76 76 thread = post.thread_new
77 77 thread.last_edit_time = timezone.now()
78 78 thread.save()
79 79
80 80 post.delete()
81 81
82 82 def delete_posts_by_ip(self, ip):
83 83 posts = self.filter(poster_ip=ip)
84 84 map(self.delete_post, posts)
85 85
86 86 # TODO Move this method to thread manager
87 87 def get_threads(self, tag=None, page=ALL_PAGES,
88 88 order_by='-bump_time'):
89 89 if tag:
90 90 threads = tag.threads
91 91
92 92 if not threads.exists():
93 93 raise Http404
94 94 else:
95 95 threads = Thread.objects.all()
96 96
97 97 threads = threads.order_by(order_by)
98 98
99 99 if page != ALL_PAGES:
100 100 thread_count = threads.count()
101 101
102 102 if page < self._get_page_count(thread_count):
103 103 start_thread = page * settings.THREADS_PER_PAGE
104 104 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
105 105 thread_count)
106 106 threads = threads[start_thread:end_thread]
107 107
108 108 return threads
109 109
110 110 # TODO Move this method to thread manager
111 111 def get_thread_page_count(self, tag=None):
112 112 if tag:
113 113 threads = Thread.objects.filter(tags=tag)
114 114 else:
115 115 threads = Thread.objects.all()
116 116
117 117 return self._get_page_count(threads.count())
118 118
119 119 # TODO Move this method to thread manager
120 120 def _delete_old_threads(self):
121 121 """
122 122 Preserves maximum thread count. If there are too many threads,
123 123 delete the old ones.
124 124 """
125 125
126 126 # TODO Move old threads to the archive instead of deleting them.
127 127 # Maybe make some 'old' field in the model to indicate the thread
128 128 # must not be shown and be able for replying.
129 129
130 130 threads = Thread.objects.all()
131 131 thread_count = threads.count()
132 132
133 133 if thread_count > settings.MAX_THREAD_COUNT:
134 134 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
135 135 old_threads = threads[thread_count - num_threads_to_delete:]
136 136
137 137 map(Thread.delete_with_posts, old_threads)
138 138
139 139 def connect_replies(self, post):
140 140 """Connect replies to a post to show them as a refmap"""
141 141
142 142 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
143 143 post_id = reply_number.group(1)
144 144 ref_post = self.filter(id=post_id)
145 145 if ref_post.count() > 0:
146 146 referenced_post = ref_post[0]
147 147 referenced_post.referenced_posts.add(post)
148 148 referenced_post.last_edit_time = post.pub_time
149 149 referenced_post.save()
150 150
151 151 def _get_page_count(self, thread_count):
152 152 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
153 153
154 154
155 155 class Post(models.Model):
156 156 """A post is a message."""
157 157
158 158 objects = PostManager()
159 159
160 160 class Meta:
161 161 app_label = 'boards'
162 162
163 163 def _update_image_filename(self, filename):
164 164 """Get unique image filename"""
165 165
166 166 path = IMAGES_DIRECTORY
167 167 new_name = str(int(time.mktime(time.gmtime())))
168 168 new_name += str(int(random() * 1000))
169 169 new_name += FILE_EXTENSION_DELIMITER
170 170 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
171 171
172 172 return os.path.join(path, new_name)
173 173
174 174 title = models.CharField(max_length=TITLE_MAX_LENGTH)
175 175 pub_time = models.DateTimeField()
176 176 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
177 177 escape_html=False)
178 178
179 179 image_width = models.IntegerField(default=0)
180 180 image_height = models.IntegerField(default=0)
181 181
182 182 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
183 183 blank=True, sizes=(IMAGE_THUMB_SIZE,),
184 184 width_field='image_width',
185 185 height_field='image_height')
186 186
187 187 poster_ip = models.GenericIPAddressField()
188 188 poster_user_agent = models.TextField()
189 189
190 190 thread = models.ForeignKey('Post', null=True, default=None)
191 191 thread_new = models.ForeignKey('Thread', null=True, default=None)
192 192 last_edit_time = models.DateTimeField()
193 193 user = models.ForeignKey('User', null=True, default=None)
194 194
195 195 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
196 196 null=True,
197 197 blank=True, related_name='rfp+')
198 198
199 199 def __unicode__(self):
200 200 return '#' + str(self.id) + ' ' + self.title + ' (' + \
201 201 self.text.raw[:50] + ')'
202 202
203 203 def get_title(self):
204 204 title = self.title
205 205 if len(title) == 0:
206 206 title = self.text.raw[:20]
207 207
208 208 return title
209 209
210 210 def get_sorted_referenced_posts(self):
211 211 return self.referenced_posts.order_by('id')
212 212
213 213 def is_referenced(self):
214 214 return self.referenced_posts.all().exists()
215 215
216 216 def is_opening(self):
217 217 return self.thread_new.get_replies()[0] == self
218 218
219 219
220 220 class Thread(models.Model):
221 221
222 222 class Meta:
223 223 app_label = 'boards'
224 224
225 225 tags = models.ManyToManyField('Tag')
226 226 bump_time = models.DateTimeField()
227 227 last_edit_time = models.DateTimeField()
228 228 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
229 229 blank=True, related_name='tre+')
230 230
231 231 def get_tags(self):
232 232 """Get a sorted tag list"""
233 233
234 234 return self.tags.order_by('name')
235 235
236 236 def bump(self):
237 237 """Bump (move to up) thread"""
238 238
239 239 if self.can_bump():
240 240 self.bump_time = timezone.now()
241 241
242 242 def get_reply_count(self):
243 243 return self.replies.count()
244 244
245 245 def get_images_count(self):
246 246 return self.replies.filter(image_width__gt=0).count()
247 247
248 248 def can_bump(self):
249 249 """Check if the thread can be bumped by replying"""
250 250
251 251 post_count = self.get_reply_count()
252 252
253 253 return post_count <= settings.MAX_POSTS_PER_THREAD
254 254
255 255 def delete_with_posts(self):
256 256 """Completely delete thread"""
257 257
258 258 if self.replies.count() > 0:
259 259 map(Post.objects.delete_post, self.replies.all())
260 260
261 261 self.delete()
262 262
263 263 def get_last_replies(self):
264 264 """Get last replies, not including opening post"""
265 265
266 266 if settings.LAST_REPLIES_COUNT > 0:
267 267 reply_count = self.get_reply_count()
268 268
269 269 if reply_count > 0:
270 270 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
271 271 reply_count - 1)
272 272 last_replies = self.replies.all().order_by('pub_time')[
273 273 reply_count - reply_count_to_show:]
274 274
275 275 return last_replies
276 276
277 277 def get_replies(self):
278 278 """Get sorted thread posts"""
279 279
280 280 return self.replies.all().order_by('pub_time')
281 281
282 282 def add_tag(self, tag):
283 283 """Connect thread to a tag and tag to a thread"""
284 284
285 285 self.tags.add(tag)
286 286 tag.threads.add(self)
287 287
288 288 def get_opening_post(self):
289 289 return self.get_replies()[0]
290 290
291 291 def __unicode__(self):
292 return str(self.get_replies()[0].id) No newline at end of file
292 return str(self.get_replies()[0].id)
293
294 def get_pub_time(self):
295 return self.get_opening_post().pub_time No newline at end of file
@@ -1,80 +1,79 b''
1 1 from django.contrib.syndication.views import Feed
2 2 from django.core.urlresolvers import reverse
3 3 from django.shortcuts import get_object_or_404
4 4 from boards.models import Post, Tag
5 5 from neboard import settings
6 6
7 7 __author__ = 'neko259'
8 8
9 9
10 10 # TODO Make tests for all of these
11 11 class AllThreadsFeed(Feed):
12 12
13 13 title = settings.SITE_NAME + ' - All threads'
14 14 link = '/'
15 15 description_template = 'boards/rss/post.html'
16 16
17 17 def items(self):
18 return Post.objects.get_threads(order_by='-pub_time')
18 return Post.objects.get_threads() # TODO Order this by OP's pub time
19 19
20 20 def item_title(self, item):
21 21 return item.get_opening_post().title
22 22
23 23 def item_link(self, item):
24 24 return reverse('thread', args={item.get_opening_post().id})
25 25
26 26 def item_pubdate(self, item):
27 27 return item.pub_time
28 28
29 29
30 30 class TagThreadsFeed(Feed):
31 31
32 32 link = '/'
33 33 description_template = 'boards/rss/post.html'
34 34
35 35 def items(self, obj):
36 return Post.objects.get_threads(tag=obj,
37 order_by='-pub_time')
36 return Post.objects.get_threads(tag=obj) # TODO Order this by OP's pub time
38 37
39 38 def get_object(self, request, tag_name):
40 39 return get_object_or_404(Tag, name=tag_name)
41 40
42 41 def item_title(self, item):
43 42 return item.get_opening_post().title
44 43
45 44 def item_link(self, item):
46 45 return reverse('thread', args={item.get_opening_post().id})
47 46
48 47 def item_pubdate(self, item):
49 return item.pub_time
48 return item.get_pub_time()
50 49
51 50 def title(self, obj):
52 51 return obj.name
53 52
54 53
55 54 class ThreadPostsFeed(Feed):
56 55
57 56 link = '/'
58 57 description_template = 'boards/rss/post.html'
59 58
60 59 def items(self, obj):
61 60 return get_object_or_404(Post, id=obj).thread_new.get_replies()
62 61
63 62 def get_object(self, request, post_id):
64 63 return post_id
65 64
66 65 def item_title(self, item):
67 66 return item.title
68 67
69 68 def item_link(self, item):
70 if item.thread:
71 return reverse('thread', args={item.thread.get_opening_post()
69 if not item.is_opening():
70 return reverse('thread', args={item.thread_new.get_opening_post()
72 71 .id}) + "#" + str(item.id)
73 72 else:
74 73 return reverse('thread', args={item.id})
75 74
76 75 def item_pubdate(self, item):
77 76 return item.pub_time
78 77
79 78 def title(self, obj):
80 79 return get_object_or_404(Post, id=obj).title
@@ -1,161 +1,161 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7
8 8 {% block head %}
9 <title>Neboard - {{ posts.0.get_title }}</title>
9 <title>Neboard - {{ thread.get_replies.0.get_title }}</title>
10 10 {% endblock %}
11 11
12 12 {% block content %}
13 13 {% get_current_language as LANGUAGE_CODE %}
14 14
15 15 <script src="{% static 'js/thread_update.js' %}"></script>
16 16 <script src="{% static 'js/thread.js' %}"></script>
17 17
18 {% cache 600 thread_view thread.last_edit_time moderator LANGUAGE_CODE %}
18 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
19 19 {% if bumpable %}
20 20 <div class="bar-bg">
21 21 <div class="bar-value" style="width:{{ bumplimit_progress }}%">
22 22 </div>
23 23 <div class="bar-text">
24 24 {{ posts_left }} {% trans 'posts to bumplimit' %}
25 25 </div>
26 26 </div>
27 27 {% endif %}
28 28 <div class="thread">
29 29 {% for post in thread.get_replies %}
30 30 {% if bumpable %}
31 31 <div class="post" id="{{ post.id }}">
32 32 {% else %}
33 33 <div class="post dead_post" id="{{ post.id }}">
34 34 {% endif %}
35 35 {% if post.image %}
36 36 <div class="image">
37 37 <a
38 38 class="thumb"
39 39 href="{{ post.image.url }}"><img
40 40 src="{{ post.image.url_200x150 }}"
41 41 alt="{{ post.id }}"
42 42 data-width="{{ post.image_width }}"
43 43 data-height="{{ post.image_height }}"/>
44 44 </a>
45 45 </div>
46 46 {% endif %}
47 47 <div class="message">
48 48 <div class="post-info">
49 49 <span class="title">{{ post.title }}</span>
50 50 <a class="post_id" href="#{{ post.id }}">
51 51 ({{ post.id }})</a>
52 52 [{{ post.pub_time }}]
53 53 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
54 54 ; return false;">&gt;&gt;</a>]
55 55
56 56 {% if moderator %}
57 57 <span class="moderator_info">
58 58 [<a href="{% url 'delete' post_id=post.id %}"
59 59 >{% trans 'Delete' %}</a>]
60 60 ({{ post.poster_ip }})
61 61 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
62 62 >{% trans 'Ban IP' %}</a>]
63 63 </span>
64 64 {% endif %}
65 65 </div>
66 66 {% autoescape off %}
67 67 {{ post.text.rendered }}
68 68 {% endautoescape %}
69 69 {% if post.is_referenced %}
70 70 <div class="refmap">
71 71 {% trans "Replies" %}:
72 72 {% for ref_post in post.get_sorted_referenced_posts %}
73 73 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
74 74 >{% if not forloop.last %},{% endif %}
75 75 {% endfor %}
76 76 </div>
77 77 {% endif %}
78 78 </div>
79 79 {% if forloop.first %}
80 80 <div class="metadata">
81 81 <span class="tags">
82 82 {% for tag in thread.get_tags %}
83 83 <a class="tag" href="{% url 'tag' tag.name %}">
84 84 #{{ tag.name }}</a
85 85 >{% if not forloop.last %},{% endif %}
86 86 {% endfor %}
87 87 </span>
88 88 </div>
89 89 {% endif %}
90 90 </div>
91 91 {% endfor %}
92 92 </div>
93 93 {% endcache %}
94 94
95 95 <form id="form" enctype="multipart/form-data" method="post"
96 96 >{% csrf_token %}
97 97 <div class="post-form-w">
98 98 <div class="form-title">{% trans "Reply to thread" %} #{{ posts.0.id }}</div>
99 99 <div class="post-form">
100 100 <div class="form-row">
101 101 <div class="form-label">{% trans 'Title' %}</div>
102 102 <div class="form-input">{{ form.title }}</div>
103 103 <div class="form-errors">{{ form.title.errors }}</div>
104 104 </div>
105 105 <div class="form-row">
106 106 <div class="form-label">{% trans 'Formatting' %}</div>
107 107 <div class="form-input" id="mark_panel">
108 108 <span class="mark_btn" id="quote"><span class="quote">&gt;{% trans 'quote' %}</span></span>
109 109 <span class="mark_btn" id="italic"><i>{% trans 'italic' %}</i></span>
110 110 <span class="mark_btn" id="bold"><b>{% trans 'bold' %}</b></span>
111 111 <span class="mark_btn" id="spoiler"><span class="spoiler">{% trans 'spoiler' %}</span></span>
112 112 <span class="mark_btn" id="comment"><span class="comment">// {% trans 'comment' %}</span></span>
113 113 </div>
114 114 </div>
115 115 <div class="form-row">
116 116 <div class="form-label">{% trans 'Text' %}</div>
117 117 <div class="form-input">{{ form.text }}</div>
118 118 <div class="form-errors">{{ form.text.errors }}</div>
119 119 </div>
120 120 <div class="form-row">
121 121 <div class="form-label">{% trans 'Image' %}</div>
122 122 <div class="form-input">{{ form.image }}</div>
123 123 <div class="form-errors">{{ form.image.errors }}</div>
124 124 </div>
125 125 <div class="form-row form-email">
126 126 <div class="form-label">{% trans 'e-mail' %}</div>
127 127 <div class="form-input">{{ form.email }}</div>
128 128 <div class="form-errors">{{ form.email.errors }}</div>
129 129 </div>
130 130 <div class="form-row">
131 131 {{ form.captcha }}
132 132 <div class="form-errors">{{ form.captcha.errors }}</div>
133 133 </div>
134 134 <div class="form-row">
135 135 <div class="form-errors">{{ form.other.errors }}</div>
136 136 </div>
137 137 </div>
138 138
139 139 <div class="form-submit"><input type="submit"
140 140 value="{% trans "Post" %}"/></div>
141 141 <div><a href="{% url "staticpage" name="help" %}">
142 142 {% trans 'Text syntax' %}</a></div>
143 143 </div>
144 144 </form>
145 145
146 146 {% endblock %}
147 147
148 148 {% block metapanel %}
149 149
150 150 {% get_current_language as LANGUAGE_CODE %}
151 151
152 152 <span class="metapanel" data-last-update="{{ last_update }}">
153 153 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
154 154 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
155 155 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
156 156 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
157 157 [<a href="rss/">RSS</a>]
158 158 {% endcache %}
159 159 </span>
160 160
161 161 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now