##// END OF EJS Templates
Split up user models
neko259 -
r386:309b8129 default
parent child Browse files
Show More
@@ -0,0 +1,108 b''
1 from django.db import models
2 from django.db.models import Count
3 from boards import settings
4 from boards.models import Post
5 from django.core.cache import cache
6
7 __author__ = 'neko259'
8
9 RANK_ADMIN = 0
10 RANK_MODERATOR = 10
11 RANK_USER = 100
12
13 BAN_REASON_AUTO = 'Auto'
14 BAN_REASON_MAX_LENGTH = 200
15
16
17 class User(models.Model):
18
19 class Meta:
20 app_label = 'boards'
21
22 user_id = models.CharField(max_length=50)
23 rank = models.IntegerField()
24
25 registration_time = models.DateTimeField()
26
27 fav_tags = models.ManyToManyField('Tag', null=True, blank=True)
28 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
29 blank=True)
30
31 def save_setting(self, name, value):
32 setting, created = Setting.objects.get_or_create(name=name, user=self)
33 setting.value = str(value)
34 setting.save()
35
36 return setting
37
38 def get_setting(self, name):
39 if Setting.objects.filter(name=name, user=self).exists():
40 setting = Setting.objects.get(name=name, user=self)
41 setting_value = setting.value
42 else:
43 setting_value = None
44
45 return setting_value
46
47 def is_moderator(self):
48 return RANK_MODERATOR >= self.rank
49
50 def get_sorted_fav_tags(self):
51 cache_key = self._get_tag_cache_key()
52 fav_tags = cache.get(cache_key)
53 if fav_tags:
54 return fav_tags
55
56 tags = self.fav_tags.annotate(Count('threads')) \
57 .filter(threads__count__gt=0).order_by('name')
58
59 if tags:
60 cache.set(cache_key, tags, settings.CACHE_TIMEOUT)
61
62 return tags
63
64 def get_post_count(self):
65 return Post.objects.filter(user=self).count()
66
67 def __unicode__(self):
68 return self.user_id + '(' + str(self.rank) + ')'
69
70 def get_last_access_time(self):
71 posts = Post.objects.filter(user=self)
72 if posts.count() > 0:
73 return posts.latest('pub_time').pub_time
74
75 def add_tag(self, tag):
76 self.fav_tags.add(tag)
77 cache.delete(self._get_tag_cache_key())
78
79 def remove_tag(self, tag):
80 self.fav_tags.remove(tag)
81 cache.delete(self._get_tag_cache_key())
82
83 def _get_tag_cache_key(self):
84 return self.user_id + '_tags'
85
86
87 class Setting(models.Model):
88
89 class Meta:
90 app_label = 'boards'
91
92 name = models.CharField(max_length=50)
93 value = models.CharField(max_length=50)
94 user = models.ForeignKey(User)
95
96
97 class Ban(models.Model):
98
99 class Meta:
100 app_label = 'boards'
101
102 ip = models.GenericIPAddressField()
103 reason = models.CharField(default=BAN_REASON_AUTO,
104 max_length=BAN_REASON_MAX_LENGTH)
105 can_read = models.BooleanField(default=True)
106
107 def __unicode__(self):
108 return self.ip
@@ -1,252 +1,253 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 django.utils.translation import ugettext_lazy as _
5 from django.utils.translation import ugettext_lazy as _
6 import time
6 import time
7 from boards.models.post import TITLE_MAX_LENGTH, User
7 from boards.models.post import TITLE_MAX_LENGTH
8 from boards.models import User
8 from neboard import settings
9 from neboard import settings
9 from boards import utils
10 from boards import utils
10 import boards.settings as board_settings
11 import boards.settings as board_settings
11
12
12 LAST_POST_TIME = "last_post_time"
13 LAST_POST_TIME = "last_post_time"
13 LAST_LOGIN_TIME = "last_login_time"
14 LAST_LOGIN_TIME = "last_login_time"
14
15
15
16
16 class PlainErrorList(ErrorList):
17 class PlainErrorList(ErrorList):
17 def __unicode__(self):
18 def __unicode__(self):
18 return self.as_text()
19 return self.as_text()
19
20
20 def as_text(self):
21 def as_text(self):
21 return ''.join([u'(!) %s ' % e for e in self])
22 return ''.join([u'(!) %s ' % e for e in self])
22
23
23
24
24 class NeboardForm(forms.Form):
25 class NeboardForm(forms.Form):
25
26
26 def as_p(self):
27 def as_p(self):
27 "Returns this form rendered as HTML <p>s."
28 "Returns this form rendered as HTML <p>s."
28 return self._html_output(
29 return self._html_output(
29 normal_row='<div class="form-row">'
30 normal_row='<div class="form-row">'
30 '<div class="form-label">'
31 '<div class="form-label">'
31 '%(label)s'
32 '%(label)s'
32 '</div>'
33 '</div>'
33 '<div class="form-input">'
34 '<div class="form-input">'
34 '%(field)s'
35 '%(field)s'
35 '</div>'
36 '</div>'
36 '%(help_text)s'
37 '%(help_text)s'
37 '</div>',
38 '</div>',
38 error_row='<div class="form-errors">%s</div>',
39 error_row='<div class="form-errors">%s</div>',
39 row_ender='</p>',
40 row_ender='</p>',
40 help_text_html=' <span class="helptext">%s</span>',
41 help_text_html=' <span class="helptext">%s</span>',
41 errors_on_separate_row=True)
42 errors_on_separate_row=True)
42
43
43
44
44 class PostForm(NeboardForm):
45 class PostForm(NeboardForm):
45
46
46 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
47 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
47 label=_('Title'))
48 label=_('Title'))
48 text = forms.CharField(widget=forms.Textarea, required=False,
49 text = forms.CharField(widget=forms.Textarea, required=False,
49 label=_('Text'))
50 label=_('Text'))
50 image = forms.ImageField(required=False, label=_('Image'))
51 image = forms.ImageField(required=False, label=_('Image'))
51
52
52 # This field is for spam prevention only
53 # This field is for spam prevention only
53 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
54 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
54 widget=forms.TextInput(attrs={
55 widget=forms.TextInput(attrs={
55 'class': 'form-email'}))
56 'class': 'form-email'}))
56
57
57 session = None
58 session = None
58 need_to_ban = False
59 need_to_ban = False
59
60
60 def clean_title(self):
61 def clean_title(self):
61 title = self.cleaned_data['title']
62 title = self.cleaned_data['title']
62 if title:
63 if title:
63 if len(title) > TITLE_MAX_LENGTH:
64 if len(title) > TITLE_MAX_LENGTH:
64 raise forms.ValidationError(_('Title must have less than %s '
65 raise forms.ValidationError(_('Title must have less than %s '
65 'characters') %
66 'characters') %
66 str(TITLE_MAX_LENGTH))
67 str(TITLE_MAX_LENGTH))
67 return title
68 return title
68
69
69 def clean_text(self):
70 def clean_text(self):
70 text = self.cleaned_data['text']
71 text = self.cleaned_data['text']
71 if text:
72 if text:
72 if len(text) > board_settings.MAX_TEXT_LENGTH:
73 if len(text) > board_settings.MAX_TEXT_LENGTH:
73 raise forms.ValidationError(_('Text must have less than %s '
74 raise forms.ValidationError(_('Text must have less than %s '
74 'characters') %
75 'characters') %
75 str(board_settings
76 str(board_settings
76 .MAX_TEXT_LENGTH))
77 .MAX_TEXT_LENGTH))
77 return text
78 return text
78
79
79 def clean_image(self):
80 def clean_image(self):
80 image = self.cleaned_data['image']
81 image = self.cleaned_data['image']
81 if image:
82 if image:
82 if image._size > board_settings.MAX_IMAGE_SIZE:
83 if image._size > board_settings.MAX_IMAGE_SIZE:
83 raise forms.ValidationError(
84 raise forms.ValidationError(
84 _('Image must be less than %s bytes')
85 _('Image must be less than %s bytes')
85 % str(board_settings.MAX_IMAGE_SIZE))
86 % str(board_settings.MAX_IMAGE_SIZE))
86 return image
87 return image
87
88
88 def clean(self):
89 def clean(self):
89 cleaned_data = super(PostForm, self).clean()
90 cleaned_data = super(PostForm, self).clean()
90
91
91 if not self.session:
92 if not self.session:
92 raise forms.ValidationError('Humans have sessions')
93 raise forms.ValidationError('Humans have sessions')
93
94
94 if cleaned_data['email']:
95 if cleaned_data['email']:
95 self.need_to_ban = True
96 self.need_to_ban = True
96 raise forms.ValidationError('A human cannot enter a hidden field')
97 raise forms.ValidationError('A human cannot enter a hidden field')
97
98
98 if not self.errors:
99 if not self.errors:
99 self._clean_text_image()
100 self._clean_text_image()
100
101
101 if not self.errors and self.session:
102 if not self.errors and self.session:
102 self._validate_posting_speed()
103 self._validate_posting_speed()
103
104
104 return cleaned_data
105 return cleaned_data
105
106
106 def _clean_text_image(self):
107 def _clean_text_image(self):
107 text = self.cleaned_data.get('text')
108 text = self.cleaned_data.get('text')
108 image = self.cleaned_data.get('image')
109 image = self.cleaned_data.get('image')
109
110
110 if (not text) and (not image):
111 if (not text) and (not image):
111 error_message = _('Either text or image must be entered.')
112 error_message = _('Either text or image must be entered.')
112 self._errors['text'] = self.error_class([error_message])
113 self._errors['text'] = self.error_class([error_message])
113
114
114 def _validate_posting_speed(self):
115 def _validate_posting_speed(self):
115 can_post = True
116 can_post = True
116
117
117 if LAST_POST_TIME in self.session:
118 if LAST_POST_TIME in self.session:
118 now = time.time()
119 now = time.time()
119 last_post_time = self.session[LAST_POST_TIME]
120 last_post_time = self.session[LAST_POST_TIME]
120
121
121 current_delay = int(now - last_post_time)
122 current_delay = int(now - last_post_time)
122
123
123 if current_delay < settings.POSTING_DELAY:
124 if current_delay < settings.POSTING_DELAY:
124 error_message = _('Wait %s seconds after last posting') % str(
125 error_message = _('Wait %s seconds after last posting') % str(
125 settings.POSTING_DELAY - current_delay)
126 settings.POSTING_DELAY - current_delay)
126 self._errors['text'] = self.error_class([error_message])
127 self._errors['text'] = self.error_class([error_message])
127
128
128 can_post = False
129 can_post = False
129
130
130 if can_post:
131 if can_post:
131 self.session[LAST_POST_TIME] = time.time()
132 self.session[LAST_POST_TIME] = time.time()
132
133
133
134
134 class ThreadForm(PostForm):
135 class ThreadForm(PostForm):
135
136
136 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
137 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
137
138
138 tags = forms.CharField(max_length=100, label=_('Tags'))
139 tags = forms.CharField(max_length=100, label=_('Tags'))
139
140
140 def clean_tags(self):
141 def clean_tags(self):
141 tags = self.cleaned_data['tags']
142 tags = self.cleaned_data['tags']
142
143
143 if tags:
144 if tags:
144 if not self.regex_tags.match(tags):
145 if not self.regex_tags.match(tags):
145 raise forms.ValidationError(
146 raise forms.ValidationError(
146 _('Inappropriate characters in tags.'))
147 _('Inappropriate characters in tags.'))
147
148
148 return tags
149 return tags
149
150
150 def clean(self):
151 def clean(self):
151 cleaned_data = super(ThreadForm, self).clean()
152 cleaned_data = super(ThreadForm, self).clean()
152
153
153 return cleaned_data
154 return cleaned_data
154
155
155
156
156 class PostCaptchaForm(PostForm):
157 class PostCaptchaForm(PostForm):
157 captcha = CaptchaField()
158 captcha = CaptchaField()
158
159
159 def __init__(self, *args, **kwargs):
160 def __init__(self, *args, **kwargs):
160 self.request = kwargs['request']
161 self.request = kwargs['request']
161 del kwargs['request']
162 del kwargs['request']
162
163
163 super(PostCaptchaForm, self).__init__(*args, **kwargs)
164 super(PostCaptchaForm, self).__init__(*args, **kwargs)
164
165
165 def clean(self):
166 def clean(self):
166 cleaned_data = super(PostCaptchaForm, self).clean()
167 cleaned_data = super(PostCaptchaForm, self).clean()
167
168
168 success = self.is_valid()
169 success = self.is_valid()
169 utils.update_captcha_access(self.request, success)
170 utils.update_captcha_access(self.request, success)
170
171
171 if success:
172 if success:
172 return cleaned_data
173 return cleaned_data
173 else:
174 else:
174 raise forms.ValidationError(_("Captcha validation failed"))
175 raise forms.ValidationError(_("Captcha validation failed"))
175
176
176
177
177 class ThreadCaptchaForm(ThreadForm):
178 class ThreadCaptchaForm(ThreadForm):
178 captcha = CaptchaField()
179 captcha = CaptchaField()
179
180
180 def __init__(self, *args, **kwargs):
181 def __init__(self, *args, **kwargs):
181 self.request = kwargs['request']
182 self.request = kwargs['request']
182 del kwargs['request']
183 del kwargs['request']
183
184
184 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
185 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
185
186
186 def clean(self):
187 def clean(self):
187 cleaned_data = super(ThreadCaptchaForm, self).clean()
188 cleaned_data = super(ThreadCaptchaForm, self).clean()
188
189
189 success = self.is_valid()
190 success = self.is_valid()
190 utils.update_captcha_access(self.request, success)
191 utils.update_captcha_access(self.request, success)
191
192
192 if success:
193 if success:
193 return cleaned_data
194 return cleaned_data
194 else:
195 else:
195 raise forms.ValidationError(_("Captcha validation failed"))
196 raise forms.ValidationError(_("Captcha validation failed"))
196
197
197
198
198 class SettingsForm(NeboardForm):
199 class SettingsForm(NeboardForm):
199
200
200 theme = forms.ChoiceField(choices=settings.THEMES,
201 theme = forms.ChoiceField(choices=settings.THEMES,
201 label=_('Theme'))
202 label=_('Theme'))
202
203
203
204
204 class ModeratorSettingsForm(SettingsForm):
205 class ModeratorSettingsForm(SettingsForm):
205
206
206 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
207 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
207 'panel'))
208 'panel'))
208
209
209
210
210 class LoginForm(NeboardForm):
211 class LoginForm(NeboardForm):
211
212
212 user_id = forms.CharField()
213 user_id = forms.CharField()
213
214
214 session = None
215 session = None
215
216
216 def clean_user_id(self):
217 def clean_user_id(self):
217 user_id = self.cleaned_data['user_id']
218 user_id = self.cleaned_data['user_id']
218 if user_id:
219 if user_id:
219 users = User.objects.filter(user_id=user_id)
220 users = User.objects.filter(user_id=user_id)
220 if len(users) == 0:
221 if len(users) == 0:
221 raise forms.ValidationError(_('No such user found'))
222 raise forms.ValidationError(_('No such user found'))
222
223
223 return user_id
224 return user_id
224
225
225 def _validate_login_speed(self):
226 def _validate_login_speed(self):
226 can_post = True
227 can_post = True
227
228
228 if LAST_LOGIN_TIME in self.session:
229 if LAST_LOGIN_TIME in self.session:
229 now = time.time()
230 now = time.time()
230 last_login_time = self.session[LAST_LOGIN_TIME]
231 last_login_time = self.session[LAST_LOGIN_TIME]
231
232
232 current_delay = int(now - last_login_time)
233 current_delay = int(now - last_login_time)
233
234
234 if current_delay < board_settings.LOGIN_TIMEOUT:
235 if current_delay < board_settings.LOGIN_TIMEOUT:
235 error_message = _('Wait %s minutes after last login') % str(
236 error_message = _('Wait %s minutes after last login') % str(
236 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
237 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
237 self._errors['user_id'] = self.error_class([error_message])
238 self._errors['user_id'] = self.error_class([error_message])
238
239
239 can_post = False
240 can_post = False
240
241
241 if can_post:
242 if can_post:
242 self.session[LAST_LOGIN_TIME] = time.time()
243 self.session[LAST_LOGIN_TIME] = time.time()
243
244
244 def clean(self):
245 def clean(self):
245 if not self.session:
246 if not self.session:
246 raise forms.ValidationError('Humans have sessions')
247 raise forms.ValidationError('Humans have sessions')
247
248
248 self._validate_login_speed()
249 self._validate_login_speed()
249
250
250 cleaned_data = super(LoginForm, self).clean()
251 cleaned_data = super(LoginForm, self).clean()
251
252
252 return cleaned_data
253 return cleaned_data
@@ -1,7 +1,7 b''
1 __author__ = 'neko259'
1 __author__ = 'neko259'
2
2
3 from boards.models.post import Post
3 from boards.models.post import Post
4 from boards.models.tag import Tag
4 from boards.models.tag import Tag
5 from boards.models.post import Ban
5 from boards.models.user import Ban
6 from boards.models.post import Setting
6 from boards.models.user import Setting
7 from boards.models.post import User
7 from boards.models.user import User
@@ -1,397 +1,295 b''
1 import os
1 import os
2 from random import random
2 from random import random
3 import time
3 import time
4 import math
4 import math
5 from django.core.cache import cache
5 import re
6
6
7 from django.db import models
7 from django.db import models
8 from django.db.models import Count
9 from django.http import Http404
8 from django.http import Http404
10 from django.utils import timezone
9 from django.utils import timezone
11 from markupfield.fields import MarkupField
10 from markupfield.fields import MarkupField
12 from boards import settings as board_settings
13
11
14 from neboard import settings
12 from neboard import settings
15 from boards import thumbs
13 from boards import thumbs
16
14
17 import re
18
19 BAN_REASON_MAX_LENGTH = 200
20
21 BAN_REASON_AUTO = 'Auto'
15 BAN_REASON_AUTO = 'Auto'
22
16
23 IMAGE_THUMB_SIZE = (200, 150)
17 IMAGE_THUMB_SIZE = (200, 150)
24
18
25 TITLE_MAX_LENGTH = 50
19 TITLE_MAX_LENGTH = 50
26
20
27 DEFAULT_MARKUP_TYPE = 'markdown'
21 DEFAULT_MARKUP_TYPE = 'markdown'
28
22
29 NO_PARENT = -1
23 NO_PARENT = -1
30 NO_IP = '0.0.0.0'
24 NO_IP = '0.0.0.0'
31 UNKNOWN_UA = ''
25 UNKNOWN_UA = ''
32 ALL_PAGES = -1
26 ALL_PAGES = -1
33 IMAGES_DIRECTORY = 'images/'
27 IMAGES_DIRECTORY = 'images/'
34 FILE_EXTENSION_DELIMITER = '.'
28 FILE_EXTENSION_DELIMITER = '.'
35
29
36 RANK_ADMIN = 0
37 RANK_MODERATOR = 10
38 RANK_USER = 100
39
40 SETTING_MODERATE = "moderate"
30 SETTING_MODERATE = "moderate"
41
31
42 REGEX_REPLY = re.compile('>>(\d+)')
32 REGEX_REPLY = re.compile('>>(\d+)')
43
33
44
34
45 class PostManager(models.Manager):
35 class PostManager(models.Manager):
46
36
47 def create_post(self, title, text, image=None, thread=None,
37 def create_post(self, title, text, image=None, thread=None,
48 ip=NO_IP, tags=None, user=None):
38 ip=NO_IP, tags=None, user=None):
49 posting_time = timezone.now()
39 posting_time = timezone.now()
50
40
51 post = self.create(title=title,
41 post = self.create(title=title,
52 text=text,
42 text=text,
53 pub_time=posting_time,
43 pub_time=posting_time,
54 thread=thread,
44 thread=thread,
55 image=image,
45 image=image,
56 poster_ip=ip,
46 poster_ip=ip,
57 poster_user_agent=UNKNOWN_UA,
47 poster_user_agent=UNKNOWN_UA,
58 last_edit_time=posting_time,
48 last_edit_time=posting_time,
59 bump_time=posting_time,
49 bump_time=posting_time,
60 user=user)
50 user=user)
61
51
62 if tags:
52 if tags:
63 linked_tags = []
53 linked_tags = []
64 for tag in tags:
54 for tag in tags:
65 tag_linked_tags = tag.get_linked_tags()
55 tag_linked_tags = tag.get_linked_tags()
66 if len(tag_linked_tags) > 0:
56 if len(tag_linked_tags) > 0:
67 linked_tags.extend(tag_linked_tags)
57 linked_tags.extend(tag_linked_tags)
68
58
69 tags.extend(linked_tags)
59 tags.extend(linked_tags)
70 map(post.tags.add, tags)
60 map(post.tags.add, tags)
71 for tag in tags:
61 for tag in tags:
72 tag.threads.add(post)
62 tag.threads.add(post)
73
63
74 if thread:
64 if thread:
75 thread.replies.add(post)
65 thread.replies.add(post)
76 thread.bump()
66 thread.bump()
77 thread.last_edit_time = posting_time
67 thread.last_edit_time = posting_time
78 thread.save()
68 thread.save()
79
69
80 #cache_key = thread.get_cache_key()
70 #cache_key = thread.get_cache_key()
81 #cache.delete(cache_key)
71 #cache.delete(cache_key)
82
72
83 else:
73 else:
84 self._delete_old_threads()
74 self._delete_old_threads()
85
75
86 self.connect_replies(post)
76 self.connect_replies(post)
87
77
88 return post
78 return post
89
79
90 def delete_post(self, post):
80 def delete_post(self, post):
91 if post.replies.count() > 0:
81 if post.replies.count() > 0:
92 map(self.delete_post, post.replies.all())
82 map(self.delete_post, post.replies.all())
93
83
94 # Update thread's last edit time (used as cache key)
84 # Update thread's last edit time (used as cache key)
95 thread = post.thread
85 thread = post.thread
96 if thread:
86 if thread:
97 thread.last_edit_time = timezone.now()
87 thread.last_edit_time = timezone.now()
98 thread.save()
88 thread.save()
99
89
100 #cache_key = thread.get_cache_key()
90 #cache_key = thread.get_cache_key()
101 #cache.delete(cache_key)
91 #cache.delete(cache_key)
102
92
103 post.delete()
93 post.delete()
104
94
105 def delete_posts_by_ip(self, ip):
95 def delete_posts_by_ip(self, ip):
106 posts = self.filter(poster_ip=ip)
96 posts = self.filter(poster_ip=ip)
107 map(self.delete_post, posts)
97 map(self.delete_post, posts)
108
98
109 def get_threads(self, tag=None, page=ALL_PAGES,
99 def get_threads(self, tag=None, page=ALL_PAGES,
110 order_by='-bump_time'):
100 order_by='-bump_time'):
111 if tag:
101 if tag:
112 threads = tag.threads
102 threads = tag.threads
113
103
114 if threads.count() == 0:
104 if threads.count() == 0:
115 raise Http404
105 raise Http404
116 else:
106 else:
117 threads = self.filter(thread=None)
107 threads = self.filter(thread=None)
118
108
119 threads = threads.order_by(order_by)
109 threads = threads.order_by(order_by)
120
110
121 if page != ALL_PAGES:
111 if page != ALL_PAGES:
122 thread_count = threads.count()
112 thread_count = threads.count()
123
113
124 if page < self._get_page_count(thread_count):
114 if page < self._get_page_count(thread_count):
125 start_thread = page * settings.THREADS_PER_PAGE
115 start_thread = page * settings.THREADS_PER_PAGE
126 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
116 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
127 thread_count)
117 thread_count)
128 threads = threads[start_thread:end_thread]
118 threads = threads[start_thread:end_thread]
129
119
130 return threads
120 return threads
131
121
132 def get_thread(self, opening_post_id):
122 def get_thread(self, opening_post_id):
133 try:
123 try:
134 opening_post = self.get(id=opening_post_id, thread=None)
124 opening_post = self.get(id=opening_post_id, thread=None)
135 except Post.DoesNotExist:
125 except Post.DoesNotExist:
136 raise Http404
126 raise Http404
137
127
138 #cache_key = opening_post.get_cache_key()
128 #cache_key = opening_post.get_cache_key()
139 #thread = cache.get(cache_key)
129 #thread = cache.get(cache_key)
140 #if thread:
130 #if thread:
141 # return thread
131 # return thread
142
132
143 if opening_post.replies:
133 if opening_post.replies:
144 thread = [opening_post]
134 thread = [opening_post]
145 thread.extend(opening_post.replies.all().order_by('pub_time'))
135 thread.extend(opening_post.replies.all().order_by('pub_time'))
146
136
147 #cache.set(cache_key, thread, board_settings.CACHE_TIMEOUT)
137 #cache.set(cache_key, thread, board_settings.CACHE_TIMEOUT)
148
138
149 return thread
139 return thread
150
140
151 def exists(self, post_id):
141 def exists(self, post_id):
152 posts = self.filter(id=post_id)
142 posts = self.filter(id=post_id)
153
143
154 return posts.count() > 0
144 return posts.count() > 0
155
145
156 def get_thread_page_count(self, tag=None):
146 def get_thread_page_count(self, tag=None):
157 if tag:
147 if tag:
158 threads = self.filter(thread=None, tags=tag)
148 threads = self.filter(thread=None, tags=tag)
159 else:
149 else:
160 threads = self.filter(thread=None)
150 threads = self.filter(thread=None)
161
151
162 return self._get_page_count(threads.count())
152 return self._get_page_count(threads.count())
163
153
164 def _delete_old_threads(self):
154 def _delete_old_threads(self):
165 """
155 """
166 Preserves maximum thread count. If there are too many threads,
156 Preserves maximum thread count. If there are too many threads,
167 delete the old ones.
157 delete the old ones.
168 """
158 """
169
159
170 # TODO Move old threads to the archive instead of deleting them.
160 # TODO Move old threads to the archive instead of deleting them.
171 # Maybe make some 'old' field in the model to indicate the thread
161 # Maybe make some 'old' field in the model to indicate the thread
172 # must not be shown and be able for replying.
162 # must not be shown and be able for replying.
173
163
174 threads = self.get_threads()
164 threads = self.get_threads()
175 thread_count = threads.count()
165 thread_count = threads.count()
176
166
177 if thread_count > settings.MAX_THREAD_COUNT:
167 if thread_count > settings.MAX_THREAD_COUNT:
178 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
168 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
179 old_threads = threads[thread_count - num_threads_to_delete:]
169 old_threads = threads[thread_count - num_threads_to_delete:]
180
170
181 map(self.delete_post, old_threads)
171 map(self.delete_post, old_threads)
182
172
183 def connect_replies(self, post):
173 def connect_replies(self, post):
184 """Connect replies to a post to show them as a refmap"""
174 """Connect replies to a post to show them as a refmap"""
185
175
186 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
176 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
187 post_id = reply_number.group(1)
177 post_id = reply_number.group(1)
188 ref_post = self.filter(id=post_id)
178 ref_post = self.filter(id=post_id)
189 if ref_post.count() > 0:
179 if ref_post.count() > 0:
190 referenced_post = ref_post[0]
180 referenced_post = ref_post[0]
191 referenced_post.referenced_posts.add(post)
181 referenced_post.referenced_posts.add(post)
192 referenced_post.last_edit_time = post.pub_time
182 referenced_post.last_edit_time = post.pub_time
193 referenced_post.save()
183 referenced_post.save()
194
184
195 def _get_page_count(self, thread_count):
185 def _get_page_count(self, thread_count):
196 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
186 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
197
187
198
188
199 class Post(models.Model):
189 class Post(models.Model):
200 """A post is a message."""
190 """A post is a message."""
201
191
202 objects = PostManager()
192 objects = PostManager()
203
193
204 class Meta:
194 class Meta:
205 app_label = 'boards'
195 app_label = 'boards'
206
196
207 def _update_image_filename(self, filename):
197 def _update_image_filename(self, filename):
208 """Get unique image filename"""
198 """Get unique image filename"""
209
199
210 path = IMAGES_DIRECTORY
200 path = IMAGES_DIRECTORY
211 new_name = str(int(time.mktime(time.gmtime())))
201 new_name = str(int(time.mktime(time.gmtime())))
212 new_name += str(int(random() * 1000))
202 new_name += str(int(random() * 1000))
213 new_name += FILE_EXTENSION_DELIMITER
203 new_name += FILE_EXTENSION_DELIMITER
214 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
204 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
215
205
216 return os.path.join(path, new_name)
206 return os.path.join(path, new_name)
217
207
218 title = models.CharField(max_length=TITLE_MAX_LENGTH)
208 title = models.CharField(max_length=TITLE_MAX_LENGTH)
219 pub_time = models.DateTimeField()
209 pub_time = models.DateTimeField()
220 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
210 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
221 escape_html=False)
211 escape_html=False)
222
212
223 image_width = models.IntegerField(default=0)
213 image_width = models.IntegerField(default=0)
224 image_height = models.IntegerField(default=0)
214 image_height = models.IntegerField(default=0)
225
215
226 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
216 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
227 blank=True, sizes=(IMAGE_THUMB_SIZE,),
217 blank=True, sizes=(IMAGE_THUMB_SIZE,),
228 width_field='image_width',
218 width_field='image_width',
229 height_field='image_height')
219 height_field='image_height')
230
220
231 poster_ip = models.GenericIPAddressField()
221 poster_ip = models.GenericIPAddressField()
232 poster_user_agent = models.TextField()
222 poster_user_agent = models.TextField()
233
223
234 thread = models.ForeignKey('Post', null=True, default=None)
224 thread = models.ForeignKey('Post', null=True, default=None)
235 tags = models.ManyToManyField('Tag')
225 tags = models.ManyToManyField('Tag')
236 last_edit_time = models.DateTimeField()
226 last_edit_time = models.DateTimeField()
237 bump_time = models.DateTimeField()
227 bump_time = models.DateTimeField()
238 user = models.ForeignKey('User', null=True, default=None)
228 user = models.ForeignKey('User', null=True, default=None)
239
229
240 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
230 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
241 blank=True, related_name='re+')
231 blank=True, related_name='re+')
242 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
232 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
243 null=True,
233 null=True,
244 blank=True, related_name='rfp+')
234 blank=True, related_name='rfp+')
245
235
246 def __unicode__(self):
236 def __unicode__(self):
247 return '#' + str(self.id) + ' ' + self.title + ' (' + \
237 return '#' + str(self.id) + ' ' + self.title + ' (' + \
248 self.text.raw[:50] + ')'
238 self.text.raw[:50] + ')'
249
239
250 def get_title(self):
240 def get_title(self):
251 title = self.title
241 title = self.title
252 if len(title) == 0:
242 if len(title) == 0:
253 title = self.text.raw[:20]
243 title = self.text.raw[:20]
254
244
255 return title
245 return title
256
246
257 def get_reply_count(self):
247 def get_reply_count(self):
258 return self.replies.count()
248 return self.replies.count()
259
249
260 def get_images_count(self):
250 def get_images_count(self):
261 images_count = 1 if self.image else 0
251 images_count = 1 if self.image else 0
262 images_count += self.replies.filter(image_width__gt=0).count()
252 images_count += self.replies.filter(image_width__gt=0).count()
263
253
264 return images_count
254 return images_count
265
255
266 def can_bump(self):
256 def can_bump(self):
267 """Check if the thread can be bumped by replying"""
257 """Check if the thread can be bumped by replying"""
268
258
269 post_count = self.get_reply_count()
259 post_count = self.get_reply_count()
270
260
271 return post_count <= settings.MAX_POSTS_PER_THREAD
261 return post_count <= settings.MAX_POSTS_PER_THREAD
272
262
273 def bump(self):
263 def bump(self):
274 """Bump (move to up) thread"""
264 """Bump (move to up) thread"""
275
265
276 if self.can_bump():
266 if self.can_bump():
277 self.bump_time = timezone.now()
267 self.bump_time = timezone.now()
278
268
279 def get_last_replies(self):
269 def get_last_replies(self):
280 if settings.LAST_REPLIES_COUNT > 0:
270 if settings.LAST_REPLIES_COUNT > 0:
281 reply_count = self.get_reply_count()
271 reply_count = self.get_reply_count()
282
272
283 if reply_count > 0:
273 if reply_count > 0:
284 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
274 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
285 reply_count)
275 reply_count)
286 last_replies = self.replies.all().order_by('pub_time')[
276 last_replies = self.replies.all().order_by('pub_time')[
287 reply_count - reply_count_to_show:]
277 reply_count - reply_count_to_show:]
288
278
289 return last_replies
279 return last_replies
290
280
291 def get_tags(self):
281 def get_tags(self):
292 """Get a sorted tag list"""
282 """Get a sorted tag list"""
293
283
294 return self.tags.order_by('name')
284 return self.tags.order_by('name')
295
285
296 def get_cache_key(self):
286 def get_cache_key(self):
297 return str(self.id) + str(self.last_edit_time.microsecond)
287 return str(self.id) + str(self.last_edit_time.microsecond)
298
288
299 def get_sorted_referenced_posts(self):
289 def get_sorted_referenced_posts(self):
300 return self.referenced_posts.order_by('id')
290 return self.referenced_posts.order_by('id')
301
291
302 def is_referenced(self):
292 def is_referenced(self):
303 return self.referenced_posts.count() > 0
293 return self.referenced_posts.count() > 0
304
294
305
295
306 class User(models.Model):
307
308 class Meta:
309 app_label = 'boards'
310
311 user_id = models.CharField(max_length=50)
312 rank = models.IntegerField()
313
314 registration_time = models.DateTimeField()
315
316 fav_tags = models.ManyToManyField('Tag', null=True, blank=True)
317 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
318 blank=True)
319
320 def save_setting(self, name, value):
321 setting, created = Setting.objects.get_or_create(name=name, user=self)
322 setting.value = str(value)
323 setting.save()
324
325 return setting
326
327 def get_setting(self, name):
328 if Setting.objects.filter(name=name, user=self).exists():
329 setting = Setting.objects.get(name=name, user=self)
330 setting_value = setting.value
331 else:
332 setting_value = None
333
334 return setting_value
335
336 def is_moderator(self):
337 return RANK_MODERATOR >= self.rank
338
339 def get_sorted_fav_tags(self):
340 cache_key = self._get_tag_cache_key()
341 fav_tags = cache.get(cache_key)
342 if fav_tags:
343 return fav_tags
344
345 tags = self.fav_tags.annotate(Count('threads')) \
346 .filter(threads__count__gt=0).order_by('name')
347
348 if tags:
349 cache.set(cache_key, tags, board_settings.CACHE_TIMEOUT)
350
351 return tags
352
353 def get_post_count(self):
354 return Post.objects.filter(user=self).count()
355
356 def __unicode__(self):
357 return self.user_id + '(' + str(self.rank) + ')'
358
359 def get_last_access_time(self):
360 posts = Post.objects.filter(user=self)
361 if posts.count() > 0:
362 return posts.latest('pub_time').pub_time
363
364 def add_tag(self, tag):
365 self.fav_tags.add(tag)
366 cache.delete(self._get_tag_cache_key())
367
368 def remove_tag(self, tag):
369 self.fav_tags.remove(tag)
370 cache.delete(self._get_tag_cache_key())
371
372 def _get_tag_cache_key(self):
373 return self.user_id + '_tags'
374
375
376 class Setting(models.Model):
377
378 class Meta:
379 app_label = 'boards'
380
381 name = models.CharField(max_length=50)
382 value = models.CharField(max_length=50)
383 user = models.ForeignKey(User)
384
385
386 class Ban(models.Model):
387
388 class Meta:
389 app_label = 'boards'
390
391 ip = models.GenericIPAddressField()
392 reason = models.CharField(default=BAN_REASON_AUTO,
393 max_length=BAN_REASON_MAX_LENGTH)
394 can_read = models.BooleanField(default=True)
395
396 def __unicode__(self):
397 return self.ip
@@ -1,85 +1,83 b''
1 from boards.models import Post
1 from boards.models import Post
2 from django.db import models
3 from django.db.models import Count
2
4
3 __author__ = 'neko259'
5 __author__ = 'neko259'
4
6
5
6 from django.db import models
7 from django.db.models import Count
8
9 TAG_FONT_MULTIPLIER = 0.1
7 TAG_FONT_MULTIPLIER = 0.1
10 MAX_TAG_FONT = 10
8 MAX_TAG_FONT = 10
11 OPENING_POST_POPULARITY_WEIGHT = 2
9 OPENING_POST_POPULARITY_WEIGHT = 2
12
10
13
11
14 class TagManager(models.Manager):
12 class TagManager(models.Manager):
15
13
16 def get_not_empty_tags(self):
14 def get_not_empty_tags(self):
17 tags = self.annotate(Count('threads')) \
15 tags = self.annotate(Count('threads')) \
18 .filter(threads__count__gt=0).order_by('name')
16 .filter(threads__count__gt=0).order_by('name')
19
17
20 return tags
18 return tags
21
19
22
20
23 class Tag(models.Model):
21 class Tag(models.Model):
24 """
22 """
25 A tag is a text node assigned to the post. The tag serves as a board
23 A tag is a text node assigned to the post. The tag serves as a board
26 section. There can be multiple tags for each message
24 section. There can be multiple tags for each message
27 """
25 """
28
26
29 objects = TagManager()
27 objects = TagManager()
30
28
31 class Meta:
29 class Meta:
32 app_label = 'boards'
30 app_label = 'boards'
33
31
34 name = models.CharField(max_length=100)
32 name = models.CharField(max_length=100)
35 threads = models.ManyToManyField('Post', null=True,
33 threads = models.ManyToManyField('Post', null=True,
36 blank=True, related_name='tag+')
34 blank=True, related_name='tag+')
37 linked = models.ForeignKey('Tag', null=True, blank=True)
35 linked = models.ForeignKey('Tag', null=True, blank=True)
38
36
39 def __unicode__(self):
37 def __unicode__(self):
40 return self.name
38 return self.name
41
39
42 def is_empty(self):
40 def is_empty(self):
43 return self.get_post_count() == 0
41 return self.get_post_count() == 0
44
42
45 def get_post_count(self):
43 def get_post_count(self):
46 return self.threads.count()
44 return self.threads.count()
47
45
48 def get_popularity(self):
46 def get_popularity(self):
49 posts_with_tag = Post.objects.get_threads(tag=self)
47 posts_with_tag = Post.objects.get_threads(tag=self)
50 reply_count = 0
48 reply_count = 0
51 for post in posts_with_tag:
49 for post in posts_with_tag:
52 reply_count += post.get_reply_count()
50 reply_count += post.get_reply_count()
53 reply_count += OPENING_POST_POPULARITY_WEIGHT
51 reply_count += OPENING_POST_POPULARITY_WEIGHT
54
52
55 return reply_count
53 return reply_count
56
54
57 def get_linked_tags(self):
55 def get_linked_tags(self):
58 tag_list = []
56 tag_list = []
59 self.get_linked_tags_list(tag_list)
57 self.get_linked_tags_list(tag_list)
60
58
61 return tag_list
59 return tag_list
62
60
63 def get_linked_tags_list(self, tag_list=[]):
61 def get_linked_tags_list(self, tag_list=[]):
64 """
62 """
65 Returns the list of tags linked to current. The list can be got
63 Returns the list of tags linked to current. The list can be got
66 through returned value or tag_list parameter
64 through returned value or tag_list parameter
67 """
65 """
68
66
69 linked_tag = self.linked
67 linked_tag = self.linked
70
68
71 if linked_tag and not (linked_tag in tag_list):
69 if linked_tag and not (linked_tag in tag_list):
72 tag_list.append(linked_tag)
70 tag_list.append(linked_tag)
73
71
74 linked_tag.get_linked_tags_list(tag_list)
72 linked_tag.get_linked_tags_list(tag_list)
75
73
76 def get_font_value(self):
74 def get_font_value(self):
77 """Get tag font value to differ most popular tags in the list"""
75 """Get tag font value to differ most popular tags in the list"""
78
76
79 post_count = self.get_post_count()
77 post_count = self.get_post_count()
80 if post_count > MAX_TAG_FONT:
78 if post_count > MAX_TAG_FONT:
81 post_count = MAX_TAG_FONT
79 post_count = MAX_TAG_FONT
82
80
83 font_value = str(1 + (post_count - 1) * TAG_FONT_MULTIPLIER)
81 font_value = str(1 + (post_count - 1) * TAG_FONT_MULTIPLIER)
84
82
85 return font_value No newline at end of file
83 return font_value
@@ -1,559 +1,560 b''
1 import hashlib
1 import hashlib
2 import json
2 import json
3 import string
3 import string
4 import time
4 import time
5 from datetime import datetime
5 from datetime import datetime
6 import re
6 import re
7
7
8 from django.core import serializers
8 from django.core import serializers
9 from django.core.urlresolvers import reverse
9 from django.core.urlresolvers import reverse
10 from django.http import HttpResponseRedirect
10 from django.http import HttpResponseRedirect
11 from django.http.response import HttpResponse
11 from django.http.response import HttpResponse
12 from django.template import RequestContext
12 from django.template import RequestContext
13 from django.shortcuts import render, redirect, get_object_or_404
13 from django.shortcuts import render, redirect, get_object_or_404
14 from django.utils import timezone
14 from django.utils import timezone
15 from django.db import transaction
15 from django.db import transaction
16
16
17 from boards import forms
17 from boards import forms
18 import boards
18 import boards
19 from boards import utils
19 from boards import utils
20 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
20 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
21 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
21 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
22 from boards.models import Post, Tag, Ban, User
22 from boards.models import Post, Tag, Ban, User
23 from boards.models.post import RANK_USER, SETTING_MODERATE, REGEX_REPLY
23 from boards.models.post import SETTING_MODERATE, REGEX_REPLY
24 from boards.models.user import RANK_USER
24 from boards import authors
25 from boards import authors
25 from boards.utils import get_client_ip
26 from boards.utils import get_client_ip
26 import neboard
27 import neboard
27
28
28
29
29 BAN_REASON_SPAM = 'Autoban: spam bot'
30 BAN_REASON_SPAM = 'Autoban: spam bot'
30
31
31
32
32 def index(request, page=0):
33 def index(request, page=0):
33 context = _init_default_context(request)
34 context = _init_default_context(request)
34
35
35 if utils.need_include_captcha(request):
36 if utils.need_include_captcha(request):
36 threadFormClass = ThreadCaptchaForm
37 threadFormClass = ThreadCaptchaForm
37 kwargs = {'request': request}
38 kwargs = {'request': request}
38 else:
39 else:
39 threadFormClass = ThreadForm
40 threadFormClass = ThreadForm
40 kwargs = {}
41 kwargs = {}
41
42
42 if request.method == 'POST':
43 if request.method == 'POST':
43 form = threadFormClass(request.POST, request.FILES,
44 form = threadFormClass(request.POST, request.FILES,
44 error_class=PlainErrorList, **kwargs)
45 error_class=PlainErrorList, **kwargs)
45 form.session = request.session
46 form.session = request.session
46
47
47 if form.is_valid():
48 if form.is_valid():
48 return _new_post(request, form)
49 return _new_post(request, form)
49 if form.need_to_ban:
50 if form.need_to_ban:
50 # Ban user because he is suspected to be a bot
51 # Ban user because he is suspected to be a bot
51 _ban_current_user(request)
52 _ban_current_user(request)
52 else:
53 else:
53 form = threadFormClass(error_class=PlainErrorList, **kwargs)
54 form = threadFormClass(error_class=PlainErrorList, **kwargs)
54
55
55 threads = []
56 threads = []
56 for thread in Post.objects.get_threads(page=int(page)):
57 for thread in Post.objects.get_threads(page=int(page)):
57 threads.append({
58 threads.append({
58 'thread': thread,
59 'thread': thread,
59 'bumpable': thread.can_bump(),
60 'bumpable': thread.can_bump(),
60 'last_replies': thread.get_last_replies(),
61 'last_replies': thread.get_last_replies(),
61 })
62 })
62
63
63 # TODO Make this generic for tag and threads list pages
64 # TODO Make this generic for tag and threads list pages
64 context['threads'] = None if len(threads) == 0 else threads
65 context['threads'] = None if len(threads) == 0 else threads
65 context['form'] = form
66 context['form'] = form
66
67
67 page_count = Post.objects.get_thread_page_count()
68 page_count = Post.objects.get_thread_page_count()
68 context['pages'] = range(page_count)
69 context['pages'] = range(page_count)
69 page = int(page)
70 page = int(page)
70 if page < page_count - 1:
71 if page < page_count - 1:
71 context['next_page'] = str(page + 1)
72 context['next_page'] = str(page + 1)
72 if page > 0:
73 if page > 0:
73 context['prev_page'] = str(page - 1)
74 context['prev_page'] = str(page - 1)
74
75
75 return render(request, 'boards/posting_general.html',
76 return render(request, 'boards/posting_general.html',
76 context)
77 context)
77
78
78
79
79 @transaction.commit_on_success
80 @transaction.commit_on_success
80 def _new_post(request, form, opening_post=None):
81 def _new_post(request, form, opening_post=None):
81 """Add a new post (in thread or as a reply)."""
82 """Add a new post (in thread or as a reply)."""
82
83
83 ip = get_client_ip(request)
84 ip = get_client_ip(request)
84 is_banned = Ban.objects.filter(ip=ip).exists()
85 is_banned = Ban.objects.filter(ip=ip).exists()
85
86
86 if is_banned:
87 if is_banned:
87 return redirect(you_are_banned)
88 return redirect(you_are_banned)
88
89
89 data = form.cleaned_data
90 data = form.cleaned_data
90
91
91 title = data['title']
92 title = data['title']
92 text = data['text']
93 text = data['text']
93
94
94 text = _remove_invalid_links(text)
95 text = _remove_invalid_links(text)
95
96
96 if 'image' in data.keys():
97 if 'image' in data.keys():
97 image = data['image']
98 image = data['image']
98 else:
99 else:
99 image = None
100 image = None
100
101
101 tags = []
102 tags = []
102
103
103 if not opening_post:
104 if not opening_post:
104 tag_strings = data['tags']
105 tag_strings = data['tags']
105
106
106 if tag_strings:
107 if tag_strings:
107 tag_strings = tag_strings.split(' ')
108 tag_strings = tag_strings.split(' ')
108 for tag_name in tag_strings:
109 for tag_name in tag_strings:
109 tag_name = string.lower(tag_name.strip())
110 tag_name = string.lower(tag_name.strip())
110 if len(tag_name) > 0:
111 if len(tag_name) > 0:
111 tag, created = Tag.objects.get_or_create(name=tag_name)
112 tag, created = Tag.objects.get_or_create(name=tag_name)
112 tags.append(tag)
113 tags.append(tag)
113
114
114 post = Post.objects.create_post(title=title, text=text, ip=ip,
115 post = Post.objects.create_post(title=title, text=text, ip=ip,
115 thread=opening_post, image=image,
116 thread=opening_post, image=image,
116 tags=tags, user=_get_user(request))
117 tags=tags, user=_get_user(request))
117
118
118 thread_to_show = (opening_post.id if opening_post else post.id)
119 thread_to_show = (opening_post.id if opening_post else post.id)
119
120
120 if opening_post:
121 if opening_post:
121 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
122 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
122 '#' + str(post.id))
123 '#' + str(post.id))
123 else:
124 else:
124 return redirect(thread, post_id=thread_to_show)
125 return redirect(thread, post_id=thread_to_show)
125
126
126
127
127 def tag(request, tag_name, page=0):
128 def tag(request, tag_name, page=0):
128 """
129 """
129 Get all tag threads. Threads are split in pages, so some page is
130 Get all tag threads. Threads are split in pages, so some page is
130 requested. Default page is 0.
131 requested. Default page is 0.
131 """
132 """
132
133
133 tag = get_object_or_404(Tag, name=tag_name)
134 tag = get_object_or_404(Tag, name=tag_name)
134 threads = []
135 threads = []
135 for thread in Post.objects.get_threads(tag=tag, page=int(page)):
136 for thread in Post.objects.get_threads(tag=tag, page=int(page)):
136 threads.append({
137 threads.append({
137 'thread': thread,
138 'thread': thread,
138 'bumpable': thread.can_bump(),
139 'bumpable': thread.can_bump(),
139 'last_replies': thread.get_last_replies(),
140 'last_replies': thread.get_last_replies(),
140 })
141 })
141
142
142 if request.method == 'POST':
143 if request.method == 'POST':
143 form = ThreadForm(request.POST, request.FILES,
144 form = ThreadForm(request.POST, request.FILES,
144 error_class=PlainErrorList)
145 error_class=PlainErrorList)
145 form.session = request.session
146 form.session = request.session
146
147
147 if form.is_valid():
148 if form.is_valid():
148 return _new_post(request, form)
149 return _new_post(request, form)
149 if form.need_to_ban:
150 if form.need_to_ban:
150 # Ban user because he is suspected to be a bot
151 # Ban user because he is suspected to be a bot
151 _ban_current_user(request)
152 _ban_current_user(request)
152 else:
153 else:
153 form = forms.ThreadForm(initial={'tags': tag_name},
154 form = forms.ThreadForm(initial={'tags': tag_name},
154 error_class=PlainErrorList)
155 error_class=PlainErrorList)
155
156
156 context = _init_default_context(request)
157 context = _init_default_context(request)
157 context['threads'] = None if len(threads) == 0 else threads
158 context['threads'] = None if len(threads) == 0 else threads
158 context['tag'] = tag
159 context['tag'] = tag
159
160
160 page_count = Post.objects.get_thread_page_count(tag=tag)
161 page_count = Post.objects.get_thread_page_count(tag=tag)
161 context['pages'] = range(page_count)
162 context['pages'] = range(page_count)
162 page = int(page)
163 page = int(page)
163 if page < page_count - 1:
164 if page < page_count - 1:
164 context['next_page'] = str(page + 1)
165 context['next_page'] = str(page + 1)
165 if page > 0:
166 if page > 0:
166 context['prev_page'] = str(page - 1)
167 context['prev_page'] = str(page - 1)
167
168
168 context['form'] = form
169 context['form'] = form
169
170
170 return render(request, 'boards/posting_general.html',
171 return render(request, 'boards/posting_general.html',
171 context)
172 context)
172
173
173
174
174 def thread(request, post_id):
175 def thread(request, post_id):
175 """Get all thread posts"""
176 """Get all thread posts"""
176
177
177 if utils.need_include_captcha(request):
178 if utils.need_include_captcha(request):
178 postFormClass = PostCaptchaForm
179 postFormClass = PostCaptchaForm
179 kwargs = {'request': request}
180 kwargs = {'request': request}
180 else:
181 else:
181 postFormClass = PostForm
182 postFormClass = PostForm
182 kwargs = {}
183 kwargs = {}
183
184
184 if request.method == 'POST':
185 if request.method == 'POST':
185 form = postFormClass(request.POST, request.FILES,
186 form = postFormClass(request.POST, request.FILES,
186 error_class=PlainErrorList, **kwargs)
187 error_class=PlainErrorList, **kwargs)
187 form.session = request.session
188 form.session = request.session
188
189
189 opening_post = get_object_or_404(Post, id=post_id)
190 opening_post = get_object_or_404(Post, id=post_id)
190 if form.is_valid():
191 if form.is_valid():
191 return _new_post(request, form, opening_post)
192 return _new_post(request, form, opening_post)
192 if form.need_to_ban:
193 if form.need_to_ban:
193 # Ban user because he is suspected to be a bot
194 # Ban user because he is suspected to be a bot
194 _ban_current_user(request)
195 _ban_current_user(request)
195 else:
196 else:
196 form = postFormClass(error_class=PlainErrorList, **kwargs)
197 form = postFormClass(error_class=PlainErrorList, **kwargs)
197
198
198 posts = Post.objects.get_thread(post_id)
199 posts = Post.objects.get_thread(post_id)
199
200
200 context = _init_default_context(request)
201 context = _init_default_context(request)
201
202
202 context['posts'] = posts
203 context['posts'] = posts
203 context['form'] = form
204 context['form'] = form
204 context['bumpable'] = posts[0].can_bump()
205 context['bumpable'] = posts[0].can_bump()
205 if context['bumpable']:
206 if context['bumpable']:
206 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - len(
207 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - len(
207 posts)
208 posts)
208 context['bumplimit_progress'] = str(
209 context['bumplimit_progress'] = str(
209 float(context['posts_left']) /
210 float(context['posts_left']) /
210 neboard.settings.MAX_POSTS_PER_THREAD * 100)
211 neboard.settings.MAX_POSTS_PER_THREAD * 100)
211 context["last_update"] = _datetime_to_epoch(posts[0].last_edit_time)
212 context["last_update"] = _datetime_to_epoch(posts[0].last_edit_time)
212
213
213 return render(request, 'boards/thread.html', context)
214 return render(request, 'boards/thread.html', context)
214
215
215
216
216 def login(request):
217 def login(request):
217 """Log in with user id"""
218 """Log in with user id"""
218
219
219 context = _init_default_context(request)
220 context = _init_default_context(request)
220
221
221 if request.method == 'POST':
222 if request.method == 'POST':
222 form = LoginForm(request.POST, request.FILES,
223 form = LoginForm(request.POST, request.FILES,
223 error_class=PlainErrorList)
224 error_class=PlainErrorList)
224 form.session = request.session
225 form.session = request.session
225
226
226 if form.is_valid():
227 if form.is_valid():
227 user = User.objects.get(user_id=form.cleaned_data['user_id'])
228 user = User.objects.get(user_id=form.cleaned_data['user_id'])
228 request.session['user_id'] = user.id
229 request.session['user_id'] = user.id
229 return redirect(index)
230 return redirect(index)
230
231
231 else:
232 else:
232 form = LoginForm()
233 form = LoginForm()
233
234
234 context['form'] = form
235 context['form'] = form
235
236
236 return render(request, 'boards/login.html', context)
237 return render(request, 'boards/login.html', context)
237
238
238
239
239 def settings(request):
240 def settings(request):
240 """User's settings"""
241 """User's settings"""
241
242
242 context = _init_default_context(request)
243 context = _init_default_context(request)
243 user = _get_user(request)
244 user = _get_user(request)
244 is_moderator = user.is_moderator()
245 is_moderator = user.is_moderator()
245
246
246 if request.method == 'POST':
247 if request.method == 'POST':
247 with transaction.commit_on_success():
248 with transaction.commit_on_success():
248 if is_moderator:
249 if is_moderator:
249 form = ModeratorSettingsForm(request.POST,
250 form = ModeratorSettingsForm(request.POST,
250 error_class=PlainErrorList)
251 error_class=PlainErrorList)
251 else:
252 else:
252 form = SettingsForm(request.POST, error_class=PlainErrorList)
253 form = SettingsForm(request.POST, error_class=PlainErrorList)
253
254
254 if form.is_valid():
255 if form.is_valid():
255 selected_theme = form.cleaned_data['theme']
256 selected_theme = form.cleaned_data['theme']
256
257
257 user.save_setting('theme', selected_theme)
258 user.save_setting('theme', selected_theme)
258
259
259 if is_moderator:
260 if is_moderator:
260 moderate = form.cleaned_data['moderate']
261 moderate = form.cleaned_data['moderate']
261 user.save_setting(SETTING_MODERATE, moderate)
262 user.save_setting(SETTING_MODERATE, moderate)
262
263
263 return redirect(settings)
264 return redirect(settings)
264 else:
265 else:
265 selected_theme = _get_theme(request)
266 selected_theme = _get_theme(request)
266
267
267 if is_moderator:
268 if is_moderator:
268 form = ModeratorSettingsForm(initial={'theme': selected_theme,
269 form = ModeratorSettingsForm(initial={'theme': selected_theme,
269 'moderate': context['moderator']},
270 'moderate': context['moderator']},
270 error_class=PlainErrorList)
271 error_class=PlainErrorList)
271 else:
272 else:
272 form = SettingsForm(initial={'theme': selected_theme},
273 form = SettingsForm(initial={'theme': selected_theme},
273 error_class=PlainErrorList)
274 error_class=PlainErrorList)
274
275
275 context['form'] = form
276 context['form'] = form
276
277
277 return render(request, 'boards/settings.html', context)
278 return render(request, 'boards/settings.html', context)
278
279
279
280
280 def all_tags(request):
281 def all_tags(request):
281 """All tags list"""
282 """All tags list"""
282
283
283 context = _init_default_context(request)
284 context = _init_default_context(request)
284 context['all_tags'] = Tag.objects.get_not_empty_tags()
285 context['all_tags'] = Tag.objects.get_not_empty_tags()
285
286
286 return render(request, 'boards/tags.html', context)
287 return render(request, 'boards/tags.html', context)
287
288
288
289
289 def jump_to_post(request, post_id):
290 def jump_to_post(request, post_id):
290 """Determine thread in which the requested post is and open it's page"""
291 """Determine thread in which the requested post is and open it's page"""
291
292
292 post = get_object_or_404(Post, id=post_id)
293 post = get_object_or_404(Post, id=post_id)
293
294
294 if not post.thread:
295 if not post.thread:
295 return redirect(thread, post_id=post.id)
296 return redirect(thread, post_id=post.id)
296 else:
297 else:
297 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
298 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
298 + '#' + str(post.id))
299 + '#' + str(post.id))
299
300
300
301
301 def authors(request):
302 def authors(request):
302 """Show authors list"""
303 """Show authors list"""
303
304
304 context = _init_default_context(request)
305 context = _init_default_context(request)
305 context['authors'] = boards.authors.authors
306 context['authors'] = boards.authors.authors
306
307
307 return render(request, 'boards/authors.html', context)
308 return render(request, 'boards/authors.html', context)
308
309
309
310
310 @transaction.commit_on_success
311 @transaction.commit_on_success
311 def delete(request, post_id):
312 def delete(request, post_id):
312 """Delete post"""
313 """Delete post"""
313
314
314 user = _get_user(request)
315 user = _get_user(request)
315 post = get_object_or_404(Post, id=post_id)
316 post = get_object_or_404(Post, id=post_id)
316
317
317 if user.is_moderator():
318 if user.is_moderator():
318 # TODO Show confirmation page before deletion
319 # TODO Show confirmation page before deletion
319 Post.objects.delete_post(post)
320 Post.objects.delete_post(post)
320
321
321 if not post.thread:
322 if not post.thread:
322 return _redirect_to_next(request)
323 return _redirect_to_next(request)
323 else:
324 else:
324 return redirect(thread, post_id=post.thread.id)
325 return redirect(thread, post_id=post.thread.id)
325
326
326
327
327 @transaction.commit_on_success
328 @transaction.commit_on_success
328 def ban(request, post_id):
329 def ban(request, post_id):
329 """Ban user"""
330 """Ban user"""
330
331
331 user = _get_user(request)
332 user = _get_user(request)
332 post = get_object_or_404(Post, id=post_id)
333 post = get_object_or_404(Post, id=post_id)
333
334
334 if user.is_moderator():
335 if user.is_moderator():
335 # TODO Show confirmation page before ban
336 # TODO Show confirmation page before ban
336 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
337 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
337 if created:
338 if created:
338 ban.reason = 'Banned for post ' + str(post_id)
339 ban.reason = 'Banned for post ' + str(post_id)
339 ban.save()
340 ban.save()
340
341
341 return _redirect_to_next(request)
342 return _redirect_to_next(request)
342
343
343
344
344 def you_are_banned(request):
345 def you_are_banned(request):
345 """Show the page that notifies that user is banned"""
346 """Show the page that notifies that user is banned"""
346
347
347 context = _init_default_context(request)
348 context = _init_default_context(request)
348
349
349 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
350 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
350 context['ban_reason'] = ban.reason
351 context['ban_reason'] = ban.reason
351 return render(request, 'boards/staticpages/banned.html', context)
352 return render(request, 'boards/staticpages/banned.html', context)
352
353
353
354
354 def page_404(request):
355 def page_404(request):
355 """Show page 404 (not found error)"""
356 """Show page 404 (not found error)"""
356
357
357 context = _init_default_context(request)
358 context = _init_default_context(request)
358 return render(request, 'boards/404.html', context)
359 return render(request, 'boards/404.html', context)
359
360
360
361
361 @transaction.commit_on_success
362 @transaction.commit_on_success
362 def tag_subscribe(request, tag_name):
363 def tag_subscribe(request, tag_name):
363 """Add tag to favorites"""
364 """Add tag to favorites"""
364
365
365 user = _get_user(request)
366 user = _get_user(request)
366 tag = get_object_or_404(Tag, name=tag_name)
367 tag = get_object_or_404(Tag, name=tag_name)
367
368
368 if not tag in user.fav_tags.all():
369 if not tag in user.fav_tags.all():
369 user.add_tag(tag)
370 user.add_tag(tag)
370
371
371 return _redirect_to_next(request)
372 return _redirect_to_next(request)
372
373
373
374
374 @transaction.commit_on_success
375 @transaction.commit_on_success
375 def tag_unsubscribe(request, tag_name):
376 def tag_unsubscribe(request, tag_name):
376 """Remove tag from favorites"""
377 """Remove tag from favorites"""
377
378
378 user = _get_user(request)
379 user = _get_user(request)
379 tag = get_object_or_404(Tag, name=tag_name)
380 tag = get_object_or_404(Tag, name=tag_name)
380
381
381 if tag in user.fav_tags.all():
382 if tag in user.fav_tags.all():
382 user.remove_tag(tag)
383 user.remove_tag(tag)
383
384
384 return _redirect_to_next(request)
385 return _redirect_to_next(request)
385
386
386
387
387 def static_page(request, name):
388 def static_page(request, name):
388 """Show a static page that needs only tags list and a CSS"""
389 """Show a static page that needs only tags list and a CSS"""
389
390
390 context = _init_default_context(request)
391 context = _init_default_context(request)
391 return render(request, 'boards/staticpages/' + name + '.html', context)
392 return render(request, 'boards/staticpages/' + name + '.html', context)
392
393
393
394
394 def api_get_post(request, post_id):
395 def api_get_post(request, post_id):
395 """
396 """
396 Get the JSON of a post. This can be
397 Get the JSON of a post. This can be
397 used as and API for external clients.
398 used as and API for external clients.
398 """
399 """
399
400
400 post = get_object_or_404(Post, id=post_id)
401 post = get_object_or_404(Post, id=post_id)
401
402
402 json = serializers.serialize("json", [post], fields=(
403 json = serializers.serialize("json", [post], fields=(
403 "pub_time", "_text_rendered", "title", "text", "image",
404 "pub_time", "_text_rendered", "title", "text", "image",
404 "image_width", "image_height", "replies", "tags"
405 "image_width", "image_height", "replies", "tags"
405 ))
406 ))
406
407
407 return HttpResponse(content=json)
408 return HttpResponse(content=json)
408
409
409
410
410 def api_get_threaddiff(request, thread_id, last_update_time):
411 def api_get_threaddiff(request, thread_id, last_update_time):
411 """Get posts that were changed or added since time"""
412 """Get posts that were changed or added since time"""
412
413
413 thread = get_object_or_404(Post, id=thread_id)
414 thread = get_object_or_404(Post, id=thread_id)
414
415
415 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
416 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
416 timezone.get_current_timezone())
417 timezone.get_current_timezone())
417
418
418 json_data = {
419 json_data = {
419 'added': [],
420 'added': [],
420 'updated': [],
421 'updated': [],
421 'last_update': None,
422 'last_update': None,
422 }
423 }
423 added_posts = Post.objects.filter(thread=thread,
424 added_posts = Post.objects.filter(thread=thread,
424 pub_time__gt=filter_time)\
425 pub_time__gt=filter_time)\
425 .order_by('pub_time')
426 .order_by('pub_time')
426 updated_posts = Post.objects.filter(thread=thread,
427 updated_posts = Post.objects.filter(thread=thread,
427 pub_time__lte=filter_time,
428 pub_time__lte=filter_time,
428 last_edit_time__gt=filter_time)
429 last_edit_time__gt=filter_time)
429 for post in added_posts:
430 for post in added_posts:
430 json_data['added'].append(get_post(request, post.id).content.strip())
431 json_data['added'].append(get_post(request, post.id).content.strip())
431 for post in updated_posts:
432 for post in updated_posts:
432 json_data['updated'].append(get_post(request, post.id).content.strip())
433 json_data['updated'].append(get_post(request, post.id).content.strip())
433 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
434 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
434
435
435 return HttpResponse(content=json.dumps(json_data))
436 return HttpResponse(content=json.dumps(json_data))
436
437
437
438
438 def get_post(request, post_id):
439 def get_post(request, post_id):
439 """Get the html of a post. Used for popups."""
440 """Get the html of a post. Used for popups."""
440
441
441 post = get_object_or_404(Post, id=post_id)
442 post = get_object_or_404(Post, id=post_id)
442 thread = post.thread
443 thread = post.thread
443 if not thread:
444 if not thread:
444 thread = post
445 thread = post
445
446
446 context = RequestContext(request)
447 context = RequestContext(request)
447 context["post"] = post
448 context["post"] = post
448 context["can_bump"] = thread.can_bump()
449 context["can_bump"] = thread.can_bump()
449 if "truncated" in request.GET:
450 if "truncated" in request.GET:
450 context["truncated"] = True
451 context["truncated"] = True
451
452
452 return render(request, 'boards/post.html', context)
453 return render(request, 'boards/post.html', context)
453
454
454
455
455 def _get_theme(request, user=None):
456 def _get_theme(request, user=None):
456 """Get user's CSS theme"""
457 """Get user's CSS theme"""
457
458
458 if not user:
459 if not user:
459 user = _get_user(request)
460 user = _get_user(request)
460 theme = user.get_setting('theme')
461 theme = user.get_setting('theme')
461 if not theme:
462 if not theme:
462 theme = neboard.settings.DEFAULT_THEME
463 theme = neboard.settings.DEFAULT_THEME
463
464
464 return theme
465 return theme
465
466
466
467
467 def _init_default_context(request):
468 def _init_default_context(request):
468 """Create context with default values that are used in most views"""
469 """Create context with default values that are used in most views"""
469
470
470 context = RequestContext(request)
471 context = RequestContext(request)
471
472
472 user = _get_user(request)
473 user = _get_user(request)
473 context['user'] = user
474 context['user'] = user
474 context['tags'] = user.get_sorted_fav_tags()
475 context['tags'] = user.get_sorted_fav_tags()
475
476
476 theme = _get_theme(request, user)
477 theme = _get_theme(request, user)
477 context['theme'] = theme
478 context['theme'] = theme
478 context['theme_css'] = 'css/' + theme + '/base_page.css'
479 context['theme_css'] = 'css/' + theme + '/base_page.css'
479
480
480 # This shows the moderator panel
481 # This shows the moderator panel
481 moderate = user.get_setting(SETTING_MODERATE)
482 moderate = user.get_setting(SETTING_MODERATE)
482 if moderate == 'True':
483 if moderate == 'True':
483 context['moderator'] = user.is_moderator()
484 context['moderator'] = user.is_moderator()
484 else:
485 else:
485 context['moderator'] = False
486 context['moderator'] = False
486
487
487 return context
488 return context
488
489
489
490
490 def _get_user(request):
491 def _get_user(request):
491 """
492 """
492 Get current user from the session. If the user does not exist, create
493 Get current user from the session. If the user does not exist, create
493 a new one.
494 a new one.
494 """
495 """
495
496
496 session = request.session
497 session = request.session
497 if not 'user_id' in session:
498 if not 'user_id' in session:
498 request.session.save()
499 request.session.save()
499
500
500 md5 = hashlib.md5()
501 md5 = hashlib.md5()
501 md5.update(session.session_key)
502 md5.update(session.session_key)
502 new_id = md5.hexdigest()
503 new_id = md5.hexdigest()
503
504
504 time_now = timezone.now()
505 time_now = timezone.now()
505 user = User.objects.create(user_id=new_id, rank=RANK_USER,
506 user = User.objects.create(user_id=new_id, rank=RANK_USER,
506 registration_time=time_now)
507 registration_time=time_now)
507
508
508 session['user_id'] = user.id
509 session['user_id'] = user.id
509 else:
510 else:
510 user = User.objects.get(id=session['user_id'])
511 user = User.objects.get(id=session['user_id'])
511
512
512 return user
513 return user
513
514
514
515
515 def _redirect_to_next(request):
516 def _redirect_to_next(request):
516 """
517 """
517 If a 'next' parameter was specified, redirect to the next page. This is
518 If a 'next' parameter was specified, redirect to the next page. This is
518 used when the user is required to return to some page after the current
519 used when the user is required to return to some page after the current
519 view has finished its work.
520 view has finished its work.
520 """
521 """
521
522
522 if 'next' in request.GET:
523 if 'next' in request.GET:
523 next_page = request.GET['next']
524 next_page = request.GET['next']
524 return HttpResponseRedirect(next_page)
525 return HttpResponseRedirect(next_page)
525 else:
526 else:
526 return redirect(index)
527 return redirect(index)
527
528
528
529
529 @transaction.commit_on_success
530 @transaction.commit_on_success
530 def _ban_current_user(request):
531 def _ban_current_user(request):
531 """Add current user to the IP ban list"""
532 """Add current user to the IP ban list"""
532
533
533 ip = utils.get_client_ip(request)
534 ip = utils.get_client_ip(request)
534 ban, created = Ban.objects.get_or_create(ip=ip)
535 ban, created = Ban.objects.get_or_create(ip=ip)
535 if created:
536 if created:
536 ban.can_read = False
537 ban.can_read = False
537 ban.reason = BAN_REASON_SPAM
538 ban.reason = BAN_REASON_SPAM
538 ban.save()
539 ban.save()
539
540
540
541
541 def _remove_invalid_links(text):
542 def _remove_invalid_links(text):
542 """
543 """
543 Replace invalid links in posts so that they won't be parsed.
544 Replace invalid links in posts so that they won't be parsed.
544 Invalid links are links to non-existent posts
545 Invalid links are links to non-existent posts
545 """
546 """
546
547
547 for reply_number in re.finditer(REGEX_REPLY, text):
548 for reply_number in re.finditer(REGEX_REPLY, text):
548 post_id = reply_number.group(1)
549 post_id = reply_number.group(1)
549 post = Post.objects.filter(id=post_id)
550 post = Post.objects.filter(id=post_id)
550 if not post.exists():
551 if not post.exists():
551 text = string.replace(text, '>>' + id, id)
552 text = string.replace(text, '>>' + id, id)
552
553
553 return text
554 return text
554
555
555
556
556 def _datetime_to_epoch(datetime):
557 def _datetime_to_epoch(datetime):
557 return int(time.mktime(timezone.localtime(
558 return int(time.mktime(timezone.localtime(
558 datetime,timezone.get_current_timezone()).timetuple())
559 datetime,timezone.get_current_timezone()).timetuple())
559 * 1000000 + datetime.microsecond)
560 * 1000000 + datetime.microsecond)
General Comments 0
You need to be logged in to leave comments. Login now