##// END OF EJS Templates
Download attached filed to the post during sync
neko259 -
r1511:ea51d39c decentral
parent child Browse files
Show More
@@ -1,111 +1,124 b''
1 from django.contrib import admin
1 from django.contrib import admin
2 from django.utils.translation import ugettext_lazy as _
2 from django.utils.translation import ugettext_lazy as _
3 from django.core.urlresolvers import reverse
3 from django.core.urlresolvers import reverse
4 from boards.models import Post, Tag, Ban, Thread, Banner, PostImage, KeyPair
4 from boards.models import Post, Tag, Ban, Thread, Banner, PostImage, KeyPair, GlobalId
5
5
6
6
7 @admin.register(Post)
7 @admin.register(Post)
8 class PostAdmin(admin.ModelAdmin):
8 class PostAdmin(admin.ModelAdmin):
9
9
10 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images')
10 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images')
11 list_filter = ('pub_time',)
11 list_filter = ('pub_time',)
12 search_fields = ('id', 'title', 'text', 'poster_ip')
12 search_fields = ('id', 'title', 'text', 'poster_ip')
13 exclude = ('referenced_posts', 'refmap')
13 exclude = ('referenced_posts', 'refmap')
14 readonly_fields = ('poster_ip', 'threads', 'thread', 'linked_images',
14 readonly_fields = ('poster_ip', 'threads', 'thread', 'linked_images',
15 'attachments', 'uid', 'url', 'pub_time', 'opening')
15 'attachments', 'uid', 'url', 'pub_time', 'opening')
16
16
17 def ban_poster(self, request, queryset):
17 def ban_poster(self, request, queryset):
18 bans = 0
18 bans = 0
19 for post in queryset:
19 for post in queryset:
20 poster_ip = post.poster_ip
20 poster_ip = post.poster_ip
21 ban, created = Ban.objects.get_or_create(ip=poster_ip)
21 ban, created = Ban.objects.get_or_create(ip=poster_ip)
22 if created:
22 if created:
23 bans += 1
23 bans += 1
24 self.message_user(request, _('{} posters were banned').format(bans))
24 self.message_user(request, _('{} posters were banned').format(bans))
25
25
26 def ban_with_hiding(self, request, queryset):
26 def ban_with_hiding(self, request, queryset):
27 bans = 0
27 bans = 0
28 hidden = 0
28 hidden = 0
29 for post in queryset:
29 for post in queryset:
30 poster_ip = post.poster_ip
30 poster_ip = post.poster_ip
31 ban, created = Ban.objects.get_or_create(ip=poster_ip)
31 ban, created = Ban.objects.get_or_create(ip=poster_ip)
32 if created:
32 if created:
33 bans += 1
33 bans += 1
34 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)
35 hidden += posts.count()
35 hidden += posts.count()
36 posts.update(hidden=True)
36 posts.update(hidden=True)
37 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))
38
38
39 def linked_images(self, obj: Post):
39 def linked_images(self, obj: Post):
40 images = obj.images.all()
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]
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)
42 return ', '.join(image_urls)
43 linked_images.allow_tags = True
43 linked_images.allow_tags = True
44
44
45
45
46 actions = ['ban_poster', 'ban_with_hiding']
46 actions = ['ban_poster', 'ban_with_hiding']
47
47
48
48
49 @admin.register(Tag)
49 @admin.register(Tag)
50 class TagAdmin(admin.ModelAdmin):
50 class TagAdmin(admin.ModelAdmin):
51
51
52 def thread_count(self, obj: Tag) -> int:
52 def thread_count(self, obj: Tag) -> int:
53 return obj.get_thread_count()
53 return obj.get_thread_count()
54
54
55 def display_children(self, obj: Tag):
55 def display_children(self, obj: Tag):
56 return ', '.join([str(child) for child in obj.get_children().all()])
56 return ', '.join([str(child) for child in obj.get_children().all()])
57
57
58 def save_model(self, request, obj, form, change):
58 def save_model(self, request, obj, form, change):
59 super().save_model(request, obj, form, change)
59 super().save_model(request, obj, form, change)
60 for thread in obj.get_threads().all():
60 for thread in obj.get_threads().all():
61 thread.refresh_tags()
61 thread.refresh_tags()
62 list_display = ('name', 'thread_count', 'display_children')
62 list_display = ('name', 'thread_count', 'display_children')
63 search_fields = ('name',)
63 search_fields = ('name',)
64
64
65
65
66 @admin.register(Thread)
66 @admin.register(Thread)
67 class ThreadAdmin(admin.ModelAdmin):
67 class ThreadAdmin(admin.ModelAdmin):
68
68
69 def title(self, obj: Thread) -> str:
69 def title(self, obj: Thread) -> str:
70 return obj.get_opening_post().get_title()
70 return obj.get_opening_post().get_title()
71
71
72 def reply_count(self, obj: Thread) -> int:
72 def reply_count(self, obj: Thread) -> int:
73 return obj.get_reply_count()
73 return obj.get_reply_count()
74
74
75 def ip(self, obj: Thread):
75 def ip(self, obj: Thread):
76 return obj.get_opening_post().poster_ip
76 return obj.get_opening_post().poster_ip
77
77
78 def display_tags(self, obj: Thread):
78 def display_tags(self, obj: Thread):
79 return ', '.join([str(tag) for tag in obj.get_tags().all()])
79 return ', '.join([str(tag) for tag in obj.get_tags().all()])
80
80
81 def op(self, obj: Thread):
81 def op(self, obj: Thread):
82 return obj.get_opening_post_id()
82 return obj.get_opening_post_id()
83
83
84 # Save parent tags when editing tags
84 # Save parent tags when editing tags
85 def save_related(self, request, form, formsets, change):
85 def save_related(self, request, form, formsets, change):
86 super().save_related(request, form, formsets, change)
86 super().save_related(request, form, formsets, change)
87 form.instance.refresh_tags()
87 form.instance.refresh_tags()
88 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
88 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
89 'display_tags')
89 'display_tags')
90 list_filter = ('bump_time', 'status')
90 list_filter = ('bump_time', 'status')
91 search_fields = ('id', 'title')
91 search_fields = ('id', 'title')
92 filter_horizontal = ('tags',)
92 filter_horizontal = ('tags',)
93
93
94
94
95 @admin.register(KeyPair)
95 @admin.register(KeyPair)
96 class KeyPairAdmin(admin.ModelAdmin):
96 class KeyPairAdmin(admin.ModelAdmin):
97 list_display = ('public_key', 'primary')
97 list_display = ('public_key', 'primary')
98 list_filter = ('primary',)
98 list_filter = ('primary',)
99 search_fields = ('public_key',)
99 search_fields = ('public_key',)
100
100
101
101
102 @admin.register(Ban)
102 @admin.register(Ban)
103 class BanAdmin(admin.ModelAdmin):
103 class BanAdmin(admin.ModelAdmin):
104 list_display = ('ip', 'can_read')
104 list_display = ('ip', 'can_read')
105 list_filter = ('can_read',)
105 list_filter = ('can_read',)
106 search_fields = ('ip',)
106 search_fields = ('ip',)
107
107
108
108
109 @admin.register(Banner)
109 @admin.register(Banner)
110 class BannerAdmin(admin.ModelAdmin):
110 class BannerAdmin(admin.ModelAdmin):
111 list_display = ('title', 'text')
111 list_display = ('title', 'text')
112
113
114 @admin.register(PostImage)
115 class PostImageAdmin(admin.ModelAdmin):
116 search_fields = ('alias',)
117
118
119 @admin.register(GlobalId)
120 class GlobalIdAdmin(admin.ModelAdmin):
121 def is_linked(self, obj):
122 return Post.objects.filter(global_id=obj).exists()
123
124 list_display = ('__str__', 'is_linked',) No newline at end of file
@@ -1,469 +1,464 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.abstracts.attachment_alias import get_image_by_alias
17 from boards.mdx_neboard import formatters
17 from boards.mdx_neboard import formatters
18 from boards.models.attachment.downloaders import Downloader
18 from boards.models.attachment.downloaders import download
19 from boards.models.post import TITLE_MAX_LENGTH
19 from boards.models.post import TITLE_MAX_LENGTH
20 from boards.models import Tag, Post
20 from boards.models import Tag, Post
21 from boards.utils import validate_file_size, get_file_mimetype, \
21 from boards.utils import validate_file_size, get_file_mimetype, \
22 FILE_EXTENSION_DELIMITER
22 FILE_EXTENSION_DELIMITER
23 from neboard import settings
23 from neboard import settings
24 import boards.settings as board_settings
24 import boards.settings as board_settings
25 import neboard
25 import neboard
26
26
27 POW_HASH_LENGTH = 16
27 POW_HASH_LENGTH = 16
28 POW_LIFE_MINUTES = 5
28 POW_LIFE_MINUTES = 5
29
29
30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
32
32
33 VETERAN_POSTING_DELAY = 5
33 VETERAN_POSTING_DELAY = 5
34
34
35 ATTRIBUTE_PLACEHOLDER = 'placeholder'
35 ATTRIBUTE_PLACEHOLDER = 'placeholder'
36 ATTRIBUTE_ROWS = 'rows'
36 ATTRIBUTE_ROWS = 'rows'
37
37
38 LAST_POST_TIME = 'last_post_time'
38 LAST_POST_TIME = 'last_post_time'
39 LAST_LOGIN_TIME = 'last_login_time'
39 LAST_LOGIN_TIME = 'last_login_time'
40 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.')
41 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
41 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
42
42
43 LABEL_TITLE = _('Title')
43 LABEL_TITLE = _('Title')
44 LABEL_TEXT = _('Text')
44 LABEL_TEXT = _('Text')
45 LABEL_TAG = _('Tag')
45 LABEL_TAG = _('Tag')
46 LABEL_SEARCH = _('Search')
46 LABEL_SEARCH = _('Search')
47
47
48 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
48 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
49 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
49 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
50
50
51 TAG_MAX_LENGTH = 20
51 TAG_MAX_LENGTH = 20
52
52
53 TEXTAREA_ROWS = 4
53 TEXTAREA_ROWS = 4
54
54
55 TRIPCODE_DELIM = '#'
55 TRIPCODE_DELIM = '#'
56
56
57 # TODO Maybe this may be converted into the database table?
57 # TODO Maybe this may be converted into the database table?
58 MIMETYPE_EXTENSIONS = {
58 MIMETYPE_EXTENSIONS = {
59 'image/jpeg': 'jpeg',
59 'image/jpeg': 'jpeg',
60 'image/png': 'png',
60 'image/png': 'png',
61 'image/gif': 'gif',
61 'image/gif': 'gif',
62 'video/webm': 'webm',
62 'video/webm': 'webm',
63 'application/pdf': 'pdf',
63 'application/pdf': 'pdf',
64 'x-diff': 'diff',
64 'x-diff': 'diff',
65 'image/svg+xml': 'svg',
65 'image/svg+xml': 'svg',
66 'application/x-shockwave-flash': 'swf',
66 'application/x-shockwave-flash': 'swf',
67 'image/x-ms-bmp': 'bmp',
67 'image/x-ms-bmp': 'bmp',
68 'image/bmp': 'bmp',
68 'image/bmp': 'bmp',
69 }
69 }
70
70
71
71
72 def get_timezones():
72 def get_timezones():
73 timezones = []
73 timezones = []
74 for tz in pytz.common_timezones:
74 for tz in pytz.common_timezones:
75 timezones.append((tz, tz),)
75 timezones.append((tz, tz),)
76 return timezones
76 return timezones
77
77
78
78
79 class FormatPanel(forms.Textarea):
79 class FormatPanel(forms.Textarea):
80 """
80 """
81 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
82 form text area.
82 form text area.
83 """
83 """
84
84
85 def render(self, name, value, attrs=None):
85 def render(self, name, value, attrs=None):
86 output = '<div id="mark-panel">'
86 output = '<div id="mark-panel">'
87 for formatter in formatters:
87 for formatter in formatters:
88 output += '<span class="mark_btn"' + \
88 output += '<span class="mark_btn"' + \
89 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
89 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
90 '\', \'' + formatter.format_right + '\')">' + \
90 '\', \'' + formatter.format_right + '\')">' + \
91 formatter.preview_left + formatter.name + \
91 formatter.preview_left + formatter.name + \
92 formatter.preview_right + '</span>'
92 formatter.preview_right + '</span>'
93
93
94 output += '</div>'
94 output += '</div>'
95 output += super(FormatPanel, self).render(name, value, attrs=attrs)
95 output += super(FormatPanel, self).render(name, value, attrs=attrs)
96
96
97 return output
97 return output
98
98
99
99
100 class PlainErrorList(ErrorList):
100 class PlainErrorList(ErrorList):
101 def __unicode__(self):
101 def __unicode__(self):
102 return self.as_text()
102 return self.as_text()
103
103
104 def as_text(self):
104 def as_text(self):
105 return ''.join(['(!) %s ' % e for e in self])
105 return ''.join(['(!) %s ' % e for e in self])
106
106
107
107
108 class NeboardForm(forms.Form):
108 class NeboardForm(forms.Form):
109 """
109 """
110 Form with neboard-specific formatting.
110 Form with neboard-specific formatting.
111 """
111 """
112
112
113 def as_div(self):
113 def as_div(self):
114 """
114 """
115 Returns this form rendered as HTML <as_div>s.
115 Returns this form rendered as HTML <as_div>s.
116 """
116 """
117
117
118 return self._html_output(
118 return self._html_output(
119 # TODO Do not show hidden rows in the list here
119 # TODO Do not show hidden rows in the list here
120 normal_row='<div class="form-row">'
120 normal_row='<div class="form-row">'
121 '<div class="form-label">'
121 '<div class="form-label">'
122 '%(label)s'
122 '%(label)s'
123 '</div>'
123 '</div>'
124 '<div class="form-input">'
124 '<div class="form-input">'
125 '%(field)s'
125 '%(field)s'
126 '</div>'
126 '</div>'
127 '</div>'
127 '</div>'
128 '<div class="form-row">'
128 '<div class="form-row">'
129 '%(help_text)s'
129 '%(help_text)s'
130 '</div>',
130 '</div>',
131 error_row='<div class="form-row">'
131 error_row='<div class="form-row">'
132 '<div class="form-label"></div>'
132 '<div class="form-label"></div>'
133 '<div class="form-errors">%s</div>'
133 '<div class="form-errors">%s</div>'
134 '</div>',
134 '</div>',
135 row_ender='</div>',
135 row_ender='</div>',
136 help_text_html='%s',
136 help_text_html='%s',
137 errors_on_separate_row=True)
137 errors_on_separate_row=True)
138
138
139 def as_json_errors(self):
139 def as_json_errors(self):
140 errors = []
140 errors = []
141
141
142 for name, field in list(self.fields.items()):
142 for name, field in list(self.fields.items()):
143 if self[name].errors:
143 if self[name].errors:
144 errors.append({
144 errors.append({
145 'field': name,
145 'field': name,
146 'errors': self[name].errors.as_text(),
146 'errors': self[name].errors.as_text(),
147 })
147 })
148
148
149 return errors
149 return errors
150
150
151
151
152 class PostForm(NeboardForm):
152 class PostForm(NeboardForm):
153
153
154 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
154 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
155 label=LABEL_TITLE,
155 label=LABEL_TITLE,
156 widget=forms.TextInput(
156 widget=forms.TextInput(
157 attrs={ATTRIBUTE_PLACEHOLDER:
157 attrs={ATTRIBUTE_PLACEHOLDER:
158 'test#tripcode'}))
158 'test#tripcode'}))
159 text = forms.CharField(
159 text = forms.CharField(
160 widget=FormatPanel(attrs={
160 widget=FormatPanel(attrs={
161 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
161 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
162 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
162 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
163 }),
163 }),
164 required=False, label=LABEL_TEXT)
164 required=False, label=LABEL_TEXT)
165 file = forms.FileField(required=False, label=_('File'),
165 file = forms.FileField(required=False, label=_('File'),
166 widget=forms.ClearableFileInput(
166 widget=forms.ClearableFileInput(
167 attrs={'accept': 'file/*'}))
167 attrs={'accept': 'file/*'}))
168 file_url = forms.CharField(required=False, label=_('File URL'),
168 file_url = forms.CharField(required=False, label=_('File URL'),
169 widget=forms.TextInput(
169 widget=forms.TextInput(
170 attrs={ATTRIBUTE_PLACEHOLDER:
170 attrs={ATTRIBUTE_PLACEHOLDER:
171 'http://example.com/image.png'}))
171 'http://example.com/image.png'}))
172
172
173 # This field is for spam prevention only
173 # This field is for spam prevention only
174 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
174 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
175 widget=forms.TextInput(attrs={
175 widget=forms.TextInput(attrs={
176 'class': 'form-email'}))
176 'class': 'form-email'}))
177 threads = forms.CharField(required=False, label=_('Additional threads'),
177 threads = forms.CharField(required=False, label=_('Additional threads'),
178 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
178 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
179 '123 456 789'}))
179 '123 456 789'}))
180
180
181 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
181 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
182 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
182 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
183 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
183 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
184
184
185 session = None
185 session = None
186 need_to_ban = False
186 need_to_ban = False
187 image = None
187 image = None
188
188
189 def _update_file_extension(self, file):
189 def _update_file_extension(self, file):
190 if file:
190 if file:
191 mimetype = get_file_mimetype(file)
191 mimetype = get_file_mimetype(file)
192 extension = MIMETYPE_EXTENSIONS.get(mimetype)
192 extension = MIMETYPE_EXTENSIONS.get(mimetype)
193 if extension:
193 if extension:
194 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
194 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
195 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
195 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
196
196
197 file.name = new_filename
197 file.name = new_filename
198 else:
198 else:
199 logger = logging.getLogger('boards.forms.extension')
199 logger = logging.getLogger('boards.forms.extension')
200
200
201 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
201 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
202
202
203 def clean_title(self):
203 def clean_title(self):
204 title = self.cleaned_data['title']
204 title = self.cleaned_data['title']
205 if title:
205 if title:
206 if len(title) > TITLE_MAX_LENGTH:
206 if len(title) > TITLE_MAX_LENGTH:
207 raise forms.ValidationError(_('Title must have less than %s '
207 raise forms.ValidationError(_('Title must have less than %s '
208 'characters') %
208 'characters') %
209 str(TITLE_MAX_LENGTH))
209 str(TITLE_MAX_LENGTH))
210 return title
210 return title
211
211
212 def clean_text(self):
212 def clean_text(self):
213 text = self.cleaned_data['text'].strip()
213 text = self.cleaned_data['text'].strip()
214 if text:
214 if text:
215 max_length = board_settings.get_int('Forms', 'MaxTextLength')
215 max_length = board_settings.get_int('Forms', 'MaxTextLength')
216 if len(text) > max_length:
216 if len(text) > max_length:
217 raise forms.ValidationError(_('Text must have less than %s '
217 raise forms.ValidationError(_('Text must have less than %s '
218 'characters') % str(max_length))
218 'characters') % str(max_length))
219 return text
219 return text
220
220
221 def clean_file(self):
221 def clean_file(self):
222 file = self.cleaned_data['file']
222 file = self.cleaned_data['file']
223
223
224 if file:
224 if file:
225 validate_file_size(file.size)
225 validate_file_size(file.size)
226 self._update_file_extension(file)
226 self._update_file_extension(file)
227
227
228 return file
228 return file
229
229
230 def clean_file_url(self):
230 def clean_file_url(self):
231 url = self.cleaned_data['file_url']
231 url = self.cleaned_data['file_url']
232
232
233 file = None
233 file = None
234
234
235 if url:
235 if url:
236 file = get_image_by_alias(url, self.session)
236 file = get_image_by_alias(url, self.session)
237 self.image = file
237 self.image = file
238
238
239 if file is not None:
239 if file is not None:
240 return
240 return
241
241
242 if file is None:
242 if file is None:
243 file = self._get_file_from_url(url)
243 file = self._get_file_from_url(url)
244 if not file:
244 if not file:
245 raise forms.ValidationError(_('Invalid URL'))
245 raise forms.ValidationError(_('Invalid URL'))
246 else:
246 else:
247 validate_file_size(file.size)
247 validate_file_size(file.size)
248 self._update_file_extension(file)
248 self._update_file_extension(file)
249
249
250 return file
250 return file
251
251
252 def clean_threads(self):
252 def clean_threads(self):
253 threads_str = self.cleaned_data['threads']
253 threads_str = self.cleaned_data['threads']
254
254
255 if len(threads_str) > 0:
255 if len(threads_str) > 0:
256 threads_id_list = threads_str.split(' ')
256 threads_id_list = threads_str.split(' ')
257
257
258 threads = list()
258 threads = list()
259
259
260 for thread_id in threads_id_list:
260 for thread_id in threads_id_list:
261 try:
261 try:
262 thread = Post.objects.get(id=int(thread_id))
262 thread = Post.objects.get(id=int(thread_id))
263 if not thread.is_opening() or thread.get_thread().is_archived():
263 if not thread.is_opening() or thread.get_thread().is_archived():
264 raise ObjectDoesNotExist()
264 raise ObjectDoesNotExist()
265 threads.append(thread)
265 threads.append(thread)
266 except (ObjectDoesNotExist, ValueError):
266 except (ObjectDoesNotExist, ValueError):
267 raise forms.ValidationError(_('Invalid additional thread list'))
267 raise forms.ValidationError(_('Invalid additional thread list'))
268
268
269 return threads
269 return threads
270
270
271 def clean(self):
271 def clean(self):
272 cleaned_data = super(PostForm, self).clean()
272 cleaned_data = super(PostForm, self).clean()
273
273
274 if cleaned_data['email']:
274 if cleaned_data['email']:
275 self.need_to_ban = True
275 self.need_to_ban = True
276 raise forms.ValidationError('A human cannot enter a hidden field')
276 raise forms.ValidationError('A human cannot enter a hidden field')
277
277
278 if not self.errors:
278 if not self.errors:
279 self._clean_text_file()
279 self._clean_text_file()
280
280
281 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
281 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
282
282
283 settings_manager = get_settings_manager(self)
283 settings_manager = get_settings_manager(self)
284 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'):
285 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
285 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
286 if pow_difficulty > 0:
286 if pow_difficulty > 0:
287 # Limit only first post
287 # Limit only first post
288 if cleaned_data['timestamp'] \
288 if cleaned_data['timestamp'] \
289 and cleaned_data['iteration'] and cleaned_data['guess'] \
289 and cleaned_data['iteration'] and cleaned_data['guess'] \
290 and not settings_manager.get_setting('confirmed_user'):
290 and not settings_manager.get_setting('confirmed_user'):
291 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'])
292 else:
292 else:
293 # Limit every post
293 # Limit every post
294 self._validate_posting_speed()
294 self._validate_posting_speed()
295 settings_manager.set_setting('confirmed_user', True)
295 settings_manager.set_setting('confirmed_user', True)
296
296
297
297
298 return cleaned_data
298 return cleaned_data
299
299
300 def get_file(self):
300 def get_file(self):
301 """
301 """
302 Gets file from form or URL.
302 Gets file from form or URL.
303 """
303 """
304
304
305 file = self.cleaned_data['file']
305 file = self.cleaned_data['file']
306 return file or self.cleaned_data['file_url']
306 return file or self.cleaned_data['file_url']
307
307
308 def get_tripcode(self):
308 def get_tripcode(self):
309 title = self.cleaned_data['title']
309 title = self.cleaned_data['title']
310 if title is not None and TRIPCODE_DELIM in title:
310 if title is not None and TRIPCODE_DELIM in title:
311 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
312 tripcode = hashlib.md5(code.encode()).hexdigest()
312 tripcode = hashlib.md5(code.encode()).hexdigest()
313 else:
313 else:
314 tripcode = ''
314 tripcode = ''
315 return tripcode
315 return tripcode
316
316
317 def get_title(self):
317 def get_title(self):
318 title = self.cleaned_data['title']
318 title = self.cleaned_data['title']
319 if title is not None and TRIPCODE_DELIM in title:
319 if title is not None and TRIPCODE_DELIM in title:
320 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
320 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
321 else:
321 else:
322 return title
322 return title
323
323
324 def get_images(self):
324 def get_images(self):
325 if self.image:
325 if self.image:
326 return [self.image]
326 return [self.image]
327 else:
327 else:
328 return []
328 return []
329
329
330 def _clean_text_file(self):
330 def _clean_text_file(self):
331 text = self.cleaned_data.get('text')
331 text = self.cleaned_data.get('text')
332 file = self.get_file()
332 file = self.get_file()
333 images = self.get_images()
333 images = self.get_images()
334
334
335 if (not text) and (not file) and len(images) == 0:
335 if (not text) and (not file) and len(images) == 0:
336 error_message = _('Either text or file must be entered.')
336 error_message = _('Either text or file must be entered.')
337 self._errors['text'] = self.error_class([error_message])
337 self._errors['text'] = self.error_class([error_message])
338
338
339 def _validate_posting_speed(self):
339 def _validate_posting_speed(self):
340 can_post = True
340 can_post = True
341
341
342 posting_delay = settings.POSTING_DELAY
342 posting_delay = settings.POSTING_DELAY
343
343
344 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
344 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
345 now = time.time()
345 now = time.time()
346
346
347 current_delay = 0
347 current_delay = 0
348
348
349 if LAST_POST_TIME not in self.session:
349 if LAST_POST_TIME not in self.session:
350 self.session[LAST_POST_TIME] = now
350 self.session[LAST_POST_TIME] = now
351
351
352 need_delay = True
352 need_delay = True
353 else:
353 else:
354 last_post_time = self.session.get(LAST_POST_TIME)
354 last_post_time = self.session.get(LAST_POST_TIME)
355 current_delay = int(now - last_post_time)
355 current_delay = int(now - last_post_time)
356
356
357 need_delay = current_delay < posting_delay
357 need_delay = current_delay < posting_delay
358
358
359 if need_delay:
359 if need_delay:
360 delay = posting_delay - current_delay
360 delay = posting_delay - current_delay
361 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
361 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
362 delay) % {'delay': delay}
362 delay) % {'delay': delay}
363 self._errors['text'] = self.error_class([error_message])
363 self._errors['text'] = self.error_class([error_message])
364
364
365 can_post = False
365 can_post = False
366
366
367 if can_post:
367 if can_post:
368 self.session[LAST_POST_TIME] = now
368 self.session[LAST_POST_TIME] = now
369
369
370 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
370 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
371 """
371 """
372 Gets an file file from URL.
372 Gets an file file from URL.
373 """
373 """
374
374
375 img_temp = None
375 img_temp = None
376
376
377 try:
377 try:
378 for downloader in Downloader.__subclasses__():
378 download(url)
379 if downloader.handles(url):
380 return downloader.download(url)
381 # If nobody of the specific downloaders handles this, use generic
382 # one
383 return Downloader.download(url)
384 except forms.ValidationError as e:
379 except forms.ValidationError as e:
385 raise e
380 raise e
386 except Exception as e:
381 except Exception as e:
387 raise forms.ValidationError(e)
382 raise forms.ValidationError(e)
388
383
389 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
384 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
390 post_time = timezone.datetime.fromtimestamp(
385 post_time = timezone.datetime.fromtimestamp(
391 int(timestamp[:-3]), tz=timezone.get_current_timezone())
386 int(timestamp[:-3]), tz=timezone.get_current_timezone())
392
387
393 payload = timestamp + message.replace('\r\n', '\n')
388 payload = timestamp + message.replace('\r\n', '\n')
394 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
389 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
395 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
390 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
396 if len(target) < POW_HASH_LENGTH:
391 if len(target) < POW_HASH_LENGTH:
397 target = '0' * (POW_HASH_LENGTH - len(target)) + target
392 target = '0' * (POW_HASH_LENGTH - len(target)) + target
398
393
399 computed_guess = hashlib.sha256((payload + iteration).encode())\
394 computed_guess = hashlib.sha256((payload + iteration).encode())\
400 .hexdigest()[0:POW_HASH_LENGTH]
395 .hexdigest()[0:POW_HASH_LENGTH]
401 if guess != computed_guess or guess > target:
396 if guess != computed_guess or guess > target:
402 self._errors['text'] = self.error_class(
397 self._errors['text'] = self.error_class(
403 [_('Invalid PoW.')])
398 [_('Invalid PoW.')])
404
399
405
400
406
401
407 class ThreadForm(PostForm):
402 class ThreadForm(PostForm):
408
403
409 tags = forms.CharField(
404 tags = forms.CharField(
410 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
405 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
411 max_length=100, label=_('Tags'), required=True)
406 max_length=100, label=_('Tags'), required=True)
412 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
407 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
413
408
414 def clean_tags(self):
409 def clean_tags(self):
415 tags = self.cleaned_data['tags'].strip()
410 tags = self.cleaned_data['tags'].strip()
416
411
417 if not tags or not REGEX_TAGS.match(tags):
412 if not tags or not REGEX_TAGS.match(tags):
418 raise forms.ValidationError(
413 raise forms.ValidationError(
419 _('Inappropriate characters in tags.'))
414 _('Inappropriate characters in tags.'))
420
415
421 required_tag_exists = False
416 required_tag_exists = False
422 tag_set = set()
417 tag_set = set()
423 for tag_string in tags.split():
418 for tag_string in tags.split():
424 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
419 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
425 tag_set.add(tag)
420 tag_set.add(tag)
426
421
427 # If this is a new tag, don't check for its parents because nobody
422 # If this is a new tag, don't check for its parents because nobody
428 # added them yet
423 # added them yet
429 if not created:
424 if not created:
430 tag_set |= set(tag.get_all_parents())
425 tag_set |= set(tag.get_all_parents())
431
426
432 for tag in tag_set:
427 for tag in tag_set:
433 if tag.required:
428 if tag.required:
434 required_tag_exists = True
429 required_tag_exists = True
435 break
430 break
436
431
437 if not required_tag_exists:
432 if not required_tag_exists:
438 raise forms.ValidationError(
433 raise forms.ValidationError(
439 _('Need at least one section.'))
434 _('Need at least one section.'))
440
435
441 return tag_set
436 return tag_set
442
437
443 def clean(self):
438 def clean(self):
444 cleaned_data = super(ThreadForm, self).clean()
439 cleaned_data = super(ThreadForm, self).clean()
445
440
446 return cleaned_data
441 return cleaned_data
447
442
448 def is_monochrome(self):
443 def is_monochrome(self):
449 return self.cleaned_data['monochrome']
444 return self.cleaned_data['monochrome']
450
445
451
446
452 class SettingsForm(NeboardForm):
447 class SettingsForm(NeboardForm):
453
448
454 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
449 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
455 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
450 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
456 username = forms.CharField(label=_('User name'), required=False)
451 username = forms.CharField(label=_('User name'), required=False)
457 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
452 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
458
453
459 def clean_username(self):
454 def clean_username(self):
460 username = self.cleaned_data['username']
455 username = self.cleaned_data['username']
461
456
462 if username and not REGEX_USERNAMES.match(username):
457 if username and not REGEX_USERNAMES.match(username):
463 raise forms.ValidationError(_('Inappropriate characters.'))
458 raise forms.ValidationError(_('Inappropriate characters.'))
464
459
465 return username
460 return username
466
461
467
462
468 class SearchForm(NeboardForm):
463 class SearchForm(NeboardForm):
469 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
464 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,79 +1,80 b''
1 import re
1 import re
2 import xml.etree.ElementTree as ET
2 import xml.etree.ElementTree as ET
3
3
4 import httplib2
4 import httplib2
5 from django.core.management import BaseCommand
5 from django.core.management import BaseCommand
6
6
7 from boards.models import GlobalId
7 from boards.models import GlobalId
8 from boards.models.post.sync import SyncManager
8 from boards.models.post.sync import SyncManager
9
9
10 __author__ = 'neko259'
10 __author__ = 'neko259'
11
11
12
12
13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
13 REGEX_GLOBAL_ID = re.compile(r'(\w+)::([\w\+/]+)::(\d+)')
14
14
15
15
16 class Command(BaseCommand):
16 class Command(BaseCommand):
17 help = 'Send a sync or get request to the server.'
17 help = 'Send a sync or get request to the server.'
18
18
19 def add_arguments(self, parser):
19 def add_arguments(self, parser):
20 parser.add_argument('url', type=str, help='Server root url')
20 parser.add_argument('url', type=str, help='Server root url')
21 parser.add_argument('--global-id', type=str, default='',
21 parser.add_argument('--global-id', type=str, default='',
22 help='Post global ID')
22 help='Post global ID')
23
23
24 def handle(self, *args, **options):
24 def handle(self, *args, **options):
25 url = options.get('url')
25 url = options.get('url')
26
26
27 pull_url = url + 'api/sync/pull/'
27 pull_url = url + 'api/sync/pull/'
28 get_url = url + 'api/sync/get/'
28 get_url = url + 'api/sync/get/'
29 file_url = url[:-1]
29
30
30 global_id_str = options.get('global_id')
31 global_id_str = options.get('global_id')
31 if global_id_str:
32 if global_id_str:
32 match = REGEX_GLOBAL_ID.match(global_id_str)
33 match = REGEX_GLOBAL_ID.match(global_id_str)
33 if match:
34 if match:
34 key_type = match.group(1)
35 key_type = match.group(1)
35 key = match.group(2)
36 key = match.group(2)
36 local_id = match.group(3)
37 local_id = match.group(3)
37
38
38 global_id = GlobalId(key_type=key_type, key=key,
39 global_id = GlobalId(key_type=key_type, key=key,
39 local_id=local_id)
40 local_id=local_id)
40
41
41 xml = GlobalId.objects.generate_request_get([global_id])
42 xml = GlobalId.objects.generate_request_get([global_id])
42 # body = urllib.parse.urlencode(data)
43 # body = urllib.parse.urlencode(data)
43 h = httplib2.Http()
44 h = httplib2.Http()
44 response, content = h.request(get_url, method="POST", body=xml)
45 response, content = h.request(get_url, method="POST", body=xml)
45
46
46 SyncManager.parse_response_get(content)
47 SyncManager.parse_response_get(content, file_url)
47 else:
48 else:
48 raise Exception('Invalid global ID')
49 raise Exception('Invalid global ID')
49 else:
50 else:
50 h = httplib2.Http()
51 h = httplib2.Http()
51 xml = GlobalId.objects.generate_request_pull()
52 xml = GlobalId.objects.generate_request_pull()
52 response, content = h.request(pull_url, method="POST", body=xml)
53 response, content = h.request(pull_url, method="POST", body=xml)
53
54
54 print(content.decode() + '\n')
55 print(content.decode() + '\n')
55
56
56 root = ET.fromstring(content)
57 root = ET.fromstring(content)
57 status = root.findall('status')[0].text
58 status = root.findall('status')[0].text
58 if status == 'success':
59 if status == 'success':
59 ids_to_sync = list()
60 ids_to_sync = list()
60
61
61 models = root.findall('models')[0]
62 models = root.findall('models')[0]
62 for model in models:
63 for model in models:
63 global_id, exists = GlobalId.from_xml_element(model)
64 global_id, exists = GlobalId.from_xml_element(model)
64 if not exists:
65 if not exists:
65 print(global_id)
66 print(global_id)
66 ids_to_sync.append(global_id)
67 ids_to_sync.append(global_id)
67 print()
68 print()
68
69
69 if len(ids_to_sync) > 0:
70 if len(ids_to_sync) > 0:
70 xml = GlobalId.objects.generate_request_get(ids_to_sync)
71 xml = GlobalId.objects.generate_request_get(ids_to_sync)
71 # body = urllib.parse.urlencode(data)
72 # body = urllib.parse.urlencode(data)
72 h = httplib2.Http()
73 h = httplib2.Http()
73 response, content = h.request(get_url, method="POST", body=xml)
74 response, content = h.request(get_url, method="POST", body=xml)
74
75
75 SyncManager.parse_response_get(content)
76 SyncManager.parse_response_get(content, file_url)
76 else:
77 else:
77 print('Nothing to get, everything synced')
78 print('Nothing to get, everything synced')
78 else:
79 else:
79 raise Exception('Invalid response status')
80 raise Exception('Invalid response status')
@@ -1,70 +1,79 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 def download(url):
58 for downloader in Downloader.__subclasses__():
59 if downloader.handles(url):
60 return downloader.download(url)
61 # If nobody of the specific downloaders handles this, use generic
62 # one
63 return Downloader.download(url)
64
65
57 class YouTubeDownloader(Downloader):
66 class YouTubeDownloader(Downloader):
58 @staticmethod
67 @staticmethod
59 def download(url: str):
68 def download(url: str):
60 yt = YouTube()
69 yt = YouTube()
61 yt.from_url(url)
70 yt.from_url(url)
62 videos = yt.filter(YOUTUBE_VIDEO_FORMAT)
71 videos = yt.filter(YOUTUBE_VIDEO_FORMAT)
63 if len(videos) > 0:
72 if len(videos) > 0:
64 video = videos[0]
73 video = videos[0]
65 return Downloader.download(video.url)
74 return Downloader.download(video.url)
66
75
67 @staticmethod
76 @staticmethod
68 def handles(url: str) -> bool:
77 def handles(url: str) -> bool:
69 return YOUTUBE_URL.match(url)
78 return YOUTUBE_URL.match(url)
70
79
@@ -1,455 +1,453 b''
1 import logging
1 import logging
2 import re
3 import uuid
2 import uuid
4
3
5 from django.core.exceptions import ObjectDoesNotExist
4 import re
6 from django.core.urlresolvers import reverse
7 from django.db import models
8 from django.db.models import TextField, QuerySet
9 from django.template.defaultfilters import truncatewords, striptags
10 from django.template.loader import render_to_string
11 from django.utils import timezone
12 from django.dispatch import receiver
13 from django.db.models.signals import pre_save, post_save
14
15 from boards import settings
5 from boards import settings
16 from boards.abstracts.tripcode import Tripcode
6 from boards.abstracts.tripcode import Tripcode
17 from boards.mdx_neboard import get_parser
7 from boards.mdx_neboard import get_parser
18 from boards.models import PostImage, Attachment, KeyPair, GlobalId
8 from boards.models import PostImage, Attachment, KeyPair, GlobalId
19 from boards.models.base import Viewable
9 from boards.models.base import Viewable
20 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
10 from boards.models.post.export import get_exporter, DIFF_TYPE_JSON
21 from boards.models.post.manager import PostManager
11 from boards.models.post.manager import PostManager
22 from boards.models.user import Notification
12 from boards.models.user import Notification
13 from django.core.exceptions import ObjectDoesNotExist
14 from django.core.urlresolvers import reverse
15 from django.db import models
16 from django.db.models import TextField, QuerySet
17 from django.db.models.signals import pre_save, post_save, pre_delete, \
18 post_delete
19 from django.dispatch import receiver
20 from django.template.defaultfilters import truncatewords, striptags
21 from django.template.loader import render_to_string
22 from django.utils import timezone
23
23
24 CSS_CLS_HIDDEN_POST = 'hidden_post'
24 CSS_CLS_HIDDEN_POST = 'hidden_post'
25 CSS_CLS_DEAD_POST = 'dead_post'
25 CSS_CLS_DEAD_POST = 'dead_post'
26 CSS_CLS_ARCHIVE_POST = 'archive_post'
26 CSS_CLS_ARCHIVE_POST = 'archive_post'
27 CSS_CLS_POST = 'post'
27 CSS_CLS_POST = 'post'
28 CSS_CLS_MONOCHROME = 'monochrome'
28 CSS_CLS_MONOCHROME = 'monochrome'
29
29
30 TITLE_MAX_WORDS = 10
30 TITLE_MAX_WORDS = 10
31
31
32 APP_LABEL_BOARDS = 'boards'
32 APP_LABEL_BOARDS = 'boards'
33
33
34 BAN_REASON_AUTO = 'Auto'
34 BAN_REASON_AUTO = 'Auto'
35
35
36 IMAGE_THUMB_SIZE = (200, 150)
36 IMAGE_THUMB_SIZE = (200, 150)
37
37
38 TITLE_MAX_LENGTH = 200
38 TITLE_MAX_LENGTH = 200
39
39
40 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
40 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
41 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
41 REGEX_GLOBAL_REPLY = re.compile(r'\[post\](\w+)::([^:]+)::(\d+)\[/post\]')
42 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
42 REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
43 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
43 REGEX_NOTIFICATION = re.compile(r'\[user\](\w+)\[/user\]')
44
44
45 PARAMETER_TRUNCATED = 'truncated'
45 PARAMETER_TRUNCATED = 'truncated'
46 PARAMETER_TAG = 'tag'
46 PARAMETER_TAG = 'tag'
47 PARAMETER_OFFSET = 'offset'
47 PARAMETER_OFFSET = 'offset'
48 PARAMETER_DIFF_TYPE = 'type'
48 PARAMETER_DIFF_TYPE = 'type'
49 PARAMETER_CSS_CLASS = 'css_class'
49 PARAMETER_CSS_CLASS = 'css_class'
50 PARAMETER_THREAD = 'thread'
50 PARAMETER_THREAD = 'thread'
51 PARAMETER_IS_OPENING = 'is_opening'
51 PARAMETER_IS_OPENING = 'is_opening'
52 PARAMETER_POST = 'post'
52 PARAMETER_POST = 'post'
53 PARAMETER_OP_ID = 'opening_post_id'
53 PARAMETER_OP_ID = 'opening_post_id'
54 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
54 PARAMETER_NEED_OPEN_LINK = 'need_open_link'
55 PARAMETER_REPLY_LINK = 'reply_link'
55 PARAMETER_REPLY_LINK = 'reply_link'
56 PARAMETER_NEED_OP_DATA = 'need_op_data'
56 PARAMETER_NEED_OP_DATA = 'need_op_data'
57
57
58 POST_VIEW_PARAMS = (
58 POST_VIEW_PARAMS = (
59 'need_op_data',
59 'need_op_data',
60 'reply_link',
60 'reply_link',
61 'need_open_link',
61 'need_open_link',
62 'truncated',
62 'truncated',
63 'mode_tree',
63 'mode_tree',
64 'perms',
64 'perms',
65 'tree_depth',
65 'tree_depth',
66 )
66 )
67
67
68
68
69 class Post(models.Model, Viewable):
69 class Post(models.Model, Viewable):
70 """A post is a message."""
70 """A post is a message."""
71
71
72 objects = PostManager()
72 objects = PostManager()
73
73
74 class Meta:
74 class Meta:
75 app_label = APP_LABEL_BOARDS
75 app_label = APP_LABEL_BOARDS
76 ordering = ('id',)
76 ordering = ('id',)
77
77
78 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
78 title = models.CharField(max_length=TITLE_MAX_LENGTH, null=True, blank=True)
79 pub_time = models.DateTimeField()
79 pub_time = models.DateTimeField()
80 text = TextField(blank=True, null=True)
80 text = TextField(blank=True, null=True)
81 _text_rendered = TextField(blank=True, null=True, editable=False)
81 _text_rendered = TextField(blank=True, null=True, editable=False)
82
82
83 images = models.ManyToManyField(PostImage, null=True, blank=True,
83 images = models.ManyToManyField(PostImage, null=True, blank=True,
84 related_name='post_images', db_index=True)
84 related_name='post_images', db_index=True)
85 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
85 attachments = models.ManyToManyField(Attachment, null=True, blank=True,
86 related_name='attachment_posts')
86 related_name='attachment_posts')
87
87
88 poster_ip = models.GenericIPAddressField()
88 poster_ip = models.GenericIPAddressField()
89
89
90 # TODO This field can be removed cause UID is used for update now
90 # TODO This field can be removed cause UID is used for update now
91 last_edit_time = models.DateTimeField()
91 last_edit_time = models.DateTimeField()
92
92
93 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
93 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
94 null=True,
94 null=True,
95 blank=True, related_name='refposts',
95 blank=True, related_name='refposts',
96 db_index=True)
96 db_index=True)
97 refmap = models.TextField(null=True, blank=True)
97 refmap = models.TextField(null=True, blank=True)
98 threads = models.ManyToManyField('Thread', db_index=True,
98 threads = models.ManyToManyField('Thread', db_index=True,
99 related_name='multi_replies')
99 related_name='multi_replies')
100 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
100 thread = models.ForeignKey('Thread', db_index=True, related_name='pt+')
101
101
102 url = models.TextField()
102 url = models.TextField()
103 uid = models.TextField(db_index=True)
103 uid = models.TextField(db_index=True)
104
104
105 # Global ID with author key. If the message was downloaded from another
105 # Global ID with author key. If the message was downloaded from another
106 # server, this indicates the server.
106 # server, this indicates the server.
107 global_id = models.OneToOneField('GlobalId', null=True, blank=True)
107 global_id = models.OneToOneField(GlobalId, null=True, blank=True,
108 on_delete=models.CASCADE)
108
109
109 tripcode = models.CharField(max_length=50, blank=True, default='')
110 tripcode = models.CharField(max_length=50, blank=True, default='')
110 opening = models.BooleanField(db_index=True)
111 opening = models.BooleanField(db_index=True)
111 hidden = models.BooleanField(default=False)
112 hidden = models.BooleanField(default=False)
112
113
113 def __str__(self):
114 def __str__(self):
114 return 'P#{}/{}'.format(self.id, self.get_title())
115 return 'P#{}/{}'.format(self.id, self.get_title())
115
116
116 def get_referenced_posts(self):
117 def get_referenced_posts(self):
117 threads = self.get_threads().all()
118 threads = self.get_threads().all()
118 return self.referenced_posts.filter(threads__in=threads)\
119 return self.referenced_posts.filter(threads__in=threads)\
119 .order_by('pub_time').distinct().all()
120 .order_by('pub_time').distinct().all()
120
121
121 def get_title(self) -> str:
122 def get_title(self) -> str:
122 return self.title
123 return self.title
123
124
124 def get_title_or_text(self):
125 def get_title_or_text(self):
125 title = self.get_title()
126 title = self.get_title()
126 if not title:
127 if not title:
127 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
128 title = truncatewords(striptags(self.get_text()), TITLE_MAX_WORDS)
128
129
129 return title
130 return title
130
131
131 def build_refmap(self) -> None:
132 def build_refmap(self) -> None:
132 """
133 """
133 Builds a replies map string from replies list. This is a cache to stop
134 Builds a replies map string from replies list. This is a cache to stop
134 the server from recalculating the map on every post show.
135 the server from recalculating the map on every post show.
135 """
136 """
136
137
137 post_urls = [refpost.get_link_view()
138 post_urls = [refpost.get_link_view()
138 for refpost in self.referenced_posts.all()]
139 for refpost in self.referenced_posts.all()]
139
140
140 self.refmap = ', '.join(post_urls)
141 self.refmap = ', '.join(post_urls)
141
142
142 def is_referenced(self) -> bool:
143 def is_referenced(self) -> bool:
143 return self.refmap and len(self.refmap) > 0
144 return self.refmap and len(self.refmap) > 0
144
145
145 def is_opening(self) -> bool:
146 def is_opening(self) -> bool:
146 """
147 """
147 Checks if this is an opening post or just a reply.
148 Checks if this is an opening post or just a reply.
148 """
149 """
149
150
150 return self.opening
151 return self.opening
151
152
152 def get_absolute_url(self, thread=None):
153 def get_absolute_url(self, thread=None):
153 url = None
154 url = None
154
155
155 if thread is None:
156 if thread is None:
156 thread = self.get_thread()
157 thread = self.get_thread()
157
158
158 # Url is cached only for the "main" thread. When getting url
159 # Url is cached only for the "main" thread. When getting url
159 # for other threads, do it manually.
160 # for other threads, do it manually.
160 if self.url:
161 if self.url:
161 url = self.url
162 url = self.url
162
163
163 if url is None:
164 if url is None:
164 opening = self.is_opening()
165 opening = self.is_opening()
165 opening_id = self.id if opening else thread.get_opening_post_id()
166 opening_id = self.id if opening else thread.get_opening_post_id()
166 url = reverse('thread', kwargs={'post_id': opening_id})
167 url = reverse('thread', kwargs={'post_id': opening_id})
167 if not opening:
168 if not opening:
168 url += '#' + str(self.id)
169 url += '#' + str(self.id)
169
170
170 return url
171 return url
171
172
172 def get_thread(self):
173 def get_thread(self):
173 return self.thread
174 return self.thread
174
175
175 def get_thread_id(self):
176 def get_thread_id(self):
176 return self.thread_id
177 return self.thread_id
178
177 def get_threads(self) -> QuerySet:
179 def get_threads(self) -> QuerySet:
178 """
180 """
179 Gets post's thread.
181 Gets post's thread.
180 """
182 """
181
183
182 return self.threads
184 return self.threads
183
185
184 def get_view(self, *args, **kwargs) -> str:
186 def get_view(self, *args, **kwargs) -> str:
185 """
187 """
186 Renders post's HTML view. Some of the post params can be passed over
188 Renders post's HTML view. Some of the post params can be passed over
187 kwargs for the means of caching (if we view the thread, some params
189 kwargs for the means of caching (if we view the thread, some params
188 are same for every post and don't need to be computed over and over.
190 are same for every post and don't need to be computed over and over.
189 """
191 """
190
192
191 thread = self.get_thread()
193 thread = self.get_thread()
192
194
193 css_classes = [CSS_CLS_POST]
195 css_classes = [CSS_CLS_POST]
194 if thread.is_archived():
196 if thread.is_archived():
195 css_classes.append(CSS_CLS_ARCHIVE_POST)
197 css_classes.append(CSS_CLS_ARCHIVE_POST)
196 elif not thread.can_bump():
198 elif not thread.can_bump():
197 css_classes.append(CSS_CLS_DEAD_POST)
199 css_classes.append(CSS_CLS_DEAD_POST)
198 if self.is_hidden():
200 if self.is_hidden():
199 css_classes.append(CSS_CLS_HIDDEN_POST)
201 css_classes.append(CSS_CLS_HIDDEN_POST)
200 if thread.is_monochrome():
202 if thread.is_monochrome():
201 css_classes.append(CSS_CLS_MONOCHROME)
203 css_classes.append(CSS_CLS_MONOCHROME)
202
204
203 params = dict()
205 params = dict()
204 for param in POST_VIEW_PARAMS:
206 for param in POST_VIEW_PARAMS:
205 if param in kwargs:
207 if param in kwargs:
206 params[param] = kwargs[param]
208 params[param] = kwargs[param]
207
209
208 params.update({
210 params.update({
209 PARAMETER_POST: self,
211 PARAMETER_POST: self,
210 PARAMETER_IS_OPENING: self.is_opening(),
212 PARAMETER_IS_OPENING: self.is_opening(),
211 PARAMETER_THREAD: thread,
213 PARAMETER_THREAD: thread,
212 PARAMETER_CSS_CLASS: ' '.join(css_classes),
214 PARAMETER_CSS_CLASS: ' '.join(css_classes),
213 })
215 })
214
216
215 return render_to_string('boards/post.html', params)
217 return render_to_string('boards/post.html', params)
216
218
217 def get_search_view(self, *args, **kwargs):
219 def get_search_view(self, *args, **kwargs):
218 return self.get_view(need_op_data=True, *args, **kwargs)
220 return self.get_view(need_op_data=True, *args, **kwargs)
219
221
220 def get_first_image(self) -> PostImage:
222 def get_first_image(self) -> PostImage:
221 return self.images.earliest('id')
223 return self.images.earliest('id')
222
224
223 def delete(self, using=None):
224 """
225 Deletes all post images and the post itself.
226 """
227
228 for image in self.images.all():
229 image_refs_count = image.post_images.count()
230 if image_refs_count == 1:
231 image.delete()
232
233 for attachment in self.attachments.all():
234 attachment_refs_count = attachment.attachment_posts.count()
235 if attachment_refs_count == 1:
236 attachment.delete()
237
238 if self.global_id:
239 self.global_id.delete()
240
241 thread = self.get_thread()
242 thread.last_edit_time = timezone.now()
243 thread.save()
244
245 super(Post, self).delete(using)
246
247 logging.getLogger('boards.post.delete').info(
248 'Deleted post {}'.format(self))
249
250 def set_global_id(self, key_pair=None):
225 def set_global_id(self, key_pair=None):
251 """
226 """
252 Sets global id based on the given key pair. If no key pair is given,
227 Sets global id based on the given key pair. If no key pair is given,
253 default one is used.
228 default one is used.
254 """
229 """
255
230
256 if key_pair:
231 if key_pair:
257 key = key_pair
232 key = key_pair
258 else:
233 else:
259 try:
234 try:
260 key = KeyPair.objects.get(primary=True)
235 key = KeyPair.objects.get(primary=True)
261 except KeyPair.DoesNotExist:
236 except KeyPair.DoesNotExist:
262 # Do not update the global id because there is no key defined
237 # Do not update the global id because there is no key defined
263 return
238 return
264 global_id = GlobalId(key_type=key.key_type,
239 global_id = GlobalId(key_type=key.key_type,
265 key=key.public_key,
240 key=key.public_key,
266 local_id=self.id)
241 local_id=self.id)
267 global_id.save()
242 global_id.save()
268
243
269 self.global_id = global_id
244 self.global_id = global_id
270
245
271 self.save(update_fields=['global_id'])
246 self.save(update_fields=['global_id'])
272
247
273 def get_pub_time_str(self):
248 def get_pub_time_str(self):
274 return str(self.pub_time)
249 return str(self.pub_time)
275
250
276 def get_replied_ids(self):
251 def get_replied_ids(self):
277 """
252 """
278 Gets ID list of the posts that this post replies.
253 Gets ID list of the posts that this post replies.
279 """
254 """
280
255
281 raw_text = self.get_raw_text()
256 raw_text = self.get_raw_text()
282
257
283 local_replied = REGEX_REPLY.findall(raw_text)
258 local_replied = REGEX_REPLY.findall(raw_text)
284 global_replied = []
259 global_replied = []
285 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
260 for match in REGEX_GLOBAL_REPLY.findall(raw_text):
286 key_type = match[0]
261 key_type = match[0]
287 key = match[1]
262 key = match[1]
288 local_id = match[2]
263 local_id = match[2]
289
264
290 try:
265 try:
291 global_id = GlobalId.objects.get(key_type=key_type,
266 global_id = GlobalId.objects.get(key_type=key_type,
292 key=key, local_id=local_id)
267 key=key, local_id=local_id)
293 for post in Post.objects.filter(global_id=global_id).only('id'):
268 for post in Post.objects.filter(global_id=global_id).only('id'):
294 global_replied.append(post.id)
269 global_replied.append(post.id)
295 except GlobalId.DoesNotExist:
270 except GlobalId.DoesNotExist:
296 pass
271 pass
297 return local_replied + global_replied
272 return local_replied + global_replied
298
273
299 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
274 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
300 include_last_update=False) -> str:
275 include_last_update=False) -> str:
301 """
276 """
302 Gets post HTML or JSON data that can be rendered on a page or used by
277 Gets post HTML or JSON data that can be rendered on a page or used by
303 API.
278 API.
304 """
279 """
305
280
306 return get_exporter(format_type).export(self, request,
281 return get_exporter(format_type).export(self, request,
307 include_last_update)
282 include_last_update)
308
283
309 def notify_clients(self, recursive=True):
284 def notify_clients(self, recursive=True):
310 """
285 """
311 Sends post HTML data to the thread web socket.
286 Sends post HTML data to the thread web socket.
312 """
287 """
313
288
314 if not settings.get_bool('External', 'WebsocketsEnabled'):
289 if not settings.get_bool('External', 'WebsocketsEnabled'):
315 return
290 return
316
291
317 thread_ids = list()
292 thread_ids = list()
318 for thread in self.get_threads().all():
293 for thread in self.get_threads().all():
319 thread_ids.append(thread.id)
294 thread_ids.append(thread.id)
320
295
321 thread.notify_clients()
296 thread.notify_clients()
322
297
323 if recursive:
298 if recursive:
324 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
299 for reply_number in re.finditer(REGEX_REPLY, self.get_raw_text()):
325 post_id = reply_number.group(1)
300 post_id = reply_number.group(1)
326
301
327 try:
302 try:
328 ref_post = Post.objects.get(id=post_id)
303 ref_post = Post.objects.get(id=post_id)
329
304
330 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
305 if ref_post.get_threads().exclude(id__in=thread_ids).exists():
331 # If post is in this thread, its thread was already notified.
306 # If post is in this thread, its thread was already notified.
332 # Otherwise, notify its thread separately.
307 # Otherwise, notify its thread separately.
333 ref_post.notify_clients(recursive=False)
308 ref_post.notify_clients(recursive=False)
334 except ObjectDoesNotExist:
309 except ObjectDoesNotExist:
335 pass
310 pass
336
311
337 def build_url(self):
312 def build_url(self):
338 self.url = self.get_absolute_url()
313 self.url = self.get_absolute_url()
339 self.save(update_fields=['url'])
314 self.save(update_fields=['url'])
340
315
341 def save(self, force_insert=False, force_update=False, using=None,
316 def save(self, force_insert=False, force_update=False, using=None,
342 update_fields=None):
317 update_fields=None):
343 new_post = self.id is None
318 new_post = self.id is None
344
319
345 self.uid = str(uuid.uuid4())
320 self.uid = str(uuid.uuid4())
346 if update_fields is not None and 'uid' not in update_fields:
321 if update_fields is not None and 'uid' not in update_fields:
347 update_fields += ['uid']
322 update_fields += ['uid']
348
323
349 if not new_post:
324 if not new_post:
350 for thread in self.get_threads().all():
325 for thread in self.get_threads().all():
351 thread.last_edit_time = self.last_edit_time
326 thread.last_edit_time = self.last_edit_time
352
327
353 thread.save(update_fields=['last_edit_time', 'status'])
328 thread.save(update_fields=['last_edit_time', 'status'])
354
329
355 super().save(force_insert, force_update, using, update_fields)
330 super().save(force_insert, force_update, using, update_fields)
356
331
357 if self.url is None:
332 if self.url is None:
358 self.build_url()
333 self.build_url()
359
334
360 def get_text(self) -> str:
335 def get_text(self) -> str:
361 return self._text_rendered
336 return self._text_rendered
362
337
363 def get_raw_text(self) -> str:
338 def get_raw_text(self) -> str:
364 return self.text
339 return self.text
365
340
366 def get_sync_text(self) -> str:
341 def get_sync_text(self) -> str:
367 """
342 """
368 Returns text applicable for sync. It has absolute post reflinks.
343 Returns text applicable for sync. It has absolute post reflinks.
369 """
344 """
370
345
371 replacements = dict()
346 replacements = dict()
372 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
347 for post_id in REGEX_REPLY.findall(self.get_raw_text()):
373 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
348 absolute_post_id = str(Post.objects.get(id=post_id).global_id)
374 replacements[post_id] = absolute_post_id
349 replacements[post_id] = absolute_post_id
375
350
376 text = self.get_raw_text()
351 text = self.get_raw_text() or ''
377 for key in replacements:
352 for key in replacements:
378 text = text.replace('[post]{}[/post]'.format(key),
353 text = text.replace('[post]{}[/post]'.format(key),
379 '[post]{}[/post]'.format(replacements[key]))
354 '[post]{}[/post]'.format(replacements[key]))
380 text = text.replace('\r\n', '\n').replace('\r', '\n')
355 text = text.replace('\r\n', '\n').replace('\r', '\n')
381
356
382 return text
357 return text
383
358
384 def get_absolute_id(self) -> str:
359 def get_absolute_id(self) -> str:
385 """
360 """
386 If the post has many threads, shows its main thread OP id in the post
361 If the post has many threads, shows its main thread OP id in the post
387 ID.
362 ID.
388 """
363 """
389
364
390 if self.get_threads().count() > 1:
365 if self.get_threads().count() > 1:
391 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
366 return '{}/{}'.format(self.get_thread().get_opening_post_id(), self.id)
392 else:
367 else:
393 return str(self.id)
368 return str(self.id)
394
369
395
370
396 def connect_threads(self, opening_posts):
371 def connect_threads(self, opening_posts):
397 for opening_post in opening_posts:
372 for opening_post in opening_posts:
398 threads = opening_post.get_threads().all()
373 threads = opening_post.get_threads().all()
399 for thread in threads:
374 for thread in threads:
400 if thread.can_bump():
375 if thread.can_bump():
401 thread.update_bump_status()
376 thread.update_bump_status()
402
377
403 thread.last_edit_time = self.last_edit_time
378 thread.last_edit_time = self.last_edit_time
404 thread.save(update_fields=['last_edit_time', 'status'])
379 thread.save(update_fields=['last_edit_time', 'status'])
405 self.threads.add(opening_post.get_thread())
380 self.threads.add(opening_post.get_thread())
406
381
407 def get_tripcode(self):
382 def get_tripcode(self):
408 if self.tripcode:
383 if self.tripcode:
409 return Tripcode(self.tripcode)
384 return Tripcode(self.tripcode)
410
385
411 def get_link_view(self):
386 def get_link_view(self):
412 """
387 """
413 Gets view of a reflink to the post.
388 Gets view of a reflink to the post.
414 """
389 """
415 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
390 result = '<a href="{}">&gt;&gt;{}</a>'.format(self.get_absolute_url(),
416 self.id)
391 self.id)
417 if self.is_opening():
392 if self.is_opening():
418 result = '<b>{}</b>'.format(result)
393 result = '<b>{}</b>'.format(result)
419
394
420 return result
395 return result
421
396
422 def is_hidden(self) -> bool:
397 def is_hidden(self) -> bool:
423 return self.hidden
398 return self.hidden
424
399
425 def set_hidden(self, hidden):
400 def set_hidden(self, hidden):
426 self.hidden = hidden
401 self.hidden = hidden
427
402
428
403
429 # SIGNALS (Maybe move to other module?)
404 # SIGNALS (Maybe move to other module?)
430 @receiver(post_save, sender=Post)
405 @receiver(post_save, sender=Post)
431 def connect_replies(instance, **kwargs):
406 def connect_replies(instance, **kwargs):
432 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
407 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
433 post_id = reply_number.group(1)
408 post_id = reply_number.group(1)
434
409
435 try:
410 try:
436 referenced_post = Post.objects.get(id=post_id)
411 referenced_post = Post.objects.get(id=post_id)
437
412
438 referenced_post.referenced_posts.add(instance)
413 referenced_post.referenced_posts.add(instance)
439 referenced_post.last_edit_time = instance.pub_time
414 referenced_post.last_edit_time = instance.pub_time
440 referenced_post.build_refmap()
415 referenced_post.build_refmap()
441 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
416 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
442 except ObjectDoesNotExist:
417 except ObjectDoesNotExist:
443 pass
418 pass
444
419
445
420
446 @receiver(post_save, sender=Post)
421 @receiver(post_save, sender=Post)
447 def connect_notifications(instance, **kwargs):
422 def connect_notifications(instance, **kwargs):
448 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
423 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
449 user_name = reply_number.group(1).lower()
424 user_name = reply_number.group(1).lower()
450 Notification.objects.get_or_create(name=user_name, post=instance)
425 Notification.objects.get_or_create(name=user_name, post=instance)
451
426
452
427
453 @receiver(pre_save, sender=Post)
428 @receiver(pre_save, sender=Post)
454 def preparse_text(instance, **kwargs):
429 def preparse_text(instance, **kwargs):
455 instance._text_rendered = get_parser().parse(instance.get_raw_text())
430 instance._text_rendered = get_parser().parse(instance.get_raw_text())
431
432
433 @receiver(pre_delete, sender=Post)
434 def delete_images(instance, **kwargs):
435 for image in instance.images.all():
436 image_refs_count = image.post_images.count()
437 if image_refs_count == 1:
438 image.delete()
439
440
441 @receiver(pre_delete, sender=Post)
442 def delete_attachments(instance, **kwargs):
443 for attachment in instance.attachments.all():
444 attachment_refs_count = attachment.attachment_posts.count()
445 if attachment_refs_count == 1:
446 attachment.delete()
447
448
449 @receiver(post_delete, sender=Post)
450 def update_thread_on_delete(instance, **kwargs):
451 thread = instance.get_thread()
452 thread.last_edit_time = timezone.now()
453 thread.save()
@@ -1,154 +1,160 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, images=[]):
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
84 if file:
83 if file:
85 file_type = file.name.split('.')[-1].lower()
84 self._add_file_to_post(file, post)
86 if file_type in IMAGE_TYPES:
87 post.images.add(PostImage.objects.create_with_hash(file))
88 else:
89 post.attachments.add(Attachment.objects.create_with_hash(file))
90 for image in images:
85 for image in images:
91 post.images.add(image)
86 post.images.add(image)
92
87
93 post.connect_threads(opening_posts)
88 post.connect_threads(opening_posts)
94 post.set_global_id()
89 post.set_global_id()
95
90
96 # Thread needs to be bumped only when the post is already created
91 # Thread needs to be bumped only when the post is already created
97 if not new_thread:
92 if not new_thread:
98 thread.last_edit_time = posting_time
93 thread.last_edit_time = posting_time
99 thread.bump()
94 thread.bump()
100 thread.save()
95 thread.save()
101
96
102 return post
97 return post
103
98
104 def delete_posts_by_ip(self, ip):
99 def delete_posts_by_ip(self, ip):
105 """
100 """
106 Deletes all posts of the author with same IP
101 Deletes all posts of the author with same IP
107 """
102 """
108
103
109 posts = self.filter(poster_ip=ip)
104 posts = self.filter(poster_ip=ip)
110 for post in posts:
105 for post in posts:
111 post.delete()
106 post.delete()
112
107
113 @utils.cached_result()
108 @utils.cached_result()
114 def get_posts_per_day(self) -> float:
109 def get_posts_per_day(self) -> float:
115 """
110 """
116 Gets average count of posts per day for the last 7 days
111 Gets average count of posts per day for the last 7 days
117 """
112 """
118
113
119 day_end = date.today()
114 day_end = date.today()
120 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
115 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
121
116
122 day_time_start = timezone.make_aware(datetime.combine(
117 day_time_start = timezone.make_aware(datetime.combine(
123 day_start, dtime()), timezone.get_current_timezone())
118 day_start, dtime()), timezone.get_current_timezone())
124 day_time_end = timezone.make_aware(datetime.combine(
119 day_time_end = timezone.make_aware(datetime.combine(
125 day_end, dtime()), timezone.get_current_timezone())
120 day_end, dtime()), timezone.get_current_timezone())
126
121
127 posts_per_period = float(self.filter(
122 posts_per_period = float(self.filter(
128 pub_time__lte=day_time_end,
123 pub_time__lte=day_time_end,
129 pub_time__gte=day_time_start).count())
124 pub_time__gte=day_time_start).count())
130
125
131 ppd = posts_per_period / POSTS_PER_DAY_RANGE
126 ppd = posts_per_period / POSTS_PER_DAY_RANGE
132
127
133 return ppd
128 return ppd
134
129
135 @transaction.atomic
130 @transaction.atomic
136 def import_post(self, title: str, text: str, pub_time: str, global_id,
131 def import_post(self, title: str, text: str, pub_time: str, global_id,
137 opening_post=None, tags=list()):
132 opening_post=None, tags=list(), files=list()):
138 is_opening = opening_post is None
133 is_opening = opening_post is None
139 if is_opening:
134 if is_opening:
140 thread = boards.models.thread.Thread.objects.create(
135 thread = boards.models.thread.Thread.objects.create(
141 bump_time=pub_time, last_edit_time=pub_time)
136 bump_time=pub_time, last_edit_time=pub_time)
142 list(map(thread.tags.add, tags))
137 list(map(thread.tags.add, tags))
143 else:
138 else:
144 thread = opening_post.get_thread()
139 thread = opening_post.get_thread()
145
140
146 post = self.create(title=title, text=text,
141 post = self.create(title=title, text=text,
147 pub_time=pub_time,
142 pub_time=pub_time,
148 poster_ip=NO_IP,
143 poster_ip=NO_IP,
149 last_edit_time=pub_time,
144 last_edit_time=pub_time,
150 global_id=global_id,
145 global_id=global_id,
151 opening=is_opening,
146 opening=is_opening,
152 thread=thread)
147 thread=thread)
153
148
149 # TODO Add files
150 for file in files:
151 self._add_file_to_post(file, post)
152
154 post.threads.add(thread)
153 post.threads.add(thread)
154
155 def _add_file_to_post(self, file, post):
156 file_type = file.name.split('.')[-1].lower()
157 if file_type in IMAGE_TYPES:
158 post.images.add(PostImage.objects.create_with_hash(file))
159 else:
160 post.attachments.add(Attachment.objects.create_with_hash(file))
@@ -1,228 +1,244 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2
2
3 from boards.models.attachment.downloaders import download
3 from boards.utils import get_file_mimetype
4 from boards.utils import get_file_mimetype
4 from django.db import transaction
5 from django.db import transaction
5 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
6 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
6
7
7 ENCODING_UNICODE = 'unicode'
8 ENCODING_UNICODE = 'unicode'
8
9
9 TAG_MODEL = 'model'
10 TAG_MODEL = 'model'
10 TAG_REQUEST = 'request'
11 TAG_REQUEST = 'request'
11 TAG_RESPONSE = 'response'
12 TAG_RESPONSE = 'response'
12 TAG_ID = 'id'
13 TAG_ID = 'id'
13 TAG_STATUS = 'status'
14 TAG_STATUS = 'status'
14 TAG_MODELS = 'models'
15 TAG_MODELS = 'models'
15 TAG_TITLE = 'title'
16 TAG_TITLE = 'title'
16 TAG_TEXT = 'text'
17 TAG_TEXT = 'text'
17 TAG_THREAD = 'thread'
18 TAG_THREAD = 'thread'
18 TAG_PUB_TIME = 'pub-time'
19 TAG_PUB_TIME = 'pub-time'
19 TAG_SIGNATURES = 'signatures'
20 TAG_SIGNATURES = 'signatures'
20 TAG_SIGNATURE = 'signature'
21 TAG_SIGNATURE = 'signature'
21 TAG_CONTENT = 'content'
22 TAG_CONTENT = 'content'
22 TAG_ATTACHMENTS = 'attachments'
23 TAG_ATTACHMENTS = 'attachments'
23 TAG_ATTACHMENT = 'attachment'
24 TAG_ATTACHMENT = 'attachment'
24 TAG_TAGS = 'tags'
25 TAG_TAGS = 'tags'
25 TAG_TAG = 'tag'
26 TAG_TAG = 'tag'
26 TAG_ATTACHMENT_REFS = 'attachment-refs'
27 TAG_ATTACHMENT_REFS = 'attachment-refs'
27 TAG_ATTACHMENT_REF = 'attachment-ref'
28 TAG_ATTACHMENT_REF = 'attachment-ref'
28
29
29 TYPE_GET = 'get'
30 TYPE_GET = 'get'
30
31
31 ATTR_VERSION = 'version'
32 ATTR_VERSION = 'version'
32 ATTR_TYPE = 'type'
33 ATTR_TYPE = 'type'
33 ATTR_NAME = 'name'
34 ATTR_NAME = 'name'
34 ATTR_VALUE = 'value'
35 ATTR_VALUE = 'value'
35 ATTR_MIMETYPE = 'mimetype'
36 ATTR_MIMETYPE = 'mimetype'
36 ATTR_KEY = 'key'
37 ATTR_KEY = 'key'
37 ATTR_REF = 'ref'
38 ATTR_REF = 'ref'
38 ATTR_URL = 'url'
39 ATTR_URL = 'url'
39
40
40 STATUS_SUCCESS = 'success'
41 STATUS_SUCCESS = 'success'
41
42
42
43
44 class SyncException(Exception):
45 pass
46
47
43 class SyncManager:
48 class SyncManager:
44 @staticmethod
49 @staticmethod
45 def generate_response_get(model_list: list):
50 def generate_response_get(model_list: list):
46 response = et.Element(TAG_RESPONSE)
51 response = et.Element(TAG_RESPONSE)
47
52
48 status = et.SubElement(response, TAG_STATUS)
53 status = et.SubElement(response, TAG_STATUS)
49 status.text = STATUS_SUCCESS
54 status.text = STATUS_SUCCESS
50
55
51 models = et.SubElement(response, TAG_MODELS)
56 models = et.SubElement(response, TAG_MODELS)
52
57
53 for post in model_list:
58 for post in model_list:
54 model = et.SubElement(models, TAG_MODEL)
59 model = et.SubElement(models, TAG_MODEL)
55 model.set(ATTR_NAME, 'post')
60 model.set(ATTR_NAME, 'post')
56
61
57 content_tag = et.SubElement(model, TAG_CONTENT)
62 content_tag = et.SubElement(model, TAG_CONTENT)
58
63
59 tag_id = et.SubElement(content_tag, TAG_ID)
64 tag_id = et.SubElement(content_tag, TAG_ID)
60 post.global_id.to_xml_element(tag_id)
65 post.global_id.to_xml_element(tag_id)
61
66
62 title = et.SubElement(content_tag, TAG_TITLE)
67 title = et.SubElement(content_tag, TAG_TITLE)
63 title.text = post.title
68 title.text = post.title
64
69
65 text = et.SubElement(content_tag, TAG_TEXT)
70 text = et.SubElement(content_tag, TAG_TEXT)
66 text.text = post.get_sync_text()
71 text.text = post.get_sync_text()
67
72
68 thread = post.get_thread()
73 thread = post.get_thread()
69 if post.is_opening():
74 if post.is_opening():
70 tag_tags = et.SubElement(content_tag, TAG_TAGS)
75 tag_tags = et.SubElement(content_tag, TAG_TAGS)
71 for tag in thread.get_tags():
76 for tag in thread.get_tags():
72 tag_tag = et.SubElement(tag_tags, TAG_TAG)
77 tag_tag = et.SubElement(tag_tags, TAG_TAG)
73 tag_tag.text = tag.name
78 tag_tag.text = tag.name
74 else:
79 else:
75 tag_thread = et.SubElement(content_tag, TAG_THREAD)
80 tag_thread = et.SubElement(content_tag, TAG_THREAD)
76 thread_id = et.SubElement(tag_thread, TAG_ID)
81 thread_id = et.SubElement(tag_thread, TAG_ID)
77 thread.get_opening_post().global_id.to_xml_element(thread_id)
82 thread.get_opening_post().global_id.to_xml_element(thread_id)
78
83
79 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
84 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
80 pub_time.text = str(post.get_pub_time_str())
85 pub_time.text = str(post.get_pub_time_str())
81
86
82 images = post.images.all()
87 images = post.images.all()
83 attachments = post.attachments.all()
88 attachments = post.attachments.all()
84 if len(images) > 0 or len(attachments) > 0:
89 if len(images) > 0 or len(attachments) > 0:
85 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
90 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
86 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
91 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
87
92
88 for image in images:
93 for image in images:
89 SyncManager._attachment_to_xml(
94 SyncManager._attachment_to_xml(
90 attachments_tag, attachment_refs, image.image.file,
95 attachments_tag, attachment_refs, image.image.file,
91 image.hash, image.image.url)
96 image.hash, image.image.url)
92 for file in attachments:
97 for file in attachments:
93 SyncManager._attachment_to_xml(
98 SyncManager._attachment_to_xml(
94 attachments_tag, attachment_refs, file.file.file,
99 attachments_tag, attachment_refs, file.file.file,
95 file.hash, file.file.url)
100 file.hash, file.file.url)
96
101
97 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
102 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
98 post_signatures = post.global_id.signature_set.all()
103 post_signatures = post.global_id.signature_set.all()
99 if post_signatures:
104 if post_signatures:
100 signatures = post_signatures
105 signatures = post_signatures
101 else:
106 else:
102 key = KeyPair.objects.get(public_key=post.global_id.key)
107 key = KeyPair.objects.get(public_key=post.global_id.key)
103 signature = Signature(
108 signature = Signature(
104 key_type=key.key_type,
109 key_type=key.key_type,
105 key=key.public_key,
110 key=key.public_key,
106 signature=key.sign(et.tostring(content_tag, encoding=ENCODING_UNICODE)),
111 signature=key.sign(et.tostring(content_tag, encoding=ENCODING_UNICODE)),
107 global_id=post.global_id,
112 global_id=post.global_id,
108 )
113 )
109 signature.save()
114 signature.save()
110 signatures = [signature]
115 signatures = [signature]
111 for signature in signatures:
116 for signature in signatures:
112 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
117 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
113 signature_tag.set(ATTR_TYPE, signature.key_type)
118 signature_tag.set(ATTR_TYPE, signature.key_type)
114 signature_tag.set(ATTR_VALUE, signature.signature)
119 signature_tag.set(ATTR_VALUE, signature.signature)
115 signature_tag.set(ATTR_KEY, signature.key)
120 signature_tag.set(ATTR_KEY, signature.key)
116
121
117 return et.tostring(response, ENCODING_UNICODE)
122 return et.tostring(response, ENCODING_UNICODE)
118
123
119 @staticmethod
124 @staticmethod
120 @transaction.atomic
125 @transaction.atomic
121 def parse_response_get(response_xml):
126 def parse_response_get(response_xml, hostname):
122 tag_root = et.fromstring(response_xml)
127 tag_root = et.fromstring(response_xml)
123 tag_status = tag_root.find(TAG_STATUS)
128 tag_status = tag_root.find(TAG_STATUS)
124 if STATUS_SUCCESS == tag_status.text:
129 if STATUS_SUCCESS == tag_status.text:
125 tag_models = tag_root.find(TAG_MODELS)
130 tag_models = tag_root.find(TAG_MODELS)
126 for tag_model in tag_models:
131 for tag_model in tag_models:
127 tag_content = tag_model.find(TAG_CONTENT)
132 tag_content = tag_model.find(TAG_CONTENT)
128
133
129 signatures = SyncManager._verify_model(tag_content, tag_model)
134 signatures = SyncManager._verify_model(tag_content, tag_model)
130
135
131 tag_id = tag_content.find(TAG_ID)
136 tag_id = tag_content.find(TAG_ID)
132 global_id, exists = GlobalId.from_xml_element(tag_id)
137 global_id, exists = GlobalId.from_xml_element(tag_id)
133
138
134 if exists:
139 if exists:
135 print('Post with same ID already exists')
140 print('Post with same ID already exists')
136 else:
141 else:
137 global_id.save()
142 global_id.save()
138 for signature in signatures:
143 for signature in signatures:
139 signature.global_id = global_id
144 signature.global_id = global_id
140 signature.save()
145 signature.save()
141
146
142 title = tag_content.find(TAG_TITLE).text
147 title = tag_content.find(TAG_TITLE).text or ''
143 text = tag_content.find(TAG_TEXT).text
148 text = tag_content.find(TAG_TEXT).text or ''
144 pub_time = tag_content.find(TAG_PUB_TIME).text
149 pub_time = tag_content.find(TAG_PUB_TIME).text
145
150
146 thread = tag_content.find(TAG_THREAD)
151 thread = tag_content.find(TAG_THREAD)
147 tags = []
152 tags = []
148 if thread:
153 if thread:
149 thread_id = thread.find(TAG_ID)
154 thread_id = thread.find(TAG_ID)
150 op_global_id, exists = GlobalId.from_xml_element(thread_id)
155 op_global_id, exists = GlobalId.from_xml_element(thread_id)
151 if exists:
156 if exists:
152 opening_post = Post.objects.get(global_id=op_global_id)
157 opening_post = Post.objects.get(global_id=op_global_id)
153 else:
158 else:
154 raise Exception('Load the OP first')
159 raise SyncException('Load the OP first')
155 else:
160 else:
156 opening_post = None
161 opening_post = None
157 tag_tags = tag_content.find(TAG_TAGS)
162 tag_tags = tag_content.find(TAG_TAGS)
158 for tag_tag in tag_tags:
163 for tag_tag in tag_tags:
159 tag, created = Tag.objects.get_or_create(
164 tag, created = Tag.objects.get_or_create(
160 name=tag_tag.text)
165 name=tag_tag.text)
161 tags.append(tag)
166 tags.append(tag)
162
167
163 # TODO Check that the replied posts are already present
168 # TODO Check that the replied posts are already present
164 # before adding new ones
169 # before adding new ones
165
170
166 # TODO Get images
171 files = []
172 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
173 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
174 for attachment in tag_attachments:
175 tag_ref = tag_refs.find("{}[@ref='{}']".format(
176 TAG_ATTACHMENT_REF, attachment.text))
177 url = tag_ref.get(ATTR_URL)
178 attached_file = download(hostname + url)
179 if attached_file is None:
180 raise SyncException('File was not dowloaded')
181 files.append(attached_file)
182 # TODO Check hash
167
183
168 post = Post.objects.import_post(
184 Post.objects.import_post(
169 title=title, text=text, pub_time=pub_time,
185 title=title, text=text, pub_time=pub_time,
170 opening_post=opening_post, tags=tags,
186 opening_post=opening_post, tags=tags,
171 global_id=global_id)
187 global_id=global_id, files=files)
172 else:
188 else:
173 # TODO Throw an exception?
189 # TODO Throw an exception?
174 pass
190 pass
175
191
176 @staticmethod
192 @staticmethod
177 def generate_response_pull():
193 def generate_response_pull():
178 response = et.Element(TAG_RESPONSE)
194 response = et.Element(TAG_RESPONSE)
179
195
180 status = et.SubElement(response, TAG_STATUS)
196 status = et.SubElement(response, TAG_STATUS)
181 status.text = STATUS_SUCCESS
197 status.text = STATUS_SUCCESS
182
198
183 models = et.SubElement(response, TAG_MODELS)
199 models = et.SubElement(response, TAG_MODELS)
184
200
185 for post in Post.objects.all():
201 for post in Post.objects.all():
186 tag_id = et.SubElement(models, TAG_ID)
202 tag_id = et.SubElement(models, TAG_ID)
187 post.global_id.to_xml_element(tag_id)
203 post.global_id.to_xml_element(tag_id)
188
204
189 return et.tostring(response, ENCODING_UNICODE)
205 return et.tostring(response, ENCODING_UNICODE)
190
206
191 @staticmethod
207 @staticmethod
192 def _verify_model(tag_content, tag_model):
208 def _verify_model(tag_content, tag_model):
193 """
209 """
194 Verifies all signatures for a single model.
210 Verifies all signatures for a single model.
195 """
211 """
196
212
197 signatures = []
213 signatures = []
198
214
199 tag_signatures = tag_model.find(TAG_SIGNATURES)
215 tag_signatures = tag_model.find(TAG_SIGNATURES)
200 for tag_signature in tag_signatures:
216 for tag_signature in tag_signatures:
201 signature_type = tag_signature.get(ATTR_TYPE)
217 signature_type = tag_signature.get(ATTR_TYPE)
202 signature_value = tag_signature.get(ATTR_VALUE)
218 signature_value = tag_signature.get(ATTR_VALUE)
203 signature_key = tag_signature.get(ATTR_KEY)
219 signature_key = tag_signature.get(ATTR_KEY)
204
220
205 signature = Signature(key_type=signature_type,
221 signature = Signature(key_type=signature_type,
206 key=signature_key,
222 key=signature_key,
207 signature=signature_value)
223 signature=signature_value)
208
224
209 content = et.tostring(tag_content, ENCODING_UNICODE)
225 content = et.tostring(tag_content, ENCODING_UNICODE)
210
226
211 if not KeyPair.objects.verify(
227 if not KeyPair.objects.verify(
212 signature, content):
228 signature, content):
213 raise Exception('Invalid model signature for {}'.format(content))
229 raise SyncException('Invalid model signature for {}'.format(content))
214
230
215 signatures.append(signature)
231 signatures.append(signature)
216
232
217 return signatures
233 return signatures
218
234
219 @staticmethod
235 @staticmethod
220 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
236 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
221 mimetype = get_file_mimetype(file)
237 mimetype = get_file_mimetype(file)
222 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
238 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
223 attachment.set(ATTR_MIMETYPE, mimetype)
239 attachment.set(ATTR_MIMETYPE, mimetype)
224 attachment.text = hash
240 attachment.text = hash
225
241
226 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
242 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
227 attachment_ref.set(ATTR_REF, hash)
243 attachment_ref.set(ATTR_REF, hash)
228 attachment_ref.set(ATTR_URL, url)
244 attachment_ref.set(ATTR_URL, url)
@@ -1,102 +1,102 b''
1 from boards.models import KeyPair, Post, Tag
1 from boards.models import KeyPair, Post, Tag
2 from boards.models.post.sync import SyncManager
2 from boards.models.post.sync import SyncManager
3 from boards.tests.mocks import MockRequest
3 from boards.tests.mocks import MockRequest
4 from boards.views.sync import response_get
4 from boards.views.sync import response_get
5
5
6 __author__ = 'neko259'
6 __author__ = 'neko259'
7
7
8
8
9 from django.test import TestCase
9 from django.test import TestCase
10
10
11
11
12 class SyncTest(TestCase):
12 class SyncTest(TestCase):
13 def test_get(self):
13 def test_get(self):
14 """
14 """
15 Forms a GET request of a post and checks the response.
15 Forms a GET request of a post and checks the response.
16 """
16 """
17
17
18 key = KeyPair.objects.generate_key(primary=True)
18 key = KeyPair.objects.generate_key(primary=True)
19 tag = Tag.objects.create(name='tag1')
19 tag = Tag.objects.create(name='tag1')
20 post = Post.objects.create_post(title='test_title',
20 post = Post.objects.create_post(title='test_title',
21 text='test_text\rline two',
21 text='test_text\rline two',
22 tags=[tag])
22 tags=[tag])
23
23
24 request = MockRequest()
24 request = MockRequest()
25 request.body = (
25 request.body = (
26 '<request type="get" version="1.0">'
26 '<request type="get" version="1.0">'
27 '<model name="post" version="1.0">'
27 '<model name="post" version="1.0">'
28 '<id key="%s" local-id="%d" type="%s" />'
28 '<id key="%s" local-id="%d" type="%s" />'
29 '</model>'
29 '</model>'
30 '</request>' % (post.global_id.key,
30 '</request>' % (post.global_id.key,
31 post.id,
31 post.id,
32 post.global_id.key_type)
32 post.global_id.key_type)
33 )
33 )
34
34
35 response = response_get(request).content.decode()
35 response = response_get(request).content.decode()
36 self.assertTrue(
36 self.assertTrue(
37 '<status>success</status>'
37 '<status>success</status>'
38 '<models>'
38 '<models>'
39 '<model name="post">'
39 '<model name="post">'
40 '<content>'
40 '<content>'
41 '<id key="%s" local-id="%d" type="%s" />'
41 '<id key="%s" local-id="%d" type="%s" />'
42 '<title>%s</title>'
42 '<title>%s</title>'
43 '<text>%s</text>'
43 '<text>%s</text>'
44 '<tags><tag>%s</tag></tags>'
44 '<tags><tag>%s</tag></tags>'
45 '<pub-time>%s</pub-time>'
45 '<pub-time>%s</pub-time>'
46 '</content>' % (
46 '</content>' % (
47 post.global_id.key,
47 post.global_id.key,
48 post.global_id.local_id,
48 post.global_id.local_id,
49 post.global_id.key_type,
49 post.global_id.key_type,
50 post.title,
50 post.title,
51 post.get_sync_text(),
51 post.get_sync_text(),
52 post.get_thread().get_tags().first().name,
52 post.get_thread().get_tags().first().name,
53 post.get_pub_time_str(),
53 post.get_pub_time_str(),
54 ) in response,
54 ) in response,
55 'Wrong response generated for the GET request.')
55 'Wrong response generated for the GET request.')
56
56
57 post.delete()
57 post.delete()
58 key.delete()
58 key.delete()
59
59
60 KeyPair.objects.generate_key(primary=True)
60 KeyPair.objects.generate_key(primary=True)
61
61
62 SyncManager.parse_response_get(response)
62 SyncManager.parse_response_get(response, None)
63 self.assertEqual(1, Post.objects.count(),
63 self.assertEqual(1, Post.objects.count(),
64 'Post was not created from XML response.')
64 'Post was not created from XML response.')
65
65
66 parsed_post = Post.objects.first()
66 parsed_post = Post.objects.first()
67 self.assertEqual('tag1',
67 self.assertEqual('tag1',
68 parsed_post.get_thread().get_tags().first().name,
68 parsed_post.get_thread().get_tags().first().name,
69 'Invalid tag was parsed.')
69 'Invalid tag was parsed.')
70
70
71 SyncManager.parse_response_get(response)
71 SyncManager.parse_response_get(response, None)
72 self.assertEqual(1, Post.objects.count(),
72 self.assertEqual(1, Post.objects.count(),
73 'The same post was imported twice.')
73 'The same post was imported twice.')
74
74
75 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
75 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
76 'Signature was not saved.')
76 'Signature was not saved.')
77
77
78 post = parsed_post
78 post = parsed_post
79
79
80 # Trying to sync the same once more
80 # Trying to sync the same once more
81 response = response_get(request).content.decode()
81 response = response_get(request).content.decode()
82
82
83 self.assertTrue(
83 self.assertTrue(
84 '<status>success</status>'
84 '<status>success</status>'
85 '<models>'
85 '<models>'
86 '<model name="post">'
86 '<model name="post">'
87 '<content>'
87 '<content>'
88 '<id key="%s" local-id="%d" type="%s" />'
88 '<id key="%s" local-id="%d" type="%s" />'
89 '<title>%s</title>'
89 '<title>%s</title>'
90 '<text>%s</text>'
90 '<text>%s</text>'
91 '<tags><tag>%s</tag></tags>'
91 '<tags><tag>%s</tag></tags>'
92 '<pub-time>%s</pub-time>'
92 '<pub-time>%s</pub-time>'
93 '</content>' % (
93 '</content>' % (
94 post.global_id.key,
94 post.global_id.key,
95 post.global_id.local_id,
95 post.global_id.local_id,
96 post.global_id.key_type,
96 post.global_id.key_type,
97 post.title,
97 post.title,
98 post.get_sync_text(),
98 post.get_sync_text(),
99 post.get_thread().get_tags().first().name,
99 post.get_thread().get_tags().first().name,
100 post.get_pub_time_str(),
100 post.get_pub_time_str(),
101 ) in response,
101 ) in response,
102 'Wrong response generated for the GET request.')
102 'Wrong response generated for the GET request.')
@@ -1,56 +1,62 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2 import xml.dom.minidom
3
2 from django.http import HttpResponse, Http404
4 from django.http import HttpResponse, Http404
3 from boards.models import GlobalId, Post
5 from boards.models import GlobalId, Post
4 from boards.models.post.sync import SyncManager
6 from boards.models.post.sync import SyncManager
5
7
6
8
7 def response_pull(request):
9 def response_pull(request):
8 request_xml = request.body
10 request_xml = request.body
9
11
10 if request_xml is None:
12 if request_xml is None:
11 return HttpResponse(content='Use the API')
13 return HttpResponse(content='Use the API')
12
14
13 response_xml = SyncManager.generate_response_pull()
15 response_xml = SyncManager.generate_response_pull()
14
16
15 return HttpResponse(content=response_xml)
17 return HttpResponse(content=response_xml)
16
18
17
19
18 def response_get(request):
20 def response_get(request):
19 """
21 """
20 Processes a GET request with post ID list and returns the posts XML list.
22 Processes a GET request with post ID list and returns the posts XML list.
21 Request should contain an 'xml' post attribute with the actual request XML.
23 Request should contain an 'xml' post attribute with the actual request XML.
22 """
24 """
23
25
24 request_xml = request.body
26 request_xml = request.body
25
27
26 if request_xml is None:
28 if request_xml is None:
27 return HttpResponse(content='Use the API')
29 return HttpResponse(content='Use the API')
28
30
29 posts = []
31 posts = []
30
32
31 root_tag = et.fromstring(request_xml)
33 root_tag = et.fromstring(request_xml)
32 model_tag = root_tag[0]
34 model_tag = root_tag[0]
33 for id_tag in model_tag:
35 for id_tag in model_tag:
34 global_id, exists = GlobalId.from_xml_element(id_tag)
36 global_id, exists = GlobalId.from_xml_element(id_tag)
35 if exists:
37 if exists:
36 posts.append(Post.objects.get(global_id=global_id))
38 posts.append(Post.objects.get(global_id=global_id))
37
39
38 response_xml = SyncManager.generate_response_get(posts)
40 response_xml = SyncManager.generate_response_get(posts)
39
41
40 return HttpResponse(content=response_xml)
42 return HttpResponse(content=response_xml)
41
43
42
44
43 def get_post_sync_data(request, post_id):
45 def get_post_sync_data(request, post_id):
44 try:
46 try:
45 post = Post.objects.get(id=post_id)
47 post = Post.objects.get(id=post_id)
46 except Post.DoesNotExist:
48 except Post.DoesNotExist:
47 raise Http404()
49 raise Http404()
48
50
49 content = 'Global ID: %s\n\nXML: %s' \
51 xml_str = SyncManager.generate_response_get([post])
50 % (post.global_id, SyncManager.generate_response_get([post]))
51
52
53 xml_repr = xml.dom.minidom.parseString(xml_str)
54 xml_repr = xml_repr.toprettyxml()
55
56 content = '=Global ID=\n%s\n\n=XML=\n%s' \
57 % (post.global_id, xml_repr)
52
58
53 return HttpResponse(
59 return HttpResponse(
54 content_type='text/plain',
60 content_type='text/plain',
55 content=content,
61 content=content,
56 ) No newline at end of file
62 )
General Comments 0
You need to be logged in to leave comments. Login now