##// END OF EJS Templates
Saving image thumbnails size to the database and using this size in the HTML
neko259 -
r452:308af8e9 1.5-dev
parent child Browse files
Show More
@@ -0,0 +1,92 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import SchemaMigration
5 from django.db import models
6
7
8 class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding field 'Post.image_pre_width'
12 db.add_column(u'boards_post', 'image_pre_width',
13 self.gf('django.db.models.fields.IntegerField')(default=0),
14 keep_default=False)
15
16 # Adding field 'Post.image_pre_height'
17 db.add_column(u'boards_post', 'image_pre_height',
18 self.gf('django.db.models.fields.IntegerField')(default=0),
19 keep_default=False)
20
21
22 def backwards(self, orm):
23 # Deleting field 'Post.image_pre_width'
24 db.delete_column(u'boards_post', 'image_pre_width')
25
26 # Deleting field 'Post.image_pre_height'
27 db.delete_column(u'boards_post', 'image_pre_height')
28
29
30 models = {
31 'boards.ban': {
32 'Meta': {'object_name': 'Ban'},
33 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
34 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
35 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
36 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
37 },
38 'boards.post': {
39 'Meta': {'object_name': 'Post'},
40 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
41 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
42 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
43 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
44 'image_pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
45 'image_pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
46 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
47 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
48 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
49 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
50 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
51 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
52 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
53 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
54 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}),
55 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
56 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
57 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
58 },
59 'boards.setting': {
60 'Meta': {'object_name': 'Setting'},
61 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
63 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
64 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
65 },
66 'boards.tag': {
67 'Meta': {'object_name': 'Tag'},
68 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
69 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
70 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
71 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
72 },
73 'boards.thread': {
74 'Meta': {'object_name': 'Thread'},
75 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
76 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
77 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
78 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
79 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
80 },
81 'boards.user': {
82 'Meta': {'object_name': 'User'},
83 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
84 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
85 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
86 'rank': ('django.db.models.fields.IntegerField', [], {}),
87 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
88 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
89 }
90 }
91
92 complete_apps = ['boards'] No newline at end of file
@@ -1,374 +1,379 b''
1 from datetime import datetime, timedelta
1 from datetime import datetime, timedelta
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import os
3 import os
4 from random import random
4 from random import random
5 import time
5 import time
6 import math
6 import math
7 import re
7 import re
8 from django.core.cache import cache
8 from django.core.cache import cache
9
9
10 from django.db import models
10 from django.db import models
11 from django.http import Http404
11 from django.http import Http404
12 from django.utils import timezone
12 from django.utils import timezone
13 from markupfield.fields import MarkupField
13 from markupfield.fields import MarkupField
14
14
15 from neboard import settings
15 from neboard import settings
16 from boards import thumbs
16 from boards import thumbs
17
17
18 APP_LABEL_BOARDS = 'boards'
18 APP_LABEL_BOARDS = 'boards'
19
19
20 CACHE_KEY_PPD = 'ppd'
20 CACHE_KEY_PPD = 'ppd'
21
21
22 POSTS_PER_DAY_RANGE = range(7)
22 POSTS_PER_DAY_RANGE = range(7)
23
23
24 BAN_REASON_AUTO = 'Auto'
24 BAN_REASON_AUTO = 'Auto'
25
25
26 IMAGE_THUMB_SIZE = (200, 150)
26 IMAGE_THUMB_SIZE = (200, 150)
27
27
28 TITLE_MAX_LENGTH = 50
28 TITLE_MAX_LENGTH = 50
29
29
30 DEFAULT_MARKUP_TYPE = 'markdown'
30 DEFAULT_MARKUP_TYPE = 'markdown'
31
31
32 NO_PARENT = -1
32 NO_PARENT = -1
33 NO_IP = '0.0.0.0'
33 NO_IP = '0.0.0.0'
34 UNKNOWN_UA = ''
34 UNKNOWN_UA = ''
35 ALL_PAGES = -1
35 ALL_PAGES = -1
36 IMAGES_DIRECTORY = 'images/'
36 IMAGES_DIRECTORY = 'images/'
37 FILE_EXTENSION_DELIMITER = '.'
37 FILE_EXTENSION_DELIMITER = '.'
38
38
39 SETTING_MODERATE = "moderate"
39 SETTING_MODERATE = "moderate"
40
40
41 REGEX_REPLY = re.compile('>>(\d+)')
41 REGEX_REPLY = re.compile('>>(\d+)')
42
42
43
43
44 class PostManager(models.Manager):
44 class PostManager(models.Manager):
45
45
46 def create_post(self, title, text, image=None, thread=None,
46 def create_post(self, title, text, image=None, thread=None,
47 ip=NO_IP, tags=None, user=None):
47 ip=NO_IP, tags=None, user=None):
48 """
48 """
49 Create new post
49 Create new post
50 """
50 """
51
51
52 posting_time = timezone.now()
52 posting_time = timezone.now()
53 if not thread:
53 if not thread:
54 thread = Thread.objects.create(bump_time=posting_time,
54 thread = Thread.objects.create(bump_time=posting_time,
55 last_edit_time=posting_time)
55 last_edit_time=posting_time)
56 else:
56 else:
57 thread.bump()
57 thread.bump()
58 thread.last_edit_time = posting_time
58 thread.last_edit_time = posting_time
59 thread.save()
59 thread.save()
60
60
61 post = self.create(title=title,
61 post = self.create(title=title,
62 text=text,
62 text=text,
63 pub_time=posting_time,
63 pub_time=posting_time,
64 thread_new=thread,
64 thread_new=thread,
65 image=image,
65 image=image,
66 poster_ip=ip,
66 poster_ip=ip,
67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at last!
67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at last!
68 last_edit_time=posting_time,
68 last_edit_time=posting_time,
69 user=user)
69 user=user)
70
70
71 thread.replies.add(post)
71 thread.replies.add(post)
72 if tags:
72 if tags:
73 linked_tags = []
73 linked_tags = []
74 for tag in tags:
74 for tag in tags:
75 tag_linked_tags = tag.get_linked_tags()
75 tag_linked_tags = tag.get_linked_tags()
76 if len(tag_linked_tags) > 0:
76 if len(tag_linked_tags) > 0:
77 linked_tags.extend(tag_linked_tags)
77 linked_tags.extend(tag_linked_tags)
78
78
79 tags.extend(linked_tags)
79 tags.extend(linked_tags)
80 map(thread.add_tag, tags)
80 map(thread.add_tag, tags)
81
81
82 self._delete_old_threads()
82 self._delete_old_threads()
83 self.connect_replies(post)
83 self.connect_replies(post)
84
84
85 return post
85 return post
86
86
87 def delete_post(self, post):
87 def delete_post(self, post):
88 """
88 """
89 Delete post and update its thread
89 Delete post and update its thread
90 """
90 """
91
91
92 thread = post.thread_new
92 thread = post.thread_new
93 thread.last_edit_time = timezone.now()
93 thread.last_edit_time = timezone.now()
94 thread.save()
94 thread.save()
95
95
96 post.delete()
96 post.delete()
97
97
98 def delete_posts_by_ip(self, ip):
98 def delete_posts_by_ip(self, ip):
99 """
99 """
100 Delete all posts of the author with same IP
100 Delete all posts of the author with same IP
101 """
101 """
102
102
103 posts = self.filter(poster_ip=ip)
103 posts = self.filter(poster_ip=ip)
104 map(self.delete_post, posts)
104 map(self.delete_post, posts)
105
105
106 # TODO Move this method to thread manager
106 # TODO Move this method to thread manager
107 def get_threads(self, tag=None, page=ALL_PAGES,
107 def get_threads(self, tag=None, page=ALL_PAGES,
108 order_by='-bump_time'):
108 order_by='-bump_time'):
109 if tag:
109 if tag:
110 threads = tag.threads
110 threads = tag.threads
111
111
112 if not threads.exists():
112 if not threads.exists():
113 raise Http404
113 raise Http404
114 else:
114 else:
115 threads = Thread.objects.all()
115 threads = Thread.objects.all()
116
116
117 threads = threads.order_by(order_by)
117 threads = threads.order_by(order_by)
118
118
119 if page != ALL_PAGES:
119 if page != ALL_PAGES:
120 thread_count = threads.count()
120 thread_count = threads.count()
121
121
122 if page < self._get_page_count(thread_count):
122 if page < self._get_page_count(thread_count):
123 start_thread = page * settings.THREADS_PER_PAGE
123 start_thread = page * settings.THREADS_PER_PAGE
124 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
124 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
125 thread_count)
125 thread_count)
126 threads = threads[start_thread:end_thread]
126 threads = threads[start_thread:end_thread]
127
127
128 return threads
128 return threads
129
129
130 # TODO Move this method to thread manager
130 # TODO Move this method to thread manager
131 def get_thread_page_count(self, tag=None):
131 def get_thread_page_count(self, tag=None):
132 if tag:
132 if tag:
133 threads = Thread.objects.filter(tags=tag)
133 threads = Thread.objects.filter(tags=tag)
134 else:
134 else:
135 threads = Thread.objects.all()
135 threads = Thread.objects.all()
136
136
137 return self._get_page_count(threads.count())
137 return self._get_page_count(threads.count())
138
138
139 # TODO Move this method to thread manager
139 # TODO Move this method to thread manager
140 def _delete_old_threads(self):
140 def _delete_old_threads(self):
141 """
141 """
142 Preserves maximum thread count. If there are too many threads,
142 Preserves maximum thread count. If there are too many threads,
143 delete the old ones.
143 delete the old ones.
144 """
144 """
145
145
146 # TODO Move old threads to the archive instead of deleting them.
146 # TODO Move old threads to the archive instead of deleting them.
147 # Maybe make some 'old' field in the model to indicate the thread
147 # Maybe make some 'old' field in the model to indicate the thread
148 # must not be shown and be able for replying.
148 # must not be shown and be able for replying.
149
149
150 threads = Thread.objects.all()
150 threads = Thread.objects.all()
151 thread_count = threads.count()
151 thread_count = threads.count()
152
152
153 if thread_count > settings.MAX_THREAD_COUNT:
153 if thread_count > settings.MAX_THREAD_COUNT:
154 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
154 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
155 old_threads = threads[thread_count - num_threads_to_delete:]
155 old_threads = threads[thread_count - num_threads_to_delete:]
156
156
157 map(Thread.delete_with_posts, old_threads)
157 map(Thread.delete_with_posts, old_threads)
158
158
159 def connect_replies(self, post):
159 def connect_replies(self, post):
160 """
160 """
161 Connect replies to a post to show them as a reflink map
161 Connect replies to a post to show them as a reflink map
162 """
162 """
163
163
164 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
164 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
165 post_id = reply_number.group(1)
165 post_id = reply_number.group(1)
166 ref_post = self.filter(id=post_id)
166 ref_post = self.filter(id=post_id)
167 if ref_post.count() > 0:
167 if ref_post.count() > 0:
168 referenced_post = ref_post[0]
168 referenced_post = ref_post[0]
169 referenced_post.referenced_posts.add(post)
169 referenced_post.referenced_posts.add(post)
170 referenced_post.last_edit_time = post.pub_time
170 referenced_post.last_edit_time = post.pub_time
171 referenced_post.save()
171 referenced_post.save()
172
172
173 def _get_page_count(self, thread_count):
173 def _get_page_count(self, thread_count):
174 """
174 """
175 Get number of pages that will be needed for all threads
175 Get number of pages that will be needed for all threads
176 """
176 """
177
177
178 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
178 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
179
179
180 def get_posts_per_day(self):
180 def get_posts_per_day(self):
181 """
181 """
182 Get average count of posts per day for the last 7 days
182 Get average count of posts per day for the last 7 days
183 """
183 """
184
184
185 today = datetime.now().date()
185 today = datetime.now().date()
186 ppd = cache.get(CACHE_KEY_PPD + str(today))
186 ppd = cache.get(CACHE_KEY_PPD + str(today))
187 if ppd:
187 if ppd:
188 return ppd
188 return ppd
189
189
190 posts_per_days = []
190 posts_per_days = []
191 for i in POSTS_PER_DAY_RANGE:
191 for i in POSTS_PER_DAY_RANGE:
192 day_end = today - timedelta(i + 1)
192 day_end = today - timedelta(i + 1)
193 day_start = today - timedelta(i + 2)
193 day_start = today - timedelta(i + 2)
194
194
195 day_time_start = timezone.make_aware(datetime.combine(day_start,
195 day_time_start = timezone.make_aware(datetime.combine(day_start,
196 dtime()), timezone.get_current_timezone())
196 dtime()), timezone.get_current_timezone())
197 day_time_end = timezone.make_aware(datetime.combine(day_end,
197 day_time_end = timezone.make_aware(datetime.combine(day_end,
198 dtime()), timezone.get_current_timezone())
198 dtime()), timezone.get_current_timezone())
199
199
200 posts_per_days.append(float(self.filter(
200 posts_per_days.append(float(self.filter(
201 pub_time__lte=day_time_end,
201 pub_time__lte=day_time_end,
202 pub_time__gte=day_time_start).count()))
202 pub_time__gte=day_time_start).count()))
203
203
204 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
204 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
205 len(posts_per_days))
205 len(posts_per_days))
206 cache.set(CACHE_KEY_PPD, ppd)
206 cache.set(CACHE_KEY_PPD, ppd)
207 return ppd
207 return ppd
208
208
209
209
210 class Post(models.Model):
210 class Post(models.Model):
211 """A post is a message."""
211 """A post is a message."""
212
212
213 objects = PostManager()
213 objects = PostManager()
214
214
215 class Meta:
215 class Meta:
216 app_label = APP_LABEL_BOARDS
216 app_label = APP_LABEL_BOARDS
217
217
218 # TODO Save original file name to some field
218 # TODO Save original file name to some field
219 def _update_image_filename(self, filename):
219 def _update_image_filename(self, filename):
220 """Get unique image filename"""
220 """Get unique image filename"""
221
221
222 path = IMAGES_DIRECTORY
222 path = IMAGES_DIRECTORY
223 new_name = str(int(time.mktime(time.gmtime())))
223 new_name = str(int(time.mktime(time.gmtime())))
224 new_name += str(int(random() * 1000))
224 new_name += str(int(random() * 1000))
225 new_name += FILE_EXTENSION_DELIMITER
225 new_name += FILE_EXTENSION_DELIMITER
226 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
226 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
227
227
228 return os.path.join(path, new_name)
228 return os.path.join(path, new_name)
229
229
230 title = models.CharField(max_length=TITLE_MAX_LENGTH)
230 title = models.CharField(max_length=TITLE_MAX_LENGTH)
231 pub_time = models.DateTimeField()
231 pub_time = models.DateTimeField()
232 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
232 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
233 escape_html=False)
233 escape_html=False)
234
234
235 image_width = models.IntegerField(default=0)
235 image_width = models.IntegerField(default=0)
236 image_height = models.IntegerField(default=0)
236 image_height = models.IntegerField(default=0)
237
237
238 image_pre_width = models.IntegerField(default=0)
239 image_pre_height = models.IntegerField(default=0)
240
238 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
241 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
239 blank=True, sizes=(IMAGE_THUMB_SIZE,),
242 blank=True, sizes=(IMAGE_THUMB_SIZE,),
240 width_field='image_width',
243 width_field='image_width',
241 height_field='image_height')
244 height_field='image_height',
245 preview_width_field='image_pre_width',
246 preview_height_field='image_pre_height')
242
247
243 poster_ip = models.GenericIPAddressField()
248 poster_ip = models.GenericIPAddressField()
244 poster_user_agent = models.TextField()
249 poster_user_agent = models.TextField()
245
250
246 thread = models.ForeignKey('Post', null=True, default=None)
251 thread = models.ForeignKey('Post', null=True, default=None)
247 thread_new = models.ForeignKey('Thread', null=True, default=None)
252 thread_new = models.ForeignKey('Thread', null=True, default=None)
248 last_edit_time = models.DateTimeField()
253 last_edit_time = models.DateTimeField()
249 user = models.ForeignKey('User', null=True, default=None)
254 user = models.ForeignKey('User', null=True, default=None)
250
255
251 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
256 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
252 null=True,
257 null=True,
253 blank=True, related_name='rfp+')
258 blank=True, related_name='rfp+')
254
259
255 def __unicode__(self):
260 def __unicode__(self):
256 return '#' + str(self.id) + ' ' + self.title + ' (' + \
261 return '#' + str(self.id) + ' ' + self.title + ' (' + \
257 self.text.raw[:50] + ')'
262 self.text.raw[:50] + ')'
258
263
259 def get_title(self):
264 def get_title(self):
260 title = self.title
265 title = self.title
261 if len(title) == 0:
266 if len(title) == 0:
262 title = self.text.raw[:20]
267 title = self.text.raw[:20]
263
268
264 return title
269 return title
265
270
266 def get_sorted_referenced_posts(self):
271 def get_sorted_referenced_posts(self):
267 return self.referenced_posts.order_by('id')
272 return self.referenced_posts.order_by('id')
268
273
269 def is_referenced(self):
274 def is_referenced(self):
270 return self.referenced_posts.all().exists()
275 return self.referenced_posts.all().exists()
271
276
272 def is_opening(self):
277 def is_opening(self):
273 return self.thread_new.get_replies()[0] == self
278 return self.thread_new.get_replies()[0] == self
274
279
275
280
276 class Thread(models.Model):
281 class Thread(models.Model):
277
282
278 class Meta:
283 class Meta:
279 app_label = APP_LABEL_BOARDS
284 app_label = APP_LABEL_BOARDS
280
285
281 tags = models.ManyToManyField('Tag')
286 tags = models.ManyToManyField('Tag')
282 bump_time = models.DateTimeField()
287 bump_time = models.DateTimeField()
283 last_edit_time = models.DateTimeField()
288 last_edit_time = models.DateTimeField()
284 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
289 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
285 blank=True, related_name='tre+')
290 blank=True, related_name='tre+')
286
291
287 def get_tags(self):
292 def get_tags(self):
288 """
293 """
289 Get a sorted tag list
294 Get a sorted tag list
290 """
295 """
291
296
292 return self.tags.order_by('name')
297 return self.tags.order_by('name')
293
298
294 def bump(self):
299 def bump(self):
295 """
300 """
296 Bump (move to up) thread
301 Bump (move to up) thread
297 """
302 """
298
303
299 if self.can_bump():
304 if self.can_bump():
300 self.bump_time = timezone.now()
305 self.bump_time = timezone.now()
301
306
302 def get_reply_count(self):
307 def get_reply_count(self):
303 return self.replies.count()
308 return self.replies.count()
304
309
305 def get_images_count(self):
310 def get_images_count(self):
306 return self.replies.filter(image_width__gt=0).count()
311 return self.replies.filter(image_width__gt=0).count()
307
312
308 def can_bump(self):
313 def can_bump(self):
309 """
314 """
310 Check if the thread can be bumped by replying
315 Check if the thread can be bumped by replying
311 """
316 """
312
317
313 post_count = self.get_reply_count()
318 post_count = self.get_reply_count()
314
319
315 return post_count < settings.MAX_POSTS_PER_THREAD
320 return post_count < settings.MAX_POSTS_PER_THREAD
316
321
317 def delete_with_posts(self):
322 def delete_with_posts(self):
318 """
323 """
319 Completely delete thread and all its posts
324 Completely delete thread and all its posts
320 """
325 """
321
326
322 if self.replies.count() > 0:
327 if self.replies.count() > 0:
323 map(Post.objects.delete_post, self.replies.all())
328 map(Post.objects.delete_post, self.replies.all())
324
329
325 self.delete()
330 self.delete()
326
331
327 def get_last_replies(self):
332 def get_last_replies(self):
328 """
333 """
329 Get last replies, not including opening post
334 Get last replies, not including opening post
330 """
335 """
331
336
332 if settings.LAST_REPLIES_COUNT > 0:
337 if settings.LAST_REPLIES_COUNT > 0:
333 reply_count = self.get_reply_count()
338 reply_count = self.get_reply_count()
334
339
335 if reply_count > 0:
340 if reply_count > 0:
336 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
341 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
337 reply_count - 1)
342 reply_count - 1)
338 last_replies = self.replies.all().order_by('pub_time')[
343 last_replies = self.replies.all().order_by('pub_time')[
339 reply_count - reply_count_to_show:]
344 reply_count - reply_count_to_show:]
340
345
341 return last_replies
346 return last_replies
342
347
343 def get_replies(self):
348 def get_replies(self):
344 """
349 """
345 Get sorted thread posts
350 Get sorted thread posts
346 """
351 """
347
352
348 return self.replies.all().order_by('pub_time')
353 return self.replies.all().order_by('pub_time')
349
354
350 def add_tag(self, tag):
355 def add_tag(self, tag):
351 """
356 """
352 Connect thread to a tag and tag to a thread
357 Connect thread to a tag and tag to a thread
353 """
358 """
354
359
355 self.tags.add(tag)
360 self.tags.add(tag)
356 tag.threads.add(self)
361 tag.threads.add(self)
357
362
358 def get_opening_post(self):
363 def get_opening_post(self):
359 """
364 """
360 Get first post of the thread
365 Get first post of the thread
361 """
366 """
362
367
363 return self.get_replies()[0]
368 return self.get_replies()[0]
364
369
365 def __unicode__(self):
370 def __unicode__(self):
366 return str(self.get_replies()[0].id)
371 return str(self.get_replies()[0].id)
367
372
368 def get_pub_time(self):
373 def get_pub_time(self):
369 """
374 """
370 Thread does not have its own pub time, so we need to get it from
375 Thread does not have its own pub time, so we need to get it from
371 the opening post
376 the opening post
372 """
377 """
373
378
374 return self.get_opening_post().pub_time
379 return self.get_opening_post().pub_time
@@ -1,358 +1,356 b''
1 html {
1 html {
2 background: #555;
2 background: #555;
3 color: #ffffff;
3 color: #ffffff;
4 }
4 }
5
5
6 #admin_panel {
6 #admin_panel {
7 background: #FF0000;
7 background: #FF0000;
8 color: #00FF00
8 color: #00FF00
9 }
9 }
10
10
11 .input_field {
11 .input_field {
12
12
13 }
13 }
14
14
15 .input_field_name {
15 .input_field_name {
16
16
17 }
17 }
18
18
19 .input_field_error {
19 .input_field_error {
20 color: #FF0000;
20 color: #FF0000;
21 }
21 }
22
22
23 .title {
23 .title {
24 font-weight: bold;
24 font-weight: bold;
25 color: #ffcc00;
25 color: #ffcc00;
26 font-size: 2ex;
26 font-size: 2ex;
27 }
27 }
28
28
29 .link, a {
29 .link, a {
30 color: #afdcec;
30 color: #afdcec;
31 }
31 }
32
32
33 .block {
33 .block {
34 display: inline-block;
34 display: inline-block;
35 vertical-align: top;
35 vertical-align: top;
36 }
36 }
37
37
38 .tag {
38 .tag {
39 color: #b4cfec;
39 color: #b4cfec;
40 }
40 }
41
41
42 .post_id {
42 .post_id {
43 color: #fff380;
43 color: #fff380;
44 }
44 }
45
45
46 .post, .dead_post, #posts-table {
46 .post, .dead_post, #posts-table {
47 background: #333;
47 background: #333;
48 margin: 5px;
48 margin: 5px;
49 padding: 10px;
49 padding: 10px;
50 border: solid 1px #888;
50 border: solid 1px #888;
51 clear: left;
51 clear: left;
52 word-wrap: break-word;
52 word-wrap: break-word;
53 }
53 }
54
54
55 .metadata {
55 .metadata {
56 padding-top: 5px;
56 padding-top: 5px;
57 margin-top: 10px;
57 margin-top: 10px;
58 border-top: solid 1px #666;
58 border-top: solid 1px #666;
59 font-size: 0.9em;
59 font-size: 0.9em;
60 color: #ddd;
60 color: #ddd;
61 }
61 }
62
62
63 .navigation_panel, .tag_info {
63 .navigation_panel, .tag_info {
64 background: #444;
64 background: #444;
65 margin: 5px;
65 margin: 5px;
66 padding: 10px;
66 padding: 10px;
67 border: solid 1px #888;
67 border: solid 1px #888;
68 color: #eee;
68 color: #eee;
69 }
69 }
70
70
71 .navigation_panel .link {
71 .navigation_panel .link {
72 border-right: 1px solid #fff;
72 border-right: 1px solid #fff;
73 font-weight: bold;
73 font-weight: bold;
74 margin-right: 1ex;
74 margin-right: 1ex;
75 padding-right: 1ex;
75 padding-right: 1ex;
76 }
76 }
77 .navigation_panel .link:last-child {
77 .navigation_panel .link:last-child {
78 border-left: 1px solid #fff;
78 border-left: 1px solid #fff;
79 border-right: none;
79 border-right: none;
80 float: right;
80 float: right;
81 margin-left: 1ex;
81 margin-left: 1ex;
82 margin-right: 0;
82 margin-right: 0;
83 padding-left: 1ex;
83 padding-left: 1ex;
84 padding-right: 0;
84 padding-right: 0;
85 }
85 }
86
86
87 .navigation_panel::after, .post::after {
87 .navigation_panel::after, .post::after {
88 clear: both;
88 clear: both;
89 content: ".";
89 content: ".";
90 display: block;
90 display: block;
91 height: 0;
91 height: 0;
92 line-height: 0;
92 line-height: 0;
93 visibility: hidden;
93 visibility: hidden;
94 }
94 }
95
95
96 p {
96 p {
97 margin-top: .5em;
97 margin-top: .5em;
98 margin-bottom: .5em;
98 margin-bottom: .5em;
99 }
99 }
100
100
101 .post-form-w {
101 .post-form-w {
102 display: table;
102 display: table;
103 background: #333344;
103 background: #333344;
104 border: solid 1px #888;
104 border: solid 1px #888;
105 color: #fff;
105 color: #fff;
106 padding: 10px;
106 padding: 10px;
107 margin: 5px;
107 margin: 5px;
108 }
108 }
109
109
110 .form-row {
110 .form-row {
111 display: table-row;
111 display: table-row;
112 }
112 }
113
113
114 .form-label, .form-input, .form-errors {
114 .form-label, .form-input, .form-errors {
115 display: table-cell;
115 display: table-cell;
116 }
116 }
117
117
118 .form-label {
118 .form-label {
119 padding: .25em 1ex .25em 0;
119 padding: .25em 1ex .25em 0;
120 vertical-align: top;
120 vertical-align: top;
121 }
121 }
122
122
123 .form-input {
123 .form-input {
124 padding: .25em 0;
124 padding: .25em 0;
125 }
125 }
126
126
127 .form-errors {
127 .form-errors {
128 font-weight: bolder;
128 font-weight: bolder;
129 vertical-align: middle;
129 vertical-align: middle;
130 }
130 }
131
131
132 .post-form input, .post-form textarea {
132 .post-form input, .post-form textarea {
133 background: #333;
133 background: #333;
134 color: #fff;
134 color: #fff;
135 border: solid 1px;
135 border: solid 1px;
136 padding: 0;
136 padding: 0;
137 width: 100%;
137 width: 100%;
138 font: medium sans;
138 font: medium sans;
139 }
139 }
140
140
141 .form-submit {
141 .form-submit {
142 display: table;
142 display: table;
143 margin-bottom: 1ex;
143 margin-bottom: 1ex;
144 }
144 }
145
145
146 .form-title {
146 .form-title {
147 font-weight: bold;
147 font-weight: bold;
148 font-size: 2.5ex;
148 font-size: 2.5ex;
149 text-decoration: underline;
149 text-decoration: underline;
150 }
150 }
151
151
152 input[type="submit"] {
152 input[type="submit"] {
153 background: #222;
153 background: #222;
154 border: solid 2px #fff;
154 border: solid 2px #fff;
155 color: #fff;
155 color: #fff;
156 padding: 0.5ex;
156 padding: 0.5ex;
157 }
157 }
158
158
159 input[type="submit"]:hover {
159 input[type="submit"]:hover {
160 background: #060;
160 background: #060;
161 }
161 }
162
162
163 blockquote {
163 blockquote {
164 border-left: solid 2px;
164 border-left: solid 2px;
165 padding-left: 5px;
165 padding-left: 5px;
166 color: #B1FB17;
166 color: #B1FB17;
167 margin: 0;
167 margin: 0;
168 }
168 }
169
169
170 .post > .image {
170 .post > .image {
171 float: left;
171 float: left;
172 margin: 0 1ex .5ex 0;
172 margin: 0 1ex .5ex 0;
173 min-width: 1px;
173 min-width: 1px;
174 text-align: center;
174 text-align: center;
175 display: table-row;
175 display: table-row;
176
177 height: 150px;
178 }
176 }
179
177
180 .post > .metadata {
178 .post > .metadata {
181 clear: left;
179 clear: left;
182 }
180 }
183
181
184 .get {
182 .get {
185 font-weight: bold;
183 font-weight: bold;
186 color: #d55;
184 color: #d55;
187 }
185 }
188
186
189 * {
187 * {
190 text-decoration: none;
188 text-decoration: none;
191 }
189 }
192
190
193 .dead_post {
191 .dead_post {
194 background-color: #442222;
192 background-color: #442222;
195 }
193 }
196
194
197 .mark_btn {
195 .mark_btn {
198 border: 1px solid;
196 border: 1px solid;
199 min-width: 2ex;
197 min-width: 2ex;
200 padding: 2px 2ex;
198 padding: 2px 2ex;
201 }
199 }
202
200
203 .mark_btn:hover {
201 .mark_btn:hover {
204 background: #555;
202 background: #555;
205 }
203 }
206
204
207 .quote {
205 .quote {
208 color: #92cf38;
206 color: #92cf38;
209 font-style: italic;
207 font-style: italic;
210 }
208 }
211
209
212 .spoiler {
210 .spoiler {
213 background: white;
211 background: white;
214 color: white;
212 color: white;
215 }
213 }
216
214
217 .spoiler:hover {
215 .spoiler:hover {
218 color: black;
216 color: black;
219 }
217 }
220
218
221 .comment {
219 .comment {
222 color: #eb2;
220 color: #eb2;
223 font-style: italic;
221 font-style: italic;
224 }
222 }
225
223
226 a:hover {
224 a:hover {
227 text-decoration: underline;
225 text-decoration: underline;
228 }
226 }
229
227
230 .last-replies {
228 .last-replies {
231 margin-left: 3ex;
229 margin-left: 3ex;
232 }
230 }
233
231
234 .thread {
232 .thread {
235 margin-bottom: 3ex;
233 margin-bottom: 3ex;
236 }
234 }
237
235
238 .post:target {
236 .post:target {
239 border: solid 2px white;
237 border: solid 2px white;
240 }
238 }
241
239
242 pre{
240 pre{
243 white-space:pre-wrap
241 white-space:pre-wrap
244 }
242 }
245
243
246 li {
244 li {
247 list-style-position: inside;
245 list-style-position: inside;
248 }
246 }
249
247
250 .fancybox-skin {
248 .fancybox-skin {
251 position: relative;
249 position: relative;
252 background-color: #fff;
250 background-color: #fff;
253 color: #ddd;
251 color: #ddd;
254 text-shadow: none;
252 text-shadow: none;
255 }
253 }
256
254
257 .fancybox-image {
255 .fancybox-image {
258 border: 1px solid black;
256 border: 1px solid black;
259 }
257 }
260
258
261 .image-mode-tab {
259 .image-mode-tab {
262 background: #444;
260 background: #444;
263 color: #eee;
261 color: #eee;
264 display: table;
262 display: table;
265 margin: 5px;
263 margin: 5px;
266 padding: 5px;
264 padding: 5px;
267 border: 1px solid #888;
265 border: 1px solid #888;
268 }
266 }
269
267
270 .image-mode-tab > label {
268 .image-mode-tab > label {
271 margin: 0 1ex;
269 margin: 0 1ex;
272 }
270 }
273
271
274 .image-mode-tab > label > input {
272 .image-mode-tab > label > input {
275 margin-right: .5ex;
273 margin-right: .5ex;
276 }
274 }
277
275
278 #posts-table {
276 #posts-table {
279 margin: 5px;
277 margin: 5px;
280 }
278 }
281
279
282 .tag_info {
280 .tag_info {
283 display: table;
281 display: table;
284 }
282 }
285
283
286 .tag_info > h2 {
284 .tag_info > h2 {
287 margin: 0;
285 margin: 0;
288 }
286 }
289
287
290 .post-info {
288 .post-info {
291 color: #ddd;
289 color: #ddd;
292 }
290 }
293
291
294 .moderator_info {
292 .moderator_info {
295 color: #e99d41;
293 color: #e99d41;
296 border: dashed 1px;
294 border: dashed 1px;
297 padding: 3px;
295 padding: 3px;
298 }
296 }
299
297
300 .refmap {
298 .refmap {
301 font-size: 0.9em;
299 font-size: 0.9em;
302 color: #ccc;
300 color: #ccc;
303 margin-top: 1em;
301 margin-top: 1em;
304 }
302 }
305
303
306 .fav {
304 .fav {
307 color: yellow;
305 color: yellow;
308 }
306 }
309
307
310 .not_fav {
308 .not_fav {
311 color: #ccc;
309 color: #ccc;
312 }
310 }
313
311
314 .role {
312 .role {
315 text-decoration: underline;
313 text-decoration: underline;
316 }
314 }
317
315
318 .form-email {
316 .form-email {
319 display: none;
317 display: none;
320 }
318 }
321
319
322 .footer {
320 .footer {
323 margin: 5px;
321 margin: 5px;
324 }
322 }
325
323
326 .bar-value {
324 .bar-value {
327 background: rgba(50, 55, 164, 0.45);
325 background: rgba(50, 55, 164, 0.45);
328 font-size: 0.9em;
326 font-size: 0.9em;
329 height: 1.5em;
327 height: 1.5em;
330 }
328 }
331
329
332 .bar-bg {
330 .bar-bg {
333 position: relative;
331 position: relative;
334 border: solid 1px #888;
332 border: solid 1px #888;
335 margin: 5px;
333 margin: 5px;
336 overflow: hidden;
334 overflow: hidden;
337 }
335 }
338
336
339 .bar-text {
337 .bar-text {
340 padding: 2px;
338 padding: 2px;
341 position: absolute;
339 position: absolute;
342 left: 0;
340 left: 0;
343 top: 0;
341 top: 0;
344 }
342 }
345
343
346 .page_link {
344 .page_link {
347 display: table;
345 display: table;
348 background: #444;
346 background: #444;
349 margin: 5px;
347 margin: 5px;
350 border: solid 1px #888;
348 border: solid 1px #888;
351 padding: 5px;
349 padding: 5px;
352 font-weight: bolder;
350 font-weight: bolder;
353 color: #eee;
351 color: #eee;
354 }
352 }
355
353
356 .skipped_replies {
354 .skipped_replies {
357 margin: 5px;
355 margin: 5px;
358 }
356 }
@@ -1,344 +1,342 b''
1 html {
1 html {
2 background: rgb(238, 238, 238);
2 background: rgb(238, 238, 238);
3 color: rgb(51, 51, 51);
3 color: rgb(51, 51, 51);
4 }
4 }
5
5
6 #admin_panel {
6 #admin_panel {
7 background: #FF0000;
7 background: #FF0000;
8 color: #00FF00
8 color: #00FF00
9 }
9 }
10
10
11 .input_field {
11 .input_field {
12
12
13 }
13 }
14
14
15 .input_field_name {
15 .input_field_name {
16
16
17 }
17 }
18
18
19 .input_field_error {
19 .input_field_error {
20 color: #FF0000;
20 color: #FF0000;
21 }
21 }
22
22
23
23
24 .title {
24 .title {
25 font-weight: bold;
25 font-weight: bold;
26 color: #333;
26 color: #333;
27 font-size: 2ex;
27 font-size: 2ex;
28 }
28 }
29
29
30 .link, a {
30 .link, a {
31 color: rgb(255, 102, 0);
31 color: rgb(255, 102, 0);
32 }
32 }
33
33
34 .block {
34 .block {
35 display: inline-block;
35 display: inline-block;
36 vertical-align: top;
36 vertical-align: top;
37 }
37 }
38
38
39 .tag {
39 .tag {
40 color: #222;
40 color: #222;
41 }
41 }
42
42
43 .post_id:hover {
43 .post_id:hover {
44 color: #11f;
44 color: #11f;
45 }
45 }
46
46
47 .post_id {
47 .post_id {
48 color: #444;
48 color: #444;
49 }
49 }
50
50
51 .post, .dead_post, #posts-table {
51 .post, .dead_post, #posts-table {
52 margin: 5px;
52 margin: 5px;
53 padding: 10px;
53 padding: 10px;
54 background: rgb(221, 221, 221);
54 background: rgb(221, 221, 221);
55 border: 1px solid rgb(204, 204, 204);
55 border: 1px solid rgb(204, 204, 204);
56 border-radius: 5px 5px 5px 5px;
56 border-radius: 5px 5px 5px 5px;
57 clear: left;
57 clear: left;
58 word-wrap: break-word;
58 word-wrap: break-word;
59 display: table;
59 display: table;
60 }
60 }
61
61
62 .metadata {
62 .metadata {
63 padding: 5px;
63 padding: 5px;
64 margin-top: 10px;
64 margin-top: 10px;
65 border: solid 1px #666;
65 border: solid 1px #666;
66 font-size: 0.9em;
66 font-size: 0.9em;
67 display: table;
67 display: table;
68 }
68 }
69
69
70 .navigation_panel, .tag_info, .page_link {
70 .navigation_panel, .tag_info, .page_link {
71 margin: 5px;
71 margin: 5px;
72 padding: 10px;
72 padding: 10px;
73 border: 1px solid rgb(204, 204, 204);
73 border: 1px solid rgb(204, 204, 204);
74 border-radius: 5px 5px 5px 5px;
74 border-radius: 5px 5px 5px 5px;
75 }
75 }
76
76
77 .navigation_panel .link {
77 .navigation_panel .link {
78 border-right: 1px solid #000;
78 border-right: 1px solid #000;
79 font-weight: bold;
79 font-weight: bold;
80 margin-right: 1ex;
80 margin-right: 1ex;
81 padding-right: 1ex;
81 padding-right: 1ex;
82 }
82 }
83 .navigation_panel .link:last-child {
83 .navigation_panel .link:last-child {
84 border-left: 1px solid #000;
84 border-left: 1px solid #000;
85 border-right: none;
85 border-right: none;
86 float: right;
86 float: right;
87 margin-left: 1ex;
87 margin-left: 1ex;
88 margin-right: 0;
88 margin-right: 0;
89 padding-left: 1ex;
89 padding-left: 1ex;
90 padding-right: 0;
90 padding-right: 0;
91 }
91 }
92
92
93 .navigation_panel::after, .post::after {
93 .navigation_panel::after, .post::after {
94 clear: both;
94 clear: both;
95 content: ".";
95 content: ".";
96 display: block;
96 display: block;
97 height: 0;
97 height: 0;
98 line-height: 0;
98 line-height: 0;
99 visibility: hidden;
99 visibility: hidden;
100 }
100 }
101
101
102 p {
102 p {
103 margin-top: .5em;
103 margin-top: .5em;
104 margin-bottom: .5em;
104 margin-bottom: .5em;
105 }
105 }
106
106
107 .post-form-w {
107 .post-form-w {
108 display: table;
108 display: table;
109 padding: 10px;
109 padding: 10px;
110 margin: 5px
110 margin: 5px
111 }
111 }
112
112
113 .form-row {
113 .form-row {
114 display: table-row;
114 display: table-row;
115 }
115 }
116
116
117 .form-label, .form-input, .form-errors {
117 .form-label, .form-input, .form-errors {
118 display: table-cell;
118 display: table-cell;
119 }
119 }
120
120
121 .form-label {
121 .form-label {
122 padding: .25em 1ex .25em 0;
122 padding: .25em 1ex .25em 0;
123 vertical-align: top;
123 vertical-align: top;
124 }
124 }
125
125
126 .form-input {
126 .form-input {
127 padding: .25em 0;
127 padding: .25em 0;
128 }
128 }
129
129
130 .form-errors {
130 .form-errors {
131 padding-left: 1ex;
131 padding-left: 1ex;
132 font-weight: bold;
132 font-weight: bold;
133 vertical-align: middle;
133 vertical-align: middle;
134 }
134 }
135
135
136 .post-form input, .post-form textarea {
136 .post-form input, .post-form textarea {
137 background: #fff;
137 background: #fff;
138 color: #000;
138 color: #000;
139 border: solid 1px;
139 border: solid 1px;
140 padding: 0;
140 padding: 0;
141 width: 100%;
141 width: 100%;
142 font: medium sans;
142 font: medium sans;
143 }
143 }
144
144
145 .form-submit {
145 .form-submit {
146 border-bottom: 2px solid #ddd;
146 border-bottom: 2px solid #ddd;
147 margin-bottom: .5em;
147 margin-bottom: .5em;
148 padding-bottom: .5em;
148 padding-bottom: .5em;
149 }
149 }
150
150
151 .form-title {
151 .form-title {
152 font-weight: bold;
152 font-weight: bold;
153 }
153 }
154
154
155 input[type="submit"] {
155 input[type="submit"] {
156 background: #fff;
156 background: #fff;
157 border: solid 1px #000;
157 border: solid 1px #000;
158 color: #000;
158 color: #000;
159 }
159 }
160
160
161 blockquote {
161 blockquote {
162 border-left: solid 2px;
162 border-left: solid 2px;
163 padding-left: 5px;
163 padding-left: 5px;
164 color: #B1FB17;
164 color: #B1FB17;
165 margin: 0;
165 margin: 0;
166 }
166 }
167
167
168 .post > .image {
168 .post > .image {
169 float: left;
169 float: left;
170 margin: 0 1ex .5ex 0;
170 margin: 0 1ex .5ex 0;
171 min-width: 1px;
171 min-width: 1px;
172 text-align: center;
172 text-align: center;
173 display: table-row;
173 display: table-row;
174
175 height: 150px;
176 }
174 }
177
175
178 .post > .metadata {
176 .post > .metadata {
179 clear: left;
177 clear: left;
180 }
178 }
181
179
182 .get {
180 .get {
183 font-weight: bold;
181 font-weight: bold;
184 color: #d55;
182 color: #d55;
185 }
183 }
186
184
187 * {
185 * {
188 text-decoration: none;
186 text-decoration: none;
189 }
187 }
190
188
191 .dead_post {
189 .dead_post {
192 background-color: #ecc;
190 background-color: #ecc;
193 }
191 }
194
192
195 .quote {
193 .quote {
196 color: #080;
194 color: #080;
197 font-style: italic;
195 font-style: italic;
198 }
196 }
199
197
200 .spoiler {
198 .spoiler {
201 background: white;
199 background: white;
202 color: white;
200 color: white;
203 }
201 }
204
202
205 .spoiler:hover {
203 .spoiler:hover {
206 color: black;
204 color: black;
207 }
205 }
208
206
209 .comment {
207 .comment {
210 color: #8B6914;
208 color: #8B6914;
211 font-style: italic;
209 font-style: italic;
212 }
210 }
213
211
214 a:hover {
212 a:hover {
215 text-decoration: underline;
213 text-decoration: underline;
216 }
214 }
217
215
218 .last-replies {
216 .last-replies {
219 margin-left: 3ex;
217 margin-left: 3ex;
220 }
218 }
221
219
222 .thread {
220 .thread {
223 margin-bottom: 3ex;
221 margin-bottom: 3ex;
224 }
222 }
225
223
226 .post:target {
224 .post:target {
227 border: solid 2px black;
225 border: solid 2px black;
228 }
226 }
229
227
230 pre{
228 pre{
231 white-space:pre-wrap
229 white-space:pre-wrap
232 }
230 }
233
231
234 li {
232 li {
235 list-style-position: inside;
233 list-style-position: inside;
236 }
234 }
237
235
238 .fancybox-skin {
236 .fancybox-skin {
239 position: relative;
237 position: relative;
240 background-color: #fff;
238 background-color: #fff;
241 color: #ddd;
239 color: #ddd;
242 text-shadow: none;
240 text-shadow: none;
243 }
241 }
244
242
245 .fancybox-image {
243 .fancybox-image {
246 border: 1px solid black;
244 border: 1px solid black;
247 }
245 }
248
246
249 .image-mode-tab {
247 .image-mode-tab {
250 display: table;
248 display: table;
251 margin: 5px;
249 margin: 5px;
252 padding: 5px;
250 padding: 5px;
253 background: rgb(221, 221, 221);
251 background: rgb(221, 221, 221);
254 border: 1px solid rgb(204, 204, 204);
252 border: 1px solid rgb(204, 204, 204);
255 border-radius: 5px 5px 5px 5px;
253 border-radius: 5px 5px 5px 5px;
256 }
254 }
257
255
258 .image-mode-tab > label {
256 .image-mode-tab > label {
259 margin: 0 1ex;
257 margin: 0 1ex;
260 }
258 }
261
259
262 .image-mode-tab > label > input {
260 .image-mode-tab > label > input {
263 margin-right: .5ex;
261 margin-right: .5ex;
264 }
262 }
265
263
266 #posts-table {
264 #posts-table {
267 margin: 5px;
265 margin: 5px;
268 }
266 }
269
267
270 .tag_info, .page_link {
268 .tag_info, .page_link {
271 display: table;
269 display: table;
272 }
270 }
273
271
274 .tag_info > h2 {
272 .tag_info > h2 {
275 margin: 0;
273 margin: 0;
276 }
274 }
277
275
278 .moderator_info {
276 .moderator_info {
279 color: #e99d41;
277 color: #e99d41;
280 border: dashed 1px;
278 border: dashed 1px;
281 padding: 3px;
279 padding: 3px;
282 }
280 }
283
281
284 .refmap {
282 .refmap {
285 font-size: 0.9em;
283 font-size: 0.9em;
286 color: #444;
284 color: #444;
287 margin-top: 1em;
285 margin-top: 1em;
288 }
286 }
289
287
290 input[type="submit"]:hover {
288 input[type="submit"]:hover {
291 background: #ccc;
289 background: #ccc;
292 }
290 }
293
291
294
292
295 .fav {
293 .fav {
296 color: rgb(255, 102, 0);
294 color: rgb(255, 102, 0);
297 }
295 }
298
296
299 .not_fav {
297 .not_fav {
300 color: #555;
298 color: #555;
301 }
299 }
302
300
303 .role {
301 .role {
304 text-decoration: underline;
302 text-decoration: underline;
305 }
303 }
306
304
307 .form-email {
305 .form-email {
308 display: none;
306 display: none;
309 }
307 }
310
308
311 .mark_btn {
309 .mark_btn {
312 padding: 2px 2ex;
310 padding: 2px 2ex;
313 border: 1px solid;
311 border: 1px solid;
314 }
312 }
315
313
316 .mark_btn:hover {
314 .mark_btn:hover {
317 background: #ccc;
315 background: #ccc;
318 }
316 }
319
317
320 .bar-value {
318 .bar-value {
321 background: rgba(251, 199, 16, 0.61);
319 background: rgba(251, 199, 16, 0.61);
322 padding: 2px;
320 padding: 2px;
323 font-size: 0.9em;
321 font-size: 0.9em;
324 height: 1.5em;
322 height: 1.5em;
325 }
323 }
326
324
327 .bar-bg {
325 .bar-bg {
328 position: relative;
326 position: relative;
329 border: 1px solid rgb(204, 204, 204);
327 border: 1px solid rgb(204, 204, 204);
330 border-radius: 5px 5px 5px 5px;
328 border-radius: 5px 5px 5px 5px;
331 margin: 5px;
329 margin: 5px;
332 overflow: hidden;
330 overflow: hidden;
333 }
331 }
334
332
335 .bar-text {
333 .bar-text {
336 padding: 2px;
334 padding: 2px;
337 position: absolute;
335 position: absolute;
338 left: 0;
336 left: 0;
339 top: 0;
337 top: 0;
340 }
338 }
341
339
342 .skipped_replies {
340 .skipped_replies {
343 margin: 5px;
341 margin: 5px;
344 }
342 }
@@ -1,244 +1,248 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load board %}
5 {% load board %}
6 {% load static %}
6 {% load static %}
7
7
8 {% block head %}
8 {% block head %}
9 {% if tag %}
9 {% if tag %}
10 <title>Neboard - {{ tag.name }}</title>
10 <title>Neboard - {{ tag.name }}</title>
11 {% else %}
11 {% else %}
12 <title>Neboard</title>
12 <title>Neboard</title>
13 {% endif %}
13 {% endif %}
14
14
15 {% if prev_page %}
15 {% if prev_page %}
16 <link rel="next" href="
16 <link rel="next" href="
17 {% if tag %}
17 {% if tag %}
18 {% url "tag" tag_name=tag page=prev_page %}
18 {% url "tag" tag_name=tag page=prev_page %}
19 {% else %}
19 {% else %}
20 {% url "index" page=prev_page %}
20 {% url "index" page=prev_page %}
21 {% endif %}
21 {% endif %}
22 " />
22 " />
23 {% endif %}
23 {% endif %}
24 {% if next_page %}
24 {% if next_page %}
25 <link rel="next" href="
25 <link rel="next" href="
26 {% if tag %}
26 {% if tag %}
27 {% url "tag" tag_name=tag page=next_page %}
27 {% url "tag" tag_name=tag page=next_page %}
28 {% else %}
28 {% else %}
29 {% url "index" page=next_page %}
29 {% url "index" page=next_page %}
30 {% endif %}
30 {% endif %}
31 " />
31 " />
32 {% endif %}
32 {% endif %}
33
33
34 {% endblock %}
34 {% endblock %}
35
35
36 {% block content %}
36 {% block content %}
37
37
38 {% get_current_language as LANGUAGE_CODE %}
38 {% get_current_language as LANGUAGE_CODE %}
39
39
40 {% if tag %}
40 {% if tag %}
41 <div class="tag_info">
41 <div class="tag_info">
42 <h2>
42 <h2>
43 {% if tag in user.fav_tags.all %}
43 {% if tag in user.fav_tags.all %}
44 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
44 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
45 class="fav">β˜…</a>
45 class="fav">β˜…</a>
46 {% else %}
46 {% else %}
47 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
47 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
48 class="not_fav">β˜…</a>
48 class="not_fav">β˜…</a>
49 {% endif %}
49 {% endif %}
50 #{{ tag.name }}
50 #{{ tag.name }}
51 </h2>
51 </h2>
52 </div>
52 </div>
53 {% endif %}
53 {% endif %}
54
54
55 {% if threads %}
55 {% if threads %}
56 {% if prev_page %}
56 {% if prev_page %}
57 <div class="page_link">
57 <div class="page_link">
58 <a href="
58 <a href="
59 {% if tag %}
59 {% if tag %}
60 {% url "tag" tag_name=tag page=prev_page %}
60 {% url "tag" tag_name=tag page=prev_page %}
61 {% else %}
61 {% else %}
62 {% url "index" page=prev_page %}
62 {% url "index" page=prev_page %}
63 {% endif %}
63 {% endif %}
64 ">{% trans "Previous page" %}</a>
64 ">{% trans "Previous page" %}</a>
65 </div>
65 </div>
66 {% endif %}
66 {% endif %}
67
67
68 {% for thread in threads %}
68 {% for thread in threads %}
69 {% cache 600 thread_short thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
69 {% cache 600 thread_short thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
70 <div class="thread">
70 <div class="thread">
71 {% if thread.bumpable %}
71 {% if thread.bumpable %}
72 <div class="post" id="{{ thread.op.id }}">
72 <div class="post" id="{{ thread.op.id }}">
73 {% else %}
73 {% else %}
74 <div class="post dead_post" id="{{ thread.op.id }}">
74 <div class="post dead_post" id="{{ thread.op.id }}">
75 {% endif %}
75 {% endif %}
76 {% if thread.op.image %}
76 {% if thread.op.image %}
77 <div class="image">
77 <div class="image">
78 <a class="thumb"
78 <a class="thumb"
79 href="{{ thread.op.image.url }}"><img
79 href="{{ thread.op.image.url }}"><img
80 src="{{ thread.op.image.url_200x150 }}"
80 src="{{ thread.op.image.url_200x150 }}"
81 alt="{{ thread.op.id }}"
81 alt="{{ thread.op.id }}"
82 width="{{ thread.op.image_pre_width }}"
83 height="{{ thread.op.image_pre_height }}"
82 data-width="{{ thread.op.image_width }}"
84 data-width="{{ thread.op.image_width }}"
83 data-height="{{ thread.op.image_height }}"/>
85 data-height="{{ thread.op.image_height }}"/>
84 </a>
86 </a>
85 </div>
87 </div>
86 {% endif %}
88 {% endif %}
87 <div class="message">
89 <div class="message">
88 <div class="post-info">
90 <div class="post-info">
89 <span class="title">{{ thread.op.title }}</span>
91 <span class="title">{{ thread.op.title }}</span>
90 <a class="post_id" href="{% url 'thread' thread.op.id %}"
92 <a class="post_id" href="{% url 'thread' thread.op.id %}"
91 >({{ thread.op.id }})</a>
93 >({{ thread.op.id }})</a>
92 [{{ thread.op.pub_time }}]
94 [{{ thread.op.pub_time }}]
93 [<a class="link" href="
95 [<a class="link" href="
94 {% url 'thread' thread.op.id %}#form"
96 {% url 'thread' thread.op.id %}#form"
95 >{% trans "Reply" %}</a>]
97 >{% trans "Reply" %}</a>]
96
98
97 {% if moderator %}
99 {% if moderator %}
98 <span class="moderator_info">
100 <span class="moderator_info">
99 [<a href="
101 [<a href="
100 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
102 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
101 >{% trans 'Delete' %}</a>]
103 >{% trans 'Delete' %}</a>]
102 ({{ thread.thread.poster_ip }})
104 ({{ thread.thread.poster_ip }})
103 [<a href="
105 [<a href="
104 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
106 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
105 >{% trans 'Ban IP' %}</a>]
107 >{% trans 'Ban IP' %}</a>]
106 </span>
108 </span>
107 {% endif %}
109 {% endif %}
108 </div>
110 </div>
109 {% autoescape off %}
111 {% autoescape off %}
110 {{ thread.op.text.rendered|truncatewords_html:50 }}
112 {{ thread.op.text.rendered|truncatewords_html:50 }}
111 {% endautoescape %}
113 {% endautoescape %}
112 {% if thread.op.is_referenced %}
114 {% if thread.op.is_referenced %}
113 <div class="refmap">
115 <div class="refmap">
114 {% trans "Replies" %}:
116 {% trans "Replies" %}:
115 {% for ref_post in thread.op.get_sorted_referenced_posts %}
117 {% for ref_post in thread.op.get_sorted_referenced_posts %}
116 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
118 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
117 >{% if not forloop.last %},{% endif %}
119 >{% if not forloop.last %},{% endif %}
118 {% endfor %}
120 {% endfor %}
119 </div>
121 </div>
120 {% endif %}
122 {% endif %}
121 </div>
123 </div>
122 <div class="metadata">
124 <div class="metadata">
123 {{ thread.thread.get_images_count }} {% trans 'images' %}.
125 {{ thread.thread.get_images_count }} {% trans 'images' %}.
124 {% if thread.thread.tags %}
126 {% if thread.thread.tags %}
125 <span class="tags">
127 <span class="tags">
126 {% for tag in thread.thread.get_tags %}
128 {% for tag in thread.thread.get_tags %}
127 <a class="tag" href="
129 <a class="tag" href="
128 {% url 'tag' tag_name=tag.name %}">
130 {% url 'tag' tag_name=tag.name %}">
129 #{{ tag.name }}</a
131 #{{ tag.name }}</a
130 >{% if not forloop.last %},{% endif %}
132 >{% if not forloop.last %},{% endif %}
131 {% endfor %}
133 {% endfor %}
132 </span>
134 </span>
133 {% endif %}
135 {% endif %}
134 </div>
136 </div>
135 </div>
137 </div>
136 {% if thread.last_replies.exists %}
138 {% if thread.last_replies.exists %}
137 {% if thread.skipped_replies %}
139 {% if thread.skipped_replies %}
138 <div class="skipped_replies">
140 <div class="skipped_replies">
139 <a href="{% url 'thread' thread.op.id %}">
141 <a href="{% url 'thread' thread.op.id %}">
140 {% blocktrans with count=thread.skipped_replies %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
142 {% blocktrans with count=thread.skipped_replies %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
141 </a>
143 </a>
142 </div>
144 </div>
143 {% endif %}
145 {% endif %}
144 <div class="last-replies">
146 <div class="last-replies">
145 {% for post in thread.last_replies %}
147 {% for post in thread.last_replies %}
146 {% if thread.bumpable %}
148 {% if thread.bumpable %}
147 <div class="post" id="{{ post.id }}">
149 <div class="post" id="{{ post.id }}">
148 {% else %}
150 {% else %}
149 <div class="post dead_post" id="{{ post.id }}">
151 <div class="post dead_post" id="{{ post.id }}">
150 {% endif %}
152 {% endif %}
151 {% if post.image %}
153 {% if post.image %}
152 <div class="image">
154 <div class="image">
153 <a class="thumb"
155 <a class="thumb"
154 href="{{ post.image.url }}"><img
156 href="{{ post.image.url }}"><img
155 src=" {{ post.image.url_200x150 }}"
157 src=" {{ post.image.url_200x150 }}"
156 alt="{{ post.id }}"
158 alt="{{ post.id }}"
159 width="{{ post.image_pre_width }}"
160 height="{{ post.image_pre_height }}"
157 data-width="{{ post.image_width }}"
161 data-width="{{ post.image_width }}"
158 data-height="{{ post.image_height }}"/>
162 data-height="{{ post.image_height }}"/>
159 </a>
163 </a>
160 </div>
164 </div>
161 {% endif %}
165 {% endif %}
162 <div class="message">
166 <div class="message">
163 <div class="post-info">
167 <div class="post-info">
164 <span class="title">{{ post.title }}</span>
168 <span class="title">{{ post.title }}</span>
165 <a class="post_id" href="
169 <a class="post_id" href="
166 {% url 'thread' thread.op.id %}#{{ post.id }}">
170 {% url 'thread' thread.op.id %}#{{ post.id }}">
167 ({{ post.id }})</a>
171 ({{ post.id }})</a>
168 [{{ post.pub_time }}]
172 [{{ post.pub_time }}]
169 </div>
173 </div>
170 {% autoescape off %}
174 {% autoescape off %}
171 {{ post.text.rendered|truncatewords_html:50 }}
175 {{ post.text.rendered|truncatewords_html:50 }}
172 {% endautoescape %}
176 {% endautoescape %}
173 </div>
177 </div>
174 {% if post.is_referenced %}
178 {% if post.is_referenced %}
175 <div class="refmap">
179 <div class="refmap">
176 {% trans "Replies" %}:
180 {% trans "Replies" %}:
177 {% for ref_post in post.get_sorted_referenced_posts %}
181 {% for ref_post in post.get_sorted_referenced_posts %}
178 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
182 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
179 >{% if not forloop.last %},{% endif %}
183 >{% if not forloop.last %},{% endif %}
180 {% endfor %}
184 {% endfor %}
181 </div>
185 </div>
182 {% endif %}
186 {% endif %}
183 </div>
187 </div>
184 {% endfor %}
188 {% endfor %}
185 </div>
189 </div>
186 {% endif %}
190 {% endif %}
187 </div>
191 </div>
188 {% endcache %}
192 {% endcache %}
189 {% endfor %}
193 {% endfor %}
190
194
191 {% if next_page %}
195 {% if next_page %}
192 <div class="page_link">
196 <div class="page_link">
193 <a href="
197 <a href="
194 {% if tag %}
198 {% if tag %}
195 {% url "tag" tag_name=tag page=next_page %}
199 {% url "tag" tag_name=tag page=next_page %}
196 {% else %}
200 {% else %}
197 {% url "index" page=next_page %}
201 {% url "index" page=next_page %}
198 {% endif %}
202 {% endif %}
199 ">{% trans "Next page" %}</a>
203 ">{% trans "Next page" %}</a>
200 </div>
204 </div>
201 {% endif %}
205 {% endif %}
202 {% else %}
206 {% else %}
203 <div class="post">
207 <div class="post">
204 {% trans 'No threads exist. Create the first one!' %}</div>
208 {% trans 'No threads exist. Create the first one!' %}</div>
205 {% endif %}
209 {% endif %}
206
210
207 <div class="post-form-w">
211 <div class="post-form-w">
208 <script src="{% static 'js/panel.js' %}"></script>
212 <script src="{% static 'js/panel.js' %}"></script>
209 <div class="post-form">
213 <div class="post-form">
210 <div class="form-title">{% trans "Create new thread" %}</div>
214 <div class="form-title">{% trans "Create new thread" %}</div>
211 <form enctype="multipart/form-data" method="post">{% csrf_token %}
215 <form enctype="multipart/form-data" method="post">{% csrf_token %}
212 {{ form.as_div }}
216 {{ form.as_div }}
213 <div class="form-submit">
217 <div class="form-submit">
214 <input type="submit" value="{% trans "Post" %}"/>
218 <input type="submit" value="{% trans "Post" %}"/>
215 </div>
219 </div>
216 </form>
220 </form>
217 <div>
221 <div>
218 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
222 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
219 </div>
223 </div>
220 <div><a href="{% url "staticpage" name="help" %}">
224 <div><a href="{% url "staticpage" name="help" %}">
221 {% trans 'Text syntax' %}</a></div>
225 {% trans 'Text syntax' %}</a></div>
222 </div>
226 </div>
223 </div>
227 </div>
224
228
225 {% endblock %}
229 {% endblock %}
226
230
227 {% block metapanel %}
231 {% block metapanel %}
228
232
229 <span class="metapanel">
233 <span class="metapanel">
230 <b><a href="{% url "authors" %}">Neboard</a> 1.4.1</b>
234 <b><a href="{% url "authors" %}">Neboard</a> 1.5</b>
231 {% trans "Pages:" %}
235 {% trans "Pages:" %}
232 {% for page in pages %}
236 {% for page in pages %}
233 [<a href="
237 [<a href="
234 {% if tag %}
238 {% if tag %}
235 {% url "tag" tag_name=tag page=page %}
239 {% url "tag" tag_name=tag page=page %}
236 {% else %}
240 {% else %}
237 {% url "index" page=page %}
241 {% url "index" page=page %}
238 {% endif %}
242 {% endif %}
239 ">{{ page }}</a>]
243 ">{{ page }}</a>]
240 {% endfor %}
244 {% endfor %}
241 [<a href="rss/">RSS</a>]
245 [<a href="rss/">RSS</a>]
242 </span>
246 </span>
243
247
244 {% endblock %}
248 {% endblock %}
@@ -1,128 +1,130 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load static from staticfiles %}
5 {% load static from staticfiles %}
6 {% load board %}
6 {% load board %}
7
7
8 {% block head %}
8 {% block head %}
9 <title>Neboard - {{ thread.get_replies.0.get_title }}</title>
9 <title>Neboard - {{ thread.get_replies.0.get_title }}</title>
10 {% endblock %}
10 {% endblock %}
11
11
12 {% block content %}
12 {% block content %}
13 {% spaceless %}
13 {% spaceless %}
14 {% get_current_language as LANGUAGE_CODE %}
14 {% get_current_language as LANGUAGE_CODE %}
15
15
16 <script src="{% static 'js/thread_update.js' %}"></script>
16 <script src="{% static 'js/thread_update.js' %}"></script>
17 <script src="{% static 'js/thread.js' %}"></script>
17 <script src="{% static 'js/thread.js' %}"></script>
18
18
19 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
19 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
20 {% if bumpable %}
20 {% if bumpable %}
21 <div class="bar-bg">
21 <div class="bar-bg">
22 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
22 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
23 </div>
23 </div>
24 <div class="bar-text">
24 <div class="bar-text">
25 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
25 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
26 </div>
26 </div>
27 </div>
27 </div>
28 {% endif %}
28 {% endif %}
29 <div class="thread">
29 <div class="thread">
30 {% for post in thread.get_replies %}
30 {% for post in thread.get_replies %}
31 {% if bumpable %}
31 {% if bumpable %}
32 <div class="post" id="{{ post.id }}">
32 <div class="post" id="{{ post.id }}">
33 {% else %}
33 {% else %}
34 <div class="post dead_post" id="{{ post.id }}">
34 <div class="post dead_post" id="{{ post.id }}">
35 {% endif %}
35 {% endif %}
36 {% if post.image %}
36 {% if post.image %}
37 <div class="image">
37 <div class="image">
38 <a
38 <a
39 class="thumb"
39 class="thumb"
40 href="{{ post.image.url }}"><img
40 href="{{ post.image.url }}"><img
41 src="{{ post.image.url_200x150 }}"
41 src="{{ post.image.url_200x150 }}"
42 alt="{{ post.id }}"
42 alt="{{ post.id }}"
43 width="{{ post.image_pre_width }}"
44 height="{{ post.image_pre_height }}"
43 data-width="{{ post.image_width }}"
45 data-width="{{ post.image_width }}"
44 data-height="{{ post.image_height }}"/>
46 data-height="{{ post.image_height }}"/>
45 </a>
47 </a>
46 </div>
48 </div>
47 {% endif %}
49 {% endif %}
48 <div class="message">
50 <div class="message">
49 <div class="post-info">
51 <div class="post-info">
50 <span class="title">{{ post.title }}</span>
52 <span class="title">{{ post.title }}</span>
51 <a class="post_id" href="#{{ post.id }}">
53 <a class="post_id" href="#{{ post.id }}">
52 ({{ post.id }})</a>
54 ({{ post.id }})</a>
53 [{{ post.pub_time }}]
55 [{{ post.pub_time }}]
54 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
56 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
55 ; return false;">&gt;&gt;</a>]
57 ; return false;">&gt;&gt;</a>]
56
58
57 {% if moderator %}
59 {% if moderator %}
58 <span class="moderator_info">
60 <span class="moderator_info">
59 [<a href="{% url 'delete' post_id=post.id %}"
61 [<a href="{% url 'delete' post_id=post.id %}"
60 >{% trans 'Delete' %}</a>]
62 >{% trans 'Delete' %}</a>]
61 ({{ post.poster_ip }})
63 ({{ post.poster_ip }})
62 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
64 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
63 >{% trans 'Ban IP' %}</a>]
65 >{% trans 'Ban IP' %}</a>]
64 </span>
66 </span>
65 {% endif %}
67 {% endif %}
66 </div>
68 </div>
67 {% autoescape off %}
69 {% autoescape off %}
68 {{ post.text.rendered }}
70 {{ post.text.rendered }}
69 {% endautoescape %}
71 {% endautoescape %}
70 {% if post.is_referenced %}
72 {% if post.is_referenced %}
71 <div class="refmap">
73 <div class="refmap">
72 {% trans "Replies" %}:
74 {% trans "Replies" %}:
73 {% for ref_post in post.get_sorted_referenced_posts %}
75 {% for ref_post in post.get_sorted_referenced_posts %}
74 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
76 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
75 >{% if not forloop.last %},{% endif %}
77 >{% if not forloop.last %},{% endif %}
76 {% endfor %}
78 {% endfor %}
77 </div>
79 </div>
78 {% endif %}
80 {% endif %}
79 </div>
81 </div>
80 {% if forloop.first %}
82 {% if forloop.first %}
81 <div class="metadata">
83 <div class="metadata">
82 <span class="tags">
84 <span class="tags">
83 {% for tag in thread.get_tags %}
85 {% for tag in thread.get_tags %}
84 <a class="tag" href="{% url 'tag' tag.name %}">
86 <a class="tag" href="{% url 'tag' tag.name %}">
85 #{{ tag.name }}</a
87 #{{ tag.name }}</a
86 >{% if not forloop.last %},{% endif %}
88 >{% if not forloop.last %},{% endif %}
87 {% endfor %}
89 {% endfor %}
88 </span>
90 </span>
89 </div>
91 </div>
90 {% endif %}
92 {% endif %}
91 </div>
93 </div>
92 {% endfor %}
94 {% endfor %}
93 </div>
95 </div>
94 {% endcache %}
96 {% endcache %}
95
97
96 <div class="post-form-w">
98 <div class="post-form-w">
97 <script src="{% static 'js/panel.js' %}"></script>
99 <script src="{% static 'js/panel.js' %}"></script>
98 <div class="form-title">{% trans "Reply to thread" %} #{{ thread.get_opening_post.id }}</div>
100 <div class="form-title">{% trans "Reply to thread" %} #{{ thread.get_opening_post.id }}</div>
99 <div class="post-form">
101 <div class="post-form">
100 <form id="form" enctype="multipart/form-data" method="post"
102 <form id="form" enctype="multipart/form-data" method="post"
101 >{% csrf_token %}
103 >{% csrf_token %}
102 {{ form.as_div }}
104 {{ form.as_div }}
103 <div class="form-submit">
105 <div class="form-submit">
104 <input type="submit" value="{% trans "Post" %}"/>
106 <input type="submit" value="{% trans "Post" %}"/>
105 </div>
107 </div>
106 </form>
108 </form>
107 <div><a href="{% url "staticpage" name="help" %}">
109 <div><a href="{% url "staticpage" name="help" %}">
108 {% trans 'Text syntax' %}</a></div>
110 {% trans 'Text syntax' %}</a></div>
109 </div>
111 </div>
110 </div>
112 </div>
111
113
112 {% endspaceless %}
114 {% endspaceless %}
113 {% endblock %}
115 {% endblock %}
114
116
115 {% block metapanel %}
117 {% block metapanel %}
116
118
117 {% get_current_language as LANGUAGE_CODE %}
119 {% get_current_language as LANGUAGE_CODE %}
118
120
119 <span class="metapanel" data-last-update="{{ last_update }}">
121 <span class="metapanel" data-last-update="{{ last_update }}">
120 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
122 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
121 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
123 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
122 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
124 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
123 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
125 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
124 [<a href="rss/">RSS</a>]
126 [<a href="rss/">RSS</a>]
125 {% endcache %}
127 {% endcache %}
126 </span>
128 </span>
127
129
128 {% endblock %}
130 {% endblock %}
@@ -1,173 +1,216 b''
1 # -*- encoding: utf-8 -*-
1 # -*- encoding: utf-8 -*-
2 """
2 """
3 django-thumbs by Antonio MelΓ©
3 django-thumbs by Antonio MelΓ©
4 http://django.es
4 http://django.es
5 """
5 """
6 from django.core.files.images import ImageFile
6 from django.db.models import ImageField
7 from django.db.models import ImageField
7 from django.db.models.fields.files import ImageFieldFile
8 from django.db.models.fields.files import ImageFieldFile
8 from PIL import Image
9 from PIL import Image
9 from django.core.files.base import ContentFile
10 from django.core.files.base import ContentFile
10 import cStringIO
11 import cStringIO
11
12
12
13
13 def generate_thumb(img, thumb_size, format):
14 def generate_thumb(img, thumb_size, format):
14 """
15 """
15 Generates a thumbnail image and returns a ContentFile object with the thumbnail
16 Generates a thumbnail image and returns a ContentFile object with the thumbnail
16
17
17 Parameters:
18 Parameters:
18 ===========
19 ===========
19 img File object
20 img File object
20
21
21 thumb_size desired thumbnail size, ie: (200,120)
22 thumb_size desired thumbnail size, ie: (200,120)
22
23
23 format format of the original image ('jpeg','gif','png',...)
24 format format of the original image ('jpeg','gif','png',...)
24 (this format will be used for the generated thumbnail, too)
25 (this format will be used for the generated thumbnail, too)
25 """
26 """
26
27
27 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
28 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
28 image = Image.open(img)
29 image = Image.open(img)
29
30
30 # get size
31 # get size
31 thumb_w, thumb_h = thumb_size
32 thumb_w, thumb_h = thumb_size
32 # If you want to generate a square thumbnail
33 # If you want to generate a square thumbnail
33 if thumb_w == thumb_h:
34 if thumb_w == thumb_h:
34 # quad
35 # quad
35 xsize, ysize = image.size
36 xsize, ysize = image.size
36 # get minimum size
37 # get minimum size
37 minsize = min(xsize, ysize)
38 minsize = min(xsize, ysize)
38 # largest square possible in the image
39 # largest square possible in the image
39 xnewsize = (xsize - minsize) / 2
40 xnewsize = (xsize - minsize) / 2
40 ynewsize = (ysize - minsize) / 2
41 ynewsize = (ysize - minsize) / 2
41 # crop it
42 # crop it
42 image2 = image.crop(
43 image2 = image.crop(
43 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
44 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
44 # load is necessary after crop
45 # load is necessary after crop
45 image2.load()
46 image2.load()
46 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
47 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
47 image2.thumbnail(thumb_size, Image.ANTIALIAS)
48 image2.thumbnail(thumb_size, Image.ANTIALIAS)
48 else:
49 else:
49 # not quad
50 # not quad
50 image2 = image
51 image2 = image
51 image2.thumbnail(thumb_size, Image.ANTIALIAS)
52 image2.thumbnail(thumb_size, Image.ANTIALIAS)
52
53
53 io = cStringIO.StringIO()
54 io = cStringIO.StringIO()
54 # PNG and GIF are the same, JPG is JPEG
55 # PNG and GIF are the same, JPG is JPEG
55 if format.upper() == 'JPG':
56 if format.upper() == 'JPG':
56 format = 'JPEG'
57 format = 'JPEG'
57
58
58 image2.save(io, format)
59 image2.save(io, format)
59 return ContentFile(io.getvalue())
60 return ContentFile(io.getvalue())
60
61
61
62
62 class ImageWithThumbsFieldFile(ImageFieldFile):
63 class ImageWithThumbsFieldFile(ImageFieldFile):
63 """
64 """
64 See ImageWithThumbsField for usage example
65 See ImageWithThumbsField for usage example
65 """
66 """
66
67
67 def __init__(self, *args, **kwargs):
68 def __init__(self, *args, **kwargs):
68 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
69 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
69 self.sizes = self.field.sizes
70 self.sizes = self.field.sizes
70
71
71 if self.sizes:
72 if self.sizes:
72 def get_size(self, size):
73 def get_size(self, size):
73 if not self:
74 if not self:
74 return ''
75 return ''
75 else:
76 else:
76 split = self.url.rsplit('.', 1)
77 split = self.url.rsplit('.', 1)
77 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
78 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
78 return thumb_url
79 return thumb_url
79
80
80 for size in self.sizes:
81 for size in self.sizes:
81 (w, h) = size
82 (w, h) = size
82 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
83 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
83
84
84 def save(self, name, content, save=True):
85 def save(self, name, content, save=True):
85 super(ImageWithThumbsFieldFile, self).save(name, content, save)
86 super(ImageWithThumbsFieldFile, self).save(name, content, save)
86
87
87 if self.sizes:
88 if self.sizes:
88 for size in self.sizes:
89 for size in self.sizes:
89 (w, h) = size
90 (w, h) = size
90 split = self.name.rsplit('.', 1)
91 split = self.name.rsplit('.', 1)
91 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
92 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
92
93
93 # you can use another thumbnailing function if you like
94 # you can use another thumbnailing function if you like
94 thumb_content = generate_thumb(content, size, split[1])
95 thumb_content = generate_thumb(content, size, split[1])
95
96
96 thumb_name_ = self.storage.save(thumb_name, thumb_content)
97 thumb_name_ = self.storage.save(thumb_name, thumb_content)
97
98
98 if not thumb_name == thumb_name_:
99 if not thumb_name == thumb_name_:
99 raise ValueError(
100 raise ValueError(
100 'There is already a file named %s' % thumb_name)
101 'There is already a file named %s' % thumb_name)
101
102
102 def delete(self, save=True):
103 def delete(self, save=True):
103 name = self.name
104 name = self.name
104 super(ImageWithThumbsFieldFile, self).delete(save)
105 super(ImageWithThumbsFieldFile, self).delete(save)
105 if self.sizes:
106 if self.sizes:
106 for size in self.sizes:
107 for size in self.sizes:
107 (w, h) = size
108 (w, h) = size
108 split = name.rsplit('.', 1)
109 split = name.rsplit('.', 1)
109 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
110 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
110 try:
111 try:
111 self.storage.delete(thumb_name)
112 self.storage.delete(thumb_name)
112 except:
113 except:
113 pass
114 pass
114
115
115
116
116 class ImageWithThumbsField(ImageField):
117 class ImageWithThumbsField(ImageField):
117 attr_class = ImageWithThumbsFieldFile
118 attr_class = ImageWithThumbsFieldFile
118 """
119 """
119 Usage example:
120 Usage example:
120 ==============
121 ==============
121 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
122 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
122
123
123 To retrieve image URL, exactly the same way as with ImageField:
124 To retrieve image URL, exactly the same way as with ImageField:
124 my_object.photo.url
125 my_object.photo.url
125 To retrieve thumbnails URL's just add the size to it:
126 To retrieve thumbnails URL's just add the size to it:
126 my_object.photo.url_125x125
127 my_object.photo.url_125x125
127 my_object.photo.url_300x200
128 my_object.photo.url_300x200
128
129
129 Note: The 'sizes' attribute is not required. If you don't provide it,
130 Note: The 'sizes' attribute is not required. If you don't provide it,
130 ImageWithThumbsField will act as a normal ImageField
131 ImageWithThumbsField will act as a normal ImageField
131
132
132 How it works:
133 How it works:
133 =============
134 =============
134 For each size in the 'sizes' atribute of the field it generates a
135 For each size in the 'sizes' atribute of the field it generates a
135 thumbnail with that size and stores it following this format:
136 thumbnail with that size and stores it following this format:
136
137
137 available_filename.[width]x[height].extension
138 available_filename.[width]x[height].extension
138
139
139 Where 'available_filename' is the available filename returned by the storage
140 Where 'available_filename' is the available filename returned by the storage
140 backend for saving the original file.
141 backend for saving the original file.
141
142
142 Following the usage example above: For storing a file called "photo.jpg" it saves:
143 Following the usage example above: For storing a file called "photo.jpg" it saves:
143 photo.jpg (original file)
144 photo.jpg (original file)
144 photo.125x125.jpg (first thumbnail)
145 photo.125x125.jpg (first thumbnail)
145 photo.300x200.jpg (second thumbnail)
146 photo.300x200.jpg (second thumbnail)
146
147
147 With the default storage backend if photo.jpg already exists it will use these filenames:
148 With the default storage backend if photo.jpg already exists it will use these filenames:
148 photo_.jpg
149 photo_.jpg
149 photo_.125x125.jpg
150 photo_.125x125.jpg
150 photo_.300x200.jpg
151 photo_.300x200.jpg
151
152
152 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
153 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
153 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
154 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
154
155
155 To do:
156 To do:
156 ======
157 ======
157 Add method to regenerate thubmnails
158 Add method to regenerate thubmnails
158
159
159
160
160 """
161 """
161
162
162 def __init__(self, verbose_name=None, name=None, width_field=None,
163 def __init__(self, verbose_name=None, name=None, width_field=None,
163 height_field=None, sizes=None, **kwargs):
164 height_field=None, sizes=None,
165 preview_width_field=None, preview_height_field=None,
166 **kwargs):
164 self.verbose_name = verbose_name
167 self.verbose_name = verbose_name
165 self.name = name
168 self.name = name
166 self.width_field = width_field
169 self.width_field = width_field
167 self.height_field = height_field
170 self.height_field = height_field
168 self.sizes = sizes
171 self.sizes = sizes
169 super(ImageField, self).__init__(**kwargs)
172 super(ImageField, self).__init__(**kwargs)
170
173
174 if sizes is not None and len(sizes) == 1:
175 self.preview_width_field = preview_width_field
176 self.preview_height_field = preview_height_field
177
178 def update_dimension_fields(self, instance, force=False, *args, **kwargs):
179 """
180 Update original image dimension fields and thumb dimension fields
181 (only if 1 thumb size is defined)
182 """
183
184 super(ImageWithThumbsField, self).update_dimension_fields(instance,
185 force, *args,
186 **kwargs)
187 thumb_width_field = self.preview_width_field
188 thumb_height_field = self.preview_height_field
189
190 if thumb_width_field is None or thumb_height_field is None \
191 or len(self.sizes) != 1:
192 return
193
194 original_width = getattr(instance, self.width_field)
195 original_height = getattr(instance, self.height_field)
196
197 if original_width > 0 and original_height > 0:
198 thumb_width, thumb_height = self.sizes[0]
199
200 w_scale = float(thumb_width) / original_width
201 h_scale = float(thumb_height) / original_height
202 scale_ratio = min(w_scale, h_scale)
203
204 if scale_ratio >= 1:
205 thumb_width_ratio = original_width
206 thumb_height_ratio = original_height
207 else:
208 thumb_width_ratio = int(original_width * scale_ratio)
209 thumb_height_ratio = int(original_height * scale_ratio)
210
211 setattr(instance, thumb_width_field, thumb_width_ratio)
212 setattr(instance, thumb_height_field, thumb_height_ratio)
213
171
214
172 from south.modelsinspector import add_introspection_rules
215 from south.modelsinspector import add_introspection_rules
173 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
216 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
General Comments 0
You need to be logged in to leave comments. Login now