##// END OF EJS Templates
Show all tag parents at the tag page
neko259 -
r1361:1af05638 default
parent child Browse files
Show More
@@ -1,372 +1,371 b''
1 1 import hashlib
2 2 import re
3 3 import time
4 4
5 5 import pytz
6 6 from django import forms
7 7 from django.core.files.uploadedfile import SimpleUploadedFile
8 8 from django.core.exceptions import ObjectDoesNotExist
9 9 from django.forms.util import ErrorList
10 10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 11
12 12 from boards.mdx_neboard import formatters
13 13 from boards.models.attachment.downloaders import Downloader
14 14 from boards.models.post import TITLE_MAX_LENGTH
15 15 from boards.models import Tag, Post
16 16 from boards.utils import validate_file_size
17 17 from neboard import settings
18 18 import boards.settings as board_settings
19 19 import neboard
20 20
21 21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
22 22
23 23 VETERAN_POSTING_DELAY = 5
24 24
25 25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
26 26 ATTRIBUTE_ROWS = 'rows'
27 27
28 28 LAST_POST_TIME = 'last_post_time'
29 29 LAST_LOGIN_TIME = 'last_login_time'
30 30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
31 31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
32 32
33 33 LABEL_TITLE = _('Title')
34 34 LABEL_TEXT = _('Text')
35 35 LABEL_TAG = _('Tag')
36 36 LABEL_SEARCH = _('Search')
37 37
38 38 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
39 39 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
40 40
41 41 TAG_MAX_LENGTH = 20
42 42
43 43 TEXTAREA_ROWS = 4
44 44
45 45
46 46 def get_timezones():
47 47 timezones = []
48 48 for tz in pytz.common_timezones:
49 49 timezones.append((tz, tz),)
50 50 return timezones
51 51
52 52
53 53 class FormatPanel(forms.Textarea):
54 54 """
55 55 Panel for text formatting. Consists of buttons to add different tags to the
56 56 form text area.
57 57 """
58 58
59 59 def render(self, name, value, attrs=None):
60 60 output = '<div id="mark-panel">'
61 61 for formatter in formatters:
62 62 output += '<span class="mark_btn"' + \
63 63 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
64 64 '\', \'' + formatter.format_right + '\')">' + \
65 65 formatter.preview_left + formatter.name + \
66 66 formatter.preview_right + '</span>'
67 67
68 68 output += '</div>'
69 69 output += super(FormatPanel, self).render(name, value, attrs=None)
70 70
71 71 return output
72 72
73 73
74 74 class PlainErrorList(ErrorList):
75 75 def __unicode__(self):
76 76 return self.as_text()
77 77
78 78 def as_text(self):
79 79 return ''.join(['(!) %s ' % e for e in self])
80 80
81 81
82 82 class NeboardForm(forms.Form):
83 83 """
84 84 Form with neboard-specific formatting.
85 85 """
86 86
87 87 def as_div(self):
88 88 """
89 89 Returns this form rendered as HTML <as_div>s.
90 90 """
91 91
92 92 return self._html_output(
93 93 # TODO Do not show hidden rows in the list here
94 94 normal_row='<div class="form-row">'
95 95 '<div class="form-label">'
96 96 '%(label)s'
97 97 '</div>'
98 98 '<div class="form-input">'
99 99 '%(field)s'
100 100 '</div>'
101 101 '</div>'
102 102 '<div class="form-row">'
103 103 '%(help_text)s'
104 104 '</div>',
105 105 error_row='<div class="form-row">'
106 106 '<div class="form-label"></div>'
107 107 '<div class="form-errors">%s</div>'
108 108 '</div>',
109 109 row_ender='</div>',
110 110 help_text_html='%s',
111 111 errors_on_separate_row=True)
112 112
113 113 def as_json_errors(self):
114 114 errors = []
115 115
116 116 for name, field in list(self.fields.items()):
117 117 if self[name].errors:
118 118 errors.append({
119 119 'field': name,
120 120 'errors': self[name].errors.as_text(),
121 121 })
122 122
123 123 return errors
124 124
125 125
126 126 class PostForm(NeboardForm):
127 127
128 128 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
129 129 label=LABEL_TITLE,
130 130 widget=forms.TextInput(
131 131 attrs={ATTRIBUTE_PLACEHOLDER:
132 132 'test#tripcode'}))
133 133 text = forms.CharField(
134 134 widget=FormatPanel(attrs={
135 135 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
136 136 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
137 137 }),
138 138 required=False, label=LABEL_TEXT)
139 139 file = forms.FileField(required=False, label=_('File'),
140 140 widget=forms.ClearableFileInput(
141 141 attrs={'accept': 'file/*'}))
142 142 file_url = forms.CharField(required=False, label=_('File URL'),
143 143 widget=forms.TextInput(
144 144 attrs={ATTRIBUTE_PLACEHOLDER:
145 145 'http://example.com/image.png'}))
146 146
147 147 # This field is for spam prevention only
148 148 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
149 149 widget=forms.TextInput(attrs={
150 150 'class': 'form-email'}))
151 151 threads = forms.CharField(required=False, label=_('Additional threads'),
152 152 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
153 153 '123 456 789'}))
154 154
155 155 session = None
156 156 need_to_ban = False
157 157
158 158 def clean_title(self):
159 159 title = self.cleaned_data['title']
160 160 if title:
161 161 if len(title) > TITLE_MAX_LENGTH:
162 162 raise forms.ValidationError(_('Title must have less than %s '
163 163 'characters') %
164 164 str(TITLE_MAX_LENGTH))
165 165 return title
166 166
167 167 def clean_text(self):
168 168 text = self.cleaned_data['text'].strip()
169 169 if text:
170 170 max_length = board_settings.get_int('Forms', 'MaxTextLength')
171 171 if len(text) > max_length:
172 172 raise forms.ValidationError(_('Text must have less than %s '
173 173 'characters') % str(max_length))
174 174 return text
175 175
176 176 def clean_file(self):
177 177 file = self.cleaned_data['file']
178 178
179 179 if file:
180 180 validate_file_size(file.size)
181 181
182 182 return file
183 183
184 184 def clean_file_url(self):
185 185 url = self.cleaned_data['file_url']
186 186
187 187 file = None
188 188 if url:
189 189 file = self._get_file_from_url(url)
190 190
191 191 if not file:
192 192 raise forms.ValidationError(_('Invalid URL'))
193 193 else:
194 194 validate_file_size(file.size)
195 195
196 196 return file
197 197
198 198 def clean_threads(self):
199 199 threads_str = self.cleaned_data['threads']
200 200
201 201 if len(threads_str) > 0:
202 202 threads_id_list = threads_str.split(' ')
203 203
204 204 threads = list()
205 205
206 206 for thread_id in threads_id_list:
207 207 try:
208 208 thread = Post.objects.get(id=int(thread_id))
209 209 if not thread.is_opening() or thread.get_thread().archived:
210 210 raise ObjectDoesNotExist()
211 211 threads.append(thread)
212 212 except (ObjectDoesNotExist, ValueError):
213 213 raise forms.ValidationError(_('Invalid additional thread list'))
214 214
215 215 return threads
216 216
217 217 def clean(self):
218 218 cleaned_data = super(PostForm, self).clean()
219 219
220 220 if cleaned_data['email']:
221 221 self.need_to_ban = True
222 222 raise forms.ValidationError('A human cannot enter a hidden field')
223 223
224 224 if not self.errors:
225 225 self._clean_text_file()
226 226
227 227 if not self.errors and self.session:
228 228 self._validate_posting_speed()
229 229
230 230 return cleaned_data
231 231
232 232 def get_file(self):
233 233 """
234 234 Gets file from form or URL.
235 235 """
236 236
237 237 file = self.cleaned_data['file']
238 238 return file or self.cleaned_data['file_url']
239 239
240 240 def get_tripcode(self):
241 241 title = self.cleaned_data['title']
242 242 if title is not None and '#' in title:
243 243 code = title.split('#', maxsplit=1)[1] + neboard.settings.SECRET_KEY
244 244 return hashlib.md5(code.encode()).hexdigest()
245 245
246 246 def get_title(self):
247 247 title = self.cleaned_data['title']
248 248 if title is not None and '#' in title:
249 249 return title.split('#', maxsplit=1)[0]
250 250 else:
251 251 return title
252 252
253 253 def _clean_text_file(self):
254 254 text = self.cleaned_data.get('text')
255 255 file = self.get_file()
256 256
257 257 if (not text) and (not file):
258 258 error_message = _('Either text or file must be entered.')
259 259 self._errors['text'] = self.error_class([error_message])
260 260
261 261 def _validate_posting_speed(self):
262 262 can_post = True
263 263
264 264 posting_delay = settings.POSTING_DELAY
265 265
266 266 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
267 267 now = time.time()
268 268
269 269 current_delay = 0
270 270
271 271 if LAST_POST_TIME not in self.session:
272 272 self.session[LAST_POST_TIME] = now
273 273
274 274 need_delay = True
275 275 else:
276 276 last_post_time = self.session.get(LAST_POST_TIME)
277 277 current_delay = int(now - last_post_time)
278 278
279 279 need_delay = current_delay < posting_delay
280 280
281 281 if need_delay:
282 282 delay = posting_delay - current_delay
283 283 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
284 284 delay) % {'delay': delay}
285 285 self._errors['text'] = self.error_class([error_message])
286 286
287 287 can_post = False
288 288
289 289 if can_post:
290 290 self.session[LAST_POST_TIME] = now
291 291
292 292 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
293 293 """
294 294 Gets an file file from URL.
295 295 """
296 296
297 297 img_temp = None
298 298
299 299 try:
300 300 for downloader in Downloader.__subclasses__():
301 301 if downloader.handles(url):
302 302 return downloader.download(url)
303 303 # If nobody of the specific downloaders handles this, use generic
304 304 # one
305 305 return Downloader.download(url)
306 306 except forms.ValidationError as e:
307 307 raise e
308 308 except Exception as e:
309 309 # Just return no file
310 310 pass
311 311
312 312
313 313 class ThreadForm(PostForm):
314 314
315 315 tags = forms.CharField(
316 316 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
317 317 max_length=100, label=_('Tags'), required=True)
318 318
319 319 def clean_tags(self):
320 320 tags = self.cleaned_data['tags'].strip()
321 321
322 322 if not tags or not REGEX_TAGS.match(tags):
323 323 raise forms.ValidationError(
324 324 _('Inappropriate characters in tags.'))
325 325
326 326 required_tag_exists = False
327 327 tag_set = set()
328 328 for tag_string in tags.split():
329 329 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
330 330 tag_set.add(tag)
331 331
332 332 # If this is a new tag, don't check for its parents because nobody
333 333 # added them yet
334 334 if not created:
335 tag_set |= tag.get_all_parents()
335 tag_set |= set(tag.get_all_parents())
336 336
337 337 for tag in tag_set:
338 338 if tag.required:
339 339 required_tag_exists = True
340 340 break
341 341
342 342 if not required_tag_exists:
343 all_tags = Tag.objects.filter(required=True)
344 343 raise forms.ValidationError(
345 344 _('Need at least one section.'))
346 345
347 346 return tag_set
348 347
349 348 def clean(self):
350 349 cleaned_data = super(ThreadForm, self).clean()
351 350
352 351 return cleaned_data
353 352
354 353
355 354 class SettingsForm(NeboardForm):
356 355
357 356 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
358 357 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
359 358 username = forms.CharField(label=_('User name'), required=False)
360 359 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
361 360
362 361 def clean_username(self):
363 362 username = self.cleaned_data['username']
364 363
365 364 if username and not REGEX_TAGS.match(username):
366 365 raise forms.ValidationError(_('Inappropriate characters.'))
367 366
368 367 return username
369 368
370 369
371 370 class SearchForm(NeboardForm):
372 371 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,134 +1,134 b''
1 1 import hashlib
2 2 from django.template.loader import render_to_string
3 3 from django.db import models
4 4 from django.db.models import Count
5 5 from django.core.urlresolvers import reverse
6 6
7 7 from boards.models.base import Viewable
8 8 from boards.utils import cached_result
9 9 import boards
10 10
11 11 __author__ = 'neko259'
12 12
13 13
14 14 RELATED_TAGS_COUNT = 5
15 15
16 16
17 17 class TagManager(models.Manager):
18 18
19 19 def get_not_empty_tags(self):
20 20 """
21 21 Gets tags that have non-archived threads.
22 22 """
23 23
24 24 return self.annotate(num_threads=Count('thread_tags')).filter(num_threads__gt=0)\
25 25 .order_by('-required', 'name')
26 26
27 27 def get_tag_url_list(self, tags: list) -> str:
28 28 """
29 29 Gets a comma-separated list of tag links.
30 30 """
31 31
32 32 return ', '.join([tag.get_view() for tag in tags])
33 33
34 34
35 35 class Tag(models.Model, Viewable):
36 36 """
37 37 A tag is a text node assigned to the thread. The tag serves as a board
38 38 section. There can be multiple tags for each thread
39 39 """
40 40
41 41 objects = TagManager()
42 42
43 43 class Meta:
44 44 app_label = 'boards'
45 45 ordering = ('name',)
46 46
47 47 name = models.CharField(max_length=100, db_index=True, unique=True)
48 48 required = models.BooleanField(default=False, db_index=True)
49 49 description = models.TextField(blank=True)
50 50
51 51 parent = models.ForeignKey('Tag', null=True, related_name='children')
52 52
53 53 def __str__(self):
54 54 return self.name
55 55
56 56 def is_empty(self) -> bool:
57 57 """
58 58 Checks if the tag has some threads.
59 59 """
60 60
61 61 return self.get_thread_count() == 0
62 62
63 63 def get_thread_count(self, archived=None) -> int:
64 64 threads = self.get_threads()
65 65 if archived is not None:
66 66 threads = threads.filter(archived=archived)
67 67 return threads.count()
68 68
69 69 def get_active_thread_count(self) -> int:
70 70 return self.get_thread_count(archived=False)
71 71
72 72 def get_absolute_url(self):
73 73 return reverse('tag', kwargs={'tag_name': self.name})
74 74
75 75 def get_threads(self):
76 76 return self.thread_tags.order_by('-bump_time')
77 77
78 78 def is_required(self):
79 79 return self.required
80 80
81 81 def get_view(self):
82 82 link = '<a class="tag" href="{}">{}</a>'.format(
83 83 self.get_absolute_url(), self.name)
84 84 if self.is_required():
85 85 link = '<b>{}</b>'.format(link)
86 86 return link
87 87
88 88 def get_search_view(self, *args, **kwargs):
89 89 return render_to_string('boards/tag.html', {
90 90 'tag': self,
91 91 })
92 92
93 93 @cached_result()
94 94 def get_post_count(self):
95 95 return self.get_threads().aggregate(num_posts=Count('multi_replies'))['num_posts']
96 96
97 97 def get_description(self):
98 98 return self.description
99 99
100 100 def get_random_image_post(self, archived=False):
101 101 posts = boards.models.Post.objects.annotate(images_count=Count(
102 102 'images')).filter(images_count__gt=0, threads__tags__in=[self])
103 103 if archived is not None:
104 104 posts = posts.filter(thread__archived=archived)
105 105 return posts.order_by('?').first()
106 106
107 107 def get_first_letter(self):
108 108 return self.name and self.name[0] or ''
109 109
110 110 def get_related_tags(self):
111 111 return set(Tag.objects.filter(thread_tags__in=self.get_threads()).exclude(
112 112 id=self.id).order_by('?')[:RELATED_TAGS_COUNT])
113 113
114 114 @cached_result()
115 115 def get_color(self):
116 116 """
117 117 Gets color hashed from the tag name.
118 118 """
119 119 return hashlib.md5(self.name.encode()).hexdigest()[:6]
120 120
121 121 def get_parent(self):
122 122 return self.parent
123 123
124 124 def get_all_parents(self):
125 parents = set()
125 parents = list()
126 126 parent = self.get_parent()
127 127 if parent and parent not in parents:
128 parents.add(parent)
129 parents |= parent.get_all_parents()
128 parents.insert(0, parent)
129 parents = parent.get_all_parents() + parents
130 130
131 131 return parents
132 132
133 133 def get_children(self):
134 134 return self.children
@@ -1,187 +1,187 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load board %}
5 5 {% load static %}
6 6 {% load tz %}
7 7
8 8 {% block head %}
9 9 <meta name="robots" content="noindex">
10 10
11 11 {% if tag %}
12 12 <title>{{ tag.name }} - {{ site_name }}</title>
13 13 {% else %}
14 14 <title>{{ site_name }}</title>
15 15 {% endif %}
16 16
17 17 {% if prev_page_link %}
18 18 <link rel="prev" href="{{ prev_page_link }}" />
19 19 {% endif %}
20 20 {% if next_page_link %}
21 21 <link rel="next" href="{{ next_page_link }}" />
22 22 {% endif %}
23 23
24 24 {% endblock %}
25 25
26 26 {% block content %}
27 27
28 28 {% get_current_language as LANGUAGE_CODE %}
29 29 {% get_current_timezone as TIME_ZONE %}
30 30
31 31 {% for banner in banners %}
32 32 <div class="post">
33 33 <div class="title">{{ banner.title }}</div>
34 34 <div>{{ banner.text }}</div>
35 35 <div>{% trans 'Related message' %}: <a href="{{ banner.post.get_absolute_url }}">>>{{ banner.post.id }}</a></div>
36 36 </div>
37 37 {% endfor %}
38 38
39 39 {% if tag %}
40 40 <div class="tag_info" style="border-bottom: solid .5ex #{{ tag.get_color }}">
41 41 {% if random_image_post %}
42 42 <div class="tag-image">
43 43 {% with image=random_image_post.images.first %}
44 44 <a href="{{ random_image_post.get_absolute_url }}"><img
45 45 src="{{ image.image.url_200x150 }}"
46 46 width="{{ image.pre_width }}"
47 47 height="{{ image.pre_height }}"/></a>
48 48 {% endwith %}
49 49 </div>
50 50 {% endif %}
51 51 <div class="tag-text-data">
52 52 <h2>
53 53 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
54 54 {% if is_favorite %}
55 55 <button name="method" value="unsubscribe" class="fav"></button>
56 56 {% else %}
57 57 <button name="method" value="subscribe" class="not_fav"></button>
58 58 {% endif %}
59 59 </form>
60 60 <form action="{% url 'tag' tag.name %}" method="post" class="post-button-form">
61 61 {% if is_hidden %}
62 62 <button name="method" value="unhide" class="fav">H</button>
63 63 {% else %}
64 64 <button name="method" value="hide" class="not_fav">H</button>
65 65 {% endif %}
66 66 </form>
67 67 {{ tag.get_view|safe }}
68 68 {% if moderator %}
69 69 <span class="moderator_info">| <a href="{% url 'admin:boards_tag_change' tag.id %}">{% trans 'Edit tag' %}</a></span>
70 70 {% endif %}
71 71 </h2>
72 72 {% if tag.get_description %}
73 73 <p>{{ tag.get_description|safe }}</p>
74 74 {% endif %}
75 75 <p>{% blocktrans with active_thread_count=tag.get_active_thread_count thread_count=tag.get_thread_count post_count=tag.get_post_count %}This tag has {{ thread_count }} threads ({{ active_thread_count}} active) and {{ post_count }} posts.{% endblocktrans %}</p>
76 {% if tag.get_parent %}
76 {% if tag.get_all_parents %}
77 77 <p>
78 {% if tag.get_parent %}
79 {{ tag.get_parent.get_view|safe }} /
80 {% endif %}
78 {% for parent in tag.get_all_parents %}
79 {{ parent.get_view|safe }} &gt;
80 {% endfor %}
81 81 {{ tag.get_view|safe }}
82 82 </p>
83 83 {% endif %}
84 84 </div>
85 85 </div>
86 86 {% endif %}
87 87
88 88 {% if threads %}
89 89 {% if prev_page_link %}
90 90 <div class="page_link">
91 91 <a href="{{ prev_page_link }}">{% trans "Previous page" %}</a>
92 92 </div>
93 93 {% endif %}
94 94
95 95 {% for thread in threads %}
96 96 <div class="thread">
97 97 {% post_view thread.get_opening_post moderator=moderator thread=thread truncated=True need_open_link=True %}
98 98 {% if not thread.archived %}
99 99 {% with last_replies=thread.get_last_replies %}
100 100 {% if last_replies %}
101 101 {% with skipped_replies_count=thread.get_skipped_replies_count %}
102 102 {% if skipped_replies_count %}
103 103 <div class="skipped_replies">
104 104 <a href="{% url 'thread' thread.get_opening_post_id %}">
105 105 {% blocktrans count count=skipped_replies_count %}Skipped {{ count }} reply. Open thread to see all replies.{% plural %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
106 106 </a>
107 107 </div>
108 108 {% endif %}
109 109 {% endwith %}
110 110 <div class="last-replies">
111 111 {% for post in last_replies %}
112 112 {% post_view post moderator=moderator truncated=True %}
113 113 {% endfor %}
114 114 </div>
115 115 {% endif %}
116 116 {% endwith %}
117 117 {% endif %}
118 118 </div>
119 119 {% endfor %}
120 120
121 121 {% if next_page_link %}
122 122 <div class="page_link">
123 123 <a href="{{ next_page_link }}">{% trans "Next page" %}</a>
124 124 </div>
125 125 {% endif %}
126 126 {% else %}
127 127 <div class="post">
128 128 {% trans 'No threads exist. Create the first one!' %}</div>
129 129 {% endif %}
130 130
131 131 <div class="post-form-w">
132 132 <script src="{% static 'js/panel.js' %}"></script>
133 133 <div class="post-form">
134 134 <div class="form-title">{% trans "Create new thread" %}</div>
135 135 <div class="swappable-form-full">
136 136 <form enctype="multipart/form-data" method="post" id="form">{% csrf_token %}
137 137 {{ form.as_div }}
138 138 <div class="form-submit">
139 139 <input type="submit" value="{% trans "Post" %}"/>
140 140 <button id="preview-button" onclick="return false;">{% trans 'Preview' %}</button>
141 141 </div>
142 142 </form>
143 143 </div>
144 144 <div>
145 145 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
146 146 </div>
147 147 <div id="preview-text"></div>
148 148 <div><a href="{% url "staticpage" name="help" %}">{% trans 'Text syntax' %}</a></div>
149 149 <div><a href="{% url "tags" "required" %}">{% trans 'Tags' %}</a></div>
150 150 </div>
151 151 </div>
152 152
153 153 <script src="{% static 'js/form.js' %}"></script>
154 154 <script src="{% static 'js/thread_create.js' %}"></script>
155 155
156 156 {% endblock %}
157 157
158 158 {% block metapanel %}
159 159
160 160 <span class="metapanel">
161 161 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
162 162 {% trans "Pages:" %}
163 163 [
164 164 {% with dividers=paginator.get_dividers %}
165 165 {% for page in paginator.get_divided_range %}
166 166 {% if page in dividers %}
167 167 …,
168 168 {% endif %}
169 169 <a
170 170 {% ifequal page current_page.number %}
171 171 class="current_page"
172 172 {% endifequal %}
173 173 href="
174 174 {% if tag %}
175 175 {% url "tag" tag_name=tag.name %}?page={{ page }}
176 176 {% else %}
177 177 {% url "index" %}?page={{ page }}
178 178 {% endif %}
179 179 ">{{ page }}</a>
180 180 {% if not forloop.last %},{% endif %}
181 181 {% endfor %}
182 182 {% endwith %}
183 183 ]
184 184 [<a href="rss/">RSS</a>]
185 185 </span>
186 186
187 187 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now