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