##// END OF EJS Templates
Added captcha support
wnc_21 -
r95:b1c17a3a default
parent child Browse files
Show More
@@ -1,99 +1,134 b''
1 import re
1 import re
2 from captcha.fields import CaptchaField
2 from captcha.fields import CaptchaField
3 from django import forms
3 from django import forms
4 from django.forms.util import ErrorList
4 from django.forms.util import ErrorList
5 from boards.models import TITLE_MAX_LENGTH
5 from boards.models import TITLE_MAX_LENGTH
6 from neboard import settings
6 from neboard import settings
7
7 from boards import utils
8
8
9 class PlainErrorList(ErrorList):
9 class PlainErrorList(ErrorList):
10 def __unicode__(self):
10 def __unicode__(self):
11 return self.as_text()
11 return self.as_text()
12
12
13 def as_text(self):
13 def as_text(self):
14 return ''.join([u'(!) %s ' % e for e in self])
14 return ''.join([u'(!) %s ' % e for e in self])
15
15
16
16
17 class PostForm(forms.Form):
17 class PostForm(forms.Form):
18
18
19 MAX_TEXT_LENGTH = 10000
19 MAX_TEXT_LENGTH = 10000
20 MAX_IMAGE_SIZE = 8 * 1024 * 1024
20 MAX_IMAGE_SIZE = 8 * 1024 * 1024
21
21
22 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False)
22 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False)
23 text = forms.CharField(widget=forms.Textarea, required=False)
23 text = forms.CharField(widget=forms.Textarea, required=False)
24 image = forms.ImageField(required=False)
24 image = forms.ImageField(required=False)
25
25
26 def clean_title(self):
26 def clean_title(self):
27 title = self.cleaned_data['title']
27 title = self.cleaned_data['title']
28 if title:
28 if title:
29 if len(title) > TITLE_MAX_LENGTH:
29 if len(title) > TITLE_MAX_LENGTH:
30 raise forms.ValidationError('Title must have less than' +
30 raise forms.ValidationError('Title must have less than' +
31 str(TITLE_MAX_LENGTH) +
31 str(TITLE_MAX_LENGTH) +
32 ' characters.')
32 ' characters.')
33 return title
33 return title
34
34
35 def clean_text(self):
35 def clean_text(self):
36 text = self.cleaned_data['text']
36 text = self.cleaned_data['text']
37 if text:
37 if text:
38 if len(text) > self.MAX_TEXT_LENGTH:
38 if len(text) > self.MAX_TEXT_LENGTH:
39 raise forms.ValidationError('Text must have less than ' +
39 raise forms.ValidationError('Text must have less than ' +
40 str(self.MAX_TEXT_LENGTH) +
40 str(self.MAX_TEXT_LENGTH) +
41 ' characters.')
41 ' characters.')
42 return text
42 return text
43
43
44 def clean_image(self):
44 def clean_image(self):
45 image = self.cleaned_data['image']
45 image = self.cleaned_data['image']
46 if image:
46 if image:
47 if image._size > self.MAX_IMAGE_SIZE:
47 if image._size > self.MAX_IMAGE_SIZE:
48 raise forms.ValidationError('Image must be less than ' +
48 raise forms.ValidationError('Image must be less than ' +
49 str(self.MAX_IMAGE_SIZE) +
49 str(self.MAX_IMAGE_SIZE) +
50 ' bytes.')
50 ' bytes.')
51 return image
51 return image
52
52
53 def clean(self):
53 def clean(self):
54 cleaned_data = super(PostForm, self).clean()
54 cleaned_data = super(PostForm, self).clean()
55
55
56 self._clean_text_image()
56 self._clean_text_image()
57
57
58 return cleaned_data
58 return cleaned_data
59
59
60 def _clean_text_image(self):
60 def _clean_text_image(self):
61 text = self.cleaned_data.get('text')
61 text = self.cleaned_data.get('text')
62 image = self.cleaned_data.get('image')
62 image = self.cleaned_data.get('image')
63
63
64 if (not text) and (not image):
64 if (not text) and (not image):
65 error_message = 'Either text or image must be entered.'
65 error_message = 'Either text or image must be entered.'
66 self._errors['text'] = self.error_class([error_message])
66 self._errors['text'] = self.error_class([error_message])
67 self._errors['image'] = self.error_class([error_message])
67 self._errors['image'] = self.error_class([error_message])
68
68
69
69
70 class PostCaptchaForm(PostForm):
71 captcha = CaptchaField()
72
73
70
74 class ThreadForm(PostForm):
71 class ThreadForm(PostForm):
75 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
72 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
76 tags = forms.CharField(max_length=100)
73 tags = forms.CharField(max_length=100)
77
74
78 def clean_tags(self):
75 def clean_tags(self):
79 tags = self.cleaned_data['tags']
76 tags = self.cleaned_data['tags']
80
77
81 if tags:
78 if tags:
82 if not self.regex_tags.match(tags):
79 if not self.regex_tags.match(tags):
83 raise forms.ValidationError(
80 raise forms.ValidationError(
84 'Inappropriate characters in tags.')
81 'Inappropriate characters in tags.')
85
82
86 return tags
83 return tags
87
84
88 def clean(self):
85 def clean(self):
89 cleaned_data = super(ThreadForm, self).clean()
86 cleaned_data = super(ThreadForm, self).clean()
90
87
91 return cleaned_data
88 return cleaned_data
92
89
93
90
91 class PostCaptchaForm(PostForm):
92 captcha = CaptchaField()
93
94 def __init__(self, *args, **kwargs):
95 self.request = kwargs['request']
96 del kwargs['request']
97
98 super(PostCaptchaForm, self).__init__(*args, **kwargs)
99
100 def clean(self):
101 cleaned_data = super(PostCaptchaForm, self).clean()
102
103 success = self.is_valid()
104 utils.update_captcha_access(self.request, success)
105
106 if success:
107 return cleaned_data
108 else:
109 raise forms.ValidationError("captcha validation failed")
110
111
94 class ThreadCaptchaForm(ThreadForm):
112 class ThreadCaptchaForm(ThreadForm):
95 captcha = CaptchaField()
113 captcha = CaptchaField()
96
114
115 def __init__(self, *args, **kwargs):
116 self.request = kwargs['request']
117 del kwargs['request']
118
119 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
120
121 def clean(self):
122 cleaned_data = super(ThreadCaptchaForm, self).clean()
123
124 success = self.is_valid()
125 utils.update_captcha_access(self.request, success)
126
127 if success:
128 return cleaned_data
129 else:
130 raise forms.ValidationError("captcha validation failed")
131
97
132
98 class SettingsForm(forms.Form):
133 class SettingsForm(forms.Form):
99 theme = forms.ChoiceField(choices=settings.THEMES, widget=forms.RadioSelect) No newline at end of file
134 theme = forms.ChoiceField(choices=settings.THEMES, widget=forms.RadioSelect)
@@ -1,288 +1,289 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
12
12 from neboard import settings
13 from neboard import settings
13 import thumbs
14 import thumbs
14
15
15 IMAGE_THUMB_SIZE = (200, 150)
16 IMAGE_THUMB_SIZE = (200, 150)
16
17
17 TITLE_MAX_LENGTH = 50
18 TITLE_MAX_LENGTH = 50
18
19
19 DEFAULT_MARKUP_TYPE = 'markdown'
20 DEFAULT_MARKUP_TYPE = 'markdown'
20
21
21 NO_PARENT = -1
22 NO_PARENT = -1
22 NO_IP = '0.0.0.0'
23 NO_IP = '0.0.0.0'
23 UNKNOWN_UA = ''
24 UNKNOWN_UA = ''
24 ALL_PAGES = -1
25 ALL_PAGES = -1
25 OPENING_POST_POPULARITY_WEIGHT = 2
26 OPENING_POST_POPULARITY_WEIGHT = 2
26 IMAGES_DIRECTORY = 'images/'
27 IMAGES_DIRECTORY = 'images/'
27 FILE_EXTENSION_DELIMITER = '.'
28 FILE_EXTENSION_DELIMITER = '.'
28
29
29 REGEX_PRETTY = re.compile(r'^\d(0)+$')
30 REGEX_PRETTY = re.compile(r'^\d(0)+$')
30 REGEX_SAME = re.compile(r'^(.)\1+$')
31 REGEX_SAME = re.compile(r'^(.)\1+$')
31
32
32
33
33 class PostManager(models.Manager):
34 class PostManager(models.Manager):
34 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
35 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
35 ip=NO_IP, tags=None):
36 ip=NO_IP, tags=None):
36 post = self.create(title=title,
37 post = self.create(title=title,
37 text=text,
38 text=text,
38 pub_time=timezone.now(),
39 pub_time=timezone.now(),
39 parent=parent_id,
40 parent=parent_id,
40 image=image,
41 image=image,
41 poster_ip=ip,
42 poster_ip=ip,
42 poster_user_agent=UNKNOWN_UA,
43 poster_user_agent=UNKNOWN_UA,
43 last_edit_time=timezone.now())
44 last_edit_time=timezone.now())
44
45
45 if tags:
46 if tags:
46 map(post.tags.add, tags)
47 map(post.tags.add, tags)
47
48
48 if parent_id != NO_PARENT:
49 if parent_id != NO_PARENT:
49 self._bump_thread(parent_id)
50 self._bump_thread(parent_id)
50 else:
51 else:
51 self._delete_old_threads()
52 self._delete_old_threads()
52
53
53 return post
54 return post
54
55
55 def delete_post(self, post):
56 def delete_post(self, post):
56 children = self.filter(parent=post.id)
57 children = self.filter(parent=post.id)
57 for child in children:
58 for child in children:
58 self.delete_post(child)
59 self.delete_post(child)
59 post.delete()
60 post.delete()
60
61
61 def delete_posts_by_ip(self, ip):
62 def delete_posts_by_ip(self, ip):
62 posts = self.filter(poster_ip=ip)
63 posts = self.filter(poster_ip=ip)
63 for post in posts:
64 for post in posts:
64 self.delete_post(post)
65 self.delete_post(post)
65
66
66 def get_threads(self, tag=None, page=ALL_PAGES,
67 def get_threads(self, tag=None, page=ALL_PAGES,
67 order_by='-last_edit_time'):
68 order_by='-last_edit_time'):
68 if tag:
69 if tag:
69 threads = self.filter(parent=NO_PARENT, tags=tag)
70 threads = self.filter(parent=NO_PARENT, tags=tag)
70
71
71 # TODO Throw error 404 if no threads for tag found?
72 # TODO Throw error 404 if no threads for tag found?
72 else:
73 else:
73 threads = self.filter(parent=NO_PARENT)
74 threads = self.filter(parent=NO_PARENT)
74
75
75 threads = threads.order_by(order_by)
76 threads = threads.order_by(order_by)
76
77
77 if page != ALL_PAGES:
78 if page != ALL_PAGES:
78 thread_count = len(threads)
79 thread_count = len(threads)
79
80
80 if page < self.get_thread_page_count(tag=tag):
81 if page < self.get_thread_page_count(tag=tag):
81 start_thread = page * settings.THREADS_PER_PAGE
82 start_thread = page * settings.THREADS_PER_PAGE
82 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
83 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
83 thread_count)
84 thread_count)
84 threads = threads[start_thread:end_thread]
85 threads = threads[start_thread:end_thread]
85
86
86 return threads
87 return threads
87
88
88 def get_thread(self, opening_post_id):
89 def get_thread(self, opening_post_id):
89 try:
90 try:
90 opening_post = self.get(id=opening_post_id, parent=NO_PARENT)
91 opening_post = self.get(id=opening_post_id, parent=NO_PARENT)
91 except Post.DoesNotExist:
92 except Post.DoesNotExist:
92 raise Http404
93 raise Http404
93
94
94 if opening_post.parent == NO_PARENT:
95 if opening_post.parent == NO_PARENT:
95 replies = self.filter(parent=opening_post_id)
96 replies = self.filter(parent=opening_post_id)
96
97
97 thread = [opening_post]
98 thread = [opening_post]
98 thread.extend(replies)
99 thread.extend(replies)
99
100
100 return thread
101 return thread
101
102
102 def exists(self, post_id):
103 def exists(self, post_id):
103 posts = self.filter(id=post_id)
104 posts = self.filter(id=post_id)
104
105
105 return posts.count() > 0
106 return posts.count() > 0
106
107
107 def get_thread_page_count(self, tag=None):
108 def get_thread_page_count(self, tag=None):
108 if tag:
109 if tag:
109 threads = self.filter(parent=NO_PARENT, tags=tag)
110 threads = self.filter(parent=NO_PARENT, tags=tag)
110 else:
111 else:
111 threads = self.filter(parent=NO_PARENT)
112 threads = self.filter(parent=NO_PARENT)
112
113
113 return int(math.ceil(threads.count() / float(
114 return int(math.ceil(threads.count() / float(
114 settings.THREADS_PER_PAGE)))
115 settings.THREADS_PER_PAGE)))
115
116
116 def _delete_old_threads(self):
117 def _delete_old_threads(self):
117 """
118 """
118 Preserves maximum thread count. If there are too many threads,
119 Preserves maximum thread count. If there are too many threads,
119 delete the old ones.
120 delete the old ones.
120 """
121 """
121
122
122 # TODO Move old threads to the archive instead of deleting them.
123 # TODO Move old threads to the archive instead of deleting them.
123 # Maybe make some 'old' field in the model to indicate the thread
124 # Maybe make some 'old' field in the model to indicate the thread
124 # must not be shown and be able for replying.
125 # must not be shown and be able for replying.
125
126
126 threads = self.get_threads()
127 threads = self.get_threads()
127 thread_count = len(threads)
128 thread_count = len(threads)
128
129
129 if thread_count > settings.MAX_THREAD_COUNT:
130 if thread_count > settings.MAX_THREAD_COUNT:
130 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
131 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
131 old_threads = threads[thread_count - num_threads_to_delete:]
132 old_threads = threads[thread_count - num_threads_to_delete:]
132
133
133 for thread in old_threads:
134 for thread in old_threads:
134 self.delete_post(thread)
135 self.delete_post(thread)
135
136
136 def _bump_thread(self, thread_id):
137 def _bump_thread(self, thread_id):
137 thread = self.get(id=thread_id)
138 thread = self.get(id=thread_id)
138
139
139 if thread.can_bump():
140 if thread.can_bump():
140 thread.last_edit_time = timezone.now()
141 thread.last_edit_time = timezone.now()
141 thread.save()
142 thread.save()
142
143
143
144
144 class TagManager(models.Manager):
145 class TagManager(models.Manager):
145 def get_not_empty_tags(self):
146 def get_not_empty_tags(self):
146 all_tags = self.all().order_by('name')
147 all_tags = self.all().order_by('name')
147 tags = []
148 tags = []
148 for tag in all_tags:
149 for tag in all_tags:
149 if not tag.is_empty():
150 if not tag.is_empty():
150 tags.append(tag)
151 tags.append(tag)
151
152
152 return tags
153 return tags
153
154
154 def get_popular_tags(self):
155 def get_popular_tags(self):
155 all_tags = self.get_not_empty_tags()
156 all_tags = self.get_not_empty_tags()
156
157
157 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
158 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
158 reverse=True)
159 reverse=True)
159
160
160 return sorted_tags[:settings.POPULAR_TAGS]
161 return sorted_tags[:settings.POPULAR_TAGS]
161
162
162
163
163 class Tag(models.Model):
164 class Tag(models.Model):
164 """
165 """
165 A tag is a text node assigned to the post. The tag serves as a board
166 A tag is a text node assigned to the post. The tag serves as a board
166 section. There can be multiple tags for each message
167 section. There can be multiple tags for each message
167 """
168 """
168
169
169 objects = TagManager()
170 objects = TagManager()
170
171
171 name = models.CharField(max_length=100)
172 name = models.CharField(max_length=100)
172 # TODO Connect the tag to its posts to check the number of threads for
173 # TODO Connect the tag to its posts to check the number of threads for
173 # the tag.
174 # the tag.
174
175
175 def __unicode__(self):
176 def __unicode__(self):
176 return self.name
177 return self.name
177
178
178 def is_empty(self):
179 def is_empty(self):
179 return self.get_post_count() == 0
180 return self.get_post_count() == 0
180
181
181 def get_post_count(self):
182 def get_post_count(self):
182 posts_with_tag = Post.objects.get_threads(tag=self)
183 posts_with_tag = Post.objects.get_threads(tag=self)
183 return posts_with_tag.count()
184 return posts_with_tag.count()
184
185
185 def get_popularity(self):
186 def get_popularity(self):
186 posts_with_tag = Post.objects.get_threads(tag=self)
187 posts_with_tag = Post.objects.get_threads(tag=self)
187 reply_count = 0
188 reply_count = 0
188 for post in posts_with_tag:
189 for post in posts_with_tag:
189 reply_count += post.get_reply_count()
190 reply_count += post.get_reply_count()
190 reply_count += OPENING_POST_POPULARITY_WEIGHT
191 reply_count += OPENING_POST_POPULARITY_WEIGHT
191
192
192 return reply_count
193 return reply_count
193
194
194
195
195 class Post(models.Model):
196 class Post(models.Model):
196 """A post is a message."""
197 """A post is a message."""
197
198
198 objects = PostManager()
199 objects = PostManager()
199
200
200 def _update_image_filename(self, filename):
201 def _update_image_filename(self, filename):
201 """Get unique image filename"""
202 """Get unique image filename"""
202
203
203 path = IMAGES_DIRECTORY
204 path = IMAGES_DIRECTORY
204 new_name = str(int(time.mktime(time.gmtime())))
205 new_name = str(int(time.mktime(time.gmtime())))
205 new_name += str(int(random() * 1000))
206 new_name += str(int(random() * 1000))
206 new_name += FILE_EXTENSION_DELIMITER
207 new_name += FILE_EXTENSION_DELIMITER
207 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
208 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
208
209
209 return os.path.join(path, new_name)
210 return os.path.join(path, new_name)
210
211
211 title = models.CharField(max_length=TITLE_MAX_LENGTH)
212 title = models.CharField(max_length=TITLE_MAX_LENGTH)
212 pub_time = models.DateTimeField()
213 pub_time = models.DateTimeField()
213 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
214 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
214 escape_html=False)
215 escape_html=False)
215 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
216 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
216 blank=True, sizes=(IMAGE_THUMB_SIZE,))
217 blank=True, sizes=(IMAGE_THUMB_SIZE,))
217 poster_ip = models.IPAddressField()
218 poster_ip = models.IPAddressField()
218 poster_user_agent = models.TextField()
219 poster_user_agent = models.TextField()
219 parent = models.BigIntegerField()
220 parent = models.BigIntegerField()
220 tags = models.ManyToManyField(Tag)
221 tags = models.ManyToManyField(Tag)
221 last_edit_time = models.DateTimeField()
222 last_edit_time = models.DateTimeField()
222
223
223 def __unicode__(self):
224 def __unicode__(self):
224 return '#' + str(self.id) + ' ' + self.title + ' (' + self.text.raw + \
225 return '#' + str(self.id) + ' ' + self.title + ' (' + self.text.raw + \
225 ')'
226 ')'
226
227
227 def _get_replies(self):
228 def _get_replies(self):
228 return Post.objects.filter(parent=self.id)
229 return Post.objects.filter(parent=self.id)
229
230
230 def get_reply_count(self):
231 def get_reply_count(self):
231 return self._get_replies().count()
232 return self._get_replies().count()
232
233
233 def get_images_count(self):
234 def get_images_count(self):
234 images_count = 1 if self.image else 0
235 images_count = 1 if self.image else 0
235 for reply in self._get_replies():
236 for reply in self._get_replies():
236 if reply.image:
237 if reply.image:
237 images_count += 1
238 images_count += 1
238
239
239 return images_count
240 return images_count
240
241
241 def get_gets_count(self):
242 def get_gets_count(self):
242 gets_count = 1 if self.is_get() else 0
243 gets_count = 1 if self.is_get() else 0
243 for reply in self._get_replies():
244 for reply in self._get_replies():
244 if reply.is_get():
245 if reply.is_get():
245 gets_count += 1
246 gets_count += 1
246
247
247 return gets_count
248 return gets_count
248
249
249 def is_get(self):
250 def is_get(self):
250 """If the post has pretty id (1, 1000, 77777), than it is called GET"""
251 """If the post has pretty id (1, 1000, 77777), than it is called GET"""
251
252
252 first = self.id == 1
253 first = self.id == 1
253
254
254 id_str = str(self.id)
255 id_str = str(self.id)
255 pretty = REGEX_PRETTY.match(id_str)
256 pretty = REGEX_PRETTY.match(id_str)
256 same_digits = REGEX_SAME.match(id_str)
257 same_digits = REGEX_SAME.match(id_str)
257
258
258 return first or pretty or same_digits
259 return first or pretty or same_digits
259
260
260 def can_bump(self):
261 def can_bump(self):
261 """Check if the thread can be bumped by replying"""
262 """Check if the thread can be bumped by replying"""
262
263
263 replies_count = len(Post.objects.get_thread(self.id))
264 replies_count = len(Post.objects.get_thread(self.id))
264
265
265 return replies_count <= settings.MAX_POSTS_PER_THREAD
266 return replies_count <= settings.MAX_POSTS_PER_THREAD
266
267
267 def get_last_replies(self):
268 def get_last_replies(self):
268 if settings.LAST_REPLIES_COUNT > 0:
269 if settings.LAST_REPLIES_COUNT > 0:
269 reply_count = self.get_reply_count()
270 reply_count = self.get_reply_count()
270
271
271 if reply_count > 0:
272 if reply_count > 0:
272 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
273 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
273 reply_count)
274 reply_count)
274 last_replies = self._get_replies()[reply_count
275 last_replies = self._get_replies()[reply_count
275 - reply_count_to_show:]
276 - reply_count_to_show:]
276
277
277 return last_replies
278 return last_replies
278
279
279
280
280 class Admin(models.Model):
281 class Admin(models.Model):
281 """
282 """
282 Model for admin users
283 Model for admin users
283 """
284 """
284 name = models.CharField(max_length=100)
285 name = models.CharField(max_length=100)
285 password = models.CharField(max_length=100)
286 password = models.CharField(max_length=100)
286
287
287 def __unicode__(self):
288 def __unicode__(self):
288 return self.name + '/' + '*' * len(self.password)
289 return self.name + '/' + '*' * len(self.password)
@@ -1,164 +1,165 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load markup %}
4 {% load markup %}
5
5
6 {% block head %}
6 {% block head %}
7 {% if tag %}
7 {% if tag %}
8 <title>Neboard - {{ tag }}</title>
8 <title>Neboard - {{ tag }}</title>
9 {% else %}
9 {% else %}
10 <title>Neboard</title>
10 <title>Neboard</title>
11 {% endif %}
11 {% endif %}
12 {% endblock %}
12 {% endblock %}
13
13
14 {% block content %}
14 {% block content %}
15
15
16 {% if tag %}
16 {% if tag %}
17 <div class="tag_info">
17 <div class="tag_info">
18 <h2>{% trans 'Tag: ' %}{{ tag }}</h2>
18 <h2>{% trans 'Tag: ' %}{{ tag }}</h2>
19 </div>
19 </div>
20 {% endif %}
20 {% endif %}
21
21
22 {% if threads %}
22 {% if threads %}
23 {% for thread in threads %}
23 {% for thread in threads %}
24 <div class="thread">
24 <div class="thread">
25 {% if thread.can_bump %}
25 {% if thread.can_bump %}
26 <div class="post">
26 <div class="post">
27 {% else %}
27 {% else %}
28 <div class="post dead_post">
28 <div class="post dead_post">
29 {% endif %}
29 {% endif %}
30 {% if thread.image %}
30 {% if thread.image %}
31 <div class="image">
31 <div class="image">
32 <a class="fancy"
32 <a class="fancy"
33 href="{{ thread.image.url }}"><img
33 href="{{ thread.image.url }}"><img
34 src="{{ thread.image.url_200x150 }}"
34 src="{{ thread.image.url_200x150 }}"
35 alt="{% trans 'Post image' %}" />
35 alt="{% trans 'Post image' %}" />
36 </a>
36 </a>
37 </div>
37 </div>
38 {% endif %}
38 {% endif %}
39 <div class="message">
39 <div class="message">
40 <div class="post-info">
40 <div class="post-info">
41 <span class="title">{{ thread.title }}</span>
41 <span class="title">{{ thread.title }}</span>
42 <a class="post_id" href="{% url 'thread' thread.id %}">
42 <a class="post_id" href="{% url 'thread' thread.id %}">
43 (#{{ thread.id }})</a>
43 (#{{ thread.id }})</a>
44 [{{ thread.pub_time }}]
44 [{{ thread.pub_time }}]
45 [<a class="link" href="{% url 'thread' thread.id %}#form"
45 [<a class="link" href="{% url 'thread' thread.id %}#form"
46 >{% trans "Reply" %}</a>]
46 >{% trans "Reply" %}</a>]
47 </div>
47 </div>
48 {% autoescape off %}
48 {% autoescape off %}
49 {{ thread.text.rendered|truncatewords_html:50 }}
49 {{ thread.text.rendered|truncatewords_html:50 }}
50 {% endautoescape %}
50 {% endautoescape %}
51 </div>
51 </div>
52 <div class="metadata">
52 <div class="metadata">
53 {{ thread.get_reply_count }} {% trans 'replies' %},
53 {{ thread.get_reply_count }} {% trans 'replies' %},
54 {{ thread.get_images_count }} {% trans 'images' %}.
54 {{ thread.get_images_count }} {% trans 'images' %}.
55 {% if thread.tags.all %}
55 {% if thread.tags.all %}
56 <span class="tags">{% trans 'Tags' %}:
56 <span class="tags">{% trans 'Tags' %}:
57 {% for tag in thread.tags.all %}
57 {% for tag in thread.tags.all %}
58 <a class="tag" href="
58 <a class="tag" href="
59 {% url 'tag' tag_name=tag.name %}">
59 {% url 'tag' tag_name=tag.name %}">
60 {{ tag.name }}</a>
60 {{ tag.name }}</a>
61 {% endfor %}
61 {% endfor %}
62 </span>
62 </span>
63 {% endif %}
63 {% endif %}
64 </div>
64 </div>
65 </div>
65 </div>
66 {% if thread.get_last_replies %}
66 {% if thread.get_last_replies %}
67 <div class="last-replies">
67 <div class="last-replies">
68 {% for post in thread.get_last_replies %}
68 {% for post in thread.get_last_replies %}
69 {% if thread.can_bump %}
69 {% if thread.can_bump %}
70 <div class="post">
70 <div class="post">
71 {% else %}
71 {% else %}
72 <div class="post dead_post">
72 <div class="post dead_post">
73 {% endif %}
73 {% endif %}
74 {% if post.image %}
74 {% if post.image %}
75 <div class="image">
75 <div class="image">
76 <a class="fancy"
76 <a class="fancy"
77 href="{{ post.image.url }}"><img
77 href="{{ post.image.url }}"><img
78 src=" {{ post.image.url_200x150 }}"
78 src=" {{ post.image.url_200x150 }}"
79 alt="{% trans 'Post image' %}" />
79 alt="{% trans 'Post image' %}" />
80 </a>
80 </a>
81 </div>
81 </div>
82 {% endif %}
82 {% endif %}
83 <div class="message">
83 <div class="message">
84 <div class="post-info">
84 <div class="post-info">
85 <span class="title">{{ post.title }}</span>
85 <span class="title">{{ post.title }}</span>
86 <a class="post_id" href="
86 <a class="post_id" href="
87 {% url 'thread' thread.id %}#{{ post.id }}">
87 {% url 'thread' thread.id %}#{{ post.id }}">
88 (#{{ post.id }})</a>
88 (#{{ post.id }})</a>
89 [{{ post.pub_time }}]
89 [{{ post.pub_time }}]
90 </div>
90 </div>
91 {% autoescape off %}
91 {% autoescape off %}
92 {{ post.text.rendered|truncatewords_html:50 }}
92 {{ post.text.rendered|truncatewords_html:50 }}
93 {% endautoescape %}
93 {% endautoescape %}
94 </div>
94 </div>
95 </div>
95 </div>
96 {% endfor %}
96 {% endfor %}
97 </div>
97 </div>
98 {% endif %}
98 {% endif %}
99 </div>
99 </div>
100 {% endfor %}
100 {% endfor %}
101 {% else %}
101 {% else %}
102 No threads found.
102 No threads found.
103 <hr />
103 <hr />
104 {% endif %}
104 {% endif %}
105
105
106 <form enctype="multipart/form-data" method="post">{% csrf_token %}
106 <form enctype="multipart/form-data" method="post">{% csrf_token %}
107 <div class="post-form-w">
107 <div class="post-form-w">
108
108
109 <div class="form-title">{% trans "Create new thread" %}</div>
109 <div class="form-title">{% trans "Create new thread" %}</div>
110 <div class="post-form">
110 <div class="post-form">
111 <div class="form-row">
111 <div class="form-row">
112 <div class="form-label">{% trans 'Title' %}</div>
112 <div class="form-label">{% trans 'Title' %}</div>
113 <div class="form-input">{{ form.title }}</div>
113 <div class="form-input">{{ form.title }}</div>
114 <div class="form-errors">{{ form.title.errors }}</div>
114 <div class="form-errors">{{ form.title.errors }}</div>
115 </div>
115 </div>
116 <div class="form-row">
116 <div class="form-row">
117 <div class="form-label">{% trans 'Text' %}</div>
117 <div class="form-label">{% trans 'Text' %}</div>
118 <div class="form-input">{{ form.text }}</div>
118 <div class="form-input">{{ form.text }}</div>
119 <div class="form-errors">{{ form.text.errors }}</div>
119 <div class="form-errors">{{ form.text.errors }}</div>
120 </div>
120 </div>
121 <div class="form-row">
121 <div class="form-row">
122 <div class="form-label">{% trans 'Image' %}</div>
122 <div class="form-label">{% trans 'Image' %}</div>
123 <div class="form-input">{{ form.image }}</div>
123 <div class="form-input">{{ form.image }}</div>
124 <div class="form-errors">{{ form.image.errors }}</div>
124 <div class="form-errors">{{ form.image.errors }}</div>
125 </div>
125 </div>
126 <div class="form-row">
126 <div class="form-row">
127 <div class="form-label">{% trans 'Tags' %}</div>
127 <div class="form-label">{% trans 'Tags' %}</div>
128 <div class="form-input">{{ form.tags }}</div>
128 <div class="form-input">{{ form.tags }}</div>
129 <div class="form-errors">{{ form.tags.errors }}</div>
129 <div class="form-errors">{{ form.tags.errors }}</div>
130 </div>
130 </div>
131 <div class="form-row">
131 <div class="form-row">
132 {{ form.captcha }}
132 {{ form.captcha }}
133 <div class="form-errors">{{ form.captcha.errors }}</div>
133 </div>
134 </div>
134 </div>
135 </div>
135 <div class="form-submit">
136 <div class="form-submit">
136 <input type="submit" value="{% trans "Post" %}"/></div>
137 <input type="submit" value="{% trans "Post" %}"/></div>
137 <div>
138 <div>
138 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
139 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
139 </div>
140 </div>
140 <div><a href="http://daringfireball.net/projects/markdown/basics">
141 <div><a href="http://daringfireball.net/projects/markdown/basics">
141 {% trans 'Basic markdown syntax.' %}</a></div>
142 {% trans 'Basic markdown syntax.' %}</a></div>
142 </div>
143 </div>
143 </form>
144 </form>
144
145
145 {% endblock %}
146 {% endblock %}
146
147
147 {% block metapanel %}
148 {% block metapanel %}
148
149
149 <span class="metapanel">
150 <span class="metapanel">
150 <b><a href="https://bitbucket.org/neko259/neboard/">Neboard</a>
151 <b><a href="https://bitbucket.org/neko259/neboard/">Neboard</a>
151 pre1.0</b>
152 pre1.0</b>
152 {% trans "Pages:" %}
153 {% trans "Pages:" %}
153 {% for page in pages %}
154 {% for page in pages %}
154 [<a href="
155 [<a href="
155 {% if tag %}
156 {% if tag %}
156 {% url "tag" tag_name=tag page=page %}
157 {% url "tag" tag_name=tag page=page %}
157 {% else %}
158 {% else %}
158 {% url "index" page=page %}
159 {% url "index" page=page %}
159 {% endif %}
160 {% endif %}
160 ">{{ page }}</a>]
161 ">{{ page }}</a>]
161 {% endfor %}
162 {% endfor %}
162 </span>
163 </span>
163
164
164 {% endblock %}
165 {% endblock %}
@@ -1,113 +1,114 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load markup %}
4 {% load markup %}
5
5
6 {% block head %}
6 {% block head %}
7 <title>Neboard - {{ posts.0.title }}</title>
7 <title>Neboard - {{ posts.0.title }}</title>
8 {% endblock %}
8 {% endblock %}
9
9
10 {% block content %}
10 {% block content %}
11 <script src="{{ STATIC_URL }}js/thread.js"></script>
11 <script src="{{ STATIC_URL }}js/thread.js"></script>
12
12
13 {% if posts %}
13 {% if posts %}
14 <div id="posts">
14 <div id="posts">
15 {% for post in posts %}
15 {% for post in posts %}
16 {% if posts.0.can_bump %}
16 {% if posts.0.can_bump %}
17 <div class="post" id="{{ post.id }}">
17 <div class="post" id="{{ post.id }}">
18 {% else %}
18 {% else %}
19 <div class="post dead_post" id="{{ post.id }}">
19 <div class="post dead_post" id="{{ post.id }}">
20 {% endif %}
20 {% endif %}
21 {% if post.image %}
21 {% if post.image %}
22 <div class="image">
22 <div class="image">
23 <a
23 <a
24 class="fancy"
24 class="fancy"
25 href="{{ post.image.url }}"><img
25 href="{{ post.image.url }}"><img
26 src="{{ post.image.url_200x150 }}"
26 src="{{ post.image.url_200x150 }}"
27 alt="{% trans 'Post image' %}" />
27 alt="{% trans 'Post image' %}" />
28 </a>
28 </a>
29 </div>
29 </div>
30 {% endif %}
30 {% endif %}
31 <div class="message">
31 <div class="message">
32 <div class="post-info">
32 <div class="post-info">
33 <span class="title">{{ post.title }}</span>
33 <span class="title">{{ post.title }}</span>
34 <a class="post_id" href="#{{ post.id }}">
34 <a class="post_id" href="#{{ post.id }}">
35 (#{{ post.id }})</a>
35 (#{{ post.id }})</a>
36 [{{ post.pub_time }}]
36 [{{ post.pub_time }}]
37 {% if post.is_get %}
37 {% if post.is_get %}
38 <span class="get">
38 <span class="get">
39 {% trans "Get!" %}
39 {% trans "Get!" %}
40 </span>
40 </span>
41 {% endif %}
41 {% endif %}
42 </div>
42 </div>
43 {% autoescape off %}
43 {% autoescape off %}
44 {{ post.text.rendered }}
44 {{ post.text.rendered }}
45 {% endautoescape %}
45 {% endautoescape %}
46 </div>
46 </div>
47 {% if post.tags.all %}
47 {% if post.tags.all %}
48 <div class="metadata">
48 <div class="metadata">
49 <span class="tags">{% trans 'Tags' %}:
49 <span class="tags">{% trans 'Tags' %}:
50 {% for tag in post.tags.all %}
50 {% for tag in post.tags.all %}
51 <a class="tag" href="{% url 'tag' tag.name %}">
51 <a class="tag" href="{% url 'tag' tag.name %}">
52 {{ tag.name }}</a>
52 {{ tag.name }}</a>
53 {% endfor %}
53 {% endfor %}
54 </span>
54 </span>
55 </div>
55 </div>
56 {% endif %}
56 {% endif %}
57 </div>
57 </div>
58 {% endfor %}
58 {% endfor %}
59 </div>
59 </div>
60 {% else %}
60 {% else %}
61 No thread found.
61 No thread found.
62 <hr />
62 <hr />
63 {% endif %}
63 {% endif %}
64
64
65 <form id="form" enctype="multipart/form-data" method="post"
65 <form id="form" enctype="multipart/form-data" method="post"
66 >{% csrf_token %}
66 >{% csrf_token %}
67 <div class="post-form-w">
67 <div class="post-form-w">
68 <div class="form-title">{% trans "Reply to thread" %}</div>
68 <div class="form-title">{% trans "Reply to thread" %}</div>
69 <div class="post-form">
69 <div class="post-form">
70 <div class="form-row">
70 <div class="form-row">
71 <div class="form-label">{% trans 'Title' %}</div>
71 <div class="form-label">{% trans 'Title' %}</div>
72 <div class="form-input">{{ form.title }}</div>
72 <div class="form-input">{{ form.title }}</div>
73 <div class="form-errors">{{ form.title.errors }}</div>
73 <div class="form-errors">{{ form.title.errors }}</div>
74 </div>
74 </div>
75 <div class="form-row">
75 <div class="form-row">
76 <div class="form-label">{% trans 'Text' %}</div>
76 <div class="form-label">{% trans 'Text' %}</div>
77 <div class="form-input">{{ form.text }}</div>
77 <div class="form-input">{{ form.text }}</div>
78 <div class="form-errors">{{ form.text.errors }}</div>
78 <div class="form-errors">{{ form.text.errors }}</div>
79 </div>
79 </div>
80 <div class="form-row">
80 <div class="form-row">
81 <div class="form-label">{% trans 'Image' %}</div>
81 <div class="form-label">{% trans 'Image' %}</div>
82 <div class="form-input">{{ form.image }}</div>
82 <div class="form-input">{{ form.image }}</div>
83 <div class="form-errors">{{ form.image.errors }}</div>
83 <div class="form-errors">{{ form.image.errors }}</div>
84 </div>
84 </div>
85 <div class="form-row">
85 <div class="form-row">
86 {{ form.captcha }}
86 {{ form.captcha }}
87 <div class="form-errors">{{ form.captcha.errors }}</div>
87 </div>
88 </div>
88 </div>
89 </div>
89
90
90 <div class="form-submit"><input type="submit"
91 <div class="form-submit"><input type="submit"
91 value="{% trans "Post" %}"/></div>
92 value="{% trans "Post" %}"/></div>
92 <div><a href="http://daringfireball.net/projects/markdown/basics">
93 <div><a href="http://daringfireball.net/projects/markdown/basics">
93 {% trans 'Basic markdown syntax.' %}</a></div>
94 {% trans 'Basic markdown syntax.' %}</a></div>
94 <div>{% trans 'Example: ' %}*<i>{% trans 'italic' %}</i>*,
95 <div>{% trans 'Example: ' %}*<i>{% trans 'italic' %}</i>*,
95 **<b>{% trans 'bold' %}</b>**</div>
96 **<b>{% trans 'bold' %}</b>**</div>
96 <div>{% trans 'Quotes can be inserted with' %} "&gt;"</div>
97 <div>{% trans 'Quotes can be inserted with' %} "&gt;"</div>
97 <div>{% trans 'Links to answers can be inserted with' %}
98 <div>{% trans 'Links to answers can be inserted with' %}
98 "&gt;&gt;123"
99 "&gt;&gt;123"
99 </div>
100 </div>
100 </div>
101 </div>
101 </form>
102 </form>
102
103
103 {% endblock %}
104 {% endblock %}
104
105
105 {% block metapanel %}
106 {% block metapanel %}
106
107
107 <span class="metapanel">
108 <span class="metapanel">
108 {{ posts.0.get_reply_count }} {% trans 'replies' %},
109 {{ posts.0.get_reply_count }} {% trans 'replies' %},
109 {{ posts.0.get_images_count }} {% trans 'images' %}.
110 {{ posts.0.get_images_count }} {% trans 'images' %}.
110 {% trans 'Last update: ' %}{{ posts.0.last_edit_time }}
111 {% trans 'Last update: ' %}{{ posts.0.last_edit_time }}
111 </span>
112 </span>
112
113
113 {% endblock %} No newline at end of file
114 {% endblock %}
@@ -1,14 +1,64 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4
4 from neboard import settings
5 from neboard import settings
6 import time
5
7
6
8
7 def check_if_human(request):
9 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
10 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
11 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
12
13
14 def need_include_captcha(request):
8 """
15 """
9 Check if request is made by a user.
16 Check if request is made by a user.
10 It contains rules which check for bots.
17 It contains rules which check for bots.
11 """
18 """
12
19
13 # FIXME: need to insert checking logic
20 if not settings.ENABLE_CAPTCHA:
14 return not settings.ENABLE_CAPTCHA
21 return False
22
23 enable_captcha = False
24
25 #newcomer
26 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
27 return settings.ENABLE_CAPTCHA
28
29 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
30 current_delay = int(time.time()) - last_activity
31
32 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
33 if KEY_CAPTCHA_DELAY_TIME in request.session
34 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
35
36 if current_delay < delay_time:
37 enable_captcha = True
38
39 print 'ENABLING' + str(enable_captcha)
40
41 return enable_captcha
42
43
44 def update_captcha_access(request, passed):
45 """
46 Update captcha fields.
47 It will reduce delay time if user passed captcha verification and
48 it will increase it otherwise.
49 """
50 session = request.session
51
52 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
53 if KEY_CAPTCHA_DELAY_TIME in request.session
54 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
55
56 print "DELAY TIME = " + str(delay_time)
57
58 if passed:
59 delay_time -= 2 if delay_time >= 7 else 5
60 else:
61 delay_time += 10
62
63 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
64 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
@@ -1,212 +1,219 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.template import RequestContext
2 from django.template import RequestContext
3 from django.shortcuts import render, redirect, get_object_or_404
3 from django.shortcuts import render, redirect, get_object_or_404
4 from django.http import HttpResponseRedirect
4 from django.http import HttpResponseRedirect
5
5
6 from boards import forms
6 from boards import forms
7 import boards
7 import boards
8 from boards import utils
8 from boards import utils
9 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
9 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
10 ThreadCaptchaForm, PostCaptchaForm
10 ThreadCaptchaForm, PostCaptchaForm
11
11
12 from boards.models import Post, Admin, Tag
12 from boards.models import Post, Admin, Tag
13 import neboard
13 import neboard
14
14
15
15
16 def index(request, page=0):
16 def index(request, page=0):
17 context = RequestContext(request)
17 context = RequestContext(request)
18
18
19 threadFormClass = (ThreadForm
19 if utils.need_include_captcha(request):
20 if utils.check_if_human(request)
20 threadFormClass = ThreadCaptchaForm
21 else ThreadCaptchaForm)
21 kwargs = {'request': request}
22 else:
23 threadFormClass = ThreadForm
24 kwargs = {}
22
25
23 if request.method == 'POST':
26 if request.method == 'POST':
24 form = threadFormClass(request.POST, request.FILES,
27 form = threadFormClass(request.POST, request.FILES,
25 error_class=PlainErrorList)
28 error_class=PlainErrorList, **kwargs)
26
29
27 if form.is_valid():
30 if form.is_valid():
28 return _new_post(request, form)
31 return _new_post(request, form)
29 else:
32 else:
30 form = threadFormClass(error_class=PlainErrorList)
33 form = threadFormClass(error_class=PlainErrorList, **kwargs)
31
34
32 threads = Post.objects.get_threads(page=int(page))
35 threads = Post.objects.get_threads(page=int(page))
33
36
34 # TODO Get rid of the duplicate code in index and tag views
37 # TODO Get rid of the duplicate code in index and tag views
35 context['threads'] = None if len(threads) == 0 else threads
38 context['threads'] = None if len(threads) == 0 else threads
36 context['form'] = form
39 context['form'] = form
37 context['tags'] = Tag.objects.get_popular_tags()
40 context['tags'] = Tag.objects.get_popular_tags()
38 context['theme'] = _get_theme(request)
41 context['theme'] = _get_theme(request)
39 context['pages'] = range(Post.objects.get_thread_page_count())
42 context['pages'] = range(Post.objects.get_thread_page_count())
40
43
41 return render(request, 'boards/posting_general.html',
44 return render(request, 'boards/posting_general.html',
42 context)
45 context)
43
46
44
47
45 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
48 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
46 """Add a new post (in thread or as a reply)."""
49 """Add a new post (in thread or as a reply)."""
47
50
48 data = form.cleaned_data
51 data = form.cleaned_data
49
52
50 title = data['title']
53 title = data['title']
51 text = data['text']
54 text = data['text']
52
55
53 if 'image' in data.keys():
56 if 'image' in data.keys():
54 image = data['image']
57 image = data['image']
55 else:
58 else:
56 image = None
59 image = None
57
60
58 ip = _get_client_ip(request)
61 ip = _get_client_ip(request)
59
62
60 tags = []
63 tags = []
61
64
62 new_thread = thread_id == boards.models.NO_PARENT
65 new_thread = thread_id == boards.models.NO_PARENT
63 if new_thread:
66 if new_thread:
64 tag_strings = data['tags']
67 tag_strings = data['tags']
65
68
66 if tag_strings:
69 if tag_strings:
67 tag_strings = tag_strings.split(' ')
70 tag_strings = tag_strings.split(' ')
68 for tag_name in tag_strings:
71 for tag_name in tag_strings:
69 tag_name = tag_name.strip()
72 tag_name = tag_name.strip()
70 if len(tag_name) > 0:
73 if len(tag_name) > 0:
71 tag, created = Tag.objects.get_or_create(name=tag_name)
74 tag, created = Tag.objects.get_or_create(name=tag_name)
72 tags.append(tag)
75 tags.append(tag)
73
76
74 # TODO Add a possibility to define a link image instead of an image file.
77 # TODO Add a possibility to define a link image instead of an image file.
75 # If a link is given, download the image automatically.
78 # If a link is given, download the image automatically.
76
79
77 post = Post.objects.create_post(title=title, text=text, ip=ip,
80 post = Post.objects.create_post(title=title, text=text, ip=ip,
78 parent_id=thread_id, image=image,
81 parent_id=thread_id, image=image,
79 tags=tags)
82 tags=tags)
80
83
81 thread_to_show = (post.id if new_thread else thread_id)
84 thread_to_show = (post.id if new_thread else thread_id)
82
85
83 if new_thread:
86 if new_thread:
84 return redirect(thread, post_id=thread_to_show)
87 return redirect(thread, post_id=thread_to_show)
85 else:
88 else:
86 return redirect(reverse(thread,
89 return redirect(reverse(thread,
87 kwargs={'post_id': thread_to_show}) + '#'
90 kwargs={'post_id': thread_to_show}) + '#'
88 + str(post.id))
91 + str(post.id))
89
92
90
93
91 def tag(request, tag_name, page=0):
94 def tag(request, tag_name, page=0):
92 """Get all tag threads (posts without a parent)."""
95 """Get all tag threads (posts without a parent)."""
93
96
94 tag = get_object_or_404(Tag, name=tag_name)
97 tag = get_object_or_404(Tag, name=tag_name)
95 threads = Post.objects.get_threads(tag=tag, page=int(page))
98 threads = Post.objects.get_threads(tag=tag, page=int(page))
96
99
97 if request.method == 'POST':
100 if request.method == 'POST':
98 form = ThreadForm(request.POST, request.FILES,
101 form = ThreadForm(request.POST, request.FILES,
99 error_class=PlainErrorList)
102 error_class=PlainErrorList)
100 if form.is_valid():
103 if form.is_valid():
101 return _new_post(request, form)
104 return _new_post(request, form)
102 else:
105 else:
103 form = forms.ThreadForm(initial={'tags': tag_name},
106 form = forms.ThreadForm(initial={'tags': tag_name},
104 error_class=PlainErrorList)
107 error_class=PlainErrorList)
105
108
106 context = RequestContext(request)
109 context = RequestContext(request)
107 context['threads'] = None if len(threads) == 0 else threads
110 context['threads'] = None if len(threads) == 0 else threads
108 context['tag'] = tag_name
111 context['tag'] = tag_name
109 context['tags'] = Tag.objects.get_popular_tags()
112 context['tags'] = Tag.objects.get_popular_tags()
110 context['theme'] = _get_theme(request)
113 context['theme'] = _get_theme(request)
111 context['pages'] = range(Post.objects.get_thread_page_count(tag=tag))
114 context['pages'] = range(Post.objects.get_thread_page_count(tag=tag))
112
115
113 context['form'] = form
116 context['form'] = form
114
117
115 return render(request, 'boards/posting_general.html',
118 return render(request, 'boards/posting_general.html',
116 context)
119 context)
117
120
118
121
119 def thread(request, post_id):
122 def thread(request, post_id):
120 """Get all thread posts"""
123 """Get all thread posts"""
121
124
122 postFormClass = (PostForm if utils.check_if_human(request) else
125 if utils.need_include_captcha(request):
123 PostCaptchaForm)
126 postFormClass = PostCaptchaForm
127 kwargs = {'request': request}
128 else:
129 postFormClass = PostForm
130 kwargs = {}
124
131
125 if request.method == 'POST':
132 if request.method == 'POST':
126 form = postFormClass(request.POST, request.FILES,
133 form = postFormClass(request.POST, request.FILES,
127 error_class=PlainErrorList)
134 error_class=PlainErrorList, **kwargs)
128 if form.is_valid():
135 if form.is_valid():
129 return _new_post(request, form, post_id)
136 return _new_post(request, form, post_id)
130 else:
137 else:
131 form = postFormClass(error_class=PlainErrorList)
138 form = postFormClass(error_class=PlainErrorList, **kwargs)
132
139
133 posts = Post.objects.get_thread(post_id)
140 posts = Post.objects.get_thread(post_id)
134
141
135 context = RequestContext(request)
142 context = RequestContext(request)
136
143
137 context['posts'] = posts
144 context['posts'] = posts
138 context['form'] = form
145 context['form'] = form
139 context['tags'] = Tag.objects.get_popular_tags()
146 context['tags'] = Tag.objects.get_popular_tags()
140 context['theme'] = _get_theme(request)
147 context['theme'] = _get_theme(request)
141
148
142 return render(request, 'boards/thread.html', context)
149 return render(request, 'boards/thread.html', context)
143
150
144
151
145 def login(request):
152 def login(request):
146 """Log in as admin"""
153 """Log in as admin"""
147
154
148 if 'name' in request.POST and 'password' in request.POST:
155 if 'name' in request.POST and 'password' in request.POST:
149 request.session['admin'] = False
156 request.session['admin'] = False
150
157
151 isAdmin = len(Admin.objects.filter(name=request.POST['name'],
158 isAdmin = len(Admin.objects.filter(name=request.POST['name'],
152 password=request.POST[
159 password=request.POST[
153 'password'])) > 0
160 'password'])) > 0
154
161
155 if isAdmin:
162 if isAdmin:
156 request.session['admin'] = True
163 request.session['admin'] = True
157
164
158 response = HttpResponseRedirect('/')
165 response = HttpResponseRedirect('/')
159
166
160 else:
167 else:
161 response = render(request, 'boards/login.html', {'error': 'Login error'})
168 response = render(request, 'boards/login.html', {'error': 'Login error'})
162 else:
169 else:
163 response = render(request, 'boards/login.html', {})
170 response = render(request, 'boards/login.html', {})
164
171
165 return response
172 return response
166
173
167
174
168 def logout(request):
175 def logout(request):
169 request.session['admin'] = False
176 request.session['admin'] = False
170 return HttpResponseRedirect('/')
177 return HttpResponseRedirect('/')
171
178
172
179
173 def settings(request):
180 def settings(request):
174 context = RequestContext(request)
181 context = RequestContext(request)
175
182
176 if request.method == 'POST':
183 if request.method == 'POST':
177 form = SettingsForm(request.POST)
184 form = SettingsForm(request.POST)
178 if form.is_valid():
185 if form.is_valid():
179 selected_theme = form.cleaned_data['theme']
186 selected_theme = form.cleaned_data['theme']
180 request.session['theme'] = selected_theme
187 request.session['theme'] = selected_theme
181
188
182 return redirect(settings)
189 return redirect(settings)
183 else:
190 else:
184 selected_theme = _get_theme(request)
191 selected_theme = _get_theme(request)
185 form = SettingsForm(initial={'theme': selected_theme})
192 form = SettingsForm(initial={'theme': selected_theme})
186 context['form'] = form
193 context['form'] = form
187 context['tags'] = Tag.objects.get_popular_tags()
194 context['tags'] = Tag.objects.get_popular_tags()
188 context['theme'] = _get_theme(request)
195 context['theme'] = _get_theme(request)
189
196
190 return render(request, 'boards/settings.html', context)
197 return render(request, 'boards/settings.html', context)
191
198
192
199
193 def all_tags(request):
200 def all_tags(request):
194 context = RequestContext(request)
201 context = RequestContext(request)
195 context['tags'] = Tag.objects.get_popular_tags()
202 context['tags'] = Tag.objects.get_popular_tags()
196 context['theme'] = _get_theme(request)
203 context['theme'] = _get_theme(request)
197 context['all_tags'] = Tag.objects.get_not_empty_tags()
204 context['all_tags'] = Tag.objects.get_not_empty_tags()
198
205
199 return render(request, 'boards/tags.html', context)
206 return render(request, 'boards/tags.html', context)
200
207
201
208
202 def _get_theme(request):
209 def _get_theme(request):
203 return request.session.get('theme', neboard.settings.DEFAULT_THEME)
210 return request.session.get('theme', neboard.settings.DEFAULT_THEME)
204
211
205
212
206 def _get_client_ip(request):
213 def _get_client_ip(request):
207 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
214 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
208 if x_forwarded_for:
215 if x_forwarded_for:
209 ip = x_forwarded_for.split(',')[-1].strip()
216 ip = x_forwarded_for.split(',')[-1].strip()
210 else:
217 else:
211 ip = request.META.get('REMOTE_ADDR')
218 ip = request.META.get('REMOTE_ADDR')
212 return ip
219 return ip
@@ -1,195 +1,198 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 = 'ru-RU'
35 LANGUAGE_CODE = 'ru-RU'
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.middleware.common.CommonMiddleware',
106 'django.middleware.common.CommonMiddleware',
107 'django.contrib.sessions.middleware.SessionMiddleware',
107 'django.contrib.sessions.middleware.SessionMiddleware',
108 # 'django.middleware.csrf.CsrfViewMiddleware',
108 # 'django.middleware.csrf.CsrfViewMiddleware',
109 'django.contrib.auth.middleware.AuthenticationMiddleware',
109 'django.contrib.auth.middleware.AuthenticationMiddleware',
110 'django.contrib.messages.middleware.MessageMiddleware',
110 'django.contrib.messages.middleware.MessageMiddleware',
111 # Uncomment the next line for simple clickjacking protection:
111 # Uncomment the next line for simple clickjacking protection:
112 # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
112 # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
113 )
113 )
114
114
115 ROOT_URLCONF = 'neboard.urls'
115 ROOT_URLCONF = 'neboard.urls'
116
116
117 # Python dotted path to the WSGI application used by Django's runserver.
117 # Python dotted path to the WSGI application used by Django's runserver.
118 WSGI_APPLICATION = 'neboard.wsgi.application'
118 WSGI_APPLICATION = 'neboard.wsgi.application'
119
119
120 TEMPLATE_DIRS = (
120 TEMPLATE_DIRS = (
121 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
121 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
122 # Always use forward slashes, even on Windows.
122 # Always use forward slashes, even on Windows.
123 # Don't forget to use absolute paths, not relative paths.
123 # Don't forget to use absolute paths, not relative paths.
124 'templates',
124 'templates',
125 )
125 )
126
126
127 INSTALLED_APPS = (
127 INSTALLED_APPS = (
128 'django.contrib.auth',
128 'django.contrib.auth',
129 'django.contrib.contenttypes',
129 'django.contrib.contenttypes',
130 'django.contrib.sessions',
130 'django.contrib.sessions',
131 # 'django.contrib.sites',
131 # 'django.contrib.sites',
132 'django.contrib.messages',
132 'django.contrib.messages',
133 'django.contrib.staticfiles',
133 'django.contrib.staticfiles',
134 # Uncomment the next line to enable the admin:
134 # Uncomment the next line to enable the admin:
135 'django.contrib.admin',
135 'django.contrib.admin',
136 # Uncomment the next line to enable admin documentation:
136 # Uncomment the next line to enable admin documentation:
137 # 'django.contrib.admindocs',
137 # 'django.contrib.admindocs',
138 'django.contrib.markup',
138 'django.contrib.markup',
139 'django_cleanup',
139 'django_cleanup',
140 'boards',
140 'boards',
141 'captcha',
141 'captcha',
142 )
142 )
143
143
144 # TODO: NEED DESIGN FIXES
144 # TODO: NEED DESIGN FIXES
145 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
145 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
146 u'<div class="form-label">%(image)s</div>'
146 u'<div class="form-label">%(image)s</div>'
147 u'<div class="form-text">%(text_field)s</div>')
147 u'<div class="form-text">%(text_field)s</div>')
148
148
149 # A sample logging configuration. The only tangible logging
149 # A sample logging configuration. The only tangible logging
150 # performed by this configuration is to send an email to
150 # performed by this configuration is to send an email to
151 # the site admins on every HTTP 500 error when DEBUG=False.
151 # the site admins on every HTTP 500 error when DEBUG=False.
152 # See http://docs.djangoproject.com/en/dev/topics/logging for
152 # See http://docs.djangoproject.com/en/dev/topics/logging for
153 # more details on how to customize your logging configuration.
153 # more details on how to customize your logging configuration.
154 LOGGING = {
154 LOGGING = {
155 'version': 1,
155 'version': 1,
156 'disable_existing_loggers': False,
156 'disable_existing_loggers': False,
157 'filters': {
157 'filters': {
158 'require_debug_false': {
158 'require_debug_false': {
159 '()': 'django.utils.log.RequireDebugFalse'
159 '()': 'django.utils.log.RequireDebugFalse'
160 }
160 }
161 },
161 },
162 'handlers': {
162 'handlers': {
163 'mail_admins': {
163 'mail_admins': {
164 'level': 'ERROR',
164 'level': 'ERROR',
165 'filters': ['require_debug_false'],
165 'filters': ['require_debug_false'],
166 'class': 'django.utils.log.AdminEmailHandler'
166 'class': 'django.utils.log.AdminEmailHandler'
167 }
167 }
168 },
168 },
169 'loggers': {
169 'loggers': {
170 'django.request': {
170 'django.request': {
171 'handlers': ['mail_admins'],
171 'handlers': ['mail_admins'],
172 'level': 'ERROR',
172 'level': 'ERROR',
173 'propagate': True,
173 'propagate': True,
174 },
174 },
175 }
175 }
176 }
176 }
177
177
178 MARKUP_FIELD_TYPES = (
178 MARKUP_FIELD_TYPES = (
179 ('markdown', markdown_extended),
179 ('markdown', markdown_extended),
180 )
180 )
181 # Custom imageboard settings
181 # Custom imageboard settings
182 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
182 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
183 MAX_THREAD_COUNT = 500 # Old threads will be deleted to preserve this count
183 MAX_THREAD_COUNT = 500 # Old threads will be deleted to preserve this count
184 THREADS_PER_PAGE = 10
184 THREADS_PER_PAGE = 10
185 SITE_NAME = 'Neboard'
185 SITE_NAME = 'Neboard'
186
186
187 THEMES = [
187 THEMES = [
188 ('md', 'Mystic Dark'),
188 ('md', 'Mystic Dark'),
189 ('sw', 'Snow White') ]
189 ('sw', 'Snow White') ]
190
190 DEFAULT_THEME = 'md'
191 DEFAULT_THEME = 'md'
191
192
192 POPULAR_TAGS = 10
193 POPULAR_TAGS = 10
193 LAST_REPLIES_COUNT = 3
194 LAST_REPLIES_COUNT = 3
194
195
195 ENABLE_CAPTCHA = False No newline at end of file
196 ENABLE_CAPTCHA = True
197 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
198 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now