##// END OF EJS Templates
Added image aliases to upload the same images (like "fake" or "gtfo")
neko259 -
r1500:9178427e default
parent child Browse files
Show More
@@ -0,0 +1,27 b''
1 from boards.abstracts.settingsmanager import SessionSettingsManager
2 from boards.models import PostImage
3
4 class AttachmentAlias:
5 def get_image(alias):
6 pass
7
8
9 class SessionAttachmentAlias(AttachmentAlias):
10 def __init__(self, session):
11 self.session = session
12
13 def get_image(self, alias):
14 settings_manager = SessionSettingsManager(self.session)
15 return settings_manager.get_image_by_alias(alias)
16
17
18 class ModelAttachmentAlias(AttachmentAlias):
19 def get_image(self, alias):
20 return PostImage.objects.filter(alias=alias).first()
21
22
23 def get_image_by_alias(alias, session):
24 image = SessionAttachmentAlias(session).get_image(alias) or ModelAttachmentAlias().get_image(alias)
25
26 if image is not None:
27 return image
@@ -0,0 +1,24 b''
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import migrations, models
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('boards', '0040_thread_monochrome'),
11 ]
12
13 operations = [
14 migrations.AddField(
15 model_name='attachment',
16 name='original_filename',
17 field=models.TextField(null=True),
18 ),
19 migrations.AddField(
20 model_name='postimage',
21 name='original_filename',
22 field=models.TextField(null=True),
23 ),
24 ]
@@ -0,0 +1,28 b''
1 # -*- coding: utf-8 -*-
2 # Generated by Django 1.9.5 on 2016-04-22 07:53
3 from __future__ import unicode_literals
4
5 from django.db import migrations, models
6
7
8 class Migration(migrations.Migration):
9
10 dependencies = [
11 ('boards', '0041_auto_20160124_2341'),
12 ]
13
14 operations = [
15 migrations.RemoveField(
16 model_name='attachment',
17 name='original_filename',
18 ),
19 migrations.RemoveField(
20 model_name='postimage',
21 name='original_filename',
22 ),
23 migrations.AddField(
24 model_name='postimage',
25 name='alias',
26 field=models.TextField(blank=True, null=True, unique=True),
27 ),
28 ]
@@ -1,183 +1,195 b''
1 from boards.models import Tag
1 from boards.models import Tag
2 from boards.models.thread import FAV_THREAD_NO_UPDATES
2 from boards.models.thread import FAV_THREAD_NO_UPDATES
3
3
4 MAX_TRIPCODE_COLLISIONS = 50
4 MAX_TRIPCODE_COLLISIONS = 50
5
5
6 __author__ = 'neko259'
6 __author__ = 'neko259'
7
7
8 SESSION_SETTING = 'setting'
8 SESSION_SETTING = 'setting'
9
9
10 # Remove this, it is not used any more cause there is a user's permission
10 # Remove this, it is not used any more cause there is a user's permission
11 PERMISSION_MODERATE = 'moderator'
11 PERMISSION_MODERATE = 'moderator'
12
12
13 SETTING_THEME = 'theme'
13 SETTING_THEME = 'theme'
14 SETTING_FAVORITE_TAGS = 'favorite_tags'
14 SETTING_FAVORITE_TAGS = 'favorite_tags'
15 SETTING_FAVORITE_THREADS = 'favorite_threads'
15 SETTING_FAVORITE_THREADS = 'favorite_threads'
16 SETTING_HIDDEN_TAGS = 'hidden_tags'
16 SETTING_HIDDEN_TAGS = 'hidden_tags'
17 SETTING_PERMISSIONS = 'permissions'
17 SETTING_PERMISSIONS = 'permissions'
18 SETTING_USERNAME = 'username'
18 SETTING_USERNAME = 'username'
19 SETTING_LAST_NOTIFICATION_ID = 'last_notification'
19 SETTING_LAST_NOTIFICATION_ID = 'last_notification'
20 SETTING_IMAGE_VIEWER = 'image_viewer'
20 SETTING_IMAGE_VIEWER = 'image_viewer'
21 SETTING_TRIPCODE = 'tripcode'
21 SETTING_TRIPCODE = 'tripcode'
22 SETTING_IMAGES = 'images_aliases'
22
23
23 DEFAULT_THEME = 'md'
24 DEFAULT_THEME = 'md'
24
25
25
26
26 class SettingsManager:
27 class SettingsManager:
27 """
28 """
28 Base settings manager class. get_setting and set_setting methods should
29 Base settings manager class. get_setting and set_setting methods should
29 be overriden.
30 be overriden.
30 """
31 """
31 def __init__(self):
32 def __init__(self):
32 pass
33 pass
33
34
34 def get_theme(self) -> str:
35 def get_theme(self) -> str:
35 theme = self.get_setting(SETTING_THEME)
36 theme = self.get_setting(SETTING_THEME)
36 if not theme:
37 if not theme:
37 theme = DEFAULT_THEME
38 theme = DEFAULT_THEME
38 self.set_setting(SETTING_THEME, theme)
39 self.set_setting(SETTING_THEME, theme)
39
40
40 return theme
41 return theme
41
42
42 def set_theme(self, theme):
43 def set_theme(self, theme):
43 self.set_setting(SETTING_THEME, theme)
44 self.set_setting(SETTING_THEME, theme)
44
45
45 def has_permission(self, permission):
46 def has_permission(self, permission):
46 permissions = self.get_setting(SETTING_PERMISSIONS)
47 permissions = self.get_setting(SETTING_PERMISSIONS)
47 if permissions:
48 if permissions:
48 return permission in permissions
49 return permission in permissions
49 else:
50 else:
50 return False
51 return False
51
52
52 def get_setting(self, setting, default=None):
53 def get_setting(self, setting, default=None):
53 pass
54 pass
54
55
55 def set_setting(self, setting, value):
56 def set_setting(self, setting, value):
56 pass
57 pass
57
58
58 def add_permission(self, permission):
59 def add_permission(self, permission):
59 permissions = self.get_setting(SETTING_PERMISSIONS)
60 permissions = self.get_setting(SETTING_PERMISSIONS)
60 if not permissions:
61 if not permissions:
61 permissions = [permission]
62 permissions = [permission]
62 else:
63 else:
63 permissions.append(permission)
64 permissions.append(permission)
64 self.set_setting(SETTING_PERMISSIONS, permissions)
65 self.set_setting(SETTING_PERMISSIONS, permissions)
65
66
66 def del_permission(self, permission):
67 def del_permission(self, permission):
67 permissions = self.get_setting(SETTING_PERMISSIONS)
68 permissions = self.get_setting(SETTING_PERMISSIONS)
68 if not permissions:
69 if not permissions:
69 permissions = []
70 permissions = []
70 else:
71 else:
71 permissions.remove(permission)
72 permissions.remove(permission)
72 self.set_setting(SETTING_PERMISSIONS, permissions)
73 self.set_setting(SETTING_PERMISSIONS, permissions)
73
74
74 def get_fav_tags(self) -> list:
75 def get_fav_tags(self) -> list:
75 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
76 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
76 tags = []
77 tags = []
77 if tag_names:
78 if tag_names:
78 tags = list(Tag.objects.filter(name__in=tag_names))
79 tags = list(Tag.objects.filter(name__in=tag_names))
79 return tags
80 return tags
80
81
81 def add_fav_tag(self, tag):
82 def add_fav_tag(self, tag):
82 tags = self.get_setting(SETTING_FAVORITE_TAGS)
83 tags = self.get_setting(SETTING_FAVORITE_TAGS)
83 if not tags:
84 if not tags:
84 tags = [tag.name]
85 tags = [tag.name]
85 else:
86 else:
86 if not tag.name in tags:
87 if not tag.name in tags:
87 tags.append(tag.name)
88 tags.append(tag.name)
88
89
89 tags.sort()
90 tags.sort()
90 self.set_setting(SETTING_FAVORITE_TAGS, tags)
91 self.set_setting(SETTING_FAVORITE_TAGS, tags)
91
92
92 def del_fav_tag(self, tag):
93 def del_fav_tag(self, tag):
93 tags = self.get_setting(SETTING_FAVORITE_TAGS)
94 tags = self.get_setting(SETTING_FAVORITE_TAGS)
94 if tag.name in tags:
95 if tag.name in tags:
95 tags.remove(tag.name)
96 tags.remove(tag.name)
96 self.set_setting(SETTING_FAVORITE_TAGS, tags)
97 self.set_setting(SETTING_FAVORITE_TAGS, tags)
97
98
98 def get_hidden_tags(self) -> list:
99 def get_hidden_tags(self) -> list:
99 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
100 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
100 tags = []
101 tags = []
101 if tag_names:
102 if tag_names:
102 tags = list(Tag.objects.filter(name__in=tag_names))
103 tags = list(Tag.objects.filter(name__in=tag_names))
103
104
104 return tags
105 return tags
105
106
106 def add_hidden_tag(self, tag):
107 def add_hidden_tag(self, tag):
107 tags = self.get_setting(SETTING_HIDDEN_TAGS)
108 tags = self.get_setting(SETTING_HIDDEN_TAGS)
108 if not tags:
109 if not tags:
109 tags = [tag.name]
110 tags = [tag.name]
110 else:
111 else:
111 if not tag.name in tags:
112 if not tag.name in tags:
112 tags.append(tag.name)
113 tags.append(tag.name)
113
114
114 tags.sort()
115 tags.sort()
115 self.set_setting(SETTING_HIDDEN_TAGS, tags)
116 self.set_setting(SETTING_HIDDEN_TAGS, tags)
116
117
117 def del_hidden_tag(self, tag):
118 def del_hidden_tag(self, tag):
118 tags = self.get_setting(SETTING_HIDDEN_TAGS)
119 tags = self.get_setting(SETTING_HIDDEN_TAGS)
119 if tag.name in tags:
120 if tag.name in tags:
120 tags.remove(tag.name)
121 tags.remove(tag.name)
121 self.set_setting(SETTING_HIDDEN_TAGS, tags)
122 self.set_setting(SETTING_HIDDEN_TAGS, tags)
122
123
123 def get_fav_threads(self) -> dict:
124 def get_fav_threads(self) -> dict:
124 return self.get_setting(SETTING_FAVORITE_THREADS, default=dict())
125 return self.get_setting(SETTING_FAVORITE_THREADS, default=dict())
125
126
126 def add_or_read_fav_thread(self, opening_post):
127 def add_or_read_fav_thread(self, opening_post):
127 threads = self.get_fav_threads()
128 threads = self.get_fav_threads()
128 thread = opening_post.get_thread()
129 thread = opening_post.get_thread()
129 # Don't check for new posts if the thread is archived already
130 # Don't check for new posts if the thread is archived already
130 if thread.is_archived():
131 if thread.is_archived():
131 last_id = FAV_THREAD_NO_UPDATES
132 last_id = FAV_THREAD_NO_UPDATES
132 else:
133 else:
133 last_id = thread.get_replies().last().id
134 last_id = thread.get_replies().last().id
134 threads[str(opening_post.id)] = last_id
135 threads[str(opening_post.id)] = last_id
135 self.set_setting(SETTING_FAVORITE_THREADS, threads)
136 self.set_setting(SETTING_FAVORITE_THREADS, threads)
136
137
137 def del_fav_thread(self, opening_post):
138 def del_fav_thread(self, opening_post):
138 threads = self.get_fav_threads()
139 threads = self.get_fav_threads()
139 if self.thread_is_fav(opening_post):
140 if self.thread_is_fav(opening_post):
140 del threads[str(opening_post.id)]
141 del threads[str(opening_post.id)]
141 self.set_setting(SETTING_FAVORITE_THREADS, threads)
142 self.set_setting(SETTING_FAVORITE_THREADS, threads)
142
143
143 def thread_is_fav(self, opening_post):
144 def thread_is_fav(self, opening_post):
144 return str(opening_post.id) in self.get_fav_threads()
145 return str(opening_post.id) in self.get_fav_threads()
145
146
146 def get_notification_usernames(self):
147 def get_notification_usernames(self):
147 names = set()
148 names = set()
148 name_list = self.get_setting(SETTING_USERNAME)
149 name_list = self.get_setting(SETTING_USERNAME)
149 if name_list is not None:
150 if name_list is not None:
150 name_list = name_list.strip()
151 name_list = name_list.strip()
151 if len(name_list) > 0:
152 if len(name_list) > 0:
152 names = name_list.lower().split(',')
153 names = name_list.lower().split(',')
153 names = set(name.strip() for name in names)
154 names = set(name.strip() for name in names)
154 return names
155 return names
155
156
157 def get_image_by_alias(self, alias):
158 images = self.get_setting(SETTING_IMAGES)
159 if images is not None and len(images) > 0:
160 return images.get(alias)
161
162 def add_image_alias(self, alias, image):
163 images = self.get_setting(SETTING_IMAGES)
164 if images is None:
165 images = dict()
166 images.put(alias, image)
167
156
168
157 class SessionSettingsManager(SettingsManager):
169 class SessionSettingsManager(SettingsManager):
158 """
170 """
159 Session-based settings manager. All settings are saved to the user's
171 Session-based settings manager. All settings are saved to the user's
160 session.
172 session.
161 """
173 """
162 def __init__(self, session):
174 def __init__(self, session):
163 SettingsManager.__init__(self)
175 SettingsManager.__init__(self)
164 self.session = session
176 self.session = session
165
177
166 def get_setting(self, setting, default=None):
178 def get_setting(self, setting, default=None):
167 if setting in self.session:
179 if setting in self.session:
168 return self.session[setting]
180 return self.session[setting]
169 else:
181 else:
170 self.set_setting(setting, default)
182 self.set_setting(setting, default)
171 return default
183 return default
172
184
173 def set_setting(self, setting, value):
185 def set_setting(self, setting, value):
174 self.session[setting] = value
186 self.session[setting] = value
175
187
176
188
177 def get_settings_manager(request) -> SettingsManager:
189 def get_settings_manager(request) -> SettingsManager:
178 """
190 """
179 Get settings manager based on the request object. Currently only
191 Get settings manager based on the request object. Currently only
180 session-based manager is supported. In the future, cookie-based or
192 session-based manager is supported. In the future, cookie-based or
181 database-based managers could be implemented.
193 database-based managers could be implemented.
182 """
194 """
183 return SessionSettingsManager(request.session)
195 return SessionSettingsManager(request.session)
@@ -1,99 +1,111 b''
1 from django.contrib import admin
1 from django.contrib import admin
2 from boards.models import Post, Tag, Ban, Thread, Banner
3 from django.utils.translation import ugettext_lazy as _
2 from django.utils.translation import ugettext_lazy as _
3 from django.core.urlresolvers import reverse
4 from boards.models import Post, Tag, Ban, Thread, Banner, PostImage
4
5
5
6
6 @admin.register(Post)
7 @admin.register(Post)
7 class PostAdmin(admin.ModelAdmin):
8 class PostAdmin(admin.ModelAdmin):
8
9
9 list_display = ('id', 'title', 'text', 'poster_ip')
10 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images')
10 list_filter = ('pub_time',)
11 list_filter = ('pub_time',)
11 search_fields = ('id', 'title', 'text', 'poster_ip')
12 search_fields = ('id', 'title', 'text', 'poster_ip')
12 exclude = ('referenced_posts', 'refmap')
13 exclude = ('referenced_posts', 'refmap')
13 readonly_fields = ('poster_ip', 'threads', 'thread', 'images',
14 readonly_fields = ('poster_ip', 'threads', 'thread', 'linked_images',
14 'attachments', 'uid', 'url', 'pub_time', 'opening')
15 'attachments', 'uid', 'url', 'pub_time', 'opening')
15
16
16 def ban_poster(self, request, queryset):
17 def ban_poster(self, request, queryset):
17 bans = 0
18 bans = 0
18 for post in queryset:
19 for post in queryset:
19 poster_ip = post.poster_ip
20 poster_ip = post.poster_ip
20 ban, created = Ban.objects.get_or_create(ip=poster_ip)
21 ban, created = Ban.objects.get_or_create(ip=poster_ip)
21 if created:
22 if created:
22 bans += 1
23 bans += 1
23 self.message_user(request, _('{} posters were banned').format(bans))
24 self.message_user(request, _('{} posters were banned').format(bans))
24
25
25 def ban_with_hiding(self, request, queryset):
26 def ban_with_hiding(self, request, queryset):
26 bans = 0
27 bans = 0
27 hidden = 0
28 hidden = 0
28 for post in queryset:
29 for post in queryset:
29 poster_ip = post.poster_ip
30 poster_ip = post.poster_ip
30 ban, created = Ban.objects.get_or_create(ip=poster_ip)
31 ban, created = Ban.objects.get_or_create(ip=poster_ip)
31 if created:
32 if created:
32 bans += 1
33 bans += 1
33 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
34 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
34 hidden += posts.count()
35 hidden += posts.count()
35 posts.update(hidden=True)
36 posts.update(hidden=True)
36 self.message_user(request, _('{} posters were banned, {} messages were hidden').format(bans, hidden))
37 self.message_user(request, _('{} posters were banned, {} messages were hidden').format(bans, hidden))
37
38
39 def linked_images(self, obj: Post):
40 images = obj.images.all()
41 image_urls = ['<a href="{}">{}</a>'.format(reverse('admin:%s_%s_change' %(image._meta.app_label, image._meta.model_name), args=[image.id]), image.hash) for image in images]
42 return ', '.join(image_urls)
43 linked_images.allow_tags = True
44
38
45
39 actions = ['ban_poster', 'ban_with_hiding']
46 actions = ['ban_poster', 'ban_with_hiding']
40
47
41
48
42 @admin.register(Tag)
49 @admin.register(Tag)
43 class TagAdmin(admin.ModelAdmin):
50 class TagAdmin(admin.ModelAdmin):
44
51
45 def thread_count(self, obj: Tag) -> int:
52 def thread_count(self, obj: Tag) -> int:
46 return obj.get_thread_count()
53 return obj.get_thread_count()
47
54
48 def display_children(self, obj: Tag):
55 def display_children(self, obj: Tag):
49 return ', '.join([str(child) for child in obj.get_children().all()])
56 return ', '.join([str(child) for child in obj.get_children().all()])
50
57
51 def save_model(self, request, obj, form, change):
58 def save_model(self, request, obj, form, change):
52 super().save_model(request, obj, form, change)
59 super().save_model(request, obj, form, change)
53 for thread in obj.get_threads().all():
60 for thread in obj.get_threads().all():
54 thread.refresh_tags()
61 thread.refresh_tags()
55
62
56 list_display = ('name', 'thread_count', 'display_children')
63 list_display = ('name', 'thread_count', 'display_children')
57 search_fields = ('name',)
64 search_fields = ('name',)
58
65
59
66
60 @admin.register(Thread)
67 @admin.register(Thread)
61 class ThreadAdmin(admin.ModelAdmin):
68 class ThreadAdmin(admin.ModelAdmin):
62
69
63 def title(self, obj: Thread) -> str:
70 def title(self, obj: Thread) -> str:
64 return obj.get_opening_post().get_title()
71 return obj.get_opening_post().get_title()
65
72
66 def reply_count(self, obj: Thread) -> int:
73 def reply_count(self, obj: Thread) -> int:
67 return obj.get_reply_count()
74 return obj.get_reply_count()
68
75
69 def ip(self, obj: Thread):
76 def ip(self, obj: Thread):
70 return obj.get_opening_post().poster_ip
77 return obj.get_opening_post().poster_ip
71
78
72 def display_tags(self, obj: Thread):
79 def display_tags(self, obj: Thread):
73 return ', '.join([str(tag) for tag in obj.get_tags().all()])
80 return ', '.join([str(tag) for tag in obj.get_tags().all()])
74
81
75 def op(self, obj: Thread):
82 def op(self, obj: Thread):
76 return obj.get_opening_post_id()
83 return obj.get_opening_post_id()
77
84
78 # Save parent tags when editing tags
85 # Save parent tags when editing tags
79 def save_related(self, request, form, formsets, change):
86 def save_related(self, request, form, formsets, change):
80 super().save_related(request, form, formsets, change)
87 super().save_related(request, form, formsets, change)
81 form.instance.refresh_tags()
88 form.instance.refresh_tags()
82
89
83 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
90 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
84 'display_tags')
91 'display_tags')
85 list_filter = ('bump_time', 'status')
92 list_filter = ('bump_time', 'status')
86 search_fields = ('id', 'title')
93 search_fields = ('id', 'title')
87 filter_horizontal = ('tags',)
94 filter_horizontal = ('tags',)
88
95
89
96
90 @admin.register(Ban)
97 @admin.register(Ban)
91 class BanAdmin(admin.ModelAdmin):
98 class BanAdmin(admin.ModelAdmin):
92 list_display = ('ip', 'can_read')
99 list_display = ('ip', 'can_read')
93 list_filter = ('can_read',)
100 list_filter = ('can_read',)
94 search_fields = ('ip',)
101 search_fields = ('ip',)
95
102
96
103
97 @admin.register(Banner)
104 @admin.register(Banner)
98 class BannerAdmin(admin.ModelAdmin):
105 class BannerAdmin(admin.ModelAdmin):
99 list_display = ('title', 'text')
106 list_display = ('title', 'text')
107
108
109 @admin.register(PostImage)
110 class PostImageAdmin(admin.ModelAdmin):
111 search_fields = ('alias',)
@@ -1,453 +1,469 b''
1 import hashlib
1 import hashlib
2 import re
2 import re
3 import time
3 import time
4 import logging
4 import logging
5
5
6 import pytz
6 import pytz
7
7
8 from django import forms
8 from django import forms
9 from django.core.files.uploadedfile import SimpleUploadedFile
9 from django.core.files.uploadedfile import SimpleUploadedFile
10 from django.core.exceptions import ObjectDoesNotExist
10 from django.core.exceptions import ObjectDoesNotExist
11 from django.forms.utils import ErrorList
11 from django.forms.utils import ErrorList
12 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
12 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
13 from django.utils import timezone
13 from django.utils import timezone
14
14
15 from boards.abstracts.settingsmanager import get_settings_manager
15 from boards.abstracts.settingsmanager import get_settings_manager
16 from boards.abstracts.attachment_alias import get_image_by_alias
16 from boards.mdx_neboard import formatters
17 from boards.mdx_neboard import formatters
17 from boards.models.attachment.downloaders import Downloader
18 from boards.models.attachment.downloaders import Downloader
18 from boards.models.post import TITLE_MAX_LENGTH
19 from boards.models.post import TITLE_MAX_LENGTH
19 from boards.models import Tag, Post
20 from boards.models import Tag, Post
20 from boards.utils import validate_file_size, get_file_mimetype, \
21 from boards.utils import validate_file_size, get_file_mimetype, \
21 FILE_EXTENSION_DELIMITER
22 FILE_EXTENSION_DELIMITER
22 from neboard import settings
23 from neboard import settings
23 import boards.settings as board_settings
24 import boards.settings as board_settings
24 import neboard
25 import neboard
25
26
26 POW_HASH_LENGTH = 16
27 POW_HASH_LENGTH = 16
27 POW_LIFE_MINUTES = 5
28 POW_LIFE_MINUTES = 5
28
29
29 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
30 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
31
32
32 VETERAN_POSTING_DELAY = 5
33 VETERAN_POSTING_DELAY = 5
33
34
34 ATTRIBUTE_PLACEHOLDER = 'placeholder'
35 ATTRIBUTE_PLACEHOLDER = 'placeholder'
35 ATTRIBUTE_ROWS = 'rows'
36 ATTRIBUTE_ROWS = 'rows'
36
37
37 LAST_POST_TIME = 'last_post_time'
38 LAST_POST_TIME = 'last_post_time'
38 LAST_LOGIN_TIME = 'last_login_time'
39 LAST_LOGIN_TIME = 'last_login_time'
39 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
40 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
40 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
41 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
41
42
42 LABEL_TITLE = _('Title')
43 LABEL_TITLE = _('Title')
43 LABEL_TEXT = _('Text')
44 LABEL_TEXT = _('Text')
44 LABEL_TAG = _('Tag')
45 LABEL_TAG = _('Tag')
45 LABEL_SEARCH = _('Search')
46 LABEL_SEARCH = _('Search')
46
47
47 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
48 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
48 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
49 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
49
50
50 TAG_MAX_LENGTH = 20
51 TAG_MAX_LENGTH = 20
51
52
52 TEXTAREA_ROWS = 4
53 TEXTAREA_ROWS = 4
53
54
54 TRIPCODE_DELIM = '#'
55 TRIPCODE_DELIM = '#'
55
56
56 # TODO Maybe this may be converted into the database table?
57 # TODO Maybe this may be converted into the database table?
57 MIMETYPE_EXTENSIONS = {
58 MIMETYPE_EXTENSIONS = {
58 'image/jpeg': 'jpeg',
59 'image/jpeg': 'jpeg',
59 'image/png': 'png',
60 'image/png': 'png',
60 'image/gif': 'gif',
61 'image/gif': 'gif',
61 'video/webm': 'webm',
62 'video/webm': 'webm',
62 'application/pdf': 'pdf',
63 'application/pdf': 'pdf',
63 'x-diff': 'diff',
64 'x-diff': 'diff',
64 'image/svg+xml': 'svg',
65 'image/svg+xml': 'svg',
65 'application/x-shockwave-flash': 'swf',
66 'application/x-shockwave-flash': 'swf',
66 'image/x-ms-bmp': 'bmp',
67 'image/x-ms-bmp': 'bmp',
67 'image/bmp': 'bmp',
68 'image/bmp': 'bmp',
68 }
69 }
69
70
70
71
71 def get_timezones():
72 def get_timezones():
72 timezones = []
73 timezones = []
73 for tz in pytz.common_timezones:
74 for tz in pytz.common_timezones:
74 timezones.append((tz, tz),)
75 timezones.append((tz, tz),)
75 return timezones
76 return timezones
76
77
77
78
78 class FormatPanel(forms.Textarea):
79 class FormatPanel(forms.Textarea):
79 """
80 """
80 Panel for text formatting. Consists of buttons to add different tags to the
81 Panel for text formatting. Consists of buttons to add different tags to the
81 form text area.
82 form text area.
82 """
83 """
83
84
84 def render(self, name, value, attrs=None):
85 def render(self, name, value, attrs=None):
85 output = '<div id="mark-panel">'
86 output = '<div id="mark-panel">'
86 for formatter in formatters:
87 for formatter in formatters:
87 output += '<span class="mark_btn"' + \
88 output += '<span class="mark_btn"' + \
88 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
89 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
89 '\', \'' + formatter.format_right + '\')">' + \
90 '\', \'' + formatter.format_right + '\')">' + \
90 formatter.preview_left + formatter.name + \
91 formatter.preview_left + formatter.name + \
91 formatter.preview_right + '</span>'
92 formatter.preview_right + '</span>'
92
93
93 output += '</div>'
94 output += '</div>'
94 output += super(FormatPanel, self).render(name, value, attrs=attrs)
95 output += super(FormatPanel, self).render(name, value, attrs=attrs)
95
96
96 return output
97 return output
97
98
98
99
99 class PlainErrorList(ErrorList):
100 class PlainErrorList(ErrorList):
100 def __unicode__(self):
101 def __unicode__(self):
101 return self.as_text()
102 return self.as_text()
102
103
103 def as_text(self):
104 def as_text(self):
104 return ''.join(['(!) %s ' % e for e in self])
105 return ''.join(['(!) %s ' % e for e in self])
105
106
106
107
107 class NeboardForm(forms.Form):
108 class NeboardForm(forms.Form):
108 """
109 """
109 Form with neboard-specific formatting.
110 Form with neboard-specific formatting.
110 """
111 """
111
112
112 def as_div(self):
113 def as_div(self):
113 """
114 """
114 Returns this form rendered as HTML <as_div>s.
115 Returns this form rendered as HTML <as_div>s.
115 """
116 """
116
117
117 return self._html_output(
118 return self._html_output(
118 # TODO Do not show hidden rows in the list here
119 # TODO Do not show hidden rows in the list here
119 normal_row='<div class="form-row">'
120 normal_row='<div class="form-row">'
120 '<div class="form-label">'
121 '<div class="form-label">'
121 '%(label)s'
122 '%(label)s'
122 '</div>'
123 '</div>'
123 '<div class="form-input">'
124 '<div class="form-input">'
124 '%(field)s'
125 '%(field)s'
125 '</div>'
126 '</div>'
126 '</div>'
127 '</div>'
127 '<div class="form-row">'
128 '<div class="form-row">'
128 '%(help_text)s'
129 '%(help_text)s'
129 '</div>',
130 '</div>',
130 error_row='<div class="form-row">'
131 error_row='<div class="form-row">'
131 '<div class="form-label"></div>'
132 '<div class="form-label"></div>'
132 '<div class="form-errors">%s</div>'
133 '<div class="form-errors">%s</div>'
133 '</div>',
134 '</div>',
134 row_ender='</div>',
135 row_ender='</div>',
135 help_text_html='%s',
136 help_text_html='%s',
136 errors_on_separate_row=True)
137 errors_on_separate_row=True)
137
138
138 def as_json_errors(self):
139 def as_json_errors(self):
139 errors = []
140 errors = []
140
141
141 for name, field in list(self.fields.items()):
142 for name, field in list(self.fields.items()):
142 if self[name].errors:
143 if self[name].errors:
143 errors.append({
144 errors.append({
144 'field': name,
145 'field': name,
145 'errors': self[name].errors.as_text(),
146 'errors': self[name].errors.as_text(),
146 })
147 })
147
148
148 return errors
149 return errors
149
150
150
151
151 class PostForm(NeboardForm):
152 class PostForm(NeboardForm):
152
153
153 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
154 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
154 label=LABEL_TITLE,
155 label=LABEL_TITLE,
155 widget=forms.TextInput(
156 widget=forms.TextInput(
156 attrs={ATTRIBUTE_PLACEHOLDER:
157 attrs={ATTRIBUTE_PLACEHOLDER:
157 'test#tripcode'}))
158 'test#tripcode'}))
158 text = forms.CharField(
159 text = forms.CharField(
159 widget=FormatPanel(attrs={
160 widget=FormatPanel(attrs={
160 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
161 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
161 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
162 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
162 }),
163 }),
163 required=False, label=LABEL_TEXT)
164 required=False, label=LABEL_TEXT)
164 file = forms.FileField(required=False, label=_('File'),
165 file = forms.FileField(required=False, label=_('File'),
165 widget=forms.ClearableFileInput(
166 widget=forms.ClearableFileInput(
166 attrs={'accept': 'file/*'}))
167 attrs={'accept': 'file/*'}))
167 file_url = forms.CharField(required=False, label=_('File URL'),
168 file_url = forms.CharField(required=False, label=_('File URL'),
168 widget=forms.TextInput(
169 widget=forms.TextInput(
169 attrs={ATTRIBUTE_PLACEHOLDER:
170 attrs={ATTRIBUTE_PLACEHOLDER:
170 'http://example.com/image.png'}))
171 'http://example.com/image.png'}))
171
172
172 # This field is for spam prevention only
173 # This field is for spam prevention only
173 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
174 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
174 widget=forms.TextInput(attrs={
175 widget=forms.TextInput(attrs={
175 'class': 'form-email'}))
176 'class': 'form-email'}))
176 threads = forms.CharField(required=False, label=_('Additional threads'),
177 threads = forms.CharField(required=False, label=_('Additional threads'),
177 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
178 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
178 '123 456 789'}))
179 '123 456 789'}))
179
180
180 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
181 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
181 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
182 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
182 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
183 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
183
184
184 session = None
185 session = None
185 need_to_ban = False
186 need_to_ban = False
187 image = None
186
188
187 def _update_file_extension(self, file):
189 def _update_file_extension(self, file):
188 if file:
190 if file:
189 mimetype = get_file_mimetype(file)
191 mimetype = get_file_mimetype(file)
190 extension = MIMETYPE_EXTENSIONS.get(mimetype)
192 extension = MIMETYPE_EXTENSIONS.get(mimetype)
191 if extension:
193 if extension:
192 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
194 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
193 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
195 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
194
196
195 file.name = new_filename
197 file.name = new_filename
196 else:
198 else:
197 logger = logging.getLogger('boards.forms.extension')
199 logger = logging.getLogger('boards.forms.extension')
198
200
199 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
201 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
200
202
201 def clean_title(self):
203 def clean_title(self):
202 title = self.cleaned_data['title']
204 title = self.cleaned_data['title']
203 if title:
205 if title:
204 if len(title) > TITLE_MAX_LENGTH:
206 if len(title) > TITLE_MAX_LENGTH:
205 raise forms.ValidationError(_('Title must have less than %s '
207 raise forms.ValidationError(_('Title must have less than %s '
206 'characters') %
208 'characters') %
207 str(TITLE_MAX_LENGTH))
209 str(TITLE_MAX_LENGTH))
208 return title
210 return title
209
211
210 def clean_text(self):
212 def clean_text(self):
211 text = self.cleaned_data['text'].strip()
213 text = self.cleaned_data['text'].strip()
212 if text:
214 if text:
213 max_length = board_settings.get_int('Forms', 'MaxTextLength')
215 max_length = board_settings.get_int('Forms', 'MaxTextLength')
214 if len(text) > max_length:
216 if len(text) > max_length:
215 raise forms.ValidationError(_('Text must have less than %s '
217 raise forms.ValidationError(_('Text must have less than %s '
216 'characters') % str(max_length))
218 'characters') % str(max_length))
217 return text
219 return text
218
220
219 def clean_file(self):
221 def clean_file(self):
220 file = self.cleaned_data['file']
222 file = self.cleaned_data['file']
221
223
222 if file:
224 if file:
223 validate_file_size(file.size)
225 validate_file_size(file.size)
224 self._update_file_extension(file)
226 self._update_file_extension(file)
225
227
226 return file
228 return file
227
229
228 def clean_file_url(self):
230 def clean_file_url(self):
229 url = self.cleaned_data['file_url']
231 url = self.cleaned_data['file_url']
230
232
231 file = None
233 file = None
234
232 if url:
235 if url:
233 file = self._get_file_from_url(url)
236 file = get_image_by_alias(url, self.session)
237 self.image = file
234
238
235 if not file:
239 if file is not None:
236 raise forms.ValidationError(_('Invalid URL'))
240 return
237 else:
241
238 validate_file_size(file.size)
242 if file is None:
243 file = self._get_file_from_url(url)
244 if not file:
245 raise forms.ValidationError(_('Invalid URL'))
246 else:
247 validate_file_size(file.size)
239 self._update_file_extension(file)
248 self._update_file_extension(file)
240
249
241 return file
250 return file
242
251
243 def clean_threads(self):
252 def clean_threads(self):
244 threads_str = self.cleaned_data['threads']
253 threads_str = self.cleaned_data['threads']
245
254
246 if len(threads_str) > 0:
255 if len(threads_str) > 0:
247 threads_id_list = threads_str.split(' ')
256 threads_id_list = threads_str.split(' ')
248
257
249 threads = list()
258 threads = list()
250
259
251 for thread_id in threads_id_list:
260 for thread_id in threads_id_list:
252 try:
261 try:
253 thread = Post.objects.get(id=int(thread_id))
262 thread = Post.objects.get(id=int(thread_id))
254 if not thread.is_opening() or thread.get_thread().is_archived():
263 if not thread.is_opening() or thread.get_thread().is_archived():
255 raise ObjectDoesNotExist()
264 raise ObjectDoesNotExist()
256 threads.append(thread)
265 threads.append(thread)
257 except (ObjectDoesNotExist, ValueError):
266 except (ObjectDoesNotExist, ValueError):
258 raise forms.ValidationError(_('Invalid additional thread list'))
267 raise forms.ValidationError(_('Invalid additional thread list'))
259
268
260 return threads
269 return threads
261
270
262 def clean(self):
271 def clean(self):
263 cleaned_data = super(PostForm, self).clean()
272 cleaned_data = super(PostForm, self).clean()
264
273
265 if cleaned_data['email']:
274 if cleaned_data['email']:
266 self.need_to_ban = True
275 self.need_to_ban = True
267 raise forms.ValidationError('A human cannot enter a hidden field')
276 raise forms.ValidationError('A human cannot enter a hidden field')
268
277
269 if not self.errors:
278 if not self.errors:
270 self._clean_text_file()
279 self._clean_text_file()
271
280
272 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
281 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
273
282
274 settings_manager = get_settings_manager(self)
283 settings_manager = get_settings_manager(self)
275 if not self.errors and limit_speed and not settings_manager.get_setting('confirmed_user'):
284 if not self.errors and limit_speed and not settings_manager.get_setting('confirmed_user'):
276 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
285 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
277 if pow_difficulty > 0:
286 if pow_difficulty > 0:
278 # Limit only first post
287 # Limit only first post
279 if cleaned_data['timestamp'] \
288 if cleaned_data['timestamp'] \
280 and cleaned_data['iteration'] and cleaned_data['guess'] \
289 and cleaned_data['iteration'] and cleaned_data['guess'] \
281 and not settings_manager.get_setting('confirmed_user'):
290 and not settings_manager.get_setting('confirmed_user'):
282 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
291 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
283 else:
292 else:
284 # Limit every post
293 # Limit every post
285 self._validate_posting_speed()
294 self._validate_posting_speed()
286 settings_manager.set_setting('confirmed_user', True)
295 settings_manager.set_setting('confirmed_user', True)
287
296
288
297
289 return cleaned_data
298 return cleaned_data
290
299
291 def get_file(self):
300 def get_file(self):
292 """
301 """
293 Gets file from form or URL.
302 Gets file from form or URL.
294 """
303 """
295
304
296 file = self.cleaned_data['file']
305 file = self.cleaned_data['file']
297 return file or self.cleaned_data['file_url']
306 return file or self.cleaned_data['file_url']
298
307
299 def get_tripcode(self):
308 def get_tripcode(self):
300 title = self.cleaned_data['title']
309 title = self.cleaned_data['title']
301 if title is not None and TRIPCODE_DELIM in title:
310 if title is not None and TRIPCODE_DELIM in title:
302 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
311 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
303 tripcode = hashlib.md5(code.encode()).hexdigest()
312 tripcode = hashlib.md5(code.encode()).hexdigest()
304 else:
313 else:
305 tripcode = ''
314 tripcode = ''
306 return tripcode
315 return tripcode
307
316
308 def get_title(self):
317 def get_title(self):
309 title = self.cleaned_data['title']
318 title = self.cleaned_data['title']
310 if title is not None and TRIPCODE_DELIM in title:
319 if title is not None and TRIPCODE_DELIM in title:
311 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
320 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
312 else:
321 else:
313 return title
322 return title
314
323
324 def get_images(self):
325 if self.image:
326 return [self.image]
327 else:
328 return []
329
315 def _clean_text_file(self):
330 def _clean_text_file(self):
316 text = self.cleaned_data.get('text')
331 text = self.cleaned_data.get('text')
317 file = self.get_file()
332 file = self.get_file()
333 images = self.get_images()
318
334
319 if (not text) and (not file):
335 if (not text) and (not file) and len(images) == 0:
320 error_message = _('Either text or file must be entered.')
336 error_message = _('Either text or file must be entered.')
321 self._errors['text'] = self.error_class([error_message])
337 self._errors['text'] = self.error_class([error_message])
322
338
323 def _validate_posting_speed(self):
339 def _validate_posting_speed(self):
324 can_post = True
340 can_post = True
325
341
326 posting_delay = settings.POSTING_DELAY
342 posting_delay = settings.POSTING_DELAY
327
343
328 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
344 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
329 now = time.time()
345 now = time.time()
330
346
331 current_delay = 0
347 current_delay = 0
332
348
333 if LAST_POST_TIME not in self.session:
349 if LAST_POST_TIME not in self.session:
334 self.session[LAST_POST_TIME] = now
350 self.session[LAST_POST_TIME] = now
335
351
336 need_delay = True
352 need_delay = True
337 else:
353 else:
338 last_post_time = self.session.get(LAST_POST_TIME)
354 last_post_time = self.session.get(LAST_POST_TIME)
339 current_delay = int(now - last_post_time)
355 current_delay = int(now - last_post_time)
340
356
341 need_delay = current_delay < posting_delay
357 need_delay = current_delay < posting_delay
342
358
343 if need_delay:
359 if need_delay:
344 delay = posting_delay - current_delay
360 delay = posting_delay - current_delay
345 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
361 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
346 delay) % {'delay': delay}
362 delay) % {'delay': delay}
347 self._errors['text'] = self.error_class([error_message])
363 self._errors['text'] = self.error_class([error_message])
348
364
349 can_post = False
365 can_post = False
350
366
351 if can_post:
367 if can_post:
352 self.session[LAST_POST_TIME] = now
368 self.session[LAST_POST_TIME] = now
353
369
354 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
370 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
355 """
371 """
356 Gets an file file from URL.
372 Gets an file file from URL.
357 """
373 """
358
374
359 img_temp = None
375 img_temp = None
360
376
361 try:
377 try:
362 for downloader in Downloader.__subclasses__():
378 for downloader in Downloader.__subclasses__():
363 if downloader.handles(url):
379 if downloader.handles(url):
364 return downloader.download(url)
380 return downloader.download(url)
365 # If nobody of the specific downloaders handles this, use generic
381 # If nobody of the specific downloaders handles this, use generic
366 # one
382 # one
367 return Downloader.download(url)
383 return Downloader.download(url)
368 except forms.ValidationError as e:
384 except forms.ValidationError as e:
369 raise e
385 raise e
370 except Exception as e:
386 except Exception as e:
371 raise forms.ValidationError(e)
387 raise forms.ValidationError(e)
372
388
373 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
389 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
374 post_time = timezone.datetime.fromtimestamp(
390 post_time = timezone.datetime.fromtimestamp(
375 int(timestamp[:-3]), tz=timezone.get_current_timezone())
391 int(timestamp[:-3]), tz=timezone.get_current_timezone())
376
392
377 payload = timestamp + message.replace('\r\n', '\n')
393 payload = timestamp + message.replace('\r\n', '\n')
378 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
394 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
379 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
395 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
380 if len(target) < POW_HASH_LENGTH:
396 if len(target) < POW_HASH_LENGTH:
381 target = '0' * (POW_HASH_LENGTH - len(target)) + target
397 target = '0' * (POW_HASH_LENGTH - len(target)) + target
382
398
383 computed_guess = hashlib.sha256((payload + iteration).encode())\
399 computed_guess = hashlib.sha256((payload + iteration).encode())\
384 .hexdigest()[0:POW_HASH_LENGTH]
400 .hexdigest()[0:POW_HASH_LENGTH]
385 if guess != computed_guess or guess > target:
401 if guess != computed_guess or guess > target:
386 self._errors['text'] = self.error_class(
402 self._errors['text'] = self.error_class(
387 [_('Invalid PoW.')])
403 [_('Invalid PoW.')])
388
404
389
405
390
406
391 class ThreadForm(PostForm):
407 class ThreadForm(PostForm):
392
408
393 tags = forms.CharField(
409 tags = forms.CharField(
394 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
410 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
395 max_length=100, label=_('Tags'), required=True)
411 max_length=100, label=_('Tags'), required=True)
396 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
412 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
397
413
398 def clean_tags(self):
414 def clean_tags(self):
399 tags = self.cleaned_data['tags'].strip()
415 tags = self.cleaned_data['tags'].strip()
400
416
401 if not tags or not REGEX_TAGS.match(tags):
417 if not tags or not REGEX_TAGS.match(tags):
402 raise forms.ValidationError(
418 raise forms.ValidationError(
403 _('Inappropriate characters in tags.'))
419 _('Inappropriate characters in tags.'))
404
420
405 required_tag_exists = False
421 required_tag_exists = False
406 tag_set = set()
422 tag_set = set()
407 for tag_string in tags.split():
423 for tag_string in tags.split():
408 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
424 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
409 tag_set.add(tag)
425 tag_set.add(tag)
410
426
411 # If this is a new tag, don't check for its parents because nobody
427 # If this is a new tag, don't check for its parents because nobody
412 # added them yet
428 # added them yet
413 if not created:
429 if not created:
414 tag_set |= set(tag.get_all_parents())
430 tag_set |= set(tag.get_all_parents())
415
431
416 for tag in tag_set:
432 for tag in tag_set:
417 if tag.required:
433 if tag.required:
418 required_tag_exists = True
434 required_tag_exists = True
419 break
435 break
420
436
421 if not required_tag_exists:
437 if not required_tag_exists:
422 raise forms.ValidationError(
438 raise forms.ValidationError(
423 _('Need at least one section.'))
439 _('Need at least one section.'))
424
440
425 return tag_set
441 return tag_set
426
442
427 def clean(self):
443 def clean(self):
428 cleaned_data = super(ThreadForm, self).clean()
444 cleaned_data = super(ThreadForm, self).clean()
429
445
430 return cleaned_data
446 return cleaned_data
431
447
432 def is_monochrome(self):
448 def is_monochrome(self):
433 return self.cleaned_data['monochrome']
449 return self.cleaned_data['monochrome']
434
450
435
451
436 class SettingsForm(NeboardForm):
452 class SettingsForm(NeboardForm):
437
453
438 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
454 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
439 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
455 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
440 username = forms.CharField(label=_('User name'), required=False)
456 username = forms.CharField(label=_('User name'), required=False)
441 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
457 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
442
458
443 def clean_username(self):
459 def clean_username(self):
444 username = self.cleaned_data['username']
460 username = self.cleaned_data['username']
445
461
446 if username and not REGEX_USERNAMES.match(username):
462 if username and not REGEX_USERNAMES.match(username):
447 raise forms.ValidationError(_('Inappropriate characters.'))
463 raise forms.ValidationError(_('Inappropriate characters.'))
448
464
449 return username
465 return username
450
466
451
467
452 class SearchForm(NeboardForm):
468 class SearchForm(NeboardForm):
453 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
469 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,69 +1,70 b''
1 import os
1 import os
2 import re
2 import re
3
3
4 from django.core.files.uploadedfile import SimpleUploadedFile, \
4 from django.core.files.uploadedfile import SimpleUploadedFile, \
5 TemporaryUploadedFile
5 TemporaryUploadedFile
6 from pytube import YouTube
6 from pytube import YouTube
7 import requests
7 import requests
8
8
9 from boards.utils import validate_file_size
9 from boards.utils import validate_file_size
10
10
11 YOUTUBE_VIDEO_FORMAT = 'webm'
11 YOUTUBE_VIDEO_FORMAT = 'webm'
12
12
13 HTTP_RESULT_OK = 200
13 HTTP_RESULT_OK = 200
14
14
15 HEADER_CONTENT_LENGTH = 'content-length'
15 HEADER_CONTENT_LENGTH = 'content-length'
16 HEADER_CONTENT_TYPE = 'content-type'
16 HEADER_CONTENT_TYPE = 'content-type'
17
17
18 FILE_DOWNLOAD_CHUNK_BYTES = 200000
18 FILE_DOWNLOAD_CHUNK_BYTES = 200000
19
19
20 YOUTUBE_URL = re.compile(r'https?://((www\.)?youtube\.com/watch\?v=|youtu.be/)\w+')
20 YOUTUBE_URL = re.compile(r'https?://((www\.)?youtube\.com/watch\?v=|youtu.be/)\w+')
21
21
22
22
23 class Downloader:
23 class Downloader:
24 @staticmethod
24 @staticmethod
25 def handles(url: str) -> bool:
25 def handles(url: str) -> bool:
26 return False
26 return False
27
27
28 @staticmethod
28 @staticmethod
29 def download(url: str):
29 def download(url: str):
30 # Verify content headers
30 # Verify content headers
31 response_head = requests.head(url, verify=False)
31 response_head = requests.head(url, verify=False)
32 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
32 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
33 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
33 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
34 if length_header:
34 if length_header:
35 length = int(length_header)
35 length = int(length_header)
36 validate_file_size(length)
36 validate_file_size(length)
37 # Get the actual content into memory
37 # Get the actual content into memory
38 response = requests.get(url, verify=False, stream=True)
38 response = requests.get(url, verify=False, stream=True)
39
39
40 # Download file, stop if the size exceeds limit
40 # Download file, stop if the size exceeds limit
41 size = 0
41 size = 0
42
42
43 # Set a dummy file name that will be replaced
43 # Set a dummy file name that will be replaced
44 # anyway, just keep the valid extension
44 # anyway, just keep the valid extension
45 filename = 'file.' + content_type.split('/')[1]
45 filename = 'file.' + content_type.split('/')[1]
46
46
47 file = TemporaryUploadedFile(filename, content_type, 0, None, None)
47 file = TemporaryUploadedFile(filename, content_type, 0, None, None)
48 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
48 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
49 size += len(chunk)
49 size += len(chunk)
50 validate_file_size(size)
50 validate_file_size(size)
51 file.write(chunk)
51 file.write(chunk)
52
52
53 if response.status_code == HTTP_RESULT_OK:
53 if response.status_code == HTTP_RESULT_OK:
54 return file
54 return file
55
55
56
56
57 class YouTubeDownloader(Downloader):
57 class YouTubeDownloader(Downloader):
58 @staticmethod
58 @staticmethod
59 def download(url: str):
59 def download(url: str):
60 yt = YouTube()
60 yt = YouTube()
61 yt.from_url(url)
61 yt.from_url(url)
62 videos = yt.filter(YOUTUBE_VIDEO_FORMAT)
62 videos = yt.filter(YOUTUBE_VIDEO_FORMAT)
63 if len(videos) > 0:
63 if len(videos) > 0:
64 video = videos[0]
64 video = videos[0]
65 return Downloader.download(video.url)
65 return Downloader.download(video.url)
66
66
67 @staticmethod
67 @staticmethod
68 def handles(url: str) -> bool:
68 def handles(url: str) -> bool:
69 return YOUTUBE_URL.match(url)
69 return YOUTUBE_URL.match(url)
70
@@ -1,97 +1,98 b''
1 from django.db import models
1 from django.db import models
2 from django.template.defaultfilters import filesizeformat
2 from django.template.defaultfilters import filesizeformat
3
3
4 from boards import thumbs, utils
4 from boards import thumbs, utils
5 import boards
5 import boards
6 from boards.models.base import Viewable
6 from boards.models.base import Viewable
7 from boards.models import STATUS_ARCHIVE
7 from boards.models import STATUS_ARCHIVE
8 from boards.utils import get_upload_filename
8 from boards.utils import get_upload_filename
9
9
10
10
11 __author__ = 'neko259'
11 __author__ = 'neko259'
12
12
13
13
14 IMAGE_THUMB_SIZE = (200, 150)
14 IMAGE_THUMB_SIZE = (200, 150)
15 HASH_LENGTH = 36
15 HASH_LENGTH = 36
16
16
17 CSS_CLASS_IMAGE = 'image'
17 CSS_CLASS_IMAGE = 'image'
18 CSS_CLASS_THUMB = 'thumb'
18 CSS_CLASS_THUMB = 'thumb'
19
19
20
20
21 class PostImageManager(models.Manager):
21 class PostImageManager(models.Manager):
22 def create_with_hash(self, image):
22 def create_with_hash(self, image):
23 image_hash = utils.get_file_hash(image)
23 image_hash = utils.get_file_hash(image)
24 existing = self.filter(hash=image_hash)
24 existing = self.filter(hash=image_hash)
25 if len(existing) > 0:
25 if len(existing) > 0:
26 post_image = existing[0]
26 post_image = existing[0]
27 else:
27 else:
28 post_image = PostImage.objects.create(image=image)
28 post_image = PostImage.objects.create(image=image)
29
29
30 return post_image
30 return post_image
31
31
32 def get_random_images(self, count, tags=None):
32 def get_random_images(self, count, tags=None):
33 images = self.exclude(post_images__thread__status=STATUS_ARCHIVE)
33 images = self.exclude(post_images__thread__status=STATUS_ARCHIVE)
34 if tags is not None:
34 if tags is not None:
35 images = images.filter(post_images__threads__tags__in=tags)
35 images = images.filter(post_images__threads__tags__in=tags)
36 return images.order_by('?')[:count]
36 return images.order_by('?')[:count]
37
37
38
38
39 class PostImage(models.Model, Viewable):
39 class PostImage(models.Model, Viewable):
40 objects = PostImageManager()
40 objects = PostImageManager()
41
41
42 class Meta:
42 class Meta:
43 app_label = 'boards'
43 app_label = 'boards'
44 ordering = ('id',)
44 ordering = ('id',)
45
45
46 width = models.IntegerField(default=0)
46 width = models.IntegerField(default=0)
47 height = models.IntegerField(default=0)
47 height = models.IntegerField(default=0)
48
48
49 pre_width = models.IntegerField(default=0)
49 pre_width = models.IntegerField(default=0)
50 pre_height = models.IntegerField(default=0)
50 pre_height = models.IntegerField(default=0)
51
51
52 image = thumbs.ImageWithThumbsField(upload_to=get_upload_filename,
52 image = thumbs.ImageWithThumbsField(upload_to=get_upload_filename,
53 blank=True, sizes=(IMAGE_THUMB_SIZE,),
53 blank=True, sizes=(IMAGE_THUMB_SIZE,),
54 width_field='width',
54 width_field='width',
55 height_field='height',
55 height_field='height',
56 preview_width_field='pre_width',
56 preview_width_field='pre_width',
57 preview_height_field='pre_height')
57 preview_height_field='pre_height')
58 hash = models.CharField(max_length=HASH_LENGTH)
58 hash = models.CharField(max_length=HASH_LENGTH)
59 alias = models.TextField(unique=True, null=True, blank=True)
59
60
60 def save(self, *args, **kwargs):
61 def save(self, *args, **kwargs):
61 """
62 """
62 Saves the model and computes the image hash for deduplication purposes.
63 Saves the model and computes the image hash for deduplication purposes.
63 """
64 """
64
65
65 if not self.pk and self.image:
66 if not self.pk and self.image:
66 self.hash = utils.get_file_hash(self.image)
67 self.hash = utils.get_file_hash(self.image)
67 super(PostImage, self).save(*args, **kwargs)
68 super(PostImage, self).save(*args, **kwargs)
68
69
69 def __str__(self):
70 def __str__(self):
70 return self.image.url
71 return self.image.url
71
72
72 def get_view(self):
73 def get_view(self):
73 metadata = '{}, {}'.format(self.image.name.split('.')[-1],
74 metadata = '{}, {}'.format(self.image.name.split('.')[-1],
74 filesizeformat(self.image.size))
75 filesizeformat(self.image.size))
75 return '<div class="{}">' \
76 return '<div class="{}">' \
76 '<a class="{}" href="{full}">' \
77 '<a class="{}" href="{full}">' \
77 '<img class="post-image-preview"' \
78 '<img class="post-image-preview"' \
78 ' src="{}"' \
79 ' src="{}"' \
79 ' alt="{}"' \
80 ' alt="{}"' \
80 ' width="{}"' \
81 ' width="{}"' \
81 ' height="{}"' \
82 ' height="{}"' \
82 ' data-width="{}"' \
83 ' data-width="{}"' \
83 ' data-height="{}" />' \
84 ' data-height="{}" />' \
84 '</a>' \
85 '</a>' \
85 '<div class="image-metadata">'\
86 '<div class="image-metadata">'\
86 '<a href="{full}" download>{image_meta}</a>'\
87 '<a href="{full}" download>{image_meta}</a>'\
87 '</div>' \
88 '</div>' \
88 '</div>'\
89 '</div>'\
89 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB,
90 .format(CSS_CLASS_IMAGE, CSS_CLASS_THUMB,
90 self.image.url_200x150,
91 self.image.url_200x150,
91 str(self.hash), str(self.pre_width),
92 str(self.hash), str(self.pre_width),
92 str(self.pre_height), str(self.width), str(self.height),
93 str(self.pre_height), str(self.width), str(self.height),
93 full=self.image.url, image_meta=metadata)
94 full=self.image.url, image_meta=metadata)
94
95
95 def get_random_associated_post(self):
96 def get_random_associated_post(self):
96 posts = boards.models.Post.objects.filter(images__in=[self])
97 posts = boards.models.Post.objects.filter(images__in=[self])
97 return posts.order_by('?').first()
98 return posts.order_by('?').first()
@@ -1,131 +1,133 b''
1 import logging
1 import logging
2
2
3 from datetime import datetime, timedelta, date
3 from datetime import datetime, timedelta, date
4 from datetime import time as dtime
4 from datetime import time as dtime
5
5
6 from django.db import models, transaction
6 from django.db import models, transaction
7 from django.utils import timezone
7 from django.utils import timezone
8
8
9 import boards
9 import boards
10
10
11 from boards.models.user import Ban
11 from boards.models.user import Ban
12 from boards.mdx_neboard import Parser
12 from boards.mdx_neboard import Parser
13 from boards.models import PostImage, Attachment
13 from boards.models import PostImage, Attachment
14 from boards import utils
14 from boards import utils
15
15
16 __author__ = 'neko259'
16 __author__ = 'neko259'
17
17
18 IMAGE_TYPES = (
18 IMAGE_TYPES = (
19 'jpeg',
19 'jpeg',
20 'jpg',
20 'jpg',
21 'png',
21 'png',
22 'bmp',
22 'bmp',
23 'gif',
23 'gif',
24 )
24 )
25
25
26 POSTS_PER_DAY_RANGE = 7
26 POSTS_PER_DAY_RANGE = 7
27 NO_IP = '0.0.0.0'
27 NO_IP = '0.0.0.0'
28
28
29
29
30 class PostManager(models.Manager):
30 class PostManager(models.Manager):
31 @transaction.atomic
31 @transaction.atomic
32 def create_post(self, title: str, text: str, file=None, thread=None,
32 def create_post(self, title: str, text: str, file=None, thread=None,
33 ip=NO_IP, tags: list=None, opening_posts: list=None,
33 ip=NO_IP, tags: list=None, opening_posts: list=None,
34 tripcode='', monochrome=False):
34 tripcode='', monochrome=False, images=[]):
35 """
35 """
36 Creates new post
36 Creates new post
37 """
37 """
38
38
39 if thread is not None and thread.is_archived():
39 if thread is not None and thread.is_archived():
40 raise Exception('Cannot post into an archived thread')
40 raise Exception('Cannot post into an archived thread')
41
41
42 if not utils.is_anonymous_mode():
42 if not utils.is_anonymous_mode():
43 is_banned = Ban.objects.filter(ip=ip).exists()
43 is_banned = Ban.objects.filter(ip=ip).exists()
44 else:
44 else:
45 is_banned = False
45 is_banned = False
46
46
47 # TODO Raise specific exception and catch it in the views
47 # TODO Raise specific exception and catch it in the views
48 if is_banned:
48 if is_banned:
49 raise Exception("This user is banned")
49 raise Exception("This user is banned")
50
50
51 if not tags:
51 if not tags:
52 tags = []
52 tags = []
53 if not opening_posts:
53 if not opening_posts:
54 opening_posts = []
54 opening_posts = []
55
55
56 posting_time = timezone.now()
56 posting_time = timezone.now()
57 new_thread = False
57 new_thread = False
58 if not thread:
58 if not thread:
59 thread = boards.models.thread.Thread.objects.create(
59 thread = boards.models.thread.Thread.objects.create(
60 bump_time=posting_time, last_edit_time=posting_time,
60 bump_time=posting_time, last_edit_time=posting_time,
61 monochrome=monochrome)
61 monochrome=monochrome)
62 list(map(thread.tags.add, tags))
62 list(map(thread.tags.add, tags))
63 boards.models.thread.Thread.objects.process_oldest_threads()
63 boards.models.thread.Thread.objects.process_oldest_threads()
64 new_thread = True
64 new_thread = True
65
65
66 pre_text = Parser().preparse(text)
66 pre_text = Parser().preparse(text)
67
67
68 post = self.create(title=title,
68 post = self.create(title=title,
69 text=pre_text,
69 text=pre_text,
70 pub_time=posting_time,
70 pub_time=posting_time,
71 poster_ip=ip,
71 poster_ip=ip,
72 thread=thread,
72 thread=thread,
73 last_edit_time=posting_time,
73 last_edit_time=posting_time,
74 tripcode=tripcode,
74 tripcode=tripcode,
75 opening=new_thread)
75 opening=new_thread)
76 post.threads.add(thread)
76 post.threads.add(thread)
77
77
78 logger = logging.getLogger('boards.post.create')
78 logger = logging.getLogger('boards.post.create')
79
79
80 logger.info('Created post [{}] with text [{}] by {}'.format(post,
80 logger.info('Created post [{}] with text [{}] by {}'.format(post,
81 post.get_text(),post.poster_ip))
81 post.get_text(),post.poster_ip))
82
82
83 # TODO Move this to other place
83 # TODO Move this to other place
84 if file:
84 if file:
85 file_type = file.name.split('.')[-1].lower()
85 file_type = file.name.split('.')[-1].lower()
86 if file_type in IMAGE_TYPES:
86 if file_type in IMAGE_TYPES:
87 post.images.add(PostImage.objects.create_with_hash(file))
87 post.images.add(PostImage.objects.create_with_hash(file))
88 else:
88 else:
89 post.attachments.add(Attachment.objects.create_with_hash(file))
89 post.attachments.add(Attachment.objects.create_with_hash(file))
90 for image in images:
91 post.images.add(image)
90
92
91 post.connect_threads(opening_posts)
93 post.connect_threads(opening_posts)
92
94
93 # Thread needs to be bumped only when the post is already created
95 # Thread needs to be bumped only when the post is already created
94 if not new_thread:
96 if not new_thread:
95 thread.last_edit_time = posting_time
97 thread.last_edit_time = posting_time
96 thread.bump()
98 thread.bump()
97 thread.save()
99 thread.save()
98
100
99 return post
101 return post
100
102
101 def delete_posts_by_ip(self, ip):
103 def delete_posts_by_ip(self, ip):
102 """
104 """
103 Deletes all posts of the author with same IP
105 Deletes all posts of the author with same IP
104 """
106 """
105
107
106 posts = self.filter(poster_ip=ip)
108 posts = self.filter(poster_ip=ip)
107 for post in posts:
109 for post in posts:
108 post.delete()
110 post.delete()
109
111
110 @utils.cached_result()
112 @utils.cached_result()
111 def get_posts_per_day(self) -> float:
113 def get_posts_per_day(self) -> float:
112 """
114 """
113 Gets average count of posts per day for the last 7 days
115 Gets average count of posts per day for the last 7 days
114 """
116 """
115
117
116 day_end = date.today()
118 day_end = date.today()
117 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
119 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
118
120
119 day_time_start = timezone.make_aware(datetime.combine(
121 day_time_start = timezone.make_aware(datetime.combine(
120 day_start, dtime()), timezone.get_current_timezone())
122 day_start, dtime()), timezone.get_current_timezone())
121 day_time_end = timezone.make_aware(datetime.combine(
123 day_time_end = timezone.make_aware(datetime.combine(
122 day_end, dtime()), timezone.get_current_timezone())
124 day_end, dtime()), timezone.get_current_timezone())
123
125
124 posts_per_period = float(self.filter(
126 posts_per_period = float(self.filter(
125 pub_time__lte=day_time_end,
127 pub_time__lte=day_time_end,
126 pub_time__gte=day_time_start).count())
128 pub_time__gte=day_time_start).count())
127
129
128 ppd = posts_per_period / POSTS_PER_DAY_RANGE
130 ppd = posts_per_period / POSTS_PER_DAY_RANGE
129
131
130 return ppd
132 return ppd
131
133
@@ -1,40 +1,44 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load tz %}
4 {% load tz %}
5
5
6 {% block head %}
6 {% block head %}
7 <meta name="robots" content="noindex">
7 <meta name="robots" content="noindex">
8 <title>{% trans 'Settings' %} - {{ site_name }}</title>
8 <title>{% trans 'Settings' %} - {{ site_name }}</title>
9 {% endblock %}
9 {% endblock %}
10
10
11 {% block content %}
11 {% block content %}
12 <div class="post">
12 <div class="post">
13 <p>
13 <p>
14 {% if moderator %}
14 {% if moderator %}
15 {% trans 'You are moderator.' %}
15 {% trans 'You are moderator.' %}
16 {% endif %}
16 {% endif %}
17 </p>
17 </p>
18 {% if hidden_tags %}
18 {% if hidden_tags %}
19 <p>{% trans 'Hidden tags:' %}
19 <p>{% trans 'Hidden tags:' %}
20 {% for tag in hidden_tags %}
20 {% for tag in hidden_tags %}
21 {{ tag.get_view|safe }}
21 {{ tag.get_view|safe }}
22 {% endfor %}
22 {% endfor %}
23 </p>
23 </p>
24 {% else %}
24 {% else %}
25 <p>{% trans 'No hidden tags.' %}</p>
25 <p>{% trans 'No hidden tags.' %}</p>
26 {% endif %}
26 {% endif %}
27
28 {% for image in image_aliases %}
29 {{ image.alias }}: <img src="{{ image.image.url_200x150 }}" /> <br />
30 {% endfor %}
27 </div>
31 </div>
28
32
29 <div class="post-form-w">
33 <div class="post-form-w">
30 <div class="post-form">
34 <div class="post-form">
31 <form method="post">{% csrf_token %}
35 <form method="post">{% csrf_token %}
32 {{ form.as_div }}
36 {{ form.as_div }}
33 <div class="form-submit">
37 <div class="form-submit">
34 <input type="submit" value="{% trans "Save" %}" />
38 <input type="submit" value="{% trans "Save" %}" />
35 </div>
39 </div>
36 </form>
40 </form>
37 </div>
41 </div>
38 </div>
42 </div>
39
43
40 {% endblock %}
44 {% endblock %}
@@ -1,168 +1,169 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.core.files import File
2 from django.core.files import File
3 from django.core.files.temp import NamedTemporaryFile
3 from django.core.files.temp import NamedTemporaryFile
4 from django.core.paginator import EmptyPage
4 from django.core.paginator import EmptyPage
5 from django.db import transaction
5 from django.db import transaction
6 from django.http import Http404
6 from django.http import Http404
7 from django.shortcuts import render, redirect
7 from django.shortcuts import render, redirect
8 import requests
8 import requests
9 from django.utils.decorators import method_decorator
9 from django.utils.decorators import method_decorator
10 from django.views.decorators.csrf import csrf_protect
10 from django.views.decorators.csrf import csrf_protect
11
11
12 from boards import utils, settings
12 from boards import utils, settings
13 from boards.abstracts.paginator import get_paginator
13 from boards.abstracts.paginator import get_paginator
14 from boards.abstracts.settingsmanager import get_settings_manager
14 from boards.abstracts.settingsmanager import get_settings_manager
15 from boards.forms import ThreadForm, PlainErrorList
15 from boards.forms import ThreadForm, PlainErrorList
16 from boards.models import Post, Thread, Ban, Tag, PostImage, Banner
16 from boards.models import Post, Thread, Ban, Tag, PostImage, Banner
17 from boards.views.banned import BannedView
17 from boards.views.banned import BannedView
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
19 from boards.views.posting_mixin import PostMixin
19 from boards.views.posting_mixin import PostMixin
20 from boards.views.mixins import FileUploadMixin, PaginatedMixin
20 from boards.views.mixins import FileUploadMixin, PaginatedMixin
21
21
22 FORM_TAGS = 'tags'
22 FORM_TAGS = 'tags'
23 FORM_TEXT = 'text'
23 FORM_TEXT = 'text'
24 FORM_TITLE = 'title'
24 FORM_TITLE = 'title'
25 FORM_IMAGE = 'image'
25 FORM_IMAGE = 'image'
26 FORM_THREADS = 'threads'
26 FORM_THREADS = 'threads'
27
27
28 TAG_DELIMITER = ' '
28 TAG_DELIMITER = ' '
29
29
30 PARAMETER_CURRENT_PAGE = 'current_page'
30 PARAMETER_CURRENT_PAGE = 'current_page'
31 PARAMETER_PAGINATOR = 'paginator'
31 PARAMETER_PAGINATOR = 'paginator'
32 PARAMETER_THREADS = 'threads'
32 PARAMETER_THREADS = 'threads'
33 PARAMETER_BANNERS = 'banners'
33 PARAMETER_BANNERS = 'banners'
34 PARAMETER_ADDITIONAL = 'additional_params'
34 PARAMETER_ADDITIONAL = 'additional_params'
35 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
35 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
36 PARAMETER_RSS_URL = 'rss_url'
36 PARAMETER_RSS_URL = 'rss_url'
37
37
38 TEMPLATE = 'boards/all_threads.html'
38 TEMPLATE = 'boards/all_threads.html'
39 DEFAULT_PAGE = 1
39 DEFAULT_PAGE = 1
40
40
41
41
42 class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin):
42 class AllThreadsView(PostMixin, FileUploadMixin, BaseBoardView, PaginatedMixin):
43
43
44 def __init__(self):
44 def __init__(self):
45 self.settings_manager = None
45 self.settings_manager = None
46 super(AllThreadsView, self).__init__()
46 super(AllThreadsView, self).__init__()
47
47
48 @method_decorator(csrf_protect)
48 @method_decorator(csrf_protect)
49 def get(self, request, form: ThreadForm=None):
49 def get(self, request, form: ThreadForm=None):
50 page = request.GET.get('page', DEFAULT_PAGE)
50 page = request.GET.get('page', DEFAULT_PAGE)
51
51
52 params = self.get_context_data(request=request)
52 params = self.get_context_data(request=request)
53
53
54 if not form:
54 if not form:
55 form = ThreadForm(error_class=PlainErrorList)
55 form = ThreadForm(error_class=PlainErrorList)
56
56
57 self.settings_manager = get_settings_manager(request)
57 self.settings_manager = get_settings_manager(request)
58
58
59 threads = self.get_threads()
59 threads = self.get_threads()
60
60
61 order = request.GET.get('order', 'bump')
61 order = request.GET.get('order', 'bump')
62 if order == 'bump':
62 if order == 'bump':
63 threads = threads.order_by('-bump_time')
63 threads = threads.order_by('-bump_time')
64 else:
64 else:
65 threads = threads.filter(multi_replies__opening=True).order_by('-multi_replies__pub_time')
65 threads = threads.filter(multi_replies__opening=True).order_by('-multi_replies__pub_time')
66 filter = request.GET.get('filter')
66 filter = request.GET.get('filter')
67 if filter == 'fav_tags':
67 if filter == 'fav_tags':
68 fav_tags = self.settings_manager.get_fav_tags()
68 fav_tags = self.settings_manager.get_fav_tags()
69 if len(fav_tags) > 0:
69 if len(fav_tags) > 0:
70 threads = threads.filter(tags__in=fav_tags)
70 threads = threads.filter(tags__in=fav_tags)
71 threads = threads.distinct()
71 threads = threads.distinct()
72
72
73 paginator = get_paginator(threads,
73 paginator = get_paginator(threads,
74 settings.get_int('View', 'ThreadsPerPage'))
74 settings.get_int('View', 'ThreadsPerPage'))
75 paginator.current_page = int(page)
75 paginator.current_page = int(page)
76
76
77 try:
77 try:
78 threads = paginator.page(page).object_list
78 threads = paginator.page(page).object_list
79 except EmptyPage:
79 except EmptyPage:
80 raise Http404()
80 raise Http404()
81
81
82 params[PARAMETER_THREADS] = threads
82 params[PARAMETER_THREADS] = threads
83 params[CONTEXT_FORM] = form
83 params[CONTEXT_FORM] = form
84 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
84 params[PARAMETER_BANNERS] = Banner.objects.order_by('-id').all()
85 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
85 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
86 params[PARAMETER_RSS_URL] = self.get_rss_url()
86 params[PARAMETER_RSS_URL] = self.get_rss_url()
87
87
88 paginator.set_url(self.get_reverse_url(), request.GET.dict())
88 paginator.set_url(self.get_reverse_url(), request.GET.dict())
89 self.get_page_context(paginator, params, page)
89 self.get_page_context(paginator, params, page)
90
90
91 return render(request, TEMPLATE, params)
91 return render(request, TEMPLATE, params)
92
92
93 @method_decorator(csrf_protect)
93 @method_decorator(csrf_protect)
94 def post(self, request):
94 def post(self, request):
95 form = ThreadForm(request.POST, request.FILES,
95 form = ThreadForm(request.POST, request.FILES,
96 error_class=PlainErrorList)
96 error_class=PlainErrorList)
97 form.session = request.session
97 form.session = request.session
98
98
99 if form.is_valid():
99 if form.is_valid():
100 return self.create_thread(request, form)
100 return self.create_thread(request, form)
101 if form.need_to_ban:
101 if form.need_to_ban:
102 # Ban user because he is suspected to be a bot
102 # Ban user because he is suspected to be a bot
103 self._ban_current_user(request)
103 self._ban_current_user(request)
104
104
105 return self.get(request, form)
105 return self.get(request, form)
106
106
107 def get_page_context(self, paginator, params, page):
107 def get_page_context(self, paginator, params, page):
108 """
108 """
109 Get pagination context variables
109 Get pagination context variables
110 """
110 """
111
111
112 params[PARAMETER_PAGINATOR] = paginator
112 params[PARAMETER_PAGINATOR] = paginator
113 current_page = paginator.page(int(page))
113 current_page = paginator.page(int(page))
114 params[PARAMETER_CURRENT_PAGE] = current_page
114 params[PARAMETER_CURRENT_PAGE] = current_page
115 self.set_page_urls(paginator, params)
115 self.set_page_urls(paginator, params)
116
116
117 def get_reverse_url(self):
117 def get_reverse_url(self):
118 return reverse('index')
118 return reverse('index')
119
119
120 @transaction.atomic
120 @transaction.atomic
121 def create_thread(self, request, form: ThreadForm, html_response=True):
121 def create_thread(self, request, form: ThreadForm, html_response=True):
122 """
122 """
123 Creates a new thread with an opening post.
123 Creates a new thread with an opening post.
124 """
124 """
125
125
126 ip = utils.get_client_ip(request)
126 ip = utils.get_client_ip(request)
127 is_banned = Ban.objects.filter(ip=ip).exists()
127 is_banned = Ban.objects.filter(ip=ip).exists()
128
128
129 if is_banned:
129 if is_banned:
130 if html_response:
130 if html_response:
131 return redirect(BannedView().as_view())
131 return redirect(BannedView().as_view())
132 else:
132 else:
133 return
133 return
134
134
135 data = form.cleaned_data
135 data = form.cleaned_data
136
136
137 title = form.get_title()
137 title = form.get_title()
138 text = data[FORM_TEXT]
138 text = data[FORM_TEXT]
139 file = form.get_file()
139 file = form.get_file()
140 threads = data[FORM_THREADS]
140 threads = data[FORM_THREADS]
141 images = form.get_images()
141
142
142 text = self._remove_invalid_links(text)
143 text = self._remove_invalid_links(text)
143
144
144 tags = data[FORM_TAGS]
145 tags = data[FORM_TAGS]
145 monochrome = form.is_monochrome()
146 monochrome = form.is_monochrome()
146
147
147 post = Post.objects.create_post(title=title, text=text, file=file,
148 post = Post.objects.create_post(title=title, text=text, file=file,
148 ip=ip, tags=tags, opening_posts=threads,
149 ip=ip, tags=tags, opening_posts=threads,
149 tripcode=form.get_tripcode(),
150 tripcode=form.get_tripcode(),
150 monochrome=monochrome)
151 monochrome=monochrome, images=images)
151
152
152 # This is required to update the threads to which posts we have replied
153 # This is required to update the threads to which posts we have replied
153 # when creating this one
154 # when creating this one
154 post.notify_clients()
155 post.notify_clients()
155
156
156 if html_response:
157 if html_response:
157 return redirect(post.get_absolute_url())
158 return redirect(post.get_absolute_url())
158
159
159 def get_threads(self):
160 def get_threads(self):
160 """
161 """
161 Gets list of threads that will be shown on a page.
162 Gets list of threads that will be shown on a page.
162 """
163 """
163
164
164 return Thread.objects\
165 return Thread.objects\
165 .exclude(tags__in=self.settings_manager.get_hidden_tags())
166 .exclude(tags__in=self.settings_manager.get_hidden_tags())
166
167
167 def get_rss_url(self):
168 def get_rss_url(self):
168 return self.get_reverse_url() + 'rss/'
169 return self.get_reverse_url() + 'rss/'
@@ -1,76 +1,79 b''
1 from django.db import transaction
1 from django.db import transaction
2 from django.shortcuts import render, redirect
2 from django.shortcuts import render, redirect
3 from django.utils import timezone
3 from django.utils import timezone
4
4
5 from boards.abstracts.settingsmanager import get_settings_manager, \
5 from boards.abstracts.settingsmanager import get_settings_manager, \
6 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
6 SETTING_USERNAME, SETTING_LAST_NOTIFICATION_ID, SETTING_IMAGE_VIEWER
7 from boards.middlewares import SESSION_TIMEZONE
7 from boards.middlewares import SESSION_TIMEZONE
8 from boards.views.base import BaseBoardView, CONTEXT_FORM
8 from boards.views.base import BaseBoardView, CONTEXT_FORM
9 from boards.forms import SettingsForm, PlainErrorList
9 from boards.forms import SettingsForm, PlainErrorList
10 from boards import settings
10 from boards import settings
11 from boards.models import PostImage
11
12
12 FORM_THEME = 'theme'
13 FORM_THEME = 'theme'
13 FORM_USERNAME = 'username'
14 FORM_USERNAME = 'username'
14 FORM_TIMEZONE = 'timezone'
15 FORM_TIMEZONE = 'timezone'
15 FORM_IMAGE_VIEWER = 'image_viewer'
16 FORM_IMAGE_VIEWER = 'image_viewer'
16
17
17 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
18 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
19 CONTEXT_IMAGE_ALIASES = 'image_aliases'
18
20
19 TEMPLATE = 'boards/settings.html'
21 TEMPLATE = 'boards/settings.html'
20
22
21
23
22 class SettingsView(BaseBoardView):
24 class SettingsView(BaseBoardView):
23
25
24 def get(self, request):
26 def get(self, request):
25 params = dict()
27 params = dict()
26 settings_manager = get_settings_manager(request)
28 settings_manager = get_settings_manager(request)
27
29
28 selected_theme = settings_manager.get_theme()
30 selected_theme = settings_manager.get_theme()
29
31
30 form = SettingsForm(
32 form = SettingsForm(
31 initial={
33 initial={
32 FORM_THEME: selected_theme,
34 FORM_THEME: selected_theme,
33 FORM_IMAGE_VIEWER: settings_manager.get_setting(
35 FORM_IMAGE_VIEWER: settings_manager.get_setting(
34 SETTING_IMAGE_VIEWER,
36 SETTING_IMAGE_VIEWER,
35 default=settings.get('View', 'DefaultImageViewer')),
37 default=settings.get('View', 'DefaultImageViewer')),
36 FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME),
38 FORM_USERNAME: settings_manager.get_setting(SETTING_USERNAME),
37 FORM_TIMEZONE: request.session.get(
39 FORM_TIMEZONE: request.session.get(
38 SESSION_TIMEZONE, timezone.get_current_timezone()),
40 SESSION_TIMEZONE, timezone.get_current_timezone()),
39 },
41 },
40 error_class=PlainErrorList)
42 error_class=PlainErrorList)
41
43
42 params[CONTEXT_FORM] = form
44 params[CONTEXT_FORM] = form
43 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
45 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
46 params[CONTEXT_IMAGE_ALIASES] = PostImage.objects.exclude(alias='').exclude(alias=None)
44
47
45 return render(request, TEMPLATE, params)
48 return render(request, TEMPLATE, params)
46
49
47 def post(self, request):
50 def post(self, request):
48 settings_manager = get_settings_manager(request)
51 settings_manager = get_settings_manager(request)
49
52
50 with transaction.atomic():
53 with transaction.atomic():
51 form = SettingsForm(request.POST, error_class=PlainErrorList)
54 form = SettingsForm(request.POST, error_class=PlainErrorList)
52
55
53 if form.is_valid():
56 if form.is_valid():
54 selected_theme = form.cleaned_data[FORM_THEME]
57 selected_theme = form.cleaned_data[FORM_THEME]
55 username = form.cleaned_data[FORM_USERNAME].lower()
58 username = form.cleaned_data[FORM_USERNAME].lower()
56
59
57 settings_manager.set_theme(selected_theme)
60 settings_manager.set_theme(selected_theme)
58 settings_manager.set_setting(SETTING_IMAGE_VIEWER,
61 settings_manager.set_setting(SETTING_IMAGE_VIEWER,
59 form.cleaned_data[FORM_IMAGE_VIEWER])
62 form.cleaned_data[FORM_IMAGE_VIEWER])
60
63
61 old_username = settings_manager.get_setting(SETTING_USERNAME)
64 old_username = settings_manager.get_setting(SETTING_USERNAME)
62 if username != old_username:
65 if username != old_username:
63 settings_manager.set_setting(SETTING_USERNAME, username)
66 settings_manager.set_setting(SETTING_USERNAME, username)
64 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None)
67 settings_manager.set_setting(SETTING_LAST_NOTIFICATION_ID, None)
65
68
66 request.session[SESSION_TIMEZONE] = form.cleaned_data[FORM_TIMEZONE]
69 request.session[SESSION_TIMEZONE] = form.cleaned_data[FORM_TIMEZONE]
67
70
68 return redirect('settings')
71 return redirect('settings')
69 else:
72 else:
70 params = dict()
73 params = dict()
71
74
72 params[CONTEXT_FORM] = form
75 params[CONTEXT_FORM] = form
73 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
76 params[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
74
77
75 return render(request, TEMPLATE, params)
78 return render(request, TEMPLATE, params)
76
79
@@ -1,175 +1,177 b''
1 from django.contrib.auth.decorators import permission_required
1 from django.contrib.auth.decorators import permission_required
2
2
3 from django.core.exceptions import ObjectDoesNotExist
3 from django.core.exceptions import ObjectDoesNotExist
4 from django.core.urlresolvers import reverse
4 from django.core.urlresolvers import reverse
5 from django.http import Http404
5 from django.http import Http404
6 from django.shortcuts import get_object_or_404, render, redirect
6 from django.shortcuts import get_object_or_404, render, redirect
7 from django.template.context_processors import csrf
7 from django.template.context_processors import csrf
8 from django.utils.decorators import method_decorator
8 from django.utils.decorators import method_decorator
9 from django.views.decorators.csrf import csrf_protect
9 from django.views.decorators.csrf import csrf_protect
10 from django.views.generic.edit import FormMixin
10 from django.views.generic.edit import FormMixin
11 from django.utils import timezone
11 from django.utils import timezone
12 from django.utils.dateformat import format
12 from django.utils.dateformat import format
13
13
14 from boards import utils, settings
14 from boards import utils, settings
15 from boards.abstracts.settingsmanager import get_settings_manager
15 from boards.abstracts.settingsmanager import get_settings_manager
16 from boards.forms import PostForm, PlainErrorList
16 from boards.forms import PostForm, PlainErrorList
17 from boards.models import Post
17 from boards.models import Post
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
18 from boards.views.base import BaseBoardView, CONTEXT_FORM
19 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
19 from boards.views.mixins import DispatcherMixin, PARAMETER_METHOD
20 from boards.views.posting_mixin import PostMixin
20 from boards.views.posting_mixin import PostMixin
21 import neboard
21 import neboard
22
22
23 REQ_POST_ID = 'post_id'
23 REQ_POST_ID = 'post_id'
24
24
25 CONTEXT_LASTUPDATE = "last_update"
25 CONTEXT_LASTUPDATE = "last_update"
26 CONTEXT_THREAD = 'thread'
26 CONTEXT_THREAD = 'thread'
27 CONTEXT_WS_TOKEN = 'ws_token'
27 CONTEXT_WS_TOKEN = 'ws_token'
28 CONTEXT_WS_PROJECT = 'ws_project'
28 CONTEXT_WS_PROJECT = 'ws_project'
29 CONTEXT_WS_HOST = 'ws_host'
29 CONTEXT_WS_HOST = 'ws_host'
30 CONTEXT_WS_PORT = 'ws_port'
30 CONTEXT_WS_PORT = 'ws_port'
31 CONTEXT_WS_TIME = 'ws_token_time'
31 CONTEXT_WS_TIME = 'ws_token_time'
32 CONTEXT_MODE = 'mode'
32 CONTEXT_MODE = 'mode'
33 CONTEXT_OP = 'opening_post'
33 CONTEXT_OP = 'opening_post'
34 CONTEXT_FAVORITE = 'is_favorite'
34 CONTEXT_FAVORITE = 'is_favorite'
35 CONTEXT_RSS_URL = 'rss_url'
35 CONTEXT_RSS_URL = 'rss_url'
36
36
37 FORM_TITLE = 'title'
37 FORM_TITLE = 'title'
38 FORM_TEXT = 'text'
38 FORM_TEXT = 'text'
39 FORM_IMAGE = 'image'
39 FORM_IMAGE = 'image'
40 FORM_THREADS = 'threads'
40 FORM_THREADS = 'threads'
41
41
42
42
43 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
43 class ThreadView(BaseBoardView, PostMixin, FormMixin, DispatcherMixin):
44
44
45 @method_decorator(csrf_protect)
45 @method_decorator(csrf_protect)
46 def get(self, request, post_id, form: PostForm=None):
46 def get(self, request, post_id, form: PostForm=None):
47 try:
47 try:
48 opening_post = Post.objects.get(id=post_id)
48 opening_post = Post.objects.get(id=post_id)
49 except ObjectDoesNotExist:
49 except ObjectDoesNotExist:
50 raise Http404
50 raise Http404
51
51
52 # If the tag is favorite, update the counter
52 # If the tag is favorite, update the counter
53 settings_manager = get_settings_manager(request)
53 settings_manager = get_settings_manager(request)
54 favorite = settings_manager.thread_is_fav(opening_post)
54 favorite = settings_manager.thread_is_fav(opening_post)
55 if favorite:
55 if favorite:
56 settings_manager.add_or_read_fav_thread(opening_post)
56 settings_manager.add_or_read_fav_thread(opening_post)
57
57
58 # If this is not OP, don't show it as it is
58 # If this is not OP, don't show it as it is
59 if not opening_post.is_opening():
59 if not opening_post.is_opening():
60 return redirect(opening_post.get_thread().get_opening_post()
60 return redirect(opening_post.get_thread().get_opening_post()
61 .get_absolute_url())
61 .get_absolute_url())
62
62
63 if not form:
63 if not form:
64 form = PostForm(error_class=PlainErrorList)
64 form = PostForm(error_class=PlainErrorList)
65
65
66 thread_to_show = opening_post.get_thread()
66 thread_to_show = opening_post.get_thread()
67
67
68 params = dict()
68 params = dict()
69
69
70 params[CONTEXT_FORM] = form
70 params[CONTEXT_FORM] = form
71 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
71 params[CONTEXT_LASTUPDATE] = str(thread_to_show.last_edit_time)
72 params[CONTEXT_THREAD] = thread_to_show
72 params[CONTEXT_THREAD] = thread_to_show
73 params[CONTEXT_MODE] = self.get_mode()
73 params[CONTEXT_MODE] = self.get_mode()
74 params[CONTEXT_OP] = opening_post
74 params[CONTEXT_OP] = opening_post
75 params[CONTEXT_FAVORITE] = favorite
75 params[CONTEXT_FAVORITE] = favorite
76 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
76 params[CONTEXT_RSS_URL] = self.get_rss_url(post_id)
77
77
78 if settings.get_bool('External', 'WebsocketsEnabled'):
78 if settings.get_bool('External', 'WebsocketsEnabled'):
79 token_time = format(timezone.now(), u'U')
79 token_time = format(timezone.now(), u'U')
80
80
81 params[CONTEXT_WS_TIME] = token_time
81 params[CONTEXT_WS_TIME] = token_time
82 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
82 params[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
83 timestamp=token_time)
83 timestamp=token_time)
84 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
84 params[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
85 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
85 params[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
86 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
86 params[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
87
87
88 params.update(self.get_data(thread_to_show))
88 params.update(self.get_data(thread_to_show))
89
89
90 return render(request, self.get_template(), params)
90 return render(request, self.get_template(), params)
91
91
92 @method_decorator(csrf_protect)
92 @method_decorator(csrf_protect)
93 def post(self, request, post_id):
93 def post(self, request, post_id):
94 opening_post = get_object_or_404(Post, id=post_id)
94 opening_post = get_object_or_404(Post, id=post_id)
95
95
96 # If this is not OP, don't show it as it is
96 # If this is not OP, don't show it as it is
97 if not opening_post.is_opening():
97 if not opening_post.is_opening():
98 raise Http404
98 raise Http404
99
99
100 if PARAMETER_METHOD in request.POST:
100 if PARAMETER_METHOD in request.POST:
101 self.dispatch_method(request, opening_post)
101 self.dispatch_method(request, opening_post)
102
102
103 return redirect('thread', post_id) # FIXME Different for different modes
103 return redirect('thread', post_id) # FIXME Different for different modes
104
104
105 if not opening_post.get_thread().is_archived():
105 if not opening_post.get_thread().is_archived():
106 form = PostForm(request.POST, request.FILES,
106 form = PostForm(request.POST, request.FILES,
107 error_class=PlainErrorList)
107 error_class=PlainErrorList)
108 form.session = request.session
108 form.session = request.session
109
109
110 if form.is_valid():
110 if form.is_valid():
111 return self.new_post(request, form, opening_post)
111 return self.new_post(request, form, opening_post)
112 if form.need_to_ban:
112 if form.need_to_ban:
113 # Ban user because he is suspected to be a bot
113 # Ban user because he is suspected to be a bot
114 self._ban_current_user(request)
114 self._ban_current_user(request)
115
115
116 return self.get(request, post_id, form)
116 return self.get(request, post_id, form)
117
117
118 def new_post(self, request, form: PostForm, opening_post: Post=None,
118 def new_post(self, request, form: PostForm, opening_post: Post=None,
119 html_response=True):
119 html_response=True):
120 """
120 """
121 Adds a new post (in thread or as a reply).
121 Adds a new post (in thread or as a reply).
122 """
122 """
123
123
124 ip = utils.get_client_ip(request)
124 ip = utils.get_client_ip(request)
125
125
126 data = form.cleaned_data
126 data = form.cleaned_data
127
127
128 title = form.get_title()
128 title = form.get_title()
129 text = data[FORM_TEXT]
129 text = data[FORM_TEXT]
130 file = form.get_file()
130 file = form.get_file()
131 threads = data[FORM_THREADS]
131 threads = data[FORM_THREADS]
132 images = form.get_images()
132
133
133 text = self._remove_invalid_links(text)
134 text = self._remove_invalid_links(text)
134
135
135 post_thread = opening_post.get_thread()
136 post_thread = opening_post.get_thread()
136
137
137 post = Post.objects.create_post(title=title, text=text, file=file,
138 post = Post.objects.create_post(title=title, text=text, file=file,
138 thread=post_thread, ip=ip,
139 thread=post_thread, ip=ip,
139 opening_posts=threads,
140 opening_posts=threads,
140 tripcode=form.get_tripcode())
141 tripcode=form.get_tripcode(),
142 images=images)
141 post.notify_clients()
143 post.notify_clients()
142
144
143 if html_response:
145 if html_response:
144 if opening_post:
146 if opening_post:
145 return redirect(post.get_absolute_url())
147 return redirect(post.get_absolute_url())
146 else:
148 else:
147 return post
149 return post
148
150
149 def get_data(self, thread) -> dict:
151 def get_data(self, thread) -> dict:
150 """
152 """
151 Returns context params for the view.
153 Returns context params for the view.
152 """
154 """
153
155
154 return dict()
156 return dict()
155
157
156 def get_template(self) -> str:
158 def get_template(self) -> str:
157 """
159 """
158 Gets template to show the thread mode on.
160 Gets template to show the thread mode on.
159 """
161 """
160
162
161 pass
163 pass
162
164
163 def get_mode(self) -> str:
165 def get_mode(self) -> str:
164 pass
166 pass
165
167
166 def subscribe(self, request, opening_post):
168 def subscribe(self, request, opening_post):
167 settings_manager = get_settings_manager(request)
169 settings_manager = get_settings_manager(request)
168 settings_manager.add_or_read_fav_thread(opening_post)
170 settings_manager.add_or_read_fav_thread(opening_post)
169
171
170 def unsubscribe(self, request, opening_post):
172 def unsubscribe(self, request, opening_post):
171 settings_manager = get_settings_manager(request)
173 settings_manager = get_settings_manager(request)
172 settings_manager.del_fav_thread(opening_post)
174 settings_manager.del_fav_thread(opening_post)
173
175
174 def get_rss_url(self, opening_id):
176 def get_rss_url(self, opening_id):
175 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
177 return reverse('thread', kwargs={'post_id': opening_id}) + 'rss/'
General Comments 0
You need to be logged in to leave comments. Login now