##// END OF EJS Templates
Added max width to tag description in MD style. Recognize bmp extension from...
neko259 -
r1454:90cde611 default
parent child Browse files
Show More
@@ -1,443 +1,445 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.util import ErrorList
11 from django.forms.util 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.mdx_neboard import formatters
15 from boards.mdx_neboard import formatters
16 from boards.models.attachment.downloaders import Downloader
16 from boards.models.attachment.downloaders import Downloader
17 from boards.models.post import TITLE_MAX_LENGTH
17 from boards.models.post import TITLE_MAX_LENGTH
18 from boards.models import Tag, Post
18 from boards.models import Tag, Post
19 from boards.utils import validate_file_size, get_file_mimetype, \
19 from boards.utils import validate_file_size, get_file_mimetype, \
20 FILE_EXTENSION_DELIMITER
20 FILE_EXTENSION_DELIMITER
21 from neboard import settings
21 from neboard import settings
22 import boards.settings as board_settings
22 import boards.settings as board_settings
23 import neboard
23 import neboard
24
24
25 POW_HASH_LENGTH = 16
25 POW_HASH_LENGTH = 16
26 POW_LIFE_MINUTES = 5
26 POW_LIFE_MINUTES = 5
27
27
28 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
28 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
29 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
29 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
30
30
31 VETERAN_POSTING_DELAY = 5
31 VETERAN_POSTING_DELAY = 5
32
32
33 ATTRIBUTE_PLACEHOLDER = 'placeholder'
33 ATTRIBUTE_PLACEHOLDER = 'placeholder'
34 ATTRIBUTE_ROWS = 'rows'
34 ATTRIBUTE_ROWS = 'rows'
35
35
36 LAST_POST_TIME = 'last_post_time'
36 LAST_POST_TIME = 'last_post_time'
37 LAST_LOGIN_TIME = 'last_login_time'
37 LAST_LOGIN_TIME = 'last_login_time'
38 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
38 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
39 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
39 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
40
40
41 LABEL_TITLE = _('Title')
41 LABEL_TITLE = _('Title')
42 LABEL_TEXT = _('Text')
42 LABEL_TEXT = _('Text')
43 LABEL_TAG = _('Tag')
43 LABEL_TAG = _('Tag')
44 LABEL_SEARCH = _('Search')
44 LABEL_SEARCH = _('Search')
45
45
46 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
46 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
47 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
47 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
48
48
49 TAG_MAX_LENGTH = 20
49 TAG_MAX_LENGTH = 20
50
50
51 TEXTAREA_ROWS = 4
51 TEXTAREA_ROWS = 4
52
52
53 TRIPCODE_DELIM = '#'
53 TRIPCODE_DELIM = '#'
54
54
55 # TODO Maybe this may be converted into the database table?
55 # TODO Maybe this may be converted into the database table?
56 MIMETYPE_EXTENSIONS = {
56 MIMETYPE_EXTENSIONS = {
57 'image/jpeg': 'jpeg',
57 'image/jpeg': 'jpeg',
58 'image/png': 'png',
58 'image/png': 'png',
59 'image/gif': 'gif',
59 'image/gif': 'gif',
60 'video/webm': 'webm',
60 'video/webm': 'webm',
61 'application/pdf': 'pdf',
61 'application/pdf': 'pdf',
62 'x-diff': 'diff',
62 'x-diff': 'diff',
63 'image/svg+xml': 'svg',
63 'image/svg+xml': 'svg',
64 'application/x-shockwave-flash': 'swf',
64 'application/x-shockwave-flash': 'swf',
65 'image/x-ms-bmp': 'bmp',
66 'image/bmp': 'bmp',
65 }
67 }
66
68
67
69
68 def get_timezones():
70 def get_timezones():
69 timezones = []
71 timezones = []
70 for tz in pytz.common_timezones:
72 for tz in pytz.common_timezones:
71 timezones.append((tz, tz),)
73 timezones.append((tz, tz),)
72 return timezones
74 return timezones
73
75
74
76
75 class FormatPanel(forms.Textarea):
77 class FormatPanel(forms.Textarea):
76 """
78 """
77 Panel for text formatting. Consists of buttons to add different tags to the
79 Panel for text formatting. Consists of buttons to add different tags to the
78 form text area.
80 form text area.
79 """
81 """
80
82
81 def render(self, name, value, attrs=None):
83 def render(self, name, value, attrs=None):
82 output = '<div id="mark-panel">'
84 output = '<div id="mark-panel">'
83 for formatter in formatters:
85 for formatter in formatters:
84 output += '<span class="mark_btn"' + \
86 output += '<span class="mark_btn"' + \
85 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
87 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
86 '\', \'' + formatter.format_right + '\')">' + \
88 '\', \'' + formatter.format_right + '\')">' + \
87 formatter.preview_left + formatter.name + \
89 formatter.preview_left + formatter.name + \
88 formatter.preview_right + '</span>'
90 formatter.preview_right + '</span>'
89
91
90 output += '</div>'
92 output += '</div>'
91 output += super(FormatPanel, self).render(name, value, attrs=attrs)
93 output += super(FormatPanel, self).render(name, value, attrs=attrs)
92
94
93 return output
95 return output
94
96
95
97
96 class PlainErrorList(ErrorList):
98 class PlainErrorList(ErrorList):
97 def __unicode__(self):
99 def __unicode__(self):
98 return self.as_text()
100 return self.as_text()
99
101
100 def as_text(self):
102 def as_text(self):
101 return ''.join(['(!) %s ' % e for e in self])
103 return ''.join(['(!) %s ' % e for e in self])
102
104
103
105
104 class NeboardForm(forms.Form):
106 class NeboardForm(forms.Form):
105 """
107 """
106 Form with neboard-specific formatting.
108 Form with neboard-specific formatting.
107 """
109 """
108
110
109 def as_div(self):
111 def as_div(self):
110 """
112 """
111 Returns this form rendered as HTML <as_div>s.
113 Returns this form rendered as HTML <as_div>s.
112 """
114 """
113
115
114 return self._html_output(
116 return self._html_output(
115 # TODO Do not show hidden rows in the list here
117 # TODO Do not show hidden rows in the list here
116 normal_row='<div class="form-row">'
118 normal_row='<div class="form-row">'
117 '<div class="form-label">'
119 '<div class="form-label">'
118 '%(label)s'
120 '%(label)s'
119 '</div>'
121 '</div>'
120 '<div class="form-input">'
122 '<div class="form-input">'
121 '%(field)s'
123 '%(field)s'
122 '</div>'
124 '</div>'
123 '</div>'
125 '</div>'
124 '<div class="form-row">'
126 '<div class="form-row">'
125 '%(help_text)s'
127 '%(help_text)s'
126 '</div>',
128 '</div>',
127 error_row='<div class="form-row">'
129 error_row='<div class="form-row">'
128 '<div class="form-label"></div>'
130 '<div class="form-label"></div>'
129 '<div class="form-errors">%s</div>'
131 '<div class="form-errors">%s</div>'
130 '</div>',
132 '</div>',
131 row_ender='</div>',
133 row_ender='</div>',
132 help_text_html='%s',
134 help_text_html='%s',
133 errors_on_separate_row=True)
135 errors_on_separate_row=True)
134
136
135 def as_json_errors(self):
137 def as_json_errors(self):
136 errors = []
138 errors = []
137
139
138 for name, field in list(self.fields.items()):
140 for name, field in list(self.fields.items()):
139 if self[name].errors:
141 if self[name].errors:
140 errors.append({
142 errors.append({
141 'field': name,
143 'field': name,
142 'errors': self[name].errors.as_text(),
144 'errors': self[name].errors.as_text(),
143 })
145 })
144
146
145 return errors
147 return errors
146
148
147
149
148 class PostForm(NeboardForm):
150 class PostForm(NeboardForm):
149
151
150 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
152 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
151 label=LABEL_TITLE,
153 label=LABEL_TITLE,
152 widget=forms.TextInput(
154 widget=forms.TextInput(
153 attrs={ATTRIBUTE_PLACEHOLDER:
155 attrs={ATTRIBUTE_PLACEHOLDER:
154 'test#tripcode'}))
156 'test#tripcode'}))
155 text = forms.CharField(
157 text = forms.CharField(
156 widget=FormatPanel(attrs={
158 widget=FormatPanel(attrs={
157 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
159 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
158 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
160 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
159 }),
161 }),
160 required=False, label=LABEL_TEXT)
162 required=False, label=LABEL_TEXT)
161 file = forms.FileField(required=False, label=_('File'),
163 file = forms.FileField(required=False, label=_('File'),
162 widget=forms.ClearableFileInput(
164 widget=forms.ClearableFileInput(
163 attrs={'accept': 'file/*'}))
165 attrs={'accept': 'file/*'}))
164 file_url = forms.CharField(required=False, label=_('File URL'),
166 file_url = forms.CharField(required=False, label=_('File URL'),
165 widget=forms.TextInput(
167 widget=forms.TextInput(
166 attrs={ATTRIBUTE_PLACEHOLDER:
168 attrs={ATTRIBUTE_PLACEHOLDER:
167 'http://example.com/image.png'}))
169 'http://example.com/image.png'}))
168
170
169 # This field is for spam prevention only
171 # This field is for spam prevention only
170 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
172 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
171 widget=forms.TextInput(attrs={
173 widget=forms.TextInput(attrs={
172 'class': 'form-email'}))
174 'class': 'form-email'}))
173 threads = forms.CharField(required=False, label=_('Additional threads'),
175 threads = forms.CharField(required=False, label=_('Additional threads'),
174 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
176 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
175 '123 456 789'}))
177 '123 456 789'}))
176
178
177 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
179 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
178 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
180 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
179 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
181 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
180
182
181 session = None
183 session = None
182 need_to_ban = False
184 need_to_ban = False
183
185
184 def _update_file_extension(self, file):
186 def _update_file_extension(self, file):
185 if file:
187 if file:
186 mimetype = get_file_mimetype(file)
188 mimetype = get_file_mimetype(file)
187 extension = MIMETYPE_EXTENSIONS.get(mimetype)
189 extension = MIMETYPE_EXTENSIONS.get(mimetype)
188 if extension:
190 if extension:
189 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
191 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
190 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
192 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
191
193
192 file.name = new_filename
194 file.name = new_filename
193 else:
195 else:
194 logger = logging.getLogger('boards.forms.extension')
196 logger = logging.getLogger('boards.forms.extension')
195
197
196 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
198 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
197
199
198 def clean_title(self):
200 def clean_title(self):
199 title = self.cleaned_data['title']
201 title = self.cleaned_data['title']
200 if title:
202 if title:
201 if len(title) > TITLE_MAX_LENGTH:
203 if len(title) > TITLE_MAX_LENGTH:
202 raise forms.ValidationError(_('Title must have less than %s '
204 raise forms.ValidationError(_('Title must have less than %s '
203 'characters') %
205 'characters') %
204 str(TITLE_MAX_LENGTH))
206 str(TITLE_MAX_LENGTH))
205 return title
207 return title
206
208
207 def clean_text(self):
209 def clean_text(self):
208 text = self.cleaned_data['text'].strip()
210 text = self.cleaned_data['text'].strip()
209 if text:
211 if text:
210 max_length = board_settings.get_int('Forms', 'MaxTextLength')
212 max_length = board_settings.get_int('Forms', 'MaxTextLength')
211 if len(text) > max_length:
213 if len(text) > max_length:
212 raise forms.ValidationError(_('Text must have less than %s '
214 raise forms.ValidationError(_('Text must have less than %s '
213 'characters') % str(max_length))
215 'characters') % str(max_length))
214 return text
216 return text
215
217
216 def clean_file(self):
218 def clean_file(self):
217 file = self.cleaned_data['file']
219 file = self.cleaned_data['file']
218
220
219 if file:
221 if file:
220 validate_file_size(file.size)
222 validate_file_size(file.size)
221 self._update_file_extension(file)
223 self._update_file_extension(file)
222
224
223 return file
225 return file
224
226
225 def clean_file_url(self):
227 def clean_file_url(self):
226 url = self.cleaned_data['file_url']
228 url = self.cleaned_data['file_url']
227
229
228 file = None
230 file = None
229 if url:
231 if url:
230 file = self._get_file_from_url(url)
232 file = self._get_file_from_url(url)
231
233
232 if not file:
234 if not file:
233 raise forms.ValidationError(_('Invalid URL'))
235 raise forms.ValidationError(_('Invalid URL'))
234 else:
236 else:
235 validate_file_size(file.size)
237 validate_file_size(file.size)
236 self._update_file_extension(file)
238 self._update_file_extension(file)
237
239
238 return file
240 return file
239
241
240 def clean_threads(self):
242 def clean_threads(self):
241 threads_str = self.cleaned_data['threads']
243 threads_str = self.cleaned_data['threads']
242
244
243 if len(threads_str) > 0:
245 if len(threads_str) > 0:
244 threads_id_list = threads_str.split(' ')
246 threads_id_list = threads_str.split(' ')
245
247
246 threads = list()
248 threads = list()
247
249
248 for thread_id in threads_id_list:
250 for thread_id in threads_id_list:
249 try:
251 try:
250 thread = Post.objects.get(id=int(thread_id))
252 thread = Post.objects.get(id=int(thread_id))
251 if not thread.is_opening() or thread.get_thread().is_archived():
253 if not thread.is_opening() or thread.get_thread().is_archived():
252 raise ObjectDoesNotExist()
254 raise ObjectDoesNotExist()
253 threads.append(thread)
255 threads.append(thread)
254 except (ObjectDoesNotExist, ValueError):
256 except (ObjectDoesNotExist, ValueError):
255 raise forms.ValidationError(_('Invalid additional thread list'))
257 raise forms.ValidationError(_('Invalid additional thread list'))
256
258
257 return threads
259 return threads
258
260
259 def clean(self):
261 def clean(self):
260 cleaned_data = super(PostForm, self).clean()
262 cleaned_data = super(PostForm, self).clean()
261
263
262 if cleaned_data['email']:
264 if cleaned_data['email']:
263 self.need_to_ban = True
265 self.need_to_ban = True
264 raise forms.ValidationError('A human cannot enter a hidden field')
266 raise forms.ValidationError('A human cannot enter a hidden field')
265
267
266 if not self.errors:
268 if not self.errors:
267 self._clean_text_file()
269 self._clean_text_file()
268
270
269 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
271 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
270 if not self.errors and limit_speed:
272 if not self.errors and limit_speed:
271 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
273 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
272 if pow_difficulty > 0 and cleaned_data['timestamp'] and cleaned_data['iteration'] and cleaned_data['guess']:
274 if pow_difficulty > 0 and cleaned_data['timestamp'] and cleaned_data['iteration'] and cleaned_data['guess']:
273 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
275 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
274 else:
276 else:
275 self._validate_posting_speed()
277 self._validate_posting_speed()
276
278
277 return cleaned_data
279 return cleaned_data
278
280
279 def get_file(self):
281 def get_file(self):
280 """
282 """
281 Gets file from form or URL.
283 Gets file from form or URL.
282 """
284 """
283
285
284 file = self.cleaned_data['file']
286 file = self.cleaned_data['file']
285 return file or self.cleaned_data['file_url']
287 return file or self.cleaned_data['file_url']
286
288
287 def get_tripcode(self):
289 def get_tripcode(self):
288 title = self.cleaned_data['title']
290 title = self.cleaned_data['title']
289 if title is not None and TRIPCODE_DELIM in title:
291 if title is not None and TRIPCODE_DELIM in title:
290 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
292 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
291 tripcode = hashlib.md5(code.encode()).hexdigest()
293 tripcode = hashlib.md5(code.encode()).hexdigest()
292 else:
294 else:
293 tripcode = ''
295 tripcode = ''
294 return tripcode
296 return tripcode
295
297
296 def get_title(self):
298 def get_title(self):
297 title = self.cleaned_data['title']
299 title = self.cleaned_data['title']
298 if title is not None and TRIPCODE_DELIM in title:
300 if title is not None and TRIPCODE_DELIM in title:
299 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
301 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
300 else:
302 else:
301 return title
303 return title
302
304
303 def _clean_text_file(self):
305 def _clean_text_file(self):
304 text = self.cleaned_data.get('text')
306 text = self.cleaned_data.get('text')
305 file = self.get_file()
307 file = self.get_file()
306
308
307 if (not text) and (not file):
309 if (not text) and (not file):
308 error_message = _('Either text or file must be entered.')
310 error_message = _('Either text or file must be entered.')
309 self._errors['text'] = self.error_class([error_message])
311 self._errors['text'] = self.error_class([error_message])
310
312
311 def _validate_posting_speed(self):
313 def _validate_posting_speed(self):
312 can_post = True
314 can_post = True
313
315
314 posting_delay = settings.POSTING_DELAY
316 posting_delay = settings.POSTING_DELAY
315
317
316 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
318 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
317 now = time.time()
319 now = time.time()
318
320
319 current_delay = 0
321 current_delay = 0
320
322
321 if LAST_POST_TIME not in self.session:
323 if LAST_POST_TIME not in self.session:
322 self.session[LAST_POST_TIME] = now
324 self.session[LAST_POST_TIME] = now
323
325
324 need_delay = True
326 need_delay = True
325 else:
327 else:
326 last_post_time = self.session.get(LAST_POST_TIME)
328 last_post_time = self.session.get(LAST_POST_TIME)
327 current_delay = int(now - last_post_time)
329 current_delay = int(now - last_post_time)
328
330
329 need_delay = current_delay < posting_delay
331 need_delay = current_delay < posting_delay
330
332
331 if need_delay:
333 if need_delay:
332 delay = posting_delay - current_delay
334 delay = posting_delay - current_delay
333 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
335 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
334 delay) % {'delay': delay}
336 delay) % {'delay': delay}
335 self._errors['text'] = self.error_class([error_message])
337 self._errors['text'] = self.error_class([error_message])
336
338
337 can_post = False
339 can_post = False
338
340
339 if can_post:
341 if can_post:
340 self.session[LAST_POST_TIME] = now
342 self.session[LAST_POST_TIME] = now
341
343
342 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
344 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
343 """
345 """
344 Gets an file file from URL.
346 Gets an file file from URL.
345 """
347 """
346
348
347 img_temp = None
349 img_temp = None
348
350
349 try:
351 try:
350 for downloader in Downloader.__subclasses__():
352 for downloader in Downloader.__subclasses__():
351 if downloader.handles(url):
353 if downloader.handles(url):
352 return downloader.download(url)
354 return downloader.download(url)
353 # If nobody of the specific downloaders handles this, use generic
355 # If nobody of the specific downloaders handles this, use generic
354 # one
356 # one
355 return Downloader.download(url)
357 return Downloader.download(url)
356 except forms.ValidationError as e:
358 except forms.ValidationError as e:
357 raise e
359 raise e
358 except Exception as e:
360 except Exception as e:
359 raise forms.ValidationError(e)
361 raise forms.ValidationError(e)
360
362
361 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
363 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
362 post_time = timezone.datetime.fromtimestamp(
364 post_time = timezone.datetime.fromtimestamp(
363 int(timestamp[:-3]), tz=timezone.get_current_timezone())
365 int(timestamp[:-3]), tz=timezone.get_current_timezone())
364 timedelta = (timezone.now() - post_time).seconds / 60
366 timedelta = (timezone.now() - post_time).seconds / 60
365 if timedelta > POW_LIFE_MINUTES:
367 if timedelta > POW_LIFE_MINUTES:
366 self._errors['text'] = self.error_class([_('Stale PoW.')])
368 self._errors['text'] = self.error_class([_('Stale PoW.')])
367
369
368 payload = timestamp + message.replace('\r\n', '\n')
370 payload = timestamp + message.replace('\r\n', '\n')
369 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
371 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
370 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
372 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
371 if len(target) < POW_HASH_LENGTH:
373 if len(target) < POW_HASH_LENGTH:
372 target = '0' * (POW_HASH_LENGTH - len(target)) + target
374 target = '0' * (POW_HASH_LENGTH - len(target)) + target
373
375
374 computed_guess = hashlib.sha256((payload + iteration).encode())\
376 computed_guess = hashlib.sha256((payload + iteration).encode())\
375 .hexdigest()[0:POW_HASH_LENGTH]
377 .hexdigest()[0:POW_HASH_LENGTH]
376 if guess != computed_guess or guess > target:
378 if guess != computed_guess or guess > target:
377 self._errors['text'] = self.error_class(
379 self._errors['text'] = self.error_class(
378 [_('Invalid PoW.')])
380 [_('Invalid PoW.')])
379
381
380
382
381 class ThreadForm(PostForm):
383 class ThreadForm(PostForm):
382
384
383 tags = forms.CharField(
385 tags = forms.CharField(
384 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
386 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
385 max_length=100, label=_('Tags'), required=True)
387 max_length=100, label=_('Tags'), required=True)
386 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
388 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
387
389
388 def clean_tags(self):
390 def clean_tags(self):
389 tags = self.cleaned_data['tags'].strip()
391 tags = self.cleaned_data['tags'].strip()
390
392
391 if not tags or not REGEX_TAGS.match(tags):
393 if not tags or not REGEX_TAGS.match(tags):
392 raise forms.ValidationError(
394 raise forms.ValidationError(
393 _('Inappropriate characters in tags.'))
395 _('Inappropriate characters in tags.'))
394
396
395 required_tag_exists = False
397 required_tag_exists = False
396 tag_set = set()
398 tag_set = set()
397 for tag_string in tags.split():
399 for tag_string in tags.split():
398 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
400 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
399 tag_set.add(tag)
401 tag_set.add(tag)
400
402
401 # If this is a new tag, don't check for its parents because nobody
403 # If this is a new tag, don't check for its parents because nobody
402 # added them yet
404 # added them yet
403 if not created:
405 if not created:
404 tag_set |= set(tag.get_all_parents())
406 tag_set |= set(tag.get_all_parents())
405
407
406 for tag in tag_set:
408 for tag in tag_set:
407 if tag.required:
409 if tag.required:
408 required_tag_exists = True
410 required_tag_exists = True
409 break
411 break
410
412
411 if not required_tag_exists:
413 if not required_tag_exists:
412 raise forms.ValidationError(
414 raise forms.ValidationError(
413 _('Need at least one section.'))
415 _('Need at least one section.'))
414
416
415 return tag_set
417 return tag_set
416
418
417 def clean(self):
419 def clean(self):
418 cleaned_data = super(ThreadForm, self).clean()
420 cleaned_data = super(ThreadForm, self).clean()
419
421
420 return cleaned_data
422 return cleaned_data
421
423
422 def is_monochrome(self):
424 def is_monochrome(self):
423 return self.cleaned_data['monochrome']
425 return self.cleaned_data['monochrome']
424
426
425
427
426 class SettingsForm(NeboardForm):
428 class SettingsForm(NeboardForm):
427
429
428 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
430 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
429 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
431 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
430 username = forms.CharField(label=_('User name'), required=False)
432 username = forms.CharField(label=_('User name'), required=False)
431 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
433 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
432
434
433 def clean_username(self):
435 def clean_username(self):
434 username = self.cleaned_data['username']
436 username = self.cleaned_data['username']
435
437
436 if username and not REGEX_USERNAMES.match(username):
438 if username and not REGEX_USERNAMES.match(username):
437 raise forms.ValidationError(_('Inappropriate characters.'))
439 raise forms.ValidationError(_('Inappropriate characters.'))
438
440
439 return username
441 return username
440
442
441
443
442 class SearchForm(NeboardForm):
444 class SearchForm(NeboardForm):
443 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
445 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,568 +1,569 b''
1 * {
1 * {
2 text-decoration: none;
2 text-decoration: none;
3 font-weight: inherit;
3 font-weight: inherit;
4 }
4 }
5
5
6 b, strong {
6 b, strong {
7 font-weight: bold;
7 font-weight: bold;
8 }
8 }
9
9
10 html {
10 html {
11 background: #555;
11 background: #555;
12 color: #ffffff;
12 color: #ffffff;
13 }
13 }
14
14
15 body {
15 body {
16 margin: 0;
16 margin: 0;
17 }
17 }
18
18
19 #admin_panel {
19 #admin_panel {
20 background: #FF0000;
20 background: #FF0000;
21 color: #00FF00
21 color: #00FF00
22 }
22 }
23
23
24 .input_field_error {
24 .input_field_error {
25 color: #FF0000;
25 color: #FF0000;
26 }
26 }
27
27
28 .title {
28 .title {
29 font-weight: bold;
29 font-weight: bold;
30 color: #ffcc00;
30 color: #ffcc00;
31 }
31 }
32
32
33 .link, a {
33 .link, a {
34 color: #afdcec;
34 color: #afdcec;
35 }
35 }
36
36
37 .block {
37 .block {
38 display: inline-block;
38 display: inline-block;
39 vertical-align: top;
39 vertical-align: top;
40 }
40 }
41
41
42 .tag {
42 .tag {
43 color: #FFD37D;
43 color: #FFD37D;
44 }
44 }
45
45
46 .post_id {
46 .post_id {
47 color: #fff380;
47 color: #fff380;
48 }
48 }
49
49
50 .post, .dead_post, .archive_post, #posts-table {
50 .post, .dead_post, .archive_post, #posts-table {
51 background: #333;
51 background: #333;
52 padding: 10px;
52 padding: 10px;
53 clear: left;
53 clear: left;
54 word-wrap: break-word;
54 word-wrap: break-word;
55 border-top: 1px solid #777;
55 border-top: 1px solid #777;
56 border-bottom: 1px solid #777;
56 border-bottom: 1px solid #777;
57 }
57 }
58
58
59 .post + .post {
59 .post + .post {
60 border-top: none;
60 border-top: none;
61 }
61 }
62
62
63 .dead_post + .dead_post {
63 .dead_post + .dead_post {
64 border-top: none;
64 border-top: none;
65 }
65 }
66
66
67 .archive_post + .archive_post {
67 .archive_post + .archive_post {
68 border-top: none;
68 border-top: none;
69 }
69 }
70
70
71 .metadata {
71 .metadata {
72 padding-top: 5px;
72 padding-top: 5px;
73 margin-top: 10px;
73 margin-top: 10px;
74 border-top: solid 1px #666;
74 border-top: solid 1px #666;
75 color: #ddd;
75 color: #ddd;
76 }
76 }
77
77
78 .navigation_panel, .tag_info {
78 .navigation_panel, .tag_info {
79 background: #222;
79 background: #222;
80 margin-bottom: 5px;
80 margin-bottom: 5px;
81 margin-top: 5px;
81 margin-top: 5px;
82 padding: 10px;
82 padding: 10px;
83 border-bottom: solid 1px #888;
83 border-bottom: solid 1px #888;
84 border-top: solid 1px #888;
84 border-top: solid 1px #888;
85 color: #eee;
85 color: #eee;
86 }
86 }
87
87
88 .navigation_panel .link:first-child {
88 .navigation_panel .link:first-child {
89 border-right: 1px solid #fff;
89 border-right: 1px solid #fff;
90 font-weight: bold;
90 font-weight: bold;
91 margin-right: 1ex;
91 margin-right: 1ex;
92 padding-right: 1ex;
92 padding-right: 1ex;
93 }
93 }
94
94
95 .navigation_panel .right-link {
95 .navigation_panel .right-link {
96 border-left: 1px solid #fff;
96 border-left: 1px solid #fff;
97 border-right: none;
97 border-right: none;
98 float: right;
98 float: right;
99 margin-left: 1ex;
99 margin-left: 1ex;
100 margin-right: 0;
100 margin-right: 0;
101 padding-left: 1ex;
101 padding-left: 1ex;
102 padding-right: 0;
102 padding-right: 0;
103 }
103 }
104
104
105 .navigation_panel .link {
105 .navigation_panel .link {
106 font-weight: bold;
106 font-weight: bold;
107 }
107 }
108
108
109 .navigation_panel::after, .post::after {
109 .navigation_panel::after, .post::after {
110 clear: both;
110 clear: both;
111 content: ".";
111 content: ".";
112 display: block;
112 display: block;
113 height: 0;
113 height: 0;
114 line-height: 0;
114 line-height: 0;
115 visibility: hidden;
115 visibility: hidden;
116 }
116 }
117
117
118 .tag_info {
118 .tag_info {
119 text-align: center;
119 text-align: center;
120 }
120 }
121
121
122 .tag_info > .tag-text-data {
122 .tag_info > .tag-text-data {
123 text-align: left;
123 text-align: left;
124 max-width: 30em;
124 }
125 }
125
126
126 .header {
127 .header {
127 border-bottom: solid 2px #ccc;
128 border-bottom: solid 2px #ccc;
128 margin-bottom: 5px;
129 margin-bottom: 5px;
129 border-top: none;
130 border-top: none;
130 margin-top: 0;
131 margin-top: 0;
131 }
132 }
132
133
133 .footer {
134 .footer {
134 border-top: solid 2px #ccc;
135 border-top: solid 2px #ccc;
135 margin-top: 5px;
136 margin-top: 5px;
136 border-bottom: none;
137 border-bottom: none;
137 margin-bottom: 0;
138 margin-bottom: 0;
138 }
139 }
139
140
140 p, .br {
141 p, .br {
141 margin-top: .5em;
142 margin-top: .5em;
142 margin-bottom: .5em;
143 margin-bottom: .5em;
143 }
144 }
144
145
145 .post-form-w {
146 .post-form-w {
146 background: #333344;
147 background: #333344;
147 border-top: solid 1px #888;
148 border-top: solid 1px #888;
148 border-bottom: solid 1px #888;
149 border-bottom: solid 1px #888;
149 color: #fff;
150 color: #fff;
150 padding: 10px;
151 padding: 10px;
151 margin-bottom: 5px;
152 margin-bottom: 5px;
152 margin-top: 5px;
153 margin-top: 5px;
153 }
154 }
154
155
155 .form-row {
156 .form-row {
156 width: 100%;
157 width: 100%;
157 display: table-row;
158 display: table-row;
158 }
159 }
159
160
160 .form-label {
161 .form-label {
161 padding: .25em 1ex .25em 0;
162 padding: .25em 1ex .25em 0;
162 vertical-align: top;
163 vertical-align: top;
163 display: table-cell;
164 display: table-cell;
164 }
165 }
165
166
166 .form-input {
167 .form-input {
167 padding: .25em 0;
168 padding: .25em 0;
168 width: 100%;
169 width: 100%;
169 display: table-cell;
170 display: table-cell;
170 }
171 }
171
172
172 .form-errors {
173 .form-errors {
173 font-weight: bolder;
174 font-weight: bolder;
174 vertical-align: middle;
175 vertical-align: middle;
175 display: table-cell;
176 display: table-cell;
176 }
177 }
177
178
178 .post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select {
179 .post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select {
179 background: #333;
180 background: #333;
180 color: #fff;
181 color: #fff;
181 border: solid 1px;
182 border: solid 1px;
182 padding: 0;
183 padding: 0;
183 font: medium sans-serif;
184 font: medium sans-serif;
184 width: 100%;
185 width: 100%;
185 }
186 }
186
187
187 .post-form textarea {
188 .post-form textarea {
188 resize: vertical;
189 resize: vertical;
189 }
190 }
190
191
191 .form-submit {
192 .form-submit {
192 display: table;
193 display: table;
193 margin-bottom: 1ex;
194 margin-bottom: 1ex;
194 margin-top: 1ex;
195 margin-top: 1ex;
195 }
196 }
196
197
197 .form-title {
198 .form-title {
198 font-weight: bold;
199 font-weight: bold;
199 font-size: 2ex;
200 font-size: 2ex;
200 margin-bottom: 0.5ex;
201 margin-bottom: 0.5ex;
201 }
202 }
202
203
203 input[type="submit"], button {
204 input[type="submit"], button {
204 background: #222;
205 background: #222;
205 border: solid 2px #fff;
206 border: solid 2px #fff;
206 color: #fff;
207 color: #fff;
207 padding: 0.5ex;
208 padding: 0.5ex;
208 margin-right: 0.5ex;
209 margin-right: 0.5ex;
209 }
210 }
210
211
211 input[type="submit"]:hover {
212 input[type="submit"]:hover {
212 background: #060;
213 background: #060;
213 }
214 }
214
215
215 .form-submit > button:hover {
216 .form-submit > button:hover {
216 background: #006;
217 background: #006;
217 }
218 }
218
219
219 blockquote {
220 blockquote {
220 border-left: solid 2px;
221 border-left: solid 2px;
221 padding-left: 5px;
222 padding-left: 5px;
222 color: #B1FB17;
223 color: #B1FB17;
223 margin: 0;
224 margin: 0;
224 }
225 }
225
226
226 .post > .image {
227 .post > .image {
227 float: left;
228 float: left;
228 margin: 0 1ex .5ex 0;
229 margin: 0 1ex .5ex 0;
229 min-width: 1px;
230 min-width: 1px;
230 text-align: center;
231 text-align: center;
231 display: table-row;
232 display: table-row;
232 }
233 }
233
234
234 .post > .metadata {
235 .post > .metadata {
235 clear: left;
236 clear: left;
236 }
237 }
237
238
238 .get {
239 .get {
239 font-weight: bold;
240 font-weight: bold;
240 color: #d55;
241 color: #d55;
241 }
242 }
242
243
243 * {
244 * {
244 text-decoration: none;
245 text-decoration: none;
245 }
246 }
246
247
247 .dead_post > .post-info {
248 .dead_post > .post-info {
248 font-style: italic;
249 font-style: italic;
249 }
250 }
250
251
251 .archive_post > .post-info {
252 .archive_post > .post-info {
252 text-decoration: line-through;
253 text-decoration: line-through;
253 }
254 }
254
255
255 .mark_btn {
256 .mark_btn {
256 border: 1px solid;
257 border: 1px solid;
257 padding: 2px 2ex;
258 padding: 2px 2ex;
258 display: inline-block;
259 display: inline-block;
259 margin: 0 5px 4px 0;
260 margin: 0 5px 4px 0;
260 }
261 }
261
262
262 .mark_btn:hover {
263 .mark_btn:hover {
263 background: #555;
264 background: #555;
264 }
265 }
265
266
266 .quote {
267 .quote {
267 color: #92cf38;
268 color: #92cf38;
268 font-style: italic;
269 font-style: italic;
269 }
270 }
270
271
271 .multiquote {
272 .multiquote {
272 padding: 3px;
273 padding: 3px;
273 display: inline-block;
274 display: inline-block;
274 background: #222;
275 background: #222;
275 border-style: solid;
276 border-style: solid;
276 border-width: 1px 1px 1px 4px;
277 border-width: 1px 1px 1px 4px;
277 font-size: 0.9em;
278 font-size: 0.9em;
278 }
279 }
279
280
280 .spoiler {
281 .spoiler {
281 background: black;
282 background: black;
282 color: black;
283 color: black;
283 padding: 0 1ex 0 1ex;
284 padding: 0 1ex 0 1ex;
284 }
285 }
285
286
286 .spoiler:hover {
287 .spoiler:hover {
287 color: #ddd;
288 color: #ddd;
288 }
289 }
289
290
290 .comment {
291 .comment {
291 color: #eb2;
292 color: #eb2;
292 }
293 }
293
294
294 a:hover {
295 a:hover {
295 text-decoration: underline;
296 text-decoration: underline;
296 }
297 }
297
298
298 .last-replies {
299 .last-replies {
299 margin-left: 3ex;
300 margin-left: 3ex;
300 margin-right: 3ex;
301 margin-right: 3ex;
301 border-left: solid 1px #777;
302 border-left: solid 1px #777;
302 border-right: solid 1px #777;
303 border-right: solid 1px #777;
303 }
304 }
304
305
305 .last-replies > .post:first-child {
306 .last-replies > .post:first-child {
306 border-top: none;
307 border-top: none;
307 }
308 }
308
309
309 .thread {
310 .thread {
310 margin-bottom: 3ex;
311 margin-bottom: 3ex;
311 margin-top: 1ex;
312 margin-top: 1ex;
312 }
313 }
313
314
314 .post:target {
315 .post:target {
315 border: solid 2px white;
316 border: solid 2px white;
316 }
317 }
317
318
318 pre{
319 pre{
319 white-space:pre-wrap
320 white-space:pre-wrap
320 }
321 }
321
322
322 li {
323 li {
323 list-style-position: inside;
324 list-style-position: inside;
324 }
325 }
325
326
326 .fancybox-skin {
327 .fancybox-skin {
327 position: relative;
328 position: relative;
328 background-color: #fff;
329 background-color: #fff;
329 color: #ddd;
330 color: #ddd;
330 text-shadow: none;
331 text-shadow: none;
331 }
332 }
332
333
333 .fancybox-image {
334 .fancybox-image {
334 border: 1px solid black;
335 border: 1px solid black;
335 }
336 }
336
337
337 .image-mode-tab {
338 .image-mode-tab {
338 background: #444;
339 background: #444;
339 color: #eee;
340 color: #eee;
340 margin-top: 5px;
341 margin-top: 5px;
341 padding: 5px;
342 padding: 5px;
342 border-top: 1px solid #888;
343 border-top: 1px solid #888;
343 border-bottom: 1px solid #888;
344 border-bottom: 1px solid #888;
344 }
345 }
345
346
346 .image-mode-tab > label {
347 .image-mode-tab > label {
347 margin: 0 1ex;
348 margin: 0 1ex;
348 }
349 }
349
350
350 .image-mode-tab > label > input {
351 .image-mode-tab > label > input {
351 margin-right: .5ex;
352 margin-right: .5ex;
352 }
353 }
353
354
354 #posts-table {
355 #posts-table {
355 margin-top: 5px;
356 margin-top: 5px;
356 margin-bottom: 5px;
357 margin-bottom: 5px;
357 }
358 }
358
359
359 .tag_info > h2 {
360 .tag_info > h2 {
360 margin: 0;
361 margin: 0;
361 }
362 }
362
363
363 .post-info {
364 .post-info {
364 color: #ddd;
365 color: #ddd;
365 margin-bottom: 1ex;
366 margin-bottom: 1ex;
366 }
367 }
367
368
368 .moderator_info {
369 .moderator_info {
369 color: #e99d41;
370 color: #e99d41;
370 opacity: 0.4;
371 opacity: 0.4;
371 }
372 }
372
373
373 .moderator_info:hover {
374 .moderator_info:hover {
374 opacity: 1;
375 opacity: 1;
375 }
376 }
376
377
377 .refmap {
378 .refmap {
378 font-size: 0.9em;
379 font-size: 0.9em;
379 color: #ccc;
380 color: #ccc;
380 margin-top: 1em;
381 margin-top: 1em;
381 }
382 }
382
383
383 .fav {
384 .fav {
384 color: yellow;
385 color: yellow;
385 }
386 }
386
387
387 .not_fav {
388 .not_fav {
388 color: #ccc;
389 color: #ccc;
389 }
390 }
390
391
391 .form-email {
392 .form-email {
392 display: none;
393 display: none;
393 }
394 }
394
395
395 .bar-value {
396 .bar-value {
396 background: rgba(50, 55, 164, 0.45);
397 background: rgba(50, 55, 164, 0.45);
397 font-size: 0.9em;
398 font-size: 0.9em;
398 height: 1.5em;
399 height: 1.5em;
399 }
400 }
400
401
401 .bar-bg {
402 .bar-bg {
402 position: relative;
403 position: relative;
403 border-top: solid 1px #888;
404 border-top: solid 1px #888;
404 border-bottom: solid 1px #888;
405 border-bottom: solid 1px #888;
405 margin-top: 5px;
406 margin-top: 5px;
406 overflow: hidden;
407 overflow: hidden;
407 }
408 }
408
409
409 .bar-text {
410 .bar-text {
410 padding: 2px;
411 padding: 2px;
411 position: absolute;
412 position: absolute;
412 left: 0;
413 left: 0;
413 top: 0;
414 top: 0;
414 }
415 }
415
416
416 .page_link {
417 .page_link {
417 background: #444;
418 background: #444;
418 border-top: solid 1px #888;
419 border-top: solid 1px #888;
419 border-bottom: solid 1px #888;
420 border-bottom: solid 1px #888;
420 padding: 5px;
421 padding: 5px;
421 color: #eee;
422 color: #eee;
422 font-size: 2ex;
423 font-size: 2ex;
423 margin-top: .5ex;
424 margin-top: .5ex;
424 margin-bottom: .5ex;
425 margin-bottom: .5ex;
425 }
426 }
426
427
427 .skipped_replies {
428 .skipped_replies {
428 padding: 5px;
429 padding: 5px;
429 margin-left: 3ex;
430 margin-left: 3ex;
430 margin-right: 3ex;
431 margin-right: 3ex;
431 border-left: solid 1px #888;
432 border-left: solid 1px #888;
432 border-right: solid 1px #888;
433 border-right: solid 1px #888;
433 border-bottom: solid 1px #888;
434 border-bottom: solid 1px #888;
434 background: #000;
435 background: #000;
435 }
436 }
436
437
437 .current_page {
438 .current_page {
438 padding: 2px;
439 padding: 2px;
439 background-color: #afdcec;
440 background-color: #afdcec;
440 color: #000;
441 color: #000;
441 }
442 }
442
443
443 .current_mode {
444 .current_mode {
444 font-weight: bold;
445 font-weight: bold;
445 }
446 }
446
447
447 .gallery_image {
448 .gallery_image {
448 border: solid 1px;
449 border: solid 1px;
449 margin: 0.5ex;
450 margin: 0.5ex;
450 text-align: center;
451 text-align: center;
451 padding: 1ex;
452 padding: 1ex;
452 }
453 }
453
454
454 code {
455 code {
455 border: dashed 1px #ccc;
456 border: dashed 1px #ccc;
456 background: #111;
457 background: #111;
457 padding: 2px;
458 padding: 2px;
458 font-size: 1.2em;
459 font-size: 1.2em;
459 display: inline-block;
460 display: inline-block;
460 }
461 }
461
462
462 pre {
463 pre {
463 overflow: auto;
464 overflow: auto;
464 }
465 }
465
466
466 .img-full {
467 .img-full {
467 background: #222;
468 background: #222;
468 border: solid 1px white;
469 border: solid 1px white;
469 }
470 }
470
471
471 .tag_item {
472 .tag_item {
472 display: inline-block;
473 display: inline-block;
473 }
474 }
474
475
475 #id_models li {
476 #id_models li {
476 list-style: none;
477 list-style: none;
477 }
478 }
478
479
479 #id_q {
480 #id_q {
480 margin-left: 1ex;
481 margin-left: 1ex;
481 }
482 }
482
483
483 ul {
484 ul {
484 padding-left: 0px;
485 padding-left: 0px;
485 }
486 }
486
487
487 .quote-header {
488 .quote-header {
488 border-bottom: 2px solid #ddd;
489 border-bottom: 2px solid #ddd;
489 margin-bottom: 1ex;
490 margin-bottom: 1ex;
490 padding-bottom: .5ex;
491 padding-bottom: .5ex;
491 color: #ddd;
492 color: #ddd;
492 font-size: 1.2em;
493 font-size: 1.2em;
493 }
494 }
494
495
495 /* Reflink preview */
496 /* Reflink preview */
496 .post_preview {
497 .post_preview {
497 border-left: 1px solid #777;
498 border-left: 1px solid #777;
498 border-right: 1px solid #777;
499 border-right: 1px solid #777;
499 max-width: 600px;
500 max-width: 600px;
500 }
501 }
501
502
502 /* Code highlighter */
503 /* Code highlighter */
503 .hljs {
504 .hljs {
504 color: #fff;
505 color: #fff;
505 background: #000;
506 background: #000;
506 display: inline-block;
507 display: inline-block;
507 }
508 }
508
509
509 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
510 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
510 color: #fff;
511 color: #fff;
511 }
512 }
512
513
513 #up {
514 #up {
514 position: fixed;
515 position: fixed;
515 bottom: 5px;
516 bottom: 5px;
516 right: 5px;
517 right: 5px;
517 border: 1px solid #777;
518 border: 1px solid #777;
518 background: #000;
519 background: #000;
519 padding: 4px;
520 padding: 4px;
520 opacity: 0.3;
521 opacity: 0.3;
521 }
522 }
522
523
523 #up:hover {
524 #up:hover {
524 opacity: 1;
525 opacity: 1;
525 }
526 }
526
527
527 .user-cast {
528 .user-cast {
528 border: solid #ffffff 1px;
529 border: solid #ffffff 1px;
529 padding: .2ex;
530 padding: .2ex;
530 background: #152154;
531 background: #152154;
531 color: #fff;
532 color: #fff;
532 }
533 }
533
534
534 .highlight {
535 .highlight {
535 background: #222;
536 background: #222;
536 }
537 }
537
538
538 .post-button-form > button:hover {
539 .post-button-form > button:hover {
539 text-decoration: underline;
540 text-decoration: underline;
540 }
541 }
541
542
542 .tree_reply > .post {
543 .tree_reply > .post {
543 margin-top: 1ex;
544 margin-top: 1ex;
544 border-left: solid 1px #777;
545 border-left: solid 1px #777;
545 padding-right: 0;
546 padding-right: 0;
546 }
547 }
547
548
548 #preview-text {
549 #preview-text {
549 border: solid 1px white;
550 border: solid 1px white;
550 margin: 1ex 0 1ex 0;
551 margin: 1ex 0 1ex 0;
551 padding: 1ex;
552 padding: 1ex;
552 }
553 }
553
554
554 .image-metadata {
555 .image-metadata {
555 font-size: 0.9em;
556 font-size: 0.9em;
556 }
557 }
557
558
558 .tripcode {
559 .tripcode {
559 color: white;
560 color: white;
560 }
561 }
561
562
562 #fav-panel {
563 #fav-panel {
563 border: 1px solid white;
564 border: 1px solid white;
564 }
565 }
565
566
566 .post-blink {
567 .post-blink {
567 background-color: #000;
568 background-color: #000;
568 }
569 }
General Comments 0
You need to be logged in to leave comments. Login now