##// END OF EJS Templates
Added user to the posts. This refs #12
neko259 -
r113:aabf6422 1.1
parent child Browse files
Show More
@@ -1,333 +1,335 b''
1 import os
1 import os
2 from random import random
2 from random import random
3 import re
3 import re
4 import time
4 import time
5 import math
5 import math
6
6
7 from django.db import models
7 from django.db import models
8 from django.http import Http404
8 from django.http import Http404
9 from django.utils import timezone
9 from django.utils import timezone
10 from markupfield.fields import MarkupField
10 from markupfield.fields import MarkupField
11 from threading import Thread
11 from threading import Thread
12
12
13 from neboard import settings
13 from neboard import settings
14 import thumbs
14 import thumbs
15
15
16 IMAGE_THUMB_SIZE = (200, 150)
16 IMAGE_THUMB_SIZE = (200, 150)
17
17
18 TITLE_MAX_LENGTH = 50
18 TITLE_MAX_LENGTH = 50
19
19
20 DEFAULT_MARKUP_TYPE = 'markdown'
20 DEFAULT_MARKUP_TYPE = 'markdown'
21
21
22 NO_PARENT = -1
22 NO_PARENT = -1
23 NO_IP = '0.0.0.0'
23 NO_IP = '0.0.0.0'
24 UNKNOWN_UA = ''
24 UNKNOWN_UA = ''
25 ALL_PAGES = -1
25 ALL_PAGES = -1
26 OPENING_POST_POPULARITY_WEIGHT = 2
26 OPENING_POST_POPULARITY_WEIGHT = 2
27 IMAGES_DIRECTORY = 'images/'
27 IMAGES_DIRECTORY = 'images/'
28 FILE_EXTENSION_DELIMITER = '.'
28 FILE_EXTENSION_DELIMITER = '.'
29
29
30 REGEX_PRETTY = re.compile(r'^\d(0)+$')
30 REGEX_PRETTY = re.compile(r'^\d(0)+$')
31 REGEX_SAME = re.compile(r'^(.)\1+$')
31 REGEX_SAME = re.compile(r'^(.)\1+$')
32
32
33 RANK_ADMIN = 0
33 RANK_ADMIN = 0
34 RANK_MODERATOR = 10
34 RANK_MODERATOR = 10
35 RANK_USER = 100
35 RANK_USER = 100
36
36
37
37
38 class User(models.Model):
39
40 user_id = models.CharField(max_length=50)
41 rank = models.IntegerField()
42
43 def save_setting(self, name, value):
44 setting, created = Setting.objects.get_or_create(name=name, user=self)
45 setting.value = value
46 setting.save()
47
48 return setting
49
50 def get_setting(self, name):
51 settings = Setting.objects.filter(name=name, user=self)
52 if len(settings) > 0:
53 setting = settings[0]
54 else:
55 setting = None
56
57 if setting:
58 setting_value = setting.value
59 else:
60 setting_value = None
61
62 return setting_value
63
64 def is_moderator(self):
65 return RANK_MODERATOR >= self.rank
66
67 def __unicode__(self):
68 return self.user_id
69
70
38 class PostManager(models.Manager):
71 class PostManager(models.Manager):
39 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
72 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
40 ip=NO_IP, tags=None):
73 ip=NO_IP, tags=None, user=None):
41 post = self.create(title=title,
74 post = self.create(title=title,
42 text=text,
75 text=text,
43 pub_time=timezone.now(),
76 pub_time=timezone.now(),
44 parent=parent_id,
77 parent=parent_id,
45 image=image,
78 image=image,
46 poster_ip=ip,
79 poster_ip=ip,
47 poster_user_agent=UNKNOWN_UA,
80 poster_user_agent=UNKNOWN_UA,
48 last_edit_time=timezone.now())
81 last_edit_time=timezone.now(),
82 user=user)
49
83
50 if tags:
84 if tags:
51 map(post.tags.add, tags)
85 map(post.tags.add, tags)
52
86
53 if parent_id != NO_PARENT:
87 if parent_id != NO_PARENT:
54 self._bump_thread(parent_id)
88 self._bump_thread(parent_id)
55 else:
89 else:
56 self._delete_old_threads()
90 self._delete_old_threads()
57
91
58 return post
92 return post
59
93
60 def delete_post(self, post):
94 def delete_post(self, post):
61 children = self.filter(parent=post.id)
95 children = self.filter(parent=post.id)
62 for child in children:
96 for child in children:
63 self.delete_post(child)
97 self.delete_post(child)
64 post.delete()
98 post.delete()
65
99
66 def delete_posts_by_ip(self, ip):
100 def delete_posts_by_ip(self, ip):
67 posts = self.filter(poster_ip=ip)
101 posts = self.filter(poster_ip=ip)
68 for post in posts:
102 for post in posts:
69 self.delete_post(post)
103 self.delete_post(post)
70
104
71 def get_threads(self, tag=None, page=ALL_PAGES,
105 def get_threads(self, tag=None, page=ALL_PAGES,
72 order_by='-last_edit_time'):
106 order_by='-last_edit_time'):
73 if tag:
107 if tag:
74 threads = self.filter(parent=NO_PARENT, tags=tag)
108 threads = self.filter(parent=NO_PARENT, tags=tag)
75
109
76 # TODO Throw error 404 if no threads for tag found?
110 # TODO Throw error 404 if no threads for tag found?
77 else:
111 else:
78 threads = self.filter(parent=NO_PARENT)
112 threads = self.filter(parent=NO_PARENT)
79
113
80 threads = threads.order_by(order_by)
114 threads = threads.order_by(order_by)
81
115
82 if page != ALL_PAGES:
116 if page != ALL_PAGES:
83 thread_count = len(threads)
117 thread_count = len(threads)
84
118
85 if page < self.get_thread_page_count(tag=tag):
119 if page < self.get_thread_page_count(tag=tag):
86 start_thread = page * settings.THREADS_PER_PAGE
120 start_thread = page * settings.THREADS_PER_PAGE
87 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
121 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
88 thread_count)
122 thread_count)
89 threads = threads[start_thread:end_thread]
123 threads = threads[start_thread:end_thread]
90
124
91 return threads
125 return threads
92
126
93 def get_thread(self, opening_post_id):
127 def get_thread(self, opening_post_id):
94 try:
128 try:
95 opening_post = self.get(id=opening_post_id, parent=NO_PARENT)
129 opening_post = self.get(id=opening_post_id, parent=NO_PARENT)
96 except Post.DoesNotExist:
130 except Post.DoesNotExist:
97 raise Http404
131 raise Http404
98
132
99 if opening_post.parent == NO_PARENT:
133 if opening_post.parent == NO_PARENT:
100 replies = self.filter(parent=opening_post_id)
134 replies = self.filter(parent=opening_post_id)
101
135
102 thread = [opening_post]
136 thread = [opening_post]
103 thread.extend(replies)
137 thread.extend(replies)
104
138
105 return thread
139 return thread
106
140
107 def exists(self, post_id):
141 def exists(self, post_id):
108 posts = self.filter(id=post_id)
142 posts = self.filter(id=post_id)
109
143
110 return posts.count() > 0
144 return posts.count() > 0
111
145
112 def get_thread_page_count(self, tag=None):
146 def get_thread_page_count(self, tag=None):
113 if tag:
147 if tag:
114 threads = self.filter(parent=NO_PARENT, tags=tag)
148 threads = self.filter(parent=NO_PARENT, tags=tag)
115 else:
149 else:
116 threads = self.filter(parent=NO_PARENT)
150 threads = self.filter(parent=NO_PARENT)
117
151
118 return int(math.ceil(threads.count() / float(
152 return int(math.ceil(threads.count() / float(
119 settings.THREADS_PER_PAGE)))
153 settings.THREADS_PER_PAGE)))
120
154
121 def _delete_old_threads(self):
155 def _delete_old_threads(self):
122 """
156 """
123 Preserves maximum thread count. If there are too many threads,
157 Preserves maximum thread count. If there are too many threads,
124 delete the old ones.
158 delete the old ones.
125 """
159 """
126
160
127 # TODO Move old threads to the archive instead of deleting them.
161 # TODO Move old threads to the archive instead of deleting them.
128 # Maybe make some 'old' field in the model to indicate the thread
162 # Maybe make some 'old' field in the model to indicate the thread
129 # must not be shown and be able for replying.
163 # must not be shown and be able for replying.
130
164
131 threads = self.get_threads()
165 threads = self.get_threads()
132 thread_count = len(threads)
166 thread_count = len(threads)
133
167
134 if thread_count > settings.MAX_THREAD_COUNT:
168 if thread_count > settings.MAX_THREAD_COUNT:
135 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
169 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
136 old_threads = threads[thread_count - num_threads_to_delete:]
170 old_threads = threads[thread_count - num_threads_to_delete:]
137
171
138 for thread in old_threads:
172 for thread in old_threads:
139 self.delete_post(thread)
173 self.delete_post(thread)
140
174
141 def _bump_thread(self, thread_id):
175 def _bump_thread(self, thread_id):
142 thread = self.get(id=thread_id)
176 thread = self.get(id=thread_id)
143
177
144 if thread.can_bump():
178 if thread.can_bump():
145 thread.last_edit_time = timezone.now()
179 thread.last_edit_time = timezone.now()
146 thread.save()
180 thread.save()
147
181
148
182
149 class TagManager(models.Manager):
183 class TagManager(models.Manager):
150 def get_not_empty_tags(self):
184 def get_not_empty_tags(self):
151 all_tags = self.all().order_by('name')
185 all_tags = self.all().order_by('name')
152 tags = []
186 tags = []
153 for tag in all_tags:
187 for tag in all_tags:
154 if not tag.is_empty():
188 if not tag.is_empty():
155 tags.append(tag)
189 tags.append(tag)
156
190
157 return tags
191 return tags
158
192
159 def get_popular_tags(self):
193 def get_popular_tags(self):
160 all_tags = self.get_not_empty_tags()
194 all_tags = self.get_not_empty_tags()
161
195
162 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
196 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
163 reverse=True)
197 reverse=True)
164
198
165 return sorted_tags[:settings.POPULAR_TAGS]
199 return sorted_tags[:settings.POPULAR_TAGS]
166
200
167
201
168 class Tag(models.Model):
202 class Tag(models.Model):
169 """
203 """
170 A tag is a text node assigned to the post. The tag serves as a board
204 A tag is a text node assigned to the post. The tag serves as a board
171 section. There can be multiple tags for each message
205 section. There can be multiple tags for each message
172 """
206 """
173
207
174 objects = TagManager()
208 objects = TagManager()
175
209
176 name = models.CharField(max_length=100)
210 name = models.CharField(max_length=100)
177 # TODO Connect the tag to its posts to check the number of threads for
211 # TODO Connect the tag to its posts to check the number of threads for
178 # the tag.
212 # the tag.
179
213
180 def __unicode__(self):
214 def __unicode__(self):
181 return self.name
215 return self.name
182
216
183 def is_empty(self):
217 def is_empty(self):
184 return self.get_post_count() == 0
218 return self.get_post_count() == 0
185
219
186 def get_post_count(self):
220 def get_post_count(self):
187 posts_with_tag = Post.objects.get_threads(tag=self)
221 posts_with_tag = Post.objects.get_threads(tag=self)
188 return posts_with_tag.count()
222 return posts_with_tag.count()
189
223
190 def get_popularity(self):
224 def get_popularity(self):
191 posts_with_tag = Post.objects.get_threads(tag=self)
225 posts_with_tag = Post.objects.get_threads(tag=self)
192 reply_count = 0
226 reply_count = 0
193 for post in posts_with_tag:
227 for post in posts_with_tag:
194 reply_count += post.get_reply_count()
228 reply_count += post.get_reply_count()
195 reply_count += OPENING_POST_POPULARITY_WEIGHT
229 reply_count += OPENING_POST_POPULARITY_WEIGHT
196
230
197 return reply_count
231 return reply_count
198
232
199
233
200 class Post(models.Model):
234 class Post(models.Model):
201 """A post is a message."""
235 """A post is a message."""
202
236
203 objects = PostManager()
237 objects = PostManager()
204
238
205 def _update_image_filename(self, filename):
239 def _update_image_filename(self, filename):
206 """Get unique image filename"""
240 """Get unique image filename"""
207
241
208 path = IMAGES_DIRECTORY
242 path = IMAGES_DIRECTORY
209 new_name = str(int(time.mktime(time.gmtime())))
243 new_name = str(int(time.mktime(time.gmtime())))
210 new_name += str(int(random() * 1000))
244 new_name += str(int(random() * 1000))
211 new_name += FILE_EXTENSION_DELIMITER
245 new_name += FILE_EXTENSION_DELIMITER
212 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
246 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
213
247
214 return os.path.join(path, new_name)
248 return os.path.join(path, new_name)
215
249
216 title = models.CharField(max_length=TITLE_MAX_LENGTH)
250 title = models.CharField(max_length=TITLE_MAX_LENGTH)
217 pub_time = models.DateTimeField()
251 pub_time = models.DateTimeField()
218 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
252 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
219 escape_html=False)
253 escape_html=False)
220 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
254 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
221 blank=True, sizes=(IMAGE_THUMB_SIZE,))
255 blank=True, sizes=(IMAGE_THUMB_SIZE,))
222 poster_ip = models.IPAddressField()
256 poster_ip = models.IPAddressField()
223 poster_user_agent = models.TextField()
257 poster_user_agent = models.TextField()
224 parent = models.BigIntegerField()
258 parent = models.BigIntegerField()
225 tags = models.ManyToManyField(Tag)
259 tags = models.ManyToManyField(Tag)
226 last_edit_time = models.DateTimeField()
260 last_edit_time = models.DateTimeField()
261 user = models.ForeignKey(User, null=True, default=None)
227
262
228 def __unicode__(self):
263 def __unicode__(self):
229 return '#' + str(self.id) + ' ' + self.title + ' (' + self.text.raw + \
264 return '#' + str(self.id) + ' ' + self.title + ' (' + self.text.raw + \
230 ')'
265 ')'
231
266
232 def _get_replies(self):
267 def _get_replies(self):
233 return Post.objects.filter(parent=self.id)
268 return Post.objects.filter(parent=self.id)
234
269
235 def get_reply_count(self):
270 def get_reply_count(self):
236 return self._get_replies().count()
271 return self._get_replies().count()
237
272
238 def get_images_count(self):
273 def get_images_count(self):
239 images_count = 1 if self.image else 0
274 images_count = 1 if self.image else 0
240 for reply in self._get_replies():
275 for reply in self._get_replies():
241 if reply.image:
276 if reply.image:
242 images_count += 1
277 images_count += 1
243
278
244 return images_count
279 return images_count
245
280
246 def get_gets_count(self):
281 def get_gets_count(self):
247 gets_count = 1 if self.is_get() else 0
282 gets_count = 1 if self.is_get() else 0
248 for reply in self._get_replies():
283 for reply in self._get_replies():
249 if reply.is_get():
284 if reply.is_get():
250 gets_count += 1
285 gets_count += 1
251
286
252 return gets_count
287 return gets_count
253
288
254 def is_get(self):
289 def is_get(self):
255 """If the post has pretty id (1, 1000, 77777), than it is called GET"""
290 """If the post has pretty id (1, 1000, 77777), than it is called GET"""
256
291
257 first = self.id == 1
292 first = self.id == 1
258
293
259 id_str = str(self.id)
294 id_str = str(self.id)
260 pretty = REGEX_PRETTY.match(id_str)
295 pretty = REGEX_PRETTY.match(id_str)
261 same_digits = REGEX_SAME.match(id_str)
296 same_digits = REGEX_SAME.match(id_str)
262
297
263 return first or pretty or same_digits
298 return first or pretty or same_digits
264
299
265 def can_bump(self):
300 def can_bump(self):
266 """Check if the thread can be bumped by replying"""
301 """Check if the thread can be bumped by replying"""
267
302
268 replies_count = len(Post.objects.get_thread(self.id))
303 replies_count = len(Post.objects.get_thread(self.id))
269
304
270 return replies_count <= settings.MAX_POSTS_PER_THREAD
305 return replies_count <= settings.MAX_POSTS_PER_THREAD
271
306
272 def get_last_replies(self):
307 def get_last_replies(self):
273 if settings.LAST_REPLIES_COUNT > 0:
308 if settings.LAST_REPLIES_COUNT > 0:
274 reply_count = self.get_reply_count()
309 reply_count = self.get_reply_count()
275
310
276 if reply_count > 0:
311 if reply_count > 0:
277 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
312 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
278 reply_count)
313 reply_count)
279 last_replies = self._get_replies()[reply_count
314 last_replies = self._get_replies()[reply_count
280 - reply_count_to_show:]
315 - reply_count_to_show:]
281
316
282 return last_replies
317 return last_replies
283
318
284
319
285 class Admin(models.Model):
320 class Admin(models.Model):
286 """
321 """
287 Model for admin users
322 Model for admin users
288 """
323 """
289 name = models.CharField(max_length=100)
324 name = models.CharField(max_length=100)
290 password = models.CharField(max_length=100)
325 password = models.CharField(max_length=100)
291
326
292 def __unicode__(self):
327 def __unicode__(self):
293 return self.name + '/' + '*' * len(self.password)
328 return self.name + '/' + '*' * len(self.password)
294
329
295
330
296 class User(models.Model):
297
298 user_id = models.CharField(max_length=50)
299 rank = models.IntegerField()
300
301 def save_setting(self, name, value):
302 setting, created = Setting.objects.get_or_create(name=name, user=self)
303 setting.value = value
304 setting.save()
305
306 return setting
307
308 def get_setting(self, name):
309 settings = Setting.objects.filter(name=name, user=self)
310 if len(settings) > 0:
311 setting = settings[0]
312 else:
313 setting = None
314
315 if setting:
316 setting_value = setting.value
317 else:
318 setting_value = None
319
320 return setting_value
321
322 def is_moderator(self):
323 return RANK_MODERATOR >= self.rank
324
325 def __unicode__(self):
326 return self.user_id
327
328
329 class Setting(models.Model):
331 class Setting(models.Model):
330
332
331 name = models.CharField(max_length=50)
333 name = models.CharField(max_length=50)
332 value = models.CharField(max_length=50)
334 value = models.CharField(max_length=50)
333 user = models.ForeignKey(User)
335 user = models.ForeignKey(User)
@@ -1,160 +1,173 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.db.models import ImageField
6 from django.db.models import ImageField
7 from django.db.models.fields.files import ImageFieldFile
7 from django.db.models.fields.files import ImageFieldFile
8 from PIL import Image
8 from PIL import Image
9 from django.core.files.base import ContentFile
9 from django.core.files.base import ContentFile
10 import cStringIO
10 import cStringIO
11
11
12
12 def generate_thumb(img, thumb_size, format):
13 def generate_thumb(img, thumb_size, format):
13 """
14 """
14 Generates a thumbnail image and returns a ContentFile object with the thumbnail
15 Generates a thumbnail image and returns a ContentFile object with the thumbnail
15
16
16 Parameters:
17 Parameters:
17 ===========
18 ===========
18 img File object
19 img File object
19
20
20 thumb_size desired thumbnail size, ie: (200,120)
21 thumb_size desired thumbnail size, ie: (200,120)
21
22
22 format format of the original image ('jpeg','gif','png',...)
23 format format of the original image ('jpeg','gif','png',...)
23 (this format will be used for the generated thumbnail, too)
24 (this format will be used for the generated thumbnail, too)
24 """
25 """
25
26
26 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
27 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
27 image = Image.open(img)
28 image = Image.open(img)
28
29
29 # get size
30 # get size
30 thumb_w, thumb_h = thumb_size
31 thumb_w, thumb_h = thumb_size
31 # If you want to generate a square thumbnail
32 # If you want to generate a square thumbnail
32 if thumb_w == thumb_h:
33 if thumb_w == thumb_h:
33 # quad
34 # quad
34 xsize, ysize = image.size
35 xsize, ysize = image.size
35 # get minimum size
36 # get minimum size
36 minsize = min(xsize,ysize)
37 minsize = min(xsize, ysize)
37 # largest square possible in the image
38 # largest square possible in the image
38 xnewsize = (xsize-minsize)/2
39 xnewsize = (xsize - minsize) / 2
39 ynewsize = (ysize-minsize)/2
40 ynewsize = (ysize - minsize) / 2
40 # crop it
41 # crop it
41 image2 = image.crop((xnewsize, ynewsize, xsize-xnewsize, ysize-ynewsize))
42 image2 = image.crop(
43 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
42 # load is necessary after crop
44 # load is necessary after crop
43 image2.load()
45 image2.load()
44 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
46 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
45 image2.thumbnail(thumb_size, Image.ANTIALIAS)
47 image2.thumbnail(thumb_size, Image.ANTIALIAS)
46 else:
48 else:
47 # not quad
49 # not quad
48 image2 = image
50 image2 = image
49 image2.thumbnail(thumb_size, Image.ANTIALIAS)
51 image2.thumbnail(thumb_size, Image.ANTIALIAS)
50
52
51 io = cStringIO.StringIO()
53 io = cStringIO.StringIO()
52 # PNG and GIF are the same, JPG is JPEG
54 # PNG and GIF are the same, JPG is JPEG
53 if format.upper()=='JPG':
55 if format.upper() == 'JPG':
54 format = 'JPEG'
56 format = 'JPEG'
55
57
56 image2.save(io, format)
58 image2.save(io, format)
57 return ContentFile(io.getvalue())
59 return ContentFile(io.getvalue())
58
60
61
59 class ImageWithThumbsFieldFile(ImageFieldFile):
62 class ImageWithThumbsFieldFile(ImageFieldFile):
60 """
63 """
61 See ImageWithThumbsField for usage example
64 See ImageWithThumbsField for usage example
62 """
65 """
66
63 def __init__(self, *args, **kwargs):
67 def __init__(self, *args, **kwargs):
64 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
68 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
65 self.sizes = self.field.sizes
69 self.sizes = self.field.sizes
66
70
67 if self.sizes:
71 if self.sizes:
68 def get_size(self, size):
72 def get_size(self, size):
69 if not self:
73 if not self:
70 return ''
74 return ''
71 else:
75 else:
72 split = self.url.rsplit('.',1)
76 split = self.url.rsplit('.', 1)
73 thumb_url = '%s.%sx%s.%s' % (split[0],w,h,split[1])
77 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
74 return thumb_url
78 return thumb_url
75
79
76 for size in self.sizes:
80 for size in self.sizes:
77 (w,h) = size
81 (w, h) = size
78 setattr(self, 'url_%sx%s' % (w,h), get_size(self, size))
82 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
79
83
80 def save(self, name, content, save=True):
84 def save(self, name, content, save=True):
81 super(ImageWithThumbsFieldFile, self).save(name, content, save)
85 super(ImageWithThumbsFieldFile, self).save(name, content, save)
82
86
83 if self.sizes:
87 if self.sizes:
84 for size in self.sizes:
88 for size in self.sizes:
85 (w,h) = size
89 (w, h) = size
86 split = self.name.rsplit('.',1)
90 split = self.name.rsplit('.', 1)
87 thumb_name = '%s.%sx%s.%s' % (split[0],w,h,split[1])
91 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
88
92
89 # you can use another thumbnailing function if you like
93 # you can use another thumbnailing function if you like
90 thumb_content = generate_thumb(content, size, split[1])
94 thumb_content = generate_thumb(content, size, split[1])
91
95
92 thumb_name_ = self.storage.save(thumb_name, thumb_content)
96 thumb_name_ = self.storage.save(thumb_name, thumb_content)
93
97
94 if not thumb_name == thumb_name_:
98 if not thumb_name == thumb_name_:
95 raise ValueError('There is already a file named %s' % thumb_name)
99 raise ValueError(
100 'There is already a file named %s' % thumb_name)
96
101
97 def delete(self, save=True):
102 def delete(self, save=True):
98 name=self.name
103 name = self.name
99 super(ImageWithThumbsFieldFile, self).delete(save)
104 super(ImageWithThumbsFieldFile, self).delete(save)
100 if self.sizes:
105 if self.sizes:
101 for size in self.sizes:
106 for size in self.sizes:
102 (w,h) = size
107 (w, h) = size
103 split = name.rsplit('.',1)
108 split = name.rsplit('.', 1)
104 thumb_name = '%s.%sx%s.%s' % (split[0],w,h,split[1])
109 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
105 try:
110 try:
106 self.storage.delete(thumb_name)
111 self.storage.delete(thumb_name)
107 except:
112 except:
108 pass
113 pass
109
114
115
110 class ImageWithThumbsField(ImageField):
116 class ImageWithThumbsField(ImageField):
111 attr_class = ImageWithThumbsFieldFile
117 attr_class = ImageWithThumbsFieldFile
112 """
118 """
113 Usage example:
119 Usage example:
114 ==============
120 ==============
115 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
121 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
116
122
117 To retrieve image URL, exactly the same way as with ImageField:
123 To retrieve image URL, exactly the same way as with ImageField:
118 my_object.photo.url
124 my_object.photo.url
119 To retrieve thumbnails URL's just add the size to it:
125 To retrieve thumbnails URL's just add the size to it:
120 my_object.photo.url_125x125
126 my_object.photo.url_125x125
121 my_object.photo.url_300x200
127 my_object.photo.url_300x200
122
128
123 Note: The 'sizes' attribute is not required. If you don't provide it,
129 Note: The 'sizes' attribute is not required. If you don't provide it,
124 ImageWithThumbsField will act as a normal ImageField
130 ImageWithThumbsField will act as a normal ImageField
125
131
126 How it works:
132 How it works:
127 =============
133 =============
128 For each size in the 'sizes' atribute of the field it generates a
134 For each size in the 'sizes' atribute of the field it generates a
129 thumbnail with that size and stores it following this format:
135 thumbnail with that size and stores it following this format:
130
136
131 available_filename.[width]x[height].extension
137 available_filename.[width]x[height].extension
132
138
133 Where 'available_filename' is the available filename returned by the storage
139 Where 'available_filename' is the available filename returned by the storage
134 backend for saving the original file.
140 backend for saving the original file.
135
141
136 Following the usage example above: For storing a file called "photo.jpg" it saves:
142 Following the usage example above: For storing a file called "photo.jpg" it saves:
137 photo.jpg (original file)
143 photo.jpg (original file)
138 photo.125x125.jpg (first thumbnail)
144 photo.125x125.jpg (first thumbnail)
139 photo.300x200.jpg (second thumbnail)
145 photo.300x200.jpg (second thumbnail)
140
146
141 With the default storage backend if photo.jpg already exists it will use these filenames:
147 With the default storage backend if photo.jpg already exists it will use these filenames:
142 photo_.jpg
148 photo_.jpg
143 photo_.125x125.jpg
149 photo_.125x125.jpg
144 photo_.300x200.jpg
150 photo_.300x200.jpg
145
151
146 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
152 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
147 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
153 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
148
154
149 To do:
155 To do:
150 ======
156 ======
151 Add method to regenerate thubmnails
157 Add method to regenerate thubmnails
152
158
153 """
159 """
154 def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, sizes=None, **kwargs):
160
161 def __init__(self, verbose_name=None, name=None, width_field=None,
162 height_field=None, sizes=None, **kwargs):
155 self.verbose_name=verbose_name
163 self.verbose_name = verbose_name
156 self.name=name
164 self.name = name
157 self.width_field=width_field
165 self.width_field = width_field
158 self.height_field=height_field
166 self.height_field = height_field
159 self.sizes = sizes
167 self.sizes = sizes
160 super(ImageField, self).__init__(**kwargs) No newline at end of file
168 super(ImageField, self).__init__(**kwargs)
169
170
171 from south.modelsinspector import add_introspection_rules
172
173 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"]) No newline at end of file
@@ -1,199 +1,201 b''
1 # Django settings for neboard project.
1 # Django settings for neboard project.
2 import os
2 import os
3 import markdown
3 import markdown
4 from boards.mdx_neboard import markdown_extended
4 from boards.mdx_neboard import markdown_extended
5
5
6 DEBUG = True
6 DEBUG = True
7 TEMPLATE_DEBUG = DEBUG
7 TEMPLATE_DEBUG = DEBUG
8
8
9 ADMINS = (
9 ADMINS = (
10 # ('Your Name', 'your_email@example.com'),
10 # ('Your Name', 'your_email@example.com'),
11 ('admin', 'admin@example.com')
11 ('admin', 'admin@example.com')
12 )
12 )
13
13
14 MANAGERS = ADMINS
14 MANAGERS = ADMINS
15
15
16 DATABASES = {
16 DATABASES = {
17 'default': {
17 'default': {
18 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
18 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
19 'NAME': 'database.db', # Or path to database file if using sqlite3.
19 'NAME': 'database.db', # Or path to database file if using sqlite3.
20 'USER': '', # Not used with sqlite3.
20 'USER': '', # Not used with sqlite3.
21 'PASSWORD': '', # Not used with sqlite3.
21 'PASSWORD': '', # Not used with sqlite3.
22 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
22 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
23 'PORT': '', # Set to empty string for default. Not used with sqlite3.
23 'PORT': '', # Set to empty string for default. Not used with sqlite3.
24 }
24 }
25 }
25 }
26
26
27 # Local time zone for this installation. Choices can be found here:
27 # Local time zone for this installation. Choices can be found here:
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 # although not all choices may be available on all operating systems.
29 # although not all choices may be available on all operating systems.
30 # In a Windows environment this must be set to your system time zone.
30 # In a Windows environment this must be set to your system time zone.
31 TIME_ZONE = 'Europe/Kiev'
31 TIME_ZONE = 'Europe/Kiev'
32
32
33 # Language code for this installation. All choices can be found here:
33 # Language code for this installation. All choices can be found here:
34 # http://www.i18nguy.com/unicode/language-identifiers.html
34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 LANGUAGE_CODE = 'en'
35 LANGUAGE_CODE = 'en'
36
36
37 SITE_ID = 1
37 SITE_ID = 1
38
38
39 # If you set this to False, Django will make some optimizations so as not
39 # If you set this to False, Django will make some optimizations so as not
40 # to load the internationalization machinery.
40 # to load the internationalization machinery.
41 USE_I18N = True
41 USE_I18N = True
42
42
43 # If you set this to False, Django will not format dates, numbers and
43 # If you set this to False, Django will not format dates, numbers and
44 # calendars according to the current locale.
44 # calendars according to the current locale.
45 USE_L10N = True
45 USE_L10N = True
46
46
47 # If you set this to False, Django will not use timezone-aware datetimes.
47 # If you set this to False, Django will not use timezone-aware datetimes.
48 USE_TZ = True
48 USE_TZ = True
49
49
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 # Example: "/home/media/media.lawrence.com/media/"
51 # Example: "/home/media/media.lawrence.com/media/"
52 MEDIA_ROOT = './media/'
52 MEDIA_ROOT = './media/'
53
53
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 # trailing slash.
55 # trailing slash.
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 MEDIA_URL = '/media/'
57 MEDIA_URL = '/media/'
58
58
59 # Absolute path to the directory static files should be collected to.
59 # Absolute path to the directory static files should be collected to.
60 # Don't put anything in this directory yourself; store your static files
60 # Don't put anything in this directory yourself; store your static files
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 # Example: "/home/media/media.lawrence.com/static/"
62 # Example: "/home/media/media.lawrence.com/static/"
63 STATIC_ROOT = ''
63 STATIC_ROOT = ''
64
64
65 # URL prefix for static files.
65 # URL prefix for static files.
66 # Example: "http://media.lawrence.com/static/"
66 # Example: "http://media.lawrence.com/static/"
67 STATIC_URL = '/static/'
67 STATIC_URL = '/static/'
68
68
69 # Additional locations of static files
69 # Additional locations of static files
70 # It is really a hack, put real paths, not related
70 # It is really a hack, put real paths, not related
71 STATICFILES_DIRS = (
71 STATICFILES_DIRS = (
72 os.path.dirname(__file__) + '/boards/static',
72 os.path.dirname(__file__) + '/boards/static',
73
73
74 # '/d/work/python/django/neboard/neboard/boards/static',
74 # '/d/work/python/django/neboard/neboard/boards/static',
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 # Always use forward slashes, even on Windows.
76 # Always use forward slashes, even on Windows.
77 # Don't forget to use absolute paths, not relative paths.
77 # Don't forget to use absolute paths, not relative paths.
78 )
78 )
79
79
80 # List of finder classes that know how to find static files in
80 # List of finder classes that know how to find static files in
81 # various locations.
81 # various locations.
82 STATICFILES_FINDERS = (
82 STATICFILES_FINDERS = (
83 'django.contrib.staticfiles.finders.FileSystemFinder',
83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
85 # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
86 )
86 )
87
87
88 # Make this unique, and don't share it with anybody.
88 # Make this unique, and don't share it with anybody.
89 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
89 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
90
90
91 # List of callables that know how to import templates from various sources.
91 # List of callables that know how to import templates from various sources.
92 TEMPLATE_LOADERS = (
92 TEMPLATE_LOADERS = (
93 'django.template.loaders.filesystem.Loader',
93 'django.template.loaders.filesystem.Loader',
94 'django.template.loaders.app_directories.Loader',
94 'django.template.loaders.app_directories.Loader',
95 # 'django.template.loaders.eggs.Loader',
95 # 'django.template.loaders.eggs.Loader',
96 )
96 )
97
97
98 TEMPLATE_CONTEXT_PROCESSORS = (
98 TEMPLATE_CONTEXT_PROCESSORS = (
99 'django.core.context_processors.media',
99 'django.core.context_processors.media',
100 'django.core.context_processors.static',
100 'django.core.context_processors.static',
101 'django.core.context_processors.request',
101 'django.core.context_processors.request',
102 'django.contrib.auth.context_processors.auth',
102 'django.contrib.auth.context_processors.auth',
103 )
103 )
104
104
105 MIDDLEWARE_CLASSES = (
105 MIDDLEWARE_CLASSES = (
106 'django.contrib.sessions.middleware.SessionMiddleware',
106 'django.contrib.sessions.middleware.SessionMiddleware',
107 'django.middleware.locale.LocaleMiddleware',
107 'django.middleware.locale.LocaleMiddleware',
108 'django.middleware.common.CommonMiddleware',
108 'django.middleware.common.CommonMiddleware',
109 # 'django.middleware.csrf.CsrfViewMiddleware',
109 # 'django.middleware.csrf.CsrfViewMiddleware',
110 'django.contrib.auth.middleware.AuthenticationMiddleware',
110 'django.contrib.auth.middleware.AuthenticationMiddleware',
111 'django.contrib.messages.middleware.MessageMiddleware',
111 'django.contrib.messages.middleware.MessageMiddleware',
112 # Uncomment the next line for simple clickjacking protection:
112 # Uncomment the next line for simple clickjacking protection:
113 # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
113 # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
114 )
114 )
115
115
116 ROOT_URLCONF = 'neboard.urls'
116 ROOT_URLCONF = 'neboard.urls'
117
117
118 # Python dotted path to the WSGI application used by Django's runserver.
118 # Python dotted path to the WSGI application used by Django's runserver.
119 WSGI_APPLICATION = 'neboard.wsgi.application'
119 WSGI_APPLICATION = 'neboard.wsgi.application'
120
120
121 TEMPLATE_DIRS = (
121 TEMPLATE_DIRS = (
122 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
122 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
123 # Always use forward slashes, even on Windows.
123 # Always use forward slashes, even on Windows.
124 # Don't forget to use absolute paths, not relative paths.
124 # Don't forget to use absolute paths, not relative paths.
125 'templates',
125 'templates',
126 )
126 )
127
127
128 INSTALLED_APPS = (
128 INSTALLED_APPS = (
129 'django.contrib.auth',
129 'django.contrib.auth',
130 'django.contrib.contenttypes',
130 'django.contrib.contenttypes',
131 'django.contrib.sessions',
131 'django.contrib.sessions',
132 # 'django.contrib.sites',
132 # 'django.contrib.sites',
133 'django.contrib.messages',
133 'django.contrib.messages',
134 'django.contrib.staticfiles',
134 'django.contrib.staticfiles',
135 # Uncomment the next line to enable the admin:
135 # Uncomment the next line to enable the admin:
136 'django.contrib.admin',
136 'django.contrib.admin',
137 # Uncomment the next line to enable admin documentation:
137 # Uncomment the next line to enable admin documentation:
138 # 'django.contrib.admindocs',
138 # 'django.contrib.admindocs',
139 'django.contrib.markup',
139 'django.contrib.markup',
140 'django_cleanup',
140 'django_cleanup',
141 'boards',
141 'boards',
142 'captcha',
142 'captcha',
143 'south',
143 )
144 )
144
145
145 # TODO: NEED DESIGN FIXES
146 # TODO: NEED DESIGN FIXES
146 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
147 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
147 u'<div class="form-label">%(image)s</div>'
148 u'<div class="form-label">%(image)s</div>'
148 u'<div class="form-text">%(text_field)s</div>')
149 u'<div class="form-text">%(text_field)s</div>')
149
150
150 # A sample logging configuration. The only tangible logging
151 # A sample logging configuration. The only tangible logging
151 # performed by this configuration is to send an email to
152 # performed by this configuration is to send an email to
152 # the site admins on every HTTP 500 error when DEBUG=False.
153 # the site admins on every HTTP 500 error when DEBUG=False.
153 # See http://docs.djangoproject.com/en/dev/topics/logging for
154 # See http://docs.djangoproject.com/en/dev/topics/logging for
154 # more details on how to customize your logging configuration.
155 # more details on how to customize your logging configuration.
155 LOGGING = {
156 LOGGING = {
156 'version': 1,
157 'version': 1,
157 'disable_existing_loggers': False,
158 'disable_existing_loggers': False,
158 'filters': {
159 'filters': {
159 'require_debug_false': {
160 'require_debug_false': {
160 '()': 'django.utils.log.RequireDebugFalse'
161 '()': 'django.utils.log.RequireDebugFalse'
161 }
162 }
162 },
163 },
163 'handlers': {
164 'handlers': {
164 'mail_admins': {
165 'mail_admins': {
165 'level': 'ERROR',
166 'level': 'ERROR',
166 'filters': ['require_debug_false'],
167 'filters': ['require_debug_false'],
167 'class': 'django.utils.log.AdminEmailHandler'
168 'class': 'django.utils.log.AdminEmailHandler'
168 }
169 }
169 },
170 },
170 'loggers': {
171 'loggers': {
171 'django.request': {
172 'django.request': {
172 'handlers': ['mail_admins'],
173 'handlers': ['mail_admins'],
173 'level': 'ERROR',
174 'level': 'ERROR',
174 'propagate': True,
175 'propagate': True,
175 },
176 },
176 }
177 }
177 }
178 }
178
179
179 MARKUP_FIELD_TYPES = (
180 MARKUP_FIELD_TYPES = (
180 ('markdown', markdown_extended),
181 ('markdown', markdown_extended),
181 )
182 )
182 # Custom imageboard settings
183 # Custom imageboard settings
183 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
184 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
184 MAX_THREAD_COUNT = 500 # Old threads will be deleted to preserve this count
185 MAX_THREAD_COUNT = 500 # Old threads will be deleted to preserve this count
185 THREADS_PER_PAGE = 10
186 THREADS_PER_PAGE = 10
186 SITE_NAME = 'Neboard'
187 SITE_NAME = 'Neboard'
187
188
188 THEMES = [
189 THEMES = [
189 ('md', 'Mystic Dark'),
190 ('md', 'Mystic Dark'),
190 ('sw', 'Snow White') ]
191 ('sw', 'Snow White')
192 ]
191
193
192 DEFAULT_THEME = 'md'
194 DEFAULT_THEME = 'md'
193
195
194 POPULAR_TAGS = 10
196 POPULAR_TAGS = 10
195 LAST_REPLIES_COUNT = 3
197 LAST_REPLIES_COUNT = 3
196
198
197 ENABLE_CAPTCHA = True
199 ENABLE_CAPTCHA = False
198 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
200 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
199 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
201 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
General Comments 0
You need to be logged in to leave comments. Login now