##// END OF EJS Templates
Merged with 1.5 dev branch
neko259 -
r463:8531d7b0 merge 1.5 default
parent child Browse files
Show More
@@ -0,0 +1,92 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import SchemaMigration
5 from django.db import models
6
7
8 class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding field 'Post.image_pre_width'
12 db.add_column(u'boards_post', 'image_pre_width',
13 self.gf('django.db.models.fields.IntegerField')(default=0),
14 keep_default=False)
15
16 # Adding field 'Post.image_pre_height'
17 db.add_column(u'boards_post', 'image_pre_height',
18 self.gf('django.db.models.fields.IntegerField')(default=0),
19 keep_default=False)
20
21
22 def backwards(self, orm):
23 # Deleting field 'Post.image_pre_width'
24 db.delete_column(u'boards_post', 'image_pre_width')
25
26 # Deleting field 'Post.image_pre_height'
27 db.delete_column(u'boards_post', 'image_pre_height')
28
29
30 models = {
31 'boards.ban': {
32 'Meta': {'object_name': 'Ban'},
33 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
34 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
35 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
36 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
37 },
38 'boards.post': {
39 'Meta': {'object_name': 'Post'},
40 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
41 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
42 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
43 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
44 'image_pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
45 'image_pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
46 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
47 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
48 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
49 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
50 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
51 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
52 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
53 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
54 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}),
55 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
56 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
57 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
58 },
59 'boards.setting': {
60 'Meta': {'object_name': 'Setting'},
61 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
63 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
64 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
65 },
66 'boards.tag': {
67 'Meta': {'object_name': 'Tag'},
68 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
69 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
70 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
71 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
72 },
73 'boards.thread': {
74 'Meta': {'object_name': 'Thread'},
75 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
76 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
77 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
78 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
79 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
80 },
81 'boards.user': {
82 'Meta': {'object_name': 'User'},
83 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
84 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
85 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
86 'rank': ('django.db.models.fields.IntegerField', [], {}),
87 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
88 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
89 }
90 }
91
92 complete_apps = ['boards'] No newline at end of file
@@ -0,0 +1,66 b''
1 {% extends "boards/base.html" %}
2
3 {% load i18n %}
4 {% load cache %}
5 {% load static from staticfiles %}
6 {% load board %}
7
8 {% block head %}
9 <title>Neboard - {{ thread.get_opening_post.get_title }}</title>
10 {% endblock %}
11
12 {% block content %}
13 {% spaceless %}
14 {% get_current_language as LANGUAGE_CODE %}
15
16 <script src="{% static 'js/thread.js' %}"></script>
17
18 {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE %}
19 <div class="image-mode-tab">
20 <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
21 <a class="current_mode" href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
22 </div>
23
24 <div id="posts-table">
25 {% for post in thread.get_replies %}
26 {% if post.image %}
27 <div class="gallery_image">
28 <div>
29 <a
30 class="thumb"
31 href="{{ post.image.url }}"><img
32 src="{{ post.image.url_200x150 }}"
33 alt="{{ post.id }}"
34 width="{{ post.image_pre_width }}"
35 height="{{ post.image_pre_height }}"
36 data-width="{{ post.image_width }}"
37 data-height="{{ post.image_height }}"/>
38 </a>
39 </div>
40 <div class="gallery_image_metadata">
41 {{ post.image_width }}x{{ post.image_height }}
42 {% image_actions post.image.url request.get_host %}
43 </div>
44 </div>
45 {% endif %}
46 {% endfor %}
47 </div>
48 {% endcache %}
49
50 {% endspaceless %}
51 {% endblock %}
52
53 {% block metapanel %}
54
55 {% get_current_language as LANGUAGE_CODE %}
56
57 <span class="metapanel" data-last-update="{{ last_update }}">
58 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
59 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
60 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
61 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
62 [<a href="rss/">RSS</a>]
63 {% endcache %}
64 </span>
65
66 {% endblock %}
@@ -0,0 +1,7 b''
1 # 1.5 Aker #
2 * Saving image previews size. No space will be shown below images in some
3 styles.
4 * Showing notification in page title when new posts are loaded into the open
5 thread.
6 * Thread moderation fixes
7 * Added new gallery with search links and image metadata No newline at end of file
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,381 +1,379 b''
1 # SOME DESCRIPTIVE TITLE.
1 # SOME DESCRIPTIVE TITLE.
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 # This file is distributed under the same license as the PACKAGE package.
3 # This file is distributed under the same license as the PACKAGE package.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 #
5 #
6 msgid ""
6 msgid ""
7 msgstr ""
7 msgstr ""
8 "Project-Id-Version: PACKAGE VERSION\n"
8 "Project-Id-Version: PACKAGE VERSION\n"
9 "Report-Msgid-Bugs-To: \n"
9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2013-11-27 12:34+0200\n"
10 "POT-Creation-Date: 2013-12-24 20:39+0200\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "Language: ru\n"
14 "Language: ru\n"
15 "MIME-Version: 1.0\n"
15 "MIME-Version: 1.0\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Transfer-Encoding: 8bit\n"
17 "Content-Transfer-Encoding: 8bit\n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20
20
21 #: authors.py:5
21 #: authors.py:5
22 msgid "author"
22 msgid "author"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
24
24
25 #: authors.py:6
25 #: authors.py:6
26 msgid "developer"
26 msgid "developer"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
28
28
29 #: authors.py:7
29 #: authors.py:7
30 msgid "javascript developer"
30 msgid "javascript developer"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
32
32
33 #: authors.py:8
33 #: authors.py:8
34 msgid "designer"
34 msgid "designer"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
36
36
37 #: forms.py:48 templates/boards/posting_general.html:209
37 #: forms.py:72
38 #: templates/boards/thread.html:101
39 msgid "Title"
38 msgid "Title"
40 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
39 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
41
40
42 #: forms.py:50 templates/boards/posting_general.html:224
41 #: forms.py:74
43 #: templates/boards/thread.html:116
44 msgid "Text"
42 msgid "Text"
45 msgstr "ВСкст"
43 msgstr "ВСкст"
46
44
47 #: forms.py:51 templates/boards/posting_general.html:229
45 #: forms.py:75
48 #: templates/boards/thread.html:121
49 msgid "Image"
46 msgid "Image"
50 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
47 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
51
48
52 #: forms.py:54 templates/boards/posting_general.html:239
49 #: forms.py:78
53 #: templates/boards/thread.html:126
54 msgid "e-mail"
50 msgid "e-mail"
55 msgstr ""
51 msgstr ""
56
52
57 #: forms.py:65
53 #: forms.py:89
58 #, python-format
54 #, python-format
59 msgid "Title must have less than %s characters"
55 msgid "Title must have less than %s characters"
60 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
56 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
61
57
62 #: forms.py:74
58 #: forms.py:98
63 #, python-format
59 #, python-format
64 msgid "Text must have less than %s characters"
60 msgid "Text must have less than %s characters"
65 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
61 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
66
62
67 #: forms.py:85
63 #: forms.py:109
68 #, python-format
64 #, python-format
69 msgid "Image must be less than %s bytes"
65 msgid "Image must be less than %s bytes"
70 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
66 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
71
67
72 #: forms.py:112
68 #: forms.py:136
73 msgid "Either text or image must be entered."
69 msgid "Either text or image must be entered."
74 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
70 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
75
71
76 #: forms.py:125
72 #: forms.py:149
77 #, python-format
73 #, python-format
78 msgid "Wait %s seconds after last posting"
74 msgid "Wait %s seconds after last posting"
79 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
75 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
80
76
81 #: forms.py:139 templates/boards/post.html:60
77 #: forms.py:163 templates/boards/post.html:61 templates/boards/tags.html:6
82 #: templates/boards/posting_general.html:234 templates/boards/tags.html:6
83 #: templates/boards/rss/post.html:10
78 #: templates/boards/rss/post.html:10
84 msgid "Tags"
79 msgid "Tags"
85 msgstr "Π’Π΅Π³ΠΈ"
80 msgstr "Π’Π΅Π³ΠΈ"
86
81
87 #: forms.py:147
82 #: forms.py:171
88 msgid "Inappropriate characters in tags."
83 msgid "Inappropriate characters in tags."
89 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
84 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
90
85
91 #: forms.py:175 forms.py:196
86 #: forms.py:199 forms.py:220
92 msgid "Captcha validation failed"
87 msgid "Captcha validation failed"
93 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
88 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
94
89
95 #: forms.py:202
90 #: forms.py:226
96 msgid "Theme"
91 msgid "Theme"
97 msgstr "Π’Π΅ΠΌΠ°"
92 msgstr "Π’Π΅ΠΌΠ°"
98
93
99 #: forms.py:207
94 #: forms.py:231
100 msgid "Enable moderation panel"
95 msgid "Enable moderation panel"
101 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
96 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
102
97
103 #: forms.py:222
98 #: forms.py:246
104 msgid "No such user found"
99 msgid "No such user found"
105 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
100 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
106
101
107 #: forms.py:236
102 #: forms.py:260
108 #, python-format
103 #, python-format
109 msgid "Wait %s minutes after last login"
104 msgid "Wait %s minutes after last login"
110 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
105 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
111
106
112 #: templates/boards/404.html:6
107 #: templates/boards/404.html:6
113 msgid "Not found"
108 msgid "Not found"
114 msgstr "НС найдСно"
109 msgstr "НС найдСно"
115
110
116 #: templates/boards/404.html:12
111 #: templates/boards/404.html:12
117 msgid "This page does not exist"
112 msgid "This page does not exist"
118 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
113 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
119
114
120 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
115 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
121 msgid "Authors"
116 msgid "Authors"
122 msgstr "Авторы"
117 msgstr "Авторы"
123
118
124 #: templates/boards/authors.html:25
119 #: templates/boards/authors.html:25
125 msgid "Distributed under the"
120 msgid "Distributed under the"
126 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
121 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
127
122
128 #: templates/boards/authors.html:27
123 #: templates/boards/authors.html:27
129 msgid "license"
124 msgid "license"
130 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
125 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
131
126
132 #: templates/boards/authors.html:29
127 #: templates/boards/authors.html:29
133 msgid "Repository"
128 msgid "Repository"
134 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
129 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
135
130
136 #: templates/boards/base.html:14
131 #: templates/boards/base.html:14
137 msgid "Feed"
132 msgid "Feed"
138 msgstr "Π›Π΅Π½Ρ‚Π°"
133 msgstr "Π›Π΅Π½Ρ‚Π°"
139
134
140 #: templates/boards/base.html:36
135 #: templates/boards/base.html:31
141 msgid "All threads"
136 msgid "All threads"
142 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
137 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
143
138
144 #: templates/boards/base.html:41
139 #: templates/boards/base.html:36
145 msgid "Tag management"
140 msgid "Tag management"
146 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
141 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
147
142
148 #: templates/boards/base.html:43
143 #: templates/boards/base.html:38
149 msgid "Settings"
144 msgid "Settings"
150 msgstr "Настройки"
145 msgstr "Настройки"
151
146
152 #: templates/boards/base.html:50 templates/boards/login.html:6
147 #: templates/boards/base.html:50 templates/boards/login.html:6
153 #: templates/boards/login.html.py:21
148 #: templates/boards/login.html.py:21
154 msgid "Login"
149 msgid "Login"
155 msgstr "Π’Ρ…ΠΎΠ΄"
150 msgstr "Π’Ρ…ΠΎΠ΄"
156
151
157 #: templates/boards/base.html:52
152 #: templates/boards/base.html:52
158 #, python-format
153 #, python-format
159 msgid "Speed: %(ppd)s posts per day"
154 msgid "Speed: %(ppd)s posts per day"
160 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
155 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
161
156
162 #: templates/boards/base.html:54
157 #: templates/boards/base.html:54
163 msgid "Up"
158 msgid "Up"
164 msgstr "Π’Π²Π΅Ρ€Ρ…"
159 msgstr "Π’Π²Π΅Ρ€Ρ…"
165
160
166 #: templates/boards/login.html:15
161 #: templates/boards/login.html:15
167 msgid "User ID"
162 msgid "User ID"
168 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
163 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
169
164
170 #: templates/boards/login.html:24
165 #: templates/boards/login.html:24
171 msgid "Insert your user id above"
166 msgid "Insert your user id above"
172 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
167 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
173
168
174 #: templates/boards/post.html:34 templates/boards/posting_general.html:100
169 #: templates/boards/post.html:35 templates/boards/posting_general.html:103
175 #: templates/boards/thread.html:59
170 #: templates/boards/thread.html:68
176 msgid "Delete"
171 msgid "Delete"
177 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
172 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
178
173
179 #: templates/boards/post.html:37 templates/boards/posting_general.html:104
174 #: templates/boards/post.html:38 templates/boards/posting_general.html:107
180 #: templates/boards/thread.html:62
175 #: templates/boards/thread.html:71
181 msgid "Ban IP"
176 msgid "Ban IP"
182 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
177 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
183
178
184 #: templates/boards/post.html:50 templates/boards/posting_general.html:113
179 #: templates/boards/post.html:51 templates/boards/posting_general.html:116
185 #: templates/boards/posting_general.html:172 templates/boards/thread.html:71
180 #: templates/boards/posting_general.html:180 templates/boards/thread.html:80
186 msgid "Replies"
181 msgid "Replies"
187 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
182 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
188
183
189 #: templates/boards/posting_general.html:63
184 #: templates/boards/posting_general.html:64
190 msgid "Previous page"
185 msgid "Previous page"
191 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
186 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
192
187
193 #: templates/boards/posting_general.html:94
188 #: templates/boards/posting_general.html:97
194 msgid "Reply"
189 msgid "Reply"
195 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
190 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
196
191
197 #: templates/boards/posting_general.html:122 templates/boards/thread.html:154
192 #: templates/boards/posting_general.html:125 templates/boards/thread.html:130
198 msgid "replies"
193 #: templates/boards/thread_gallery.html:52
199 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
200
201 #: templates/boards/posting_general.html:123 templates/boards/thread.html:155
202 msgid "images"
194 msgid "images"
203 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
195 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
204
196
205 #: templates/boards/posting_general.html:138
197 #: templates/boards/posting_general.html:142
206 #, python-format
198 #, python-format
207 msgid "Skipped %(count)s replies. Open thread to see all replies."
199 msgid "Skipped %(count)s replies. Open thread to see all replies."
208 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
200 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
209
201
210 #: templates/boards/posting_general.html:195
202 #: templates/boards/posting_general.html:203
211 msgid "Next page"
203 msgid "Next page"
212 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
204 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
213
205
214 #: templates/boards/posting_general.html:200
206 #: templates/boards/posting_general.html:208
215 msgid "No threads exist. Create the first one!"
207 msgid "No threads exist. Create the first one!"
216 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
208 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
217
209
218 #: templates/boards/posting_general.html:206
210 #: templates/boards/posting_general.html:214
219 msgid "Create new thread"
211 msgid "Create new thread"
220 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
212 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
221
213
222 #: templates/boards/posting_general.html:214 templates/boards/thread.html:106
214 #: templates/boards/posting_general.html:218 templates/boards/thread.html:112
223 msgid "Formatting"
224 msgstr "Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅"
225
226 #: templates/boards/posting_general.html:216 templates/boards/thread.html:108
227 msgid "quote"
228 msgstr "Ρ†ΠΈΡ‚Π°Ρ‚Π°"
229
230 #: templates/boards/posting_general.html:217 templates/boards/thread.html:109
231 msgid "italic"
232 msgstr "курсив"
233
234 #: templates/boards/posting_general.html:218 templates/boards/thread.html:110
235 msgid "bold"
236 msgstr "ΠΏΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ"
237
238 #: templates/boards/posting_general.html:219 templates/boards/thread.html:111
239 msgid "spoiler"
240 msgstr "спойлСр"
241
242 #: templates/boards/posting_general.html:220 templates/boards/thread.html:112
243 msgid "comment"
244 msgstr "ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
245
246 #: templates/boards/posting_general.html:252 templates/boards/thread.html:140
247 msgid "Post"
215 msgid "Post"
248 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
216 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
249
217
250 #: templates/boards/posting_general.html:254
218 #: templates/boards/posting_general.html:222
251 msgid "Tags must be delimited by spaces. Text or image is required."
219 msgid "Tags must be delimited by spaces. Text or image is required."
252 msgstr ""
220 msgstr ""
253 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
221 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
254
222
255 #: templates/boards/posting_general.html:257 templates/boards/thread.html:142
223 #: templates/boards/posting_general.html:225 templates/boards/thread.html:116
256 msgid "Text syntax"
224 msgid "Text syntax"
257 msgstr "Бинтаксис тСкста"
225 msgstr "Бинтаксис тСкста"
258
226
259 #: templates/boards/posting_general.html:267
227 #: templates/boards/posting_general.html:235
260 msgid "Pages:"
228 msgid "Pages:"
261 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
229 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
262
230
263 #: templates/boards/settings.html:14
231 #: templates/boards/settings.html:14
264 msgid "User:"
232 msgid "User:"
265 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
233 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
266
234
267 #: templates/boards/settings.html:16
235 #: templates/boards/settings.html:16
268 msgid "You are moderator."
236 msgid "You are moderator."
269 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
237 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
270
238
271 #: templates/boards/settings.html:19
239 #: templates/boards/settings.html:19
272 msgid "Posts:"
240 msgid "Posts:"
273 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
241 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
274
242
275 #: templates/boards/settings.html:20
243 #: templates/boards/settings.html:20
276 msgid "First access:"
244 msgid "First access:"
277 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
245 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
278
246
279 #: templates/boards/settings.html:22
247 #: templates/boards/settings.html:22
280 msgid "Last access:"
248 msgid "Last access:"
281 msgstr "ПослСдний доступ: "
249 msgstr "ПослСдний доступ: "
282
250
283 #: templates/boards/settings.html:31
251 #: templates/boards/settings.html:31
284 msgid "Save"
252 msgid "Save"
285 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
253 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
286
254
287 #: templates/boards/tags.html:24
255 #: templates/boards/tags.html:24
288 msgid "threads"
256 msgid "threads"
289 msgstr "Ρ‚Π΅ΠΌ"
257 msgstr "Ρ‚Π΅ΠΌ"
290
258
291 #: templates/boards/tags.html:37
259 #: templates/boards/tags.html:37
292 msgid "No tags found."
260 msgid "No tags found."
293 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
261 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
294
262
295 #: templates/boards/thread.html:24
263 #: templates/boards/thread.html:22 templates/boards/thread_gallery.html:20
264 msgid "Normal mode"
265 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
266
267 #: templates/boards/thread.html:23 templates/boards/thread_gallery.html:21
268 msgid "Gallery mode"
269 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
270
271 #: templates/boards/thread.html:31
296 msgid "posts to bumplimit"
272 msgid "posts to bumplimit"
297 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
273 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
298
274
299 #: templates/boards/thread.html:98
275 #: templates/boards/thread.html:106
300 msgid "Reply to thread"
276 msgid "Reply to thread"
301 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
277 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
302
278
303 #: templates/boards/thread.html:156
279 #: templates/boards/thread.html:129 templates/boards/thread_gallery.html:51
280 msgid "replies"
281 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
282
283 #: templates/boards/thread.html:131 templates/boards/thread_gallery.html:53
304 msgid "Last update: "
284 msgid "Last update: "
305 msgstr "ПослСднСС обновлСниС: "
285 msgstr "ПослСднСС обновлСниС: "
306
286
307 #: templates/boards/rss/post.html:5
287 #: templates/boards/rss/post.html:5
308 msgid "Post image"
288 msgid "Post image"
309 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
289 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
310
290
311 #: templates/boards/staticpages/banned.html:6
291 #: templates/boards/staticpages/banned.html:6
312 msgid "Banned"
292 msgid "Banned"
313 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
293 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
314
294
315 #: templates/boards/staticpages/banned.html:11
295 #: templates/boards/staticpages/banned.html:11
316 msgid "Your IP address has been banned. Contact the administrator"
296 msgid "Your IP address has been banned. Contact the administrator"
317 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
297 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
318
298
319 #: templates/boards/staticpages/help.html:6
299 #: templates/boards/staticpages/help.html:6
320 #: templates/boards/staticpages/help.html:10
300 #: templates/boards/staticpages/help.html:10
321 msgid "Syntax"
301 msgid "Syntax"
322 msgstr "Бинтаксис"
302 msgstr "Бинтаксис"
323
303
324 #: templates/boards/staticpages/help.html:11
304 #: templates/boards/staticpages/help.html:11
325 msgid "2 line breaks for a new line."
305 msgid "2 line breaks for a new line."
326 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
306 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
327
307
328 #: templates/boards/staticpages/help.html:12
308 #: templates/boards/staticpages/help.html:12
329 msgid "Italic text"
309 msgid "Italic text"
330 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
310 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
331
311
332 #: templates/boards/staticpages/help.html:13
312 #: templates/boards/staticpages/help.html:13
333 msgid "Bold text"
313 msgid "Bold text"
334 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
314 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
335
315
336 #: templates/boards/staticpages/help.html:14
316 #: templates/boards/staticpages/help.html:14
337 msgid "Spoiler"
317 msgid "Spoiler"
338 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
318 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
339
319
340 #: templates/boards/staticpages/help.html:15
320 #: templates/boards/staticpages/help.html:15
341 msgid "Comment"
321 msgid "Comment"
342 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
322 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
343
323
344 #: templates/boards/staticpages/help.html:16
324 #: templates/boards/staticpages/help.html:16
345 msgid "Quote"
325 msgid "Quote"
346 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
326 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
347
327
348 #: templates/boards/staticpages/help.html:17
328 #: templates/boards/staticpages/help.html:17
349 msgid "Link to a post"
329 msgid "Link to a post"
350 msgstr "Бсылка Π½Π° сообщСниС"
330 msgstr "Бсылка Π½Π° сообщСниС"
351
331
352 #: templates/boards/staticpages/help.html:18
332 #: templates/boards/staticpages/help.html:18
353 msgid "Strikethrough text"
333 msgid "Strikethrough text"
354 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
334 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
355
335
336 #~ msgid "Formatting"
337 #~ msgstr "Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅"
338
339 #~ msgid "quote"
340 #~ msgstr "Ρ†ΠΈΡ‚Π°Ρ‚Π°"
341
342 #~ msgid "italic"
343 #~ msgstr "курсив"
344
345 #~ msgid "bold"
346 #~ msgstr "ΠΏΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ"
347
348 #~ msgid "spoiler"
349 #~ msgstr "спойлСр"
350
351 #~ msgid "comment"
352 #~ msgstr "ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
353
356 #~ msgid "Tag: "
354 #~ msgid "Tag: "
357 #~ msgstr "Π’Π΅Π³: "
355 #~ msgstr "Π’Π΅Π³: "
358
356
359 #~ msgid "Remove"
357 #~ msgid "Remove"
360 #~ msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
358 #~ msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
361
359
362 #~ msgid "Add"
360 #~ msgid "Add"
363 #~ msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ"
361 #~ msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ"
364
362
365 #~ msgid "Basic markdown syntax."
363 #~ msgid "Basic markdown syntax."
366 #~ msgstr "Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ синтаксис markdown."
364 #~ msgstr "Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ синтаксис markdown."
367
365
368 #~ msgid "Example: "
366 #~ msgid "Example: "
369 #~ msgstr "ΠŸΡ€ΠΈΠΌΠ΅Ρ€: "
367 #~ msgstr "ΠŸΡ€ΠΈΠΌΠ΅Ρ€: "
370
368
371 #~ msgid "tags"
369 #~ msgid "tags"
372 #~ msgstr "Ρ‚Π΅Π³ΠΎΠ²"
370 #~ msgstr "Ρ‚Π΅Π³ΠΎΠ²"
373
371
374 #~ msgid "Get!"
372 #~ msgid "Get!"
375 #~ msgstr "Π“Π΅Ρ‚!"
373 #~ msgstr "Π“Π΅Ρ‚!"
376
374
377 #~ msgid "View"
375 #~ msgid "View"
378 #~ msgstr "ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
376 #~ msgstr "ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
379
377
380 #~ msgid "gets"
378 #~ msgid "gets"
381 #~ msgstr "Π³Π΅Ρ‚ΠΎΠ²"
379 #~ msgstr "Π³Π΅Ρ‚ΠΎΠ²"
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,39 +1,43 b''
1 # SOME DESCRIPTIVE TITLE.
1 # SOME DESCRIPTIVE TITLE.
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 # This file is distributed under the same license as the PACKAGE package.
3 # This file is distributed under the same license as the PACKAGE package.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 #
5 #
6 #, fuzzy
6 #, fuzzy
7 msgid ""
7 msgid ""
8 msgstr ""
8 msgstr ""
9 "Project-Id-Version: PACKAGE VERSION\n"
9 "Project-Id-Version: PACKAGE VERSION\n"
10 "Report-Msgid-Bugs-To: \n"
10 "Report-Msgid-Bugs-To: \n"
11 "POT-Creation-Date: 2013-11-13 17:25+0200\n"
11 "POT-Creation-Date: 2013-12-21 21:45+0200\n"
12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "Language-Team: LANGUAGE <LL@li.org>\n"
15 "Language: \n"
15 "Language: \n"
16 "MIME-Version: 1.0\n"
16 "MIME-Version: 1.0\n"
17 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Type: text/plain; charset=UTF-8\n"
18 "Content-Transfer-Encoding: 8bit\n"
18 "Content-Transfer-Encoding: 8bit\n"
19 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
20 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
21
21
22 #: static/js/refpopup.js:60
22 #: static/js/refpopup.js:57
23 msgid "Loading..."
23 msgid "Loading..."
24 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..."
24 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..."
25
25
26 #: static/js/refpopup.js:86
26 #: static/js/refpopup.js:76
27 msgid "Post not found"
27 msgid "Post not found"
28 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ"
28 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ"
29
29
30 #: static/js/thread.js:32
30 #: static/js/thread.js:32
31 msgid "Normal"
31 msgid "Normal"
32 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ"
32 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ"
33
33
34 #: static/js/thread.js:33
34 #: static/js/thread.js:33
35 msgid "Gallery"
35 msgid "Gallery"
36 msgstr "ГалСрСя"
36 msgstr "ГалСрСя"
37
37
38 #: static/js/thread_update.js:177
39 msgid "[new posts]"
40 msgstr "[Π½ΠΎΠ²Ρ‹Π΅ посты]"
41
38 #~ msgid "Replies"
42 #~ msgid "Replies"
39 #~ msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
43 #~ msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
@@ -1,40 +1,41 b''
1 from django.shortcuts import redirect
1 from django.shortcuts import redirect
2 from boards import views, utils
2 from boards import views, utils
3 from boards.models import Ban
3 from boards.models import Ban
4 from django.utils.html import strip_spaces_between_tags
4 from django.utils.html import strip_spaces_between_tags
5 from django.conf import settings
5 from django.conf import settings
6
6
7 RESPONSE_CONTENT_TYPE = 'Content-Type'
7 RESPONSE_CONTENT_TYPE = 'Content-Type'
8
8
9 TYPE_HTML = 'text/html'
9 TYPE_HTML = 'text/html'
10
10
11
11
12 class BanMiddleware:
12 class BanMiddleware:
13 """
13 """
14 This is run before showing the thread. Banned users don't need to see
14 This is run before showing the thread. Banned users don't need to see
15 anything
15 anything
16 """
16 """
17
17
18 def process_view(self, request, view_func, view_args, view_kwargs):
18 def process_view(self, request, view_func, view_args, view_kwargs):
19
19
20 if view_func != views.you_are_banned:
20 if view_func != views.you_are_banned:
21 ip = utils.get_client_ip(request)
21 ip = utils.get_client_ip(request)
22 bans = Ban.objects.filter(ip=ip)
22 bans = Ban.objects.filter(ip=ip)
23
23
24 if bans.exists():
24 if bans.exists():
25 ban = bans[0]
25 ban = bans[0]
26 if not ban.can_read:
26 if not ban.can_read:
27 return redirect(views.you_are_banned)
27 return redirect(views.you_are_banned)
28
28
29
29
30 class MinifyHTMLMiddleware(object):
30 class MinifyHTMLMiddleware(object):
31 def process_response(self, request, response):
31 def process_response(self, request, response):
32 try:
32 try:
33 compress_html = settings.COMPRESS_HTML
33 compress_html = settings.COMPRESS_HTML
34 except AttributeError:
34 except AttributeError:
35 compress_html = False
35 compress_html = False
36
36
37 if TYPE_HTML in response[RESPONSE_CONTENT_TYPE] and compress_html:
37 if RESPONSE_CONTENT_TYPE in response\
38 and TYPE_HTML in response[RESPONSE_CONTENT_TYPE] and compress_html:
38 response.content = strip_spaces_between_tags(
39 response.content = strip_spaces_between_tags(
39 response.content.strip())
40 response.content.strip())
40 return response No newline at end of file
41 return response
@@ -1,374 +1,385 b''
1 from datetime import datetime, timedelta
1 from datetime import datetime, timedelta
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import os
3 import os
4 from random import random
4 from random import random
5 import time
5 import time
6 import math
6 import math
7 import re
7 import re
8 from django.core.cache import cache
8 from django.core.cache import cache
9
9
10 from django.db import models
10 from django.db import models
11 from django.http import Http404
11 from django.http import Http404
12 from django.utils import timezone
12 from django.utils import timezone
13 from markupfield.fields import MarkupField
13 from markupfield.fields import MarkupField
14
14
15 from neboard import settings
15 from neboard import settings
16 from boards import thumbs
16 from boards import thumbs
17
17
18 APP_LABEL_BOARDS = 'boards'
18 APP_LABEL_BOARDS = 'boards'
19
19
20 CACHE_KEY_PPD = 'ppd'
20 CACHE_KEY_PPD = 'ppd'
21
21
22 POSTS_PER_DAY_RANGE = range(7)
22 POSTS_PER_DAY_RANGE = range(7)
23
23
24 BAN_REASON_AUTO = 'Auto'
24 BAN_REASON_AUTO = 'Auto'
25
25
26 IMAGE_THUMB_SIZE = (200, 150)
26 IMAGE_THUMB_SIZE = (200, 150)
27
27
28 TITLE_MAX_LENGTH = 50
28 TITLE_MAX_LENGTH = 50
29
29
30 DEFAULT_MARKUP_TYPE = 'markdown'
30 DEFAULT_MARKUP_TYPE = 'markdown'
31
31
32 NO_PARENT = -1
32 NO_PARENT = -1
33 NO_IP = '0.0.0.0'
33 NO_IP = '0.0.0.0'
34 UNKNOWN_UA = ''
34 UNKNOWN_UA = ''
35 ALL_PAGES = -1
35 ALL_PAGES = -1
36 IMAGES_DIRECTORY = 'images/'
36 IMAGES_DIRECTORY = 'images/'
37 FILE_EXTENSION_DELIMITER = '.'
37 FILE_EXTENSION_DELIMITER = '.'
38
38
39 SETTING_MODERATE = "moderate"
39 SETTING_MODERATE = "moderate"
40
40
41 REGEX_REPLY = re.compile('>>(\d+)')
41 REGEX_REPLY = re.compile('>>(\d+)')
42
42
43
43
44 class PostManager(models.Manager):
44 class PostManager(models.Manager):
45
45
46 def create_post(self, title, text, image=None, thread=None,
46 def create_post(self, title, text, image=None, thread=None,
47 ip=NO_IP, tags=None, user=None):
47 ip=NO_IP, tags=None, user=None):
48 """
48 """
49 Create new post
49 Create new post
50 """
50 """
51
51
52 posting_time = timezone.now()
52 posting_time = timezone.now()
53 if not thread:
53 if not thread:
54 thread = Thread.objects.create(bump_time=posting_time,
54 thread = Thread.objects.create(bump_time=posting_time,
55 last_edit_time=posting_time)
55 last_edit_time=posting_time)
56 else:
56 else:
57 thread.bump()
57 thread.bump()
58 thread.last_edit_time = posting_time
58 thread.last_edit_time = posting_time
59 thread.save()
59 thread.save()
60
60
61 post = self.create(title=title,
61 post = self.create(title=title,
62 text=text,
62 text=text,
63 pub_time=posting_time,
63 pub_time=posting_time,
64 thread_new=thread,
64 thread_new=thread,
65 image=image,
65 image=image,
66 poster_ip=ip,
66 poster_ip=ip,
67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at last!
67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at last!
68 last_edit_time=posting_time,
68 last_edit_time=posting_time,
69 user=user)
69 user=user)
70
70
71 thread.replies.add(post)
71 thread.replies.add(post)
72 if tags:
72 if tags:
73 linked_tags = []
73 linked_tags = []
74 for tag in tags:
74 for tag in tags:
75 tag_linked_tags = tag.get_linked_tags()
75 tag_linked_tags = tag.get_linked_tags()
76 if len(tag_linked_tags) > 0:
76 if len(tag_linked_tags) > 0:
77 linked_tags.extend(tag_linked_tags)
77 linked_tags.extend(tag_linked_tags)
78
78
79 tags.extend(linked_tags)
79 tags.extend(linked_tags)
80 map(thread.add_tag, tags)
80 map(thread.add_tag, tags)
81
81
82 self._delete_old_threads()
82 self._delete_old_threads()
83 self.connect_replies(post)
83 self.connect_replies(post)
84
84
85 return post
85 return post
86
86
87 def delete_post(self, post):
87 def delete_post(self, post):
88 """
88 """
89 Delete post and update its thread
89 Delete post and update or delete its thread
90 """
90 """
91
92 thread = post.thread_new
91
93
92 thread = post.thread_new
94 if thread.get_opening_post() == self:
93 thread.last_edit_time = timezone.now()
95 thread.replies.delete()
94 thread.save()
96
97 thread.delete()
98 else:
99 thread.last_edit_time = timezone.now()
100 thread.save()
95
101
96 post.delete()
102 post.delete()
97
103
98 def delete_posts_by_ip(self, ip):
104 def delete_posts_by_ip(self, ip):
99 """
105 """
100 Delete all posts of the author with same IP
106 Delete all posts of the author with same IP
101 """
107 """
102
108
103 posts = self.filter(poster_ip=ip)
109 posts = self.filter(poster_ip=ip)
104 map(self.delete_post, posts)
110 map(self.delete_post, posts)
105
111
106 # TODO Move this method to thread manager
112 # TODO Move this method to thread manager
107 def get_threads(self, tag=None, page=ALL_PAGES,
113 def get_threads(self, tag=None, page=ALL_PAGES,
108 order_by='-bump_time'):
114 order_by='-bump_time'):
109 if tag:
115 if tag:
110 threads = tag.threads
116 threads = tag.threads
111
117
112 if not threads.exists():
118 if not threads.exists():
113 raise Http404
119 raise Http404
114 else:
120 else:
115 threads = Thread.objects.all()
121 threads = Thread.objects.all()
116
122
117 threads = threads.order_by(order_by)
123 threads = threads.order_by(order_by)
118
124
119 if page != ALL_PAGES:
125 if page != ALL_PAGES:
120 thread_count = threads.count()
126 thread_count = threads.count()
121
127
122 if page < self._get_page_count(thread_count):
128 if page < self._get_page_count(thread_count):
123 start_thread = page * settings.THREADS_PER_PAGE
129 start_thread = page * settings.THREADS_PER_PAGE
124 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
130 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
125 thread_count)
131 thread_count)
126 threads = threads[start_thread:end_thread]
132 threads = threads[start_thread:end_thread]
127
133
128 return threads
134 return threads
129
135
130 # TODO Move this method to thread manager
136 # TODO Move this method to thread manager
131 def get_thread_page_count(self, tag=None):
137 def get_thread_page_count(self, tag=None):
132 if tag:
138 if tag:
133 threads = Thread.objects.filter(tags=tag)
139 threads = Thread.objects.filter(tags=tag)
134 else:
140 else:
135 threads = Thread.objects.all()
141 threads = Thread.objects.all()
136
142
137 return self._get_page_count(threads.count())
143 return self._get_page_count(threads.count())
138
144
139 # TODO Move this method to thread manager
145 # TODO Move this method to thread manager
140 def _delete_old_threads(self):
146 def _delete_old_threads(self):
141 """
147 """
142 Preserves maximum thread count. If there are too many threads,
148 Preserves maximum thread count. If there are too many threads,
143 delete the old ones.
149 delete the old ones.
144 """
150 """
145
151
146 # TODO Move old threads to the archive instead of deleting them.
152 # TODO Move old threads to the archive instead of deleting them.
147 # Maybe make some 'old' field in the model to indicate the thread
153 # Maybe make some 'old' field in the model to indicate the thread
148 # must not be shown and be able for replying.
154 # must not be shown and be able for replying.
149
155
150 threads = Thread.objects.all()
156 threads = Thread.objects.all()
151 thread_count = threads.count()
157 thread_count = threads.count()
152
158
153 if thread_count > settings.MAX_THREAD_COUNT:
159 if thread_count > settings.MAX_THREAD_COUNT:
154 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
160 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
155 old_threads = threads[thread_count - num_threads_to_delete:]
161 old_threads = threads[thread_count - num_threads_to_delete:]
156
162
157 map(Thread.delete_with_posts, old_threads)
163 map(Thread.delete_with_posts, old_threads)
158
164
159 def connect_replies(self, post):
165 def connect_replies(self, post):
160 """
166 """
161 Connect replies to a post to show them as a reflink map
167 Connect replies to a post to show them as a reflink map
162 """
168 """
163
169
164 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
170 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
165 post_id = reply_number.group(1)
171 post_id = reply_number.group(1)
166 ref_post = self.filter(id=post_id)
172 ref_post = self.filter(id=post_id)
167 if ref_post.count() > 0:
173 if ref_post.count() > 0:
168 referenced_post = ref_post[0]
174 referenced_post = ref_post[0]
169 referenced_post.referenced_posts.add(post)
175 referenced_post.referenced_posts.add(post)
170 referenced_post.last_edit_time = post.pub_time
176 referenced_post.last_edit_time = post.pub_time
171 referenced_post.save()
177 referenced_post.save()
172
178
173 def _get_page_count(self, thread_count):
179 def _get_page_count(self, thread_count):
174 """
180 """
175 Get number of pages that will be needed for all threads
181 Get number of pages that will be needed for all threads
176 """
182 """
177
183
178 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
184 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
179
185
180 def get_posts_per_day(self):
186 def get_posts_per_day(self):
181 """
187 """
182 Get average count of posts per day for the last 7 days
188 Get average count of posts per day for the last 7 days
183 """
189 """
184
190
185 today = datetime.now().date()
191 today = datetime.now().date()
186 ppd = cache.get(CACHE_KEY_PPD + str(today))
192 ppd = cache.get(CACHE_KEY_PPD + str(today))
187 if ppd:
193 if ppd:
188 return ppd
194 return ppd
189
195
190 posts_per_days = []
196 posts_per_days = []
191 for i in POSTS_PER_DAY_RANGE:
197 for i in POSTS_PER_DAY_RANGE:
192 day_end = today - timedelta(i + 1)
198 day_end = today - timedelta(i + 1)
193 day_start = today - timedelta(i + 2)
199 day_start = today - timedelta(i + 2)
194
200
195 day_time_start = timezone.make_aware(datetime.combine(day_start,
201 day_time_start = timezone.make_aware(datetime.combine(day_start,
196 dtime()), timezone.get_current_timezone())
202 dtime()), timezone.get_current_timezone())
197 day_time_end = timezone.make_aware(datetime.combine(day_end,
203 day_time_end = timezone.make_aware(datetime.combine(day_end,
198 dtime()), timezone.get_current_timezone())
204 dtime()), timezone.get_current_timezone())
199
205
200 posts_per_days.append(float(self.filter(
206 posts_per_days.append(float(self.filter(
201 pub_time__lte=day_time_end,
207 pub_time__lte=day_time_end,
202 pub_time__gte=day_time_start).count()))
208 pub_time__gte=day_time_start).count()))
203
209
204 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
210 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
205 len(posts_per_days))
211 len(posts_per_days))
206 cache.set(CACHE_KEY_PPD, ppd)
212 cache.set(CACHE_KEY_PPD, ppd)
207 return ppd
213 return ppd
208
214
209
215
210 class Post(models.Model):
216 class Post(models.Model):
211 """A post is a message."""
217 """A post is a message."""
212
218
213 objects = PostManager()
219 objects = PostManager()
214
220
215 class Meta:
221 class Meta:
216 app_label = APP_LABEL_BOARDS
222 app_label = APP_LABEL_BOARDS
217
223
218 # TODO Save original file name to some field
224 # TODO Save original file name to some field
219 def _update_image_filename(self, filename):
225 def _update_image_filename(self, filename):
220 """Get unique image filename"""
226 """Get unique image filename"""
221
227
222 path = IMAGES_DIRECTORY
228 path = IMAGES_DIRECTORY
223 new_name = str(int(time.mktime(time.gmtime())))
229 new_name = str(int(time.mktime(time.gmtime())))
224 new_name += str(int(random() * 1000))
230 new_name += str(int(random() * 1000))
225 new_name += FILE_EXTENSION_DELIMITER
231 new_name += FILE_EXTENSION_DELIMITER
226 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
232 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
227
233
228 return os.path.join(path, new_name)
234 return os.path.join(path, new_name)
229
235
230 title = models.CharField(max_length=TITLE_MAX_LENGTH)
236 title = models.CharField(max_length=TITLE_MAX_LENGTH)
231 pub_time = models.DateTimeField()
237 pub_time = models.DateTimeField()
232 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
238 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
233 escape_html=False)
239 escape_html=False)
234
240
235 image_width = models.IntegerField(default=0)
241 image_width = models.IntegerField(default=0)
236 image_height = models.IntegerField(default=0)
242 image_height = models.IntegerField(default=0)
237
243
244 image_pre_width = models.IntegerField(default=0)
245 image_pre_height = models.IntegerField(default=0)
246
238 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
247 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
239 blank=True, sizes=(IMAGE_THUMB_SIZE,),
248 blank=True, sizes=(IMAGE_THUMB_SIZE,),
240 width_field='image_width',
249 width_field='image_width',
241 height_field='image_height')
250 height_field='image_height',
251 preview_width_field='image_pre_width',
252 preview_height_field='image_pre_height')
242
253
243 poster_ip = models.GenericIPAddressField()
254 poster_ip = models.GenericIPAddressField()
244 poster_user_agent = models.TextField()
255 poster_user_agent = models.TextField()
245
256
246 thread = models.ForeignKey('Post', null=True, default=None)
257 thread = models.ForeignKey('Post', null=True, default=None)
247 thread_new = models.ForeignKey('Thread', null=True, default=None)
258 thread_new = models.ForeignKey('Thread', null=True, default=None)
248 last_edit_time = models.DateTimeField()
259 last_edit_time = models.DateTimeField()
249 user = models.ForeignKey('User', null=True, default=None)
260 user = models.ForeignKey('User', null=True, default=None)
250
261
251 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
262 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
252 null=True,
263 null=True,
253 blank=True, related_name='rfp+')
264 blank=True, related_name='rfp+')
254
265
255 def __unicode__(self):
266 def __unicode__(self):
256 return '#' + str(self.id) + ' ' + self.title + ' (' + \
267 return '#' + str(self.id) + ' ' + self.title + ' (' + \
257 self.text.raw[:50] + ')'
268 self.text.raw[:50] + ')'
258
269
259 def get_title(self):
270 def get_title(self):
260 title = self.title
271 title = self.title
261 if len(title) == 0:
272 if len(title) == 0:
262 title = self.text.raw[:20]
273 title = self.text.raw[:20]
263
274
264 return title
275 return title
265
276
266 def get_sorted_referenced_posts(self):
277 def get_sorted_referenced_posts(self):
267 return self.referenced_posts.order_by('id')
278 return self.referenced_posts.order_by('id')
268
279
269 def is_referenced(self):
280 def is_referenced(self):
270 return self.referenced_posts.all().exists()
281 return self.referenced_posts.all().exists()
271
282
272 def is_opening(self):
283 def is_opening(self):
273 return self.thread_new.get_replies()[0] == self
284 return self.thread_new.get_replies()[0] == self
274
285
275
286
276 class Thread(models.Model):
287 class Thread(models.Model):
277
288
278 class Meta:
289 class Meta:
279 app_label = APP_LABEL_BOARDS
290 app_label = APP_LABEL_BOARDS
280
291
281 tags = models.ManyToManyField('Tag')
292 tags = models.ManyToManyField('Tag')
282 bump_time = models.DateTimeField()
293 bump_time = models.DateTimeField()
283 last_edit_time = models.DateTimeField()
294 last_edit_time = models.DateTimeField()
284 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
295 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
285 blank=True, related_name='tre+')
296 blank=True, related_name='tre+')
286
297
287 def get_tags(self):
298 def get_tags(self):
288 """
299 """
289 Get a sorted tag list
300 Get a sorted tag list
290 """
301 """
291
302
292 return self.tags.order_by('name')
303 return self.tags.order_by('name')
293
304
294 def bump(self):
305 def bump(self):
295 """
306 """
296 Bump (move to up) thread
307 Bump (move to up) thread
297 """
308 """
298
309
299 if self.can_bump():
310 if self.can_bump():
300 self.bump_time = timezone.now()
311 self.bump_time = timezone.now()
301
312
302 def get_reply_count(self):
313 def get_reply_count(self):
303 return self.replies.count()
314 return self.replies.count()
304
315
305 def get_images_count(self):
316 def get_images_count(self):
306 return self.replies.filter(image_width__gt=0).count()
317 return self.replies.filter(image_width__gt=0).count()
307
318
308 def can_bump(self):
319 def can_bump(self):
309 """
320 """
310 Check if the thread can be bumped by replying
321 Check if the thread can be bumped by replying
311 """
322 """
312
323
313 post_count = self.get_reply_count()
324 post_count = self.get_reply_count()
314
325
315 return post_count < settings.MAX_POSTS_PER_THREAD
326 return post_count < settings.MAX_POSTS_PER_THREAD
316
327
317 def delete_with_posts(self):
328 def delete_with_posts(self):
318 """
329 """
319 Completely delete thread and all its posts
330 Completely delete thread and all its posts
320 """
331 """
321
332
322 if self.replies.count() > 0:
333 if self.replies.count() > 0:
323 map(Post.objects.delete_post, self.replies.all())
334 map(Post.objects.delete_post, self.replies.all())
324
335
325 self.delete()
336 self.delete()
326
337
327 def get_last_replies(self):
338 def get_last_replies(self):
328 """
339 """
329 Get last replies, not including opening post
340 Get last replies, not including opening post
330 """
341 """
331
342
332 if settings.LAST_REPLIES_COUNT > 0:
343 if settings.LAST_REPLIES_COUNT > 0:
333 reply_count = self.get_reply_count()
344 reply_count = self.get_reply_count()
334
345
335 if reply_count > 0:
346 if reply_count > 0:
336 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
347 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
337 reply_count - 1)
348 reply_count - 1)
338 last_replies = self.replies.all().order_by('pub_time')[
349 last_replies = self.replies.all().order_by('pub_time')[
339 reply_count - reply_count_to_show:]
350 reply_count - reply_count_to_show:]
340
351
341 return last_replies
352 return last_replies
342
353
343 def get_replies(self):
354 def get_replies(self):
344 """
355 """
345 Get sorted thread posts
356 Get sorted thread posts
346 """
357 """
347
358
348 return self.replies.all().order_by('pub_time')
359 return self.replies.all().order_by('pub_time')
349
360
350 def add_tag(self, tag):
361 def add_tag(self, tag):
351 """
362 """
352 Connect thread to a tag and tag to a thread
363 Connect thread to a tag and tag to a thread
353 """
364 """
354
365
355 self.tags.add(tag)
366 self.tags.add(tag)
356 tag.threads.add(self)
367 tag.threads.add(self)
357
368
358 def get_opening_post(self):
369 def get_opening_post(self):
359 """
370 """
360 Get first post of the thread
371 Get first post of the thread
361 """
372 """
362
373
363 return self.get_replies()[0]
374 return self.get_replies()[0]
364
375
365 def __unicode__(self):
376 def __unicode__(self):
366 return str(self.get_replies()[0].id)
377 return str(self.get_replies()[0].id)
367
378
368 def get_pub_time(self):
379 def get_pub_time(self):
369 """
380 """
370 Thread does not have its own pub time, so we need to get it from
381 Thread does not have its own pub time, so we need to get it from
371 the opening post
382 the opening post
372 """
383 """
373
384
374 return self.get_opening_post().pub_time
385 return self.get_opening_post().pub_time
@@ -1,29 +1,33 b''
1 .ui-button {
1 .ui-button {
2 display: none;
2 display: none;
3 }
3 }
4
4
5 .ui-dialog-content {
5 .ui-dialog-content {
6 padding: 0;
6 padding: 0;
7 min-height: 0;
7 min-height: 0;
8 }
8 }
9
9
10 .mark_btn {
10 .mark_btn {
11 cursor: pointer;
11 cursor: pointer;
12 }
12 }
13
13
14 .img-full {
14 .img-full {
15 position: fixed;
15 position: fixed;
16 z-index: 9999;
16 z-index: 9999;
17 background-color: #CCC;
17 background-color: #CCC;
18 border: 1px solid #000;
18 border: 1px solid #000;
19 cursor: pointer;
19 cursor: pointer;
20 }
20 }
21
21
22 .strikethrough {
22 .strikethrough {
23 text-decoration: line-through;
23 text-decoration: line-through;
24 }
24 }
25
25
26 .post_preview {
26 .post_preview {
27 z-index: 300;
27 z-index: 300;
28 position:absolute;
28 position:absolute;
29 }
29 }
30
31 .gallery_image {
32 display: inline-block;
33 } No newline at end of file
@@ -1,358 +1,368 b''
1 html {
1 html {
2 background: #555;
2 background: #555;
3 color: #ffffff;
3 color: #ffffff;
4 }
4 }
5
5
6 #admin_panel {
6 #admin_panel {
7 background: #FF0000;
7 background: #FF0000;
8 color: #00FF00
8 color: #00FF00
9 }
9 }
10
10
11 .input_field {
11 .input_field {
12
12
13 }
13 }
14
14
15 .input_field_name {
15 .input_field_name {
16
16
17 }
17 }
18
18
19 .input_field_error {
19 .input_field_error {
20 color: #FF0000;
20 color: #FF0000;
21 }
21 }
22
22
23 .title {
23 .title {
24 font-weight: bold;
24 font-weight: bold;
25 color: #ffcc00;
25 color: #ffcc00;
26 font-size: 2ex;
26 font-size: 2ex;
27 }
27 }
28
28
29 .link, a {
29 .link, a {
30 color: #afdcec;
30 color: #afdcec;
31 }
31 }
32
32
33 .block {
33 .block {
34 display: inline-block;
34 display: inline-block;
35 vertical-align: top;
35 vertical-align: top;
36 }
36 }
37
37
38 .tag {
38 .tag {
39 color: #b4cfec;
39 color: #b4cfec;
40 }
40 }
41
41
42 .post_id {
42 .post_id {
43 color: #fff380;
43 color: #fff380;
44 }
44 }
45
45
46 .post, .dead_post, #posts-table {
46 .post, .dead_post, #posts-table {
47 background: #333;
47 background: #333;
48 margin: 5px;
48 margin: 5px;
49 padding: 10px;
49 padding: 10px;
50 border: solid 1px #888;
50 border: solid 1px #888;
51 clear: left;
51 clear: left;
52 word-wrap: break-word;
52 word-wrap: break-word;
53 }
53 }
54
54
55 .metadata {
55 .metadata {
56 padding-top: 5px;
56 padding-top: 5px;
57 margin-top: 10px;
57 margin-top: 10px;
58 border-top: solid 1px #666;
58 border-top: solid 1px #666;
59 font-size: 0.9em;
59 font-size: 0.9em;
60 color: #ddd;
60 color: #ddd;
61 }
61 }
62
62
63 .navigation_panel, .tag_info {
63 .navigation_panel, .tag_info {
64 background: #444;
64 background: #444;
65 margin: 5px;
65 margin: 5px;
66 padding: 10px;
66 padding: 10px;
67 border: solid 1px #888;
67 border: solid 1px #888;
68 color: #eee;
68 color: #eee;
69 }
69 }
70
70
71 .navigation_panel .link {
71 .navigation_panel .link {
72 border-right: 1px solid #fff;
72 border-right: 1px solid #fff;
73 font-weight: bold;
73 font-weight: bold;
74 margin-right: 1ex;
74 margin-right: 1ex;
75 padding-right: 1ex;
75 padding-right: 1ex;
76 }
76 }
77 .navigation_panel .link:last-child {
77 .navigation_panel .link:last-child {
78 border-left: 1px solid #fff;
78 border-left: 1px solid #fff;
79 border-right: none;
79 border-right: none;
80 float: right;
80 float: right;
81 margin-left: 1ex;
81 margin-left: 1ex;
82 margin-right: 0;
82 margin-right: 0;
83 padding-left: 1ex;
83 padding-left: 1ex;
84 padding-right: 0;
84 padding-right: 0;
85 }
85 }
86
86
87 .navigation_panel::after, .post::after {
87 .navigation_panel::after, .post::after {
88 clear: both;
88 clear: both;
89 content: ".";
89 content: ".";
90 display: block;
90 display: block;
91 height: 0;
91 height: 0;
92 line-height: 0;
92 line-height: 0;
93 visibility: hidden;
93 visibility: hidden;
94 }
94 }
95
95
96 p {
96 p {
97 margin-top: .5em;
97 margin-top: .5em;
98 margin-bottom: .5em;
98 margin-bottom: .5em;
99 }
99 }
100
100
101 .post-form-w {
101 .post-form-w {
102 display: table;
102 display: table;
103 background: #333344;
103 background: #333344;
104 border: solid 1px #888;
104 border: solid 1px #888;
105 color: #fff;
105 color: #fff;
106 padding: 10px;
106 padding: 10px;
107 margin: 5px;
107 margin: 5px;
108 }
108 }
109
109
110 .form-row {
110 .form-row {
111 display: table-row;
111 display: table-row;
112 }
112 }
113
113
114 .form-label, .form-input, .form-errors {
114 .form-label, .form-input, .form-errors {
115 display: table-cell;
115 display: table-cell;
116 }
116 }
117
117
118 .form-label {
118 .form-label {
119 padding: .25em 1ex .25em 0;
119 padding: .25em 1ex .25em 0;
120 vertical-align: top;
120 vertical-align: top;
121 }
121 }
122
122
123 .form-input {
123 .form-input {
124 padding: .25em 0;
124 padding: .25em 0;
125 }
125 }
126
126
127 .form-errors {
127 .form-errors {
128 font-weight: bolder;
128 font-weight: bolder;
129 vertical-align: middle;
129 vertical-align: middle;
130 }
130 }
131
131
132 .post-form input, .post-form textarea {
132 .post-form input, .post-form textarea {
133 background: #333;
133 background: #333;
134 color: #fff;
134 color: #fff;
135 border: solid 1px;
135 border: solid 1px;
136 padding: 0;
136 padding: 0;
137 width: 100%;
137 width: 100%;
138 font: medium sans;
138 font: medium sans;
139 }
139 }
140
140
141 .form-submit {
141 .form-submit {
142 display: table;
142 display: table;
143 margin-bottom: 1ex;
143 margin-bottom: 1ex;
144 }
144 }
145
145
146 .form-title {
146 .form-title {
147 font-weight: bold;
147 font-weight: bold;
148 font-size: 2.5ex;
148 font-size: 2.5ex;
149 text-decoration: underline;
149 text-decoration: underline;
150 }
150 }
151
151
152 input[type="submit"] {
152 input[type="submit"] {
153 background: #222;
153 background: #222;
154 border: solid 2px #fff;
154 border: solid 2px #fff;
155 color: #fff;
155 color: #fff;
156 padding: 0.5ex;
156 padding: 0.5ex;
157 }
157 }
158
158
159 input[type="submit"]:hover {
159 input[type="submit"]:hover {
160 background: #060;
160 background: #060;
161 }
161 }
162
162
163 blockquote {
163 blockquote {
164 border-left: solid 2px;
164 border-left: solid 2px;
165 padding-left: 5px;
165 padding-left: 5px;
166 color: #B1FB17;
166 color: #B1FB17;
167 margin: 0;
167 margin: 0;
168 }
168 }
169
169
170 .post > .image {
170 .post > .image {
171 float: left;
171 float: left;
172 margin: 0 1ex .5ex 0;
172 margin: 0 1ex .5ex 0;
173 min-width: 1px;
173 min-width: 1px;
174 text-align: center;
174 text-align: center;
175 display: table-row;
175 display: table-row;
176
177 height: 150px;
178 }
176 }
179
177
180 .post > .metadata {
178 .post > .metadata {
181 clear: left;
179 clear: left;
182 }
180 }
183
181
184 .get {
182 .get {
185 font-weight: bold;
183 font-weight: bold;
186 color: #d55;
184 color: #d55;
187 }
185 }
188
186
189 * {
187 * {
190 text-decoration: none;
188 text-decoration: none;
191 }
189 }
192
190
193 .dead_post {
191 .dead_post {
194 background-color: #442222;
192 background-color: #442222;
195 }
193 }
196
194
197 .mark_btn {
195 .mark_btn {
198 border: 1px solid;
196 border: 1px solid;
199 min-width: 2ex;
197 min-width: 2ex;
200 padding: 2px 2ex;
198 padding: 2px 2ex;
201 }
199 }
202
200
203 .mark_btn:hover {
201 .mark_btn:hover {
204 background: #555;
202 background: #555;
205 }
203 }
206
204
207 .quote {
205 .quote {
208 color: #92cf38;
206 color: #92cf38;
209 font-style: italic;
207 font-style: italic;
210 }
208 }
211
209
212 .spoiler {
210 .spoiler {
213 background: white;
211 background: white;
214 color: white;
212 color: white;
215 }
213 }
216
214
217 .spoiler:hover {
215 .spoiler:hover {
218 color: black;
216 color: black;
219 }
217 }
220
218
221 .comment {
219 .comment {
222 color: #eb2;
220 color: #eb2;
223 font-style: italic;
221 font-style: italic;
224 }
222 }
225
223
226 a:hover {
224 a:hover {
227 text-decoration: underline;
225 text-decoration: underline;
228 }
226 }
229
227
230 .last-replies {
228 .last-replies {
231 margin-left: 3ex;
229 margin-left: 3ex;
232 }
230 }
233
231
234 .thread {
232 .thread {
235 margin-bottom: 3ex;
233 margin-bottom: 3ex;
236 }
234 }
237
235
238 .post:target {
236 .post:target {
239 border: solid 2px white;
237 border: solid 2px white;
240 }
238 }
241
239
242 pre{
240 pre{
243 white-space:pre-wrap
241 white-space:pre-wrap
244 }
242 }
245
243
246 li {
244 li {
247 list-style-position: inside;
245 list-style-position: inside;
248 }
246 }
249
247
250 .fancybox-skin {
248 .fancybox-skin {
251 position: relative;
249 position: relative;
252 background-color: #fff;
250 background-color: #fff;
253 color: #ddd;
251 color: #ddd;
254 text-shadow: none;
252 text-shadow: none;
255 }
253 }
256
254
257 .fancybox-image {
255 .fancybox-image {
258 border: 1px solid black;
256 border: 1px solid black;
259 }
257 }
260
258
261 .image-mode-tab {
259 .image-mode-tab {
262 background: #444;
260 background: #444;
263 color: #eee;
261 color: #eee;
264 display: table;
262 display: table;
265 margin: 5px;
263 margin: 5px;
266 padding: 5px;
264 padding: 5px;
267 border: 1px solid #888;
265 border: 1px solid #888;
268 }
266 }
269
267
270 .image-mode-tab > label {
268 .image-mode-tab > label {
271 margin: 0 1ex;
269 margin: 0 1ex;
272 }
270 }
273
271
274 .image-mode-tab > label > input {
272 .image-mode-tab > label > input {
275 margin-right: .5ex;
273 margin-right: .5ex;
276 }
274 }
277
275
278 #posts-table {
276 #posts-table {
279 margin: 5px;
277 margin: 5px;
280 }
278 }
281
279
282 .tag_info {
280 .tag_info {
283 display: table;
281 display: table;
284 }
282 }
285
283
286 .tag_info > h2 {
284 .tag_info > h2 {
287 margin: 0;
285 margin: 0;
288 }
286 }
289
287
290 .post-info {
288 .post-info {
291 color: #ddd;
289 color: #ddd;
292 }
290 }
293
291
294 .moderator_info {
292 .moderator_info {
295 color: #e99d41;
293 color: #e99d41;
296 border: dashed 1px;
294 border: dashed 1px;
297 padding: 3px;
295 padding: 3px;
298 }
296 }
299
297
300 .refmap {
298 .refmap {
301 font-size: 0.9em;
299 font-size: 0.9em;
302 color: #ccc;
300 color: #ccc;
303 margin-top: 1em;
301 margin-top: 1em;
304 }
302 }
305
303
306 .fav {
304 .fav {
307 color: yellow;
305 color: yellow;
308 }
306 }
309
307
310 .not_fav {
308 .not_fav {
311 color: #ccc;
309 color: #ccc;
312 }
310 }
313
311
314 .role {
312 .role {
315 text-decoration: underline;
313 text-decoration: underline;
316 }
314 }
317
315
318 .form-email {
316 .form-email {
319 display: none;
317 display: none;
320 }
318 }
321
319
322 .footer {
320 .footer {
323 margin: 5px;
321 margin: 5px;
324 }
322 }
325
323
326 .bar-value {
324 .bar-value {
327 background: rgba(50, 55, 164, 0.45);
325 background: rgba(50, 55, 164, 0.45);
328 font-size: 0.9em;
326 font-size: 0.9em;
329 height: 1.5em;
327 height: 1.5em;
330 }
328 }
331
329
332 .bar-bg {
330 .bar-bg {
333 position: relative;
331 position: relative;
334 border: solid 1px #888;
332 border: solid 1px #888;
335 margin: 5px;
333 margin: 5px;
336 overflow: hidden;
334 overflow: hidden;
337 }
335 }
338
336
339 .bar-text {
337 .bar-text {
340 padding: 2px;
338 padding: 2px;
341 position: absolute;
339 position: absolute;
342 left: 0;
340 left: 0;
343 top: 0;
341 top: 0;
344 }
342 }
345
343
346 .page_link {
344 .page_link {
347 display: table;
345 display: table;
348 background: #444;
346 background: #444;
349 margin: 5px;
347 margin: 5px;
350 border: solid 1px #888;
348 border: solid 1px #888;
351 padding: 5px;
349 padding: 5px;
352 font-weight: bolder;
350 font-weight: bolder;
353 color: #eee;
351 color: #eee;
354 }
352 }
355
353
356 .skipped_replies {
354 .skipped_replies {
357 margin: 5px;
355 margin: 5px;
358 }
356 }
357
358 .current_page, .current_mode {
359 border: solid 1px #afdcec;
360 padding: 2px;
361 }
362
363 .gallery_image {
364 border: solid 1px;
365 padding: 0.5ex;
366 margin: 0.5ex;
367 text-align: center;
368 } No newline at end of file
@@ -1,344 +1,347 b''
1 html {
1 html {
2 background: rgb(238, 238, 238);
2 background: rgb(238, 238, 238);
3 color: rgb(51, 51, 51);
3 color: rgb(51, 51, 51);
4 }
4 }
5
5
6 #admin_panel {
6 #admin_panel {
7 background: #FF0000;
7 background: #FF0000;
8 color: #00FF00
8 color: #00FF00
9 }
9 }
10
10
11 .input_field {
11 .input_field {
12
12
13 }
13 }
14
14
15 .input_field_name {
15 .input_field_name {
16
16
17 }
17 }
18
18
19 .input_field_error {
19 .input_field_error {
20 color: #FF0000;
20 color: #FF0000;
21 }
21 }
22
22
23
23
24 .title {
24 .title {
25 font-weight: bold;
25 font-weight: bold;
26 color: #333;
26 color: #333;
27 font-size: 2ex;
27 font-size: 2ex;
28 }
28 }
29
29
30 .link, a {
30 .link, a {
31 color: rgb(255, 102, 0);
31 color: rgb(255, 102, 0);
32 }
32 }
33
33
34 .block {
34 .block {
35 display: inline-block;
35 display: inline-block;
36 vertical-align: top;
36 vertical-align: top;
37 }
37 }
38
38
39 .tag {
39 .tag {
40 color: #222;
40 color: #222;
41 }
41 }
42
42
43 .post_id:hover {
43 .post_id:hover {
44 color: #11f;
44 color: #11f;
45 }
45 }
46
46
47 .post_id {
47 .post_id {
48 color: #444;
48 color: #444;
49 }
49 }
50
50
51 .post, .dead_post, #posts-table {
51 .post, .dead_post, #posts-table {
52 margin: 5px;
52 margin: 5px;
53 padding: 10px;
53 padding: 10px;
54 background: rgb(221, 221, 221);
54 background: rgb(221, 221, 221);
55 border: 1px solid rgb(204, 204, 204);
55 border: 1px solid rgb(204, 204, 204);
56 border-radius: 5px 5px 5px 5px;
56 border-radius: 5px 5px 5px 5px;
57 clear: left;
57 clear: left;
58 word-wrap: break-word;
58 word-wrap: break-word;
59 display: table;
59 display: table;
60 }
60 }
61
61
62 .metadata {
62 .metadata {
63 padding: 5px;
63 padding: 5px;
64 margin-top: 10px;
64 margin-top: 10px;
65 border: solid 1px #666;
65 border: solid 1px #666;
66 font-size: 0.9em;
66 font-size: 0.9em;
67 display: table;
67 display: table;
68 }
68 }
69
69
70 .navigation_panel, .tag_info, .page_link {
70 .navigation_panel, .tag_info, .page_link {
71 margin: 5px;
71 margin: 5px;
72 padding: 10px;
72 padding: 10px;
73 border: 1px solid rgb(204, 204, 204);
73 border: 1px solid rgb(204, 204, 204);
74 border-radius: 5px 5px 5px 5px;
74 border-radius: 5px 5px 5px 5px;
75 }
75 }
76
76
77 .navigation_panel .link {
77 .navigation_panel .link {
78 border-right: 1px solid #000;
78 border-right: 1px solid #000;
79 font-weight: bold;
79 font-weight: bold;
80 margin-right: 1ex;
80 margin-right: 1ex;
81 padding-right: 1ex;
81 padding-right: 1ex;
82 }
82 }
83 .navigation_panel .link:last-child {
83 .navigation_panel .link:last-child {
84 border-left: 1px solid #000;
84 border-left: 1px solid #000;
85 border-right: none;
85 border-right: none;
86 float: right;
86 float: right;
87 margin-left: 1ex;
87 margin-left: 1ex;
88 margin-right: 0;
88 margin-right: 0;
89 padding-left: 1ex;
89 padding-left: 1ex;
90 padding-right: 0;
90 padding-right: 0;
91 }
91 }
92
92
93 .navigation_panel::after, .post::after {
93 .navigation_panel::after, .post::after {
94 clear: both;
94 clear: both;
95 content: ".";
95 content: ".";
96 display: block;
96 display: block;
97 height: 0;
97 height: 0;
98 line-height: 0;
98 line-height: 0;
99 visibility: hidden;
99 visibility: hidden;
100 }
100 }
101
101
102 p {
102 p {
103 margin-top: .5em;
103 margin-top: .5em;
104 margin-bottom: .5em;
104 margin-bottom: .5em;
105 }
105 }
106
106
107 .post-form-w {
107 .post-form-w {
108 display: table;
108 display: table;
109 padding: 10px;
109 padding: 10px;
110 margin: 5px
110 margin: 5px
111 }
111 }
112
112
113 .form-row {
113 .form-row {
114 display: table-row;
114 display: table-row;
115 }
115 }
116
116
117 .form-label, .form-input, .form-errors {
117 .form-label, .form-input, .form-errors {
118 display: table-cell;
118 display: table-cell;
119 }
119 }
120
120
121 .form-label {
121 .form-label {
122 padding: .25em 1ex .25em 0;
122 padding: .25em 1ex .25em 0;
123 vertical-align: top;
123 vertical-align: top;
124 }
124 }
125
125
126 .form-input {
126 .form-input {
127 padding: .25em 0;
127 padding: .25em 0;
128 }
128 }
129
129
130 .form-errors {
130 .form-errors {
131 padding-left: 1ex;
131 padding-left: 1ex;
132 font-weight: bold;
132 font-weight: bold;
133 vertical-align: middle;
133 vertical-align: middle;
134 }
134 }
135
135
136 .post-form input, .post-form textarea {
136 .post-form input, .post-form textarea {
137 background: #fff;
137 background: #fff;
138 color: #000;
138 color: #000;
139 border: solid 1px;
139 border: solid 1px;
140 padding: 0;
140 padding: 0;
141 width: 100%;
141 width: 100%;
142 font: medium sans;
142 font: medium sans;
143 }
143 }
144
144
145 .form-submit {
145 .form-submit {
146 border-bottom: 2px solid #ddd;
146 border-bottom: 2px solid #ddd;
147 margin-bottom: .5em;
147 margin-bottom: .5em;
148 padding-bottom: .5em;
148 padding-bottom: .5em;
149 }
149 }
150
150
151 .form-title {
151 .form-title {
152 font-weight: bold;
152 font-weight: bold;
153 }
153 }
154
154
155 input[type="submit"] {
155 input[type="submit"] {
156 background: #fff;
156 background: #fff;
157 border: solid 1px #000;
157 border: solid 1px #000;
158 color: #000;
158 color: #000;
159 }
159 }
160
160
161 blockquote {
161 blockquote {
162 border-left: solid 2px;
162 border-left: solid 2px;
163 padding-left: 5px;
163 padding-left: 5px;
164 color: #B1FB17;
164 color: #B1FB17;
165 margin: 0;
165 margin: 0;
166 }
166 }
167
167
168 .post > .image {
168 .post > .image {
169 float: left;
169 float: left;
170 margin: 0 1ex .5ex 0;
170 margin: 0 1ex .5ex 0;
171 min-width: 1px;
171 min-width: 1px;
172 text-align: center;
172 text-align: center;
173 display: table-row;
173 display: table-row;
174
175 height: 150px;
176 }
174 }
177
175
178 .post > .metadata {
176 .post > .metadata {
179 clear: left;
177 clear: left;
180 }
178 }
181
179
182 .get {
180 .get {
183 font-weight: bold;
181 font-weight: bold;
184 color: #d55;
182 color: #d55;
185 }
183 }
186
184
187 * {
185 * {
188 text-decoration: none;
186 text-decoration: none;
189 }
187 }
190
188
191 .dead_post {
189 .dead_post {
192 background-color: #ecc;
190 background-color: #ecc;
193 }
191 }
194
192
195 .quote {
193 .quote {
196 color: #080;
194 color: #080;
197 font-style: italic;
195 font-style: italic;
198 }
196 }
199
197
200 .spoiler {
198 .spoiler {
201 background: white;
199 background: white;
202 color: white;
200 color: white;
203 }
201 }
204
202
205 .spoiler:hover {
203 .spoiler:hover {
206 color: black;
204 color: black;
207 }
205 }
208
206
209 .comment {
207 .comment {
210 color: #8B6914;
208 color: #8B6914;
211 font-style: italic;
209 font-style: italic;
212 }
210 }
213
211
214 a:hover {
212 a:hover {
215 text-decoration: underline;
213 text-decoration: underline;
216 }
214 }
217
215
218 .last-replies {
216 .last-replies {
219 margin-left: 3ex;
217 margin-left: 3ex;
220 }
218 }
221
219
222 .thread {
220 .thread {
223 margin-bottom: 3ex;
221 margin-bottom: 3ex;
224 }
222 }
225
223
226 .post:target {
224 .post:target {
227 border: solid 2px black;
225 border: solid 2px black;
228 }
226 }
229
227
230 pre{
228 pre{
231 white-space:pre-wrap
229 white-space:pre-wrap
232 }
230 }
233
231
234 li {
232 li {
235 list-style-position: inside;
233 list-style-position: inside;
236 }
234 }
237
235
238 .fancybox-skin {
236 .fancybox-skin {
239 position: relative;
237 position: relative;
240 background-color: #fff;
238 background-color: #fff;
241 color: #ddd;
239 color: #ddd;
242 text-shadow: none;
240 text-shadow: none;
243 }
241 }
244
242
245 .fancybox-image {
243 .fancybox-image {
246 border: 1px solid black;
244 border: 1px solid black;
247 }
245 }
248
246
249 .image-mode-tab {
247 .image-mode-tab {
250 display: table;
248 display: table;
251 margin: 5px;
249 margin: 5px;
252 padding: 5px;
250 padding: 5px;
253 background: rgb(221, 221, 221);
251 background: rgb(221, 221, 221);
254 border: 1px solid rgb(204, 204, 204);
252 border: 1px solid rgb(204, 204, 204);
255 border-radius: 5px 5px 5px 5px;
253 border-radius: 5px 5px 5px 5px;
256 }
254 }
257
255
258 .image-mode-tab > label {
256 .image-mode-tab > label {
259 margin: 0 1ex;
257 margin: 0 1ex;
260 }
258 }
261
259
262 .image-mode-tab > label > input {
260 .image-mode-tab > label > input {
263 margin-right: .5ex;
261 margin-right: .5ex;
264 }
262 }
265
263
266 #posts-table {
264 #posts-table {
267 margin: 5px;
265 margin: 5px;
268 }
266 }
269
267
270 .tag_info, .page_link {
268 .tag_info, .page_link {
271 display: table;
269 display: table;
272 }
270 }
273
271
274 .tag_info > h2 {
272 .tag_info > h2 {
275 margin: 0;
273 margin: 0;
276 }
274 }
277
275
278 .moderator_info {
276 .moderator_info {
279 color: #e99d41;
277 color: #e99d41;
280 border: dashed 1px;
278 border: dashed 1px;
281 padding: 3px;
279 padding: 3px;
282 }
280 }
283
281
284 .refmap {
282 .refmap {
285 font-size: 0.9em;
283 font-size: 0.9em;
286 color: #444;
284 color: #444;
287 margin-top: 1em;
285 margin-top: 1em;
288 }
286 }
289
287
290 input[type="submit"]:hover {
288 input[type="submit"]:hover {
291 background: #ccc;
289 background: #ccc;
292 }
290 }
293
291
294
292
295 .fav {
293 .fav {
296 color: rgb(255, 102, 0);
294 color: rgb(255, 102, 0);
297 }
295 }
298
296
299 .not_fav {
297 .not_fav {
300 color: #555;
298 color: #555;
301 }
299 }
302
300
303 .role {
301 .role {
304 text-decoration: underline;
302 text-decoration: underline;
305 }
303 }
306
304
307 .form-email {
305 .form-email {
308 display: none;
306 display: none;
309 }
307 }
310
308
311 .mark_btn {
309 .mark_btn {
312 padding: 2px 2ex;
310 padding: 2px 2ex;
313 border: 1px solid;
311 border: 1px solid;
314 }
312 }
315
313
316 .mark_btn:hover {
314 .mark_btn:hover {
317 background: #ccc;
315 background: #ccc;
318 }
316 }
319
317
320 .bar-value {
318 .bar-value {
321 background: rgba(251, 199, 16, 0.61);
319 background: rgba(251, 199, 16, 0.61);
322 padding: 2px;
320 padding: 2px;
323 font-size: 0.9em;
321 font-size: 0.9em;
324 height: 1.5em;
322 height: 1.5em;
325 }
323 }
326
324
327 .bar-bg {
325 .bar-bg {
328 position: relative;
326 position: relative;
329 border: 1px solid rgb(204, 204, 204);
327 border: 1px solid rgb(204, 204, 204);
330 border-radius: 5px 5px 5px 5px;
328 border-radius: 5px 5px 5px 5px;
331 margin: 5px;
329 margin: 5px;
332 overflow: hidden;
330 overflow: hidden;
333 }
331 }
334
332
335 .bar-text {
333 .bar-text {
336 padding: 2px;
334 padding: 2px;
337 position: absolute;
335 position: absolute;
338 left: 0;
336 left: 0;
339 top: 0;
337 top: 0;
340 }
338 }
341
339
342 .skipped_replies {
340 .skipped_replies {
343 margin: 5px;
341 margin: 5px;
344 }
342 }
343
344 .current_page, .current_mode {
345 border: solid 1px #000;
346 padding: 2px;
347 } No newline at end of file
@@ -1,333 +1,357 b''
1 * {
1 * {
2 font-size: inherit;
2 font-size: inherit;
3 margin: 0;
3 margin: 0;
4 padding: 0;
4 padding: 0;
5 }
5 }
6 html {
6 html {
7 background: #fff;
7 background: #fff;
8 color: #000;
8 color: #000;
9 font: medium sans-serif;
9 font: medium sans-serif;
10 }
10 }
11 a {
11 a {
12 color: inherit;
12 color: inherit;
13 text-decoration: underline;
13 text-decoration: underline;
14 }
14 }
15 li {
15 li {
16 list-style-position: inside;
16 list-style-position: inside;
17 }
17 }
18
18
19 #admin_panel {
19 #admin_panel {
20 background: #182F6F;
20 background: #182F6F;
21 color: #fff;
21 color: #fff;
22 padding: .5ex 1ex .5ex 1ex;
22 padding: .5ex 1ex .5ex 1ex;
23 }
23 }
24
24
25 .navigation_panel {
25 .navigation_panel {
26 background: #182F6F;
26 background: #182F6F;
27 color: #B4CFEC;
27 color: #B4CFEC;
28 margin-bottom: 1em;
28 margin-bottom: 1em;
29 padding: .5ex 1ex 1ex 1ex;
29 padding: .5ex 1ex 1ex 1ex;
30 }
30 }
31 .navigation_panel::after {
31 .navigation_panel::after {
32 clear: both;
32 clear: both;
33 content: ".";
33 content: ".";
34 display: block;
34 display: block;
35 height: 0;
35 height: 0;
36 line-height: 0;
36 line-height: 0;
37 visibility: hidden;
37 visibility: hidden;
38 }
38 }
39
39
40 .navigation_panel a:link, .navigation_panel a:visited, .navigation_panel a:hover {
40 .navigation_panel a:link, .navigation_panel a:visited, .navigation_panel a:hover {
41 text-decoration: none;
41 text-decoration: none;
42 }
42 }
43
43
44 .navigation_panel .link {
44 .navigation_panel .link {
45 border-right: 1px solid #fff;
45 border-right: 1px solid #fff;
46 color: #fff;
46 color: #fff;
47 font-weight: bold;
47 font-weight: bold;
48 margin-right: 1ex;
48 margin-right: 1ex;
49 padding-right: 1ex;
49 padding-right: 1ex;
50 }
50 }
51 .navigation_panel .link:last-child {
51 .navigation_panel .link:last-child {
52 border-left: 1px solid #fff;
52 border-left: 1px solid #fff;
53 border-right: none;
53 border-right: none;
54 float: right;
54 float: right;
55 margin-left: 1ex;
55 margin-left: 1ex;
56 margin-right: 0;
56 margin-right: 0;
57 padding-left: 1ex;
57 padding-left: 1ex;
58 padding-right: 0;
58 padding-right: 0;
59 }
59 }
60
60
61 .navigation_panel .tag {
61 .navigation_panel .tag {
62 color: #fff;
62 color: #fff;
63 }
63 }
64
64
65 .input_field {
65 .input_field {
66
66
67 }
67 }
68
68
69 .input_field_name {
69 .input_field_name {
70
70
71 }
71 }
72
72
73 .input_field_error {
73 .input_field_error {
74 color: #FF0000;
74 color: #FF0000;
75 }
75 }
76
76
77
77
78 .title {
78 .title {
79 color: #182F6F;
79 color: #182F6F;
80 font-weight: bold;
80 font-weight: bold;
81 }
81 }
82
82
83 .post-form-w {
83 .post-form-w {
84 background: #182F6F;
84 background: #182F6F;
85 border-radius: 1ex;
85 border-radius: 1ex;
86 color: #fff;
86 color: #fff;
87 margin: 1em 1ex;
87 margin: 1em 1ex;
88 padding: 1ex;
88 padding: 1ex;
89 }
89 }
90 .post-form {
90 .post-form {
91 display: table;
91 display: table;
92 border-collapse: collapse;
92 border-collapse: collapse;
93 width: 100%;
93 width: 100%;
94
94
95 }
95 }
96 .form-row {
96 .form-row {
97 display: table-row;
97 display: table-row;
98 }
98 }
99 .form-label, .form-input {
99 .form-label, .form-input {
100 display: table-cell;
100 display: table-cell;
101 vertical-align: top;
101 vertical-align: top;
102 }
102 }
103 .form-label {
103 .form-label {
104 padding: .25em 1ex .25em 0;
104 padding: .25em 1ex .25em 0;
105 }
105 }
106 .form-input {
106 .form-input {
107 padding: .25em 0;
107 padding: .25em 0;
108 }
108 }
109 .form-input > * {
109 .form-input > * {
110 background: #fff;
110 background: #fff;
111 color: #000;
111 color: #000;
112 border: none;
112 border: none;
113 padding: 0;
113 padding: 0;
114 resize: vertical;
114 resize: vertical;
115 width: 100%;
115 width: 100%;
116 }
116 }
117 .form-submit {
117 .form-submit {
118 border-bottom: 1px solid #666;
118 border-bottom: 1px solid #666;
119 margin-bottom: .5em;
119 margin-bottom: .5em;
120 padding-bottom: .5em;
120 padding-bottom: .5em;
121 }
121 }
122 .form-title {
122 .form-title {
123 font-weight: bold;
123 font-weight: bold;
124 margin-bottom: .5em;
124 margin-bottom: .5em;
125 }
125 }
126 .post-form .settings_item {
126 .post-form .settings_item {
127 margin: .5em 0;
127 margin: .5em 0;
128 }
128 }
129 .form-submit input {
129 .form-submit input {
130 margin-top: .5em;
130 margin-top: .5em;
131 padding: .2em 1ex;
131 padding: .2em 1ex;
132 }
132 }
133 .form-label {
133 .form-label {
134 text-align: right;
134 text-align: right;
135 }
135 }
136
136
137 .block {
137 .block {
138 display: inline-block;
138 display: inline-block;
139 vertical-align: top;
139 vertical-align: top;
140 }
140 }
141
141
142 .post_id {
142 .post_id {
143 color: #a00;
143 color: #a00;
144 }
144 }
145
145
146 .post {
146 .post {
147 clear: left;
147 clear: left;
148 margin: 0 1ex 1em 1ex;
148 margin: 0 1ex 1em 1ex;
149 overflow-x: auto;
149 overflow-x: auto;
150 word-wrap: break-word;
150 word-wrap: break-word;
151 background: #FFF;
151 background: #FFF;
152 padding: 1ex;
152 padding: 1ex;
153 border: 1px solid #666;
153 border: 1px solid #666;
154 box-shadow: 1px 1px 2px 1px #666;
154 box-shadow: 1px 1px 2px 1px #666;
155 }
155 }
156
156
157 #posts > .post:last-child {
157 #posts > .post:last-child {
158 border-bottom: none;
158 border-bottom: none;
159 padding-bottom: 0;
159 padding-bottom: 0;
160 }
160 }
161
161
162 .metadata {
162 .metadata {
163 background: #C0E4E8;
163 background: #C0E4E8;
164 border: 1px solid #7F9699;
164 border: 1px solid #7F9699;
165 border-radius: .4ex;
165 border-radius: .4ex;
166 display: table;
166 display: table;
167 margin-top: .5em;
167 margin-top: .5em;
168 padding: .4em;
168 padding: .4em;
169 }
169 }
170
170
171 .post ul, .post ol {
171 .post ul, .post ol {
172 margin: .5em 0 .5em 3ex;
172 margin: .5em 0 .5em 3ex;
173 }
173 }
174 .post li {
174 .post li {
175 margin: .2em 0;
175 margin: .2em 0;
176 }
176 }
177 .post p {
177 .post p {
178 margin: .5em 0;
178 margin: .5em 0;
179 }
179 }
180 .post blockquote {
180 .post blockquote {
181 border-left: 3px solid #182F6F;
181 border-left: 3px solid #182F6F;
182 margin: .5em 0 .5em 3ex;
182 margin: .5em 0 .5em 3ex;
183 padding-left: 1ex;
183 padding-left: 1ex;
184 }
184 }
185 .post blockquote > blockquote {
185 .post blockquote > blockquote {
186 padding-top: .1em;
186 padding-top: .1em;
187 }
187 }
188
188
189 .post > .image {
189 .post > .image {
190 float: left;
190 float: left;
191 margin-right: 1ex;
191 margin-right: 1ex;
192 }
192 }
193 .post > .metadata {
193 .post > .metadata {
194 clear: left;
194 clear: left;
195 }
195 }
196
196
197 .post > .message .get {
197 .post > .message .get {
198 color: #182F6F; font-weight: bold;
198 color: #182F6F; font-weight: bold;
199 }
199 }
200
200
201 .dead_post > .metadata {
201 .dead_post > .metadata {
202 background: #eee;
202 background: #eee;
203 }
203 }
204
204
205 .quote {
205 .quote {
206 color: #182F6F;
206 color: #182F6F;
207 }
207 }
208
208
209 .spoiler {
209 .spoiler {
210 background: black;
210 background: black;
211 color: black;
211 color: black;
212 }
212 }
213
213
214 .spoiler:hover {
214 .spoiler:hover {
215 background: #ffffff;
215 background: #ffffff;
216 }
216 }
217
217
218 .comment {
218 .comment {
219 color: #557055;
219 color: #557055;
220 }
220 }
221
221
222 .last-replies {
222 .last-replies {
223 margin-left: 6ex;
223 margin-left: 6ex;
224 }
224 }
225
225
226 .thread > .post > .message > .post-info {
226 .thread > .post > .message > .post-info {
227 border-bottom: 1px solid #ccc;
227 border-bottom: 1px solid #ccc;
228 padding-bottom: .5em;
228 padding-bottom: .5em;
229 }
229 }
230
230
231 :target .post_id {
231 :target .post_id {
232 background: #182F6F;
232 background: #182F6F;
233 color: #FFF;
233 color: #FFF;
234 text-decoration: none;
234 text-decoration: none;
235 }
235 }
236
236
237 .image-mode-tab {
237 .image-mode-tab {
238 background: #182F6F;
238 background: #182F6F;
239 color: #FFF;
239 color: #FFF;
240 display: table;
240 display: table;
241 margin: 1em auto 1em 0;
241 margin: 1em auto 1em 0;
242 padding: .2em .5ex;
242 padding: .2em .5ex;
243 }
243 }
244
244
245 .image-mode-tab > label {
245 .image-mode-tab > label {
246 margin: 0 1ex;
246 margin: 0 1ex;
247 }
247 }
248
248
249 .image-mode-tab > label > input {
249 .image-mode-tab > label > input {
250 margin-right: .5ex;
250 margin-right: .5ex;
251 }
251 }
252
252
253 .tag_info, .page_link {
253 .tag_info, .page_link {
254 margin: 1em 0;
254 margin: 1em 0;
255 text-align: center;
255 text-align: center;
256 }
256 }
257
257
258 .form-errors {
258 .form-errors {
259 margin-left: 1ex;
259 margin-left: 1ex;
260 }
260 }
261
261
262 .moderator_info {
262 .moderator_info {
263 font-weight: bold;
263 font-weight: bold;
264 float: right;
264 float: right;
265 }
265 }
266
266
267 .refmap {
267 .refmap {
268 border: 1px dashed #aaa;
268 border: 1px dashed #aaa;
269 padding: 0.5em;
269 padding: 0.5em;
270 display: table;
270 display: table;
271 }
271 }
272
272
273 .fav {
273 .fav {
274 color: blue;
274 color: blue;
275 }
275 }
276
276
277 .not_fav {
277 .not_fav {
278 color: #ccc;
278 color: #ccc;
279 }
279 }
280
280
281 .role {
281 .role {
282 text-decoration: underline;
282 text-decoration: underline;
283 }
283 }
284
284
285 .form-email {
285 .form-email {
286 display: none;
286 display: none;
287 }
287 }
288
288
289 .bar-value {
289 .bar-value {
290 background: #E3E7F2;
290 background: #E3E7F2;
291 padding: .1em 1ex;
291 padding: .1em 1ex;
292 moz-box-sizing: border-box;
292 moz-box-sizing: border-box;
293 box-sizing: border-box;
293 box-sizing: border-box;
294 height: 1.5em;
294 height: 1.5em;
295 }
295 }
296
296
297 .bar-bg {
297 .bar-bg {
298 background: #EA4649;
298 background: #EA4649;
299 border: 1px solid #666;
299 border: 1px solid #666;
300 margin: 0 1ex 1em 1ex;
300 margin: 0 1ex 1em 1ex;
301 position: relative;
301 position: relative;
302 overflow: hidden;
302 overflow: hidden;
303 }
303 }
304
304
305 .bar-text {
305 .bar-text {
306 padding: 2px;
306 padding: 2px;
307 position: absolute;
307 position: absolute;
308 left: 0;
308 left: 0;
309 top: 0;
309 top: 0;
310 }
310 }
311
311
312 .skipped_replies {
312 .skipped_replies {
313 margin: 1ex;
313 margin: 1ex;
314 }
314 }
315
315
316 #mark-panel {
316 #mark-panel {
317 background: #eee;
317 background: #eee;
318 border-bottom: 1px solid #182F6F;
318 border-bottom: 1px solid #182F6F;
319 }
319 }
320
320
321 .mark_btn {
321 .mark_btn {
322 display: inline-block;
322 display: inline-block;
323 padding: .2em 1ex;
323 padding: .2em 1ex;
324 border-left: 1px solid #182F6F;
324 border-left: 1px solid #182F6F;
325 }
325 }
326
326
327 .mark_btn:first-child {
327 .mark_btn:first-child {
328 border-left: none;
328 border-left: none;
329 }
329 }
330
330
331 .mark_btn:last-child {
331 .mark_btn:last-child {
332 border-right: 1px solid #182F6F;
332 border-right: 1px solid #182F6F;
333 }
334
335 .current_page {
336 border-bottom: 1px solid #FFF;
337 padding: 0px 0.5ex;
338 }
339
340 .image-mode-tab a {
341 text-decoration: none;
342 }
343 .image-mode-tab .current_mode::before {
344 content: "βœ“ ";
345 padding: 0 0 0 .5ex;
346 color: #182F6F;
347 background: #FFF;
348 }
349 .image-mode-tab .current_mode {
350 padding: 0 .5ex 0 0;
351 color: #182F6F;
352 background: #FFF;
353 }
354
355 .gallery_image_metadata {
356 margin-bottom: 1em;
333 } No newline at end of file
357 }
@@ -1,81 +1,47 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 function addGalleryPanel() {
27 var gallery = $('a[class="thumb"]').clone(true),
28 normal = $('.post').clone(true);
29
30 $('.navigation_panel').filter(':first').after(
31 '<div class="image-mode-tab" role="radiogroup" aria-label="Image mode2">' +
32 '<label><input type="radio" class="image-mode-normal" name="image-mode" value="0" checked="checked"/>'+ gettext('Normal') +'</label>' +
33 '<label><input type="radio" class="image-mode-table" name="image-mode" value="1"/>'+ gettext('Gallery') +'</label>' +
34 '</div>'
35 );
36
37 $('input[name="image-mode"]').change(function() {
38 //gallery mode
39 if($(this).val() === '1') {
40 $('.thread').replaceWith(
41 $('<div id="posts-table"></div>').append(gallery)
42 );
43 }
44 //normal mode
45 else {
46 $('#posts-table').replaceWith(
47 $('<div class="thread"></div>').append(normal)
48 );
49 }
50 });
51 }
52
53 function moveCaretToEnd(el) {
26 function moveCaretToEnd(el) {
54 if (typeof el.selectionStart == "number") {
27 if (typeof el.selectionStart == "number") {
55 el.selectionStart = el.selectionEnd = el.value.length;
28 el.selectionStart = el.selectionEnd = el.value.length;
56 } else if (typeof el.createTextRange != "undefined") {
29 } else if (typeof el.createTextRange != "undefined") {
57 el.focus();
30 el.focus();
58 var range = el.createTextRange();
31 var range = el.createTextRange();
59 range.collapse(false);
32 range.collapse(false);
60 range.select();
33 range.select();
61 }
34 }
62 }
35 }
63
36
64 function addQuickReply(postId) {
37 function addQuickReply(postId) {
65 var textToAdd = '>>' + postId + '\n\n';
38 var textToAdd = '>>' + postId + '\n\n';
66 var textAreaId = 'textarea';
39 var textAreaId = 'textarea';
67 $(textAreaId).val($(textAreaId).val()+ textToAdd);
40 $(textAreaId).val($(textAreaId).val()+ textToAdd);
68
41
69 var textarea = document.getElementsByTagName('textarea')[0];
42 var textarea = document.getElementsByTagName('textarea')[0];
70 $(textAreaId).focus();
43 $(textAreaId).focus();
71 moveCaretToEnd(textarea);
44 moveCaretToEnd(textarea);
72
45
73 $("html, body").animate({ scrollTop: $(textAreaId).offset().top }, "slow");
46 $("html, body").animate({ scrollTop: $(textAreaId).offset().top }, "slow");
74 }
47 }
75
76
77
78 $(document).ready(function(){
79 addGalleryPanel();
80 initAutoupdate();
81 });
@@ -1,164 +1,192 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var THREAD_UPDATE_DELAY = 10000;
26 var THREAD_UPDATE_DELAY = 10000;
27
27
28 var loading = false;
28 var loading = false;
29 var lastUpdateTime = null;
29 var lastUpdateTime = null;
30
30
31 function blink(node) {
31 function blink(node) {
32 var blinkCount = 2;
32 var blinkCount = 2;
33 var blinkDelay = 250;
33 var blinkDelay = 250;
34
34
35 var nodeToAnimate = node;
35 var nodeToAnimate = node;
36 for (var i = 0; i < blinkCount; i++) {
36 for (var i = 0; i < blinkCount; i++) {
37 nodeToAnimate = nodeToAnimate.fadeOut(blinkDelay).fadeIn(blinkDelay);
37 nodeToAnimate = nodeToAnimate.fadeOut(blinkDelay).fadeIn(blinkDelay);
38 }
38 }
39 }
39 }
40
40
41 function updateThread() {
41 function updateThread() {
42 if (loading) {
42 if (loading) {
43 return;
43 return;
44 }
44 }
45
45
46 loading = true;
46 loading = true;
47
47
48 var threadPosts = $('div.thread').children('.post');
48 var threadPosts = $('div.thread').children('.post');
49
49
50 var lastPost = threadPosts.last();
50 var lastPost = threadPosts.last();
51 var threadId = threadPosts.first().attr('id');
51 var threadId = threadPosts.first().attr('id');
52
52
53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
54 $.getJSON(diffUrl)
54 $.getJSON(diffUrl)
55 .success(function(data) {
55 .success(function(data) {
56 var bottom = isPageBottom();
56 var bottom = isPageBottom();
57
57
58 var addedPosts = data.added;
58 var addedPosts = data.added;
59 for (var i = 0; i < addedPosts.length; i++) {
59 for (var i = 0; i < addedPosts.length; i++) {
60 var postText = addedPosts[i];
60 var postText = addedPosts[i];
61
61
62 var post = $(postText);
62 var post = $(postText);
63 post.appendTo(lastPost.parent());
63 post.appendTo(lastPost.parent());
64 addRefLinkPreview(post[0]);
64 addRefLinkPreview(post[0]);
65
65
66 lastPost = post;
66 lastPost = post;
67 blink(post);
67 blink(post);
68 }
68 }
69
69
70 var updatedPosts = data.updated;
70 var updatedPosts = data.updated;
71 for (var i = 0; i < updatedPosts.length; i++) {
71 for (var i = 0; i < updatedPosts.length; i++) {
72 var postText = updatedPosts[i];
72 var postText = updatedPosts[i];
73
73
74 var post = $(postText);
74 var post = $(postText);
75 var postId = post.attr('id');
75 var postId = post.attr('id');
76
76
77 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
77 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
78
78
79 oldPost.replaceWith(post);
79 oldPost.replaceWith(post);
80 addRefLinkPreview(post[0]);
80 addRefLinkPreview(post[0]);
81
81
82 blink(post);
82 blink(post);
83 }
83 }
84
84
85 // TODO Process deleted posts
85 // TODO Process deleted posts
86
86
87 lastUpdateTime = data.last_update;
87 lastUpdateTime = data.last_update;
88 loading = false;
88 loading = false;
89
89
90 if (bottom) {
90 if (bottom) {
91 var $target = $('html,body');
91 var $target = $('html,body');
92 $target.animate({scrollTop: $target.height()}, 1000);
92 $target.animate({scrollTop: $target.height()}, 1000);
93 }
93 }
94
94
95 $('#reply-count').text(getReplyCount());
95 $('#reply-count').text(getReplyCount());
96 $('#image-count').text(getImageCount());
96 $('#image-count').text(getImageCount());
97
97
98 updateBumplimitProgress(data.added.length);
98 updateBumplimitProgress(data.added.length);
99 updatePostBumpableStatus();
99 updatePostBumpableStatus();
100
101 if (data.added.length + data.updated.length > 0) {
102 showNewPostsTitle();
103 }
100 })
104 })
101 .error(function(data) {
105 .error(function(data) {
102 // TODO Show error message that server is unavailable?
106 // TODO Show error message that server is unavailable?
103
107
104 loading = false;
108 loading = false;
105 });
109 });
106 }
110 }
107
111
108 function isPageBottom() {
112 function isPageBottom() {
109 var scroll = $(window).scrollTop() / ($(document).height()
113 var scroll = $(window).scrollTop() / ($(document).height()
110 - $(window).height())
114 - $(window).height())
111
115
112 return scroll == 1
116 return scroll == 1
113 }
117 }
114
118
115 function initAutoupdate() {
119 function initAutoupdate() {
116 loading = false;
120 loading = false;
117
121
118 lastUpdateTime = $('.metapanel').attr('data-last-update');
122 lastUpdateTime = $('.metapanel').attr('data-last-update');
119
123
120 setInterval(updateThread, THREAD_UPDATE_DELAY);
124 setInterval(updateThread, THREAD_UPDATE_DELAY);
121 }
125 }
122
126
123 function getReplyCount() {
127 function getReplyCount() {
124 return $('.thread').children('.post').length
128 return $('.thread').children('.post').length
125 }
129 }
126
130
127 function getImageCount() {
131 function getImageCount() {
128 return $('.thread').find('img').length
132 return $('.thread').find('img').length
129 }
133 }
130
134
131 /**
135 /**
132 * Update bumplimit progress bar
136 * Update bumplimit progress bar
133 */
137 */
134 function updateBumplimitProgress(postDelta) {
138 function updateBumplimitProgress(postDelta) {
135 var progressBar = $('#bumplimit_progress');
139 var progressBar = $('#bumplimit_progress');
136 if (progressBar) {
140 if (progressBar) {
137 var postsToLimitElement = $('#left_to_limit');
141 var postsToLimitElement = $('#left_to_limit');
138
142
139 var oldPostsToLimit = parseInt(postsToLimitElement.text());
143 var oldPostsToLimit = parseInt(postsToLimitElement.text());
140 var postCount = getReplyCount();
144 var postCount = getReplyCount();
141 var bumplimit = postCount - postDelta + oldPostsToLimit;
145 var bumplimit = postCount - postDelta + oldPostsToLimit;
142
146
143 var newPostsToLimit = bumplimit - postCount;
147 var newPostsToLimit = bumplimit - postCount;
144 if (newPostsToLimit <= 0) {
148 if (newPostsToLimit <= 0) {
145 $('.bar-bg').remove();
149 $('.bar-bg').remove();
146 } else {
150 } else {
147 postsToLimitElement.text(newPostsToLimit);
151 postsToLimitElement.text(newPostsToLimit);
148 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
152 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
149 }
153 }
150 }
154 }
151 }
155 }
152
156
153 /**
157 /**
154 * If the bumplimit is reached, add dead_post class to all posts
158 * If the bumplimit is reached, add dead_post class to all posts
155 */
159 */
156 function updatePostBumpableStatus() {
160 function updatePostBumpableStatus() {
157 var postCount = getReplyCount();
161 var postCount = getReplyCount();
158 var postsToLimitElement = $('#left_to_limit');
162 var postsToLimitElement = $('#left_to_limit');
159 var postsToLimit = parseInt(postsToLimitElement.text());
163 var postsToLimit = parseInt(postsToLimitElement.text());
160
164
161 if (postsToLimit <= 0) {
165 if (postsToLimit <= 0) {
162 $('.thread').find('.post').addClass('dead_post');
166 $('.thread').find('.post').addClass('dead_post');
163 }
167 }
164 }
168 }
169
170 var documentOriginalTitle = '';
171 /**
172 * Show 'new posts' text in the title if the document is not visible to a user
173 */
174 function showNewPostsTitle() {
175 if (document.hidden) {
176 documentOriginalTitle = document.title;
177 document.title = gettext('[new posts]') + ' ' + document.title;
178
179 document.addEventListener('visibilitychange', function() {
180 if (documentOriginalTitle !== '') {
181 document.title = documentOriginalTitle;
182 documentOriginalTitle = '';
183 }
184
185 document.removeEventListener('visibilitychange', null);
186 });
187 }
188 }
189
190 $(document).ready(function(){
191 initAutoupdate();
192 });
@@ -1,244 +1,254 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load board %}
5 {% load board %}
6 {% load static %}
6 {% load static %}
7
7
8 {% block head %}
8 {% block head %}
9 {% if tag %}
9 {% if tag %}
10 <title>Neboard - {{ tag.name }}</title>
10 <title>Neboard - {{ tag.name }}</title>
11 {% else %}
11 {% else %}
12 <title>Neboard</title>
12 <title>Neboard</title>
13 {% endif %}
13 {% endif %}
14
14
15 {% if prev_page %}
15 {% if prev_page %}
16 <link rel="next" href="
16 <link rel="next" href="
17 {% if tag %}
17 {% if tag %}
18 {% url "tag" tag_name=tag page=prev_page %}
18 {% url "tag" tag_name=tag page=prev_page %}
19 {% else %}
19 {% else %}
20 {% url "index" page=prev_page %}
20 {% url "index" page=prev_page %}
21 {% endif %}
21 {% endif %}
22 " />
22 " />
23 {% endif %}
23 {% endif %}
24 {% if next_page %}
24 {% if next_page %}
25 <link rel="next" href="
25 <link rel="next" href="
26 {% if tag %}
26 {% if tag %}
27 {% url "tag" tag_name=tag page=next_page %}
27 {% url "tag" tag_name=tag page=next_page %}
28 {% else %}
28 {% else %}
29 {% url "index" page=next_page %}
29 {% url "index" page=next_page %}
30 {% endif %}
30 {% endif %}
31 " />
31 " />
32 {% endif %}
32 {% endif %}
33
33
34 {% endblock %}
34 {% endblock %}
35
35
36 {% block content %}
36 {% block content %}
37
37
38 {% get_current_language as LANGUAGE_CODE %}
38 {% get_current_language as LANGUAGE_CODE %}
39
39
40 {% if tag %}
40 {% if tag %}
41 <div class="tag_info">
41 <div class="tag_info">
42 <h2>
42 <h2>
43 {% if tag in user.fav_tags.all %}
43 {% if tag in user.fav_tags.all %}
44 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
44 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
45 class="fav">β˜…</a>
45 class="fav">β˜…</a>
46 {% else %}
46 {% else %}
47 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
47 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
48 class="not_fav">β˜…</a>
48 class="not_fav">β˜…</a>
49 {% endif %}
49 {% endif %}
50 #{{ tag.name }}
50 #{{ tag.name }}
51 </h2>
51 </h2>
52 </div>
52 </div>
53 {% endif %}
53 {% endif %}
54
54
55 {% if threads %}
55 {% if threads %}
56 {% if prev_page %}
56 {% if prev_page %}
57 <div class="page_link">
57 <div class="page_link">
58 <a href="
58 <a href="
59 {% if tag %}
59 {% if tag %}
60 {% url "tag" tag_name=tag page=prev_page %}
60 {% url "tag" tag_name=tag page=prev_page %}
61 {% else %}
61 {% else %}
62 {% url "index" page=prev_page %}
62 {% url "index" page=prev_page %}
63 {% endif %}
63 {% endif %}
64 ">{% trans "Previous page" %}</a>
64 ">{% trans "Previous page" %}</a>
65 </div>
65 </div>
66 {% endif %}
66 {% endif %}
67
67
68 {% for thread in threads %}
68 {% for thread in threads %}
69 {% cache 600 thread_short thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
69 {% cache 600 thread_short thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
70 <div class="thread">
70 <div class="thread">
71 {% if thread.bumpable %}
71 {% if thread.bumpable %}
72 <div class="post" id="{{ thread.op.id }}">
72 <div class="post" id="{{ thread.op.id }}">
73 {% else %}
73 {% else %}
74 <div class="post dead_post" id="{{ thread.op.id }}">
74 <div class="post dead_post" id="{{ thread.op.id }}">
75 {% endif %}
75 {% endif %}
76 {% if thread.op.image %}
76 {% if thread.op.image %}
77 <div class="image">
77 <div class="image">
78 <a class="thumb"
78 <a class="thumb"
79 href="{{ thread.op.image.url }}"><img
79 href="{{ thread.op.image.url }}"><img
80 src="{{ thread.op.image.url_200x150 }}"
80 src="{{ thread.op.image.url_200x150 }}"
81 alt="{{ thread.op.id }}"
81 alt="{{ thread.op.id }}"
82 width="{{ thread.op.image_pre_width }}"
83 height="{{ thread.op.image_pre_height }}"
82 data-width="{{ thread.op.image_width }}"
84 data-width="{{ thread.op.image_width }}"
83 data-height="{{ thread.op.image_height }}"/>
85 data-height="{{ thread.op.image_height }}"/>
84 </a>
86 </a>
85 </div>
87 </div>
86 {% endif %}
88 {% endif %}
87 <div class="message">
89 <div class="message">
88 <div class="post-info">
90 <div class="post-info">
89 <span class="title">{{ thread.op.title }}</span>
91 <span class="title">{{ thread.op.title }}</span>
90 <a class="post_id" href="{% url 'thread' thread.op.id %}"
92 <a class="post_id" href="{% url 'thread' thread.op.id %}"
91 >({{ thread.op.id }})</a>
93 >({{ thread.op.id }})</a>
92 [{{ thread.op.pub_time }}]
94 [{{ thread.op.pub_time }}]
93 [<a class="link" href="
95 [<a class="link" href="
94 {% url 'thread' thread.op.id %}#form"
96 {% url 'thread' thread.op.id %}#form"
95 >{% trans "Reply" %}</a>]
97 >{% trans "Reply" %}</a>]
96
98
97 {% if moderator %}
99 {% if moderator %}
98 <span class="moderator_info">
100 <span class="moderator_info">
99 [<a href="
101 [<a href="
100 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
102 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
101 >{% trans 'Delete' %}</a>]
103 >{% trans 'Delete' %}</a>]
102 ({{ thread.thread.poster_ip }})
104 ({{ thread.op.poster_ip }})
103 [<a href="
105 [<a href="
104 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
106 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
105 >{% trans 'Ban IP' %}</a>]
107 >{% trans 'Ban IP' %}</a>]
106 </span>
108 </span>
107 {% endif %}
109 {% endif %}
108 </div>
110 </div>
109 {% autoescape off %}
111 {% autoescape off %}
110 {{ thread.op.text.rendered|truncatewords_html:50 }}
112 {{ thread.op.text.rendered|truncatewords_html:50 }}
111 {% endautoescape %}
113 {% endautoescape %}
112 {% if thread.op.is_referenced %}
114 {% if thread.op.is_referenced %}
113 <div class="refmap">
115 <div class="refmap">
114 {% trans "Replies" %}:
116 {% trans "Replies" %}:
115 {% for ref_post in thread.op.get_sorted_referenced_posts %}
117 {% for ref_post in thread.op.get_sorted_referenced_posts %}
116 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
118 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
117 >{% if not forloop.last %},{% endif %}
119 >{% if not forloop.last %},{% endif %}
118 {% endfor %}
120 {% endfor %}
119 </div>
121 </div>
120 {% endif %}
122 {% endif %}
121 </div>
123 </div>
122 <div class="metadata">
124 <div class="metadata">
123 {{ thread.thread.get_images_count }} {% trans 'images' %}.
125 {{ thread.thread.get_images_count }} {% trans 'images' %}.
124 {% if thread.thread.tags %}
126 {% if thread.thread.tags %}
125 <span class="tags">
127 <span class="tags">
126 {% for tag in thread.thread.get_tags %}
128 {% for tag in thread.thread.get_tags %}
127 <a class="tag" href="
129 <a class="tag" href="
128 {% url 'tag' tag_name=tag.name %}">
130 {% url 'tag' tag_name=tag.name %}">
129 #{{ tag.name }}</a
131 #{{ tag.name }}</a
130 >{% if not forloop.last %},{% endif %}
132 >{% if not forloop.last %},{% endif %}
131 {% endfor %}
133 {% endfor %}
132 </span>
134 </span>
133 {% endif %}
135 {% endif %}
134 </div>
136 </div>
135 </div>
137 </div>
136 {% if thread.last_replies.exists %}
138 {% if thread.last_replies.exists %}
137 {% if thread.skipped_replies %}
139 {% if thread.skipped_replies %}
138 <div class="skipped_replies">
140 <div class="skipped_replies">
139 <a href="{% url 'thread' thread.op.id %}">
141 <a href="{% url 'thread' thread.op.id %}">
140 {% blocktrans with count=thread.skipped_replies %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
142 {% blocktrans with count=thread.skipped_replies %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
141 </a>
143 </a>
142 </div>
144 </div>
143 {% endif %}
145 {% endif %}
144 <div class="last-replies">
146 <div class="last-replies">
145 {% for post in thread.last_replies %}
147 {% for post in thread.last_replies %}
146 {% if thread.bumpable %}
148 {% if thread.bumpable %}
147 <div class="post" id="{{ post.id }}">
149 <div class="post" id="{{ post.id }}">
148 {% else %}
150 {% else %}
149 <div class="post dead_post" id="{{ post.id }}">
151 <div class="post dead_post" id="{{ post.id }}">
150 {% endif %}
152 {% endif %}
151 {% if post.image %}
153 {% if post.image %}
152 <div class="image">
154 <div class="image">
153 <a class="thumb"
155 <a class="thumb"
154 href="{{ post.image.url }}"><img
156 href="{{ post.image.url }}"><img
155 src=" {{ post.image.url_200x150 }}"
157 src=" {{ post.image.url_200x150 }}"
156 alt="{{ post.id }}"
158 alt="{{ post.id }}"
159 width="{{ post.image_pre_width }}"
160 height="{{ post.image_pre_height }}"
157 data-width="{{ post.image_width }}"
161 data-width="{{ post.image_width }}"
158 data-height="{{ post.image_height }}"/>
162 data-height="{{ post.image_height }}"/>
159 </a>
163 </a>
160 </div>
164 </div>
161 {% endif %}
165 {% endif %}
162 <div class="message">
166 <div class="message">
163 <div class="post-info">
167 <div class="post-info">
164 <span class="title">{{ post.title }}</span>
168 <span class="title">{{ post.title }}</span>
165 <a class="post_id" href="
169 <a class="post_id" href="
166 {% url 'thread' thread.op.id %}#{{ post.id }}">
170 {% url 'thread' thread.op.id %}#{{ post.id }}">
167 ({{ post.id }})</a>
171 ({{ post.id }})</a>
168 [{{ post.pub_time }}]
172 [{{ post.pub_time }}]
169 </div>
173 </div>
170 {% autoescape off %}
174 {% autoescape off %}
171 {{ post.text.rendered|truncatewords_html:50 }}
175 {{ post.text.rendered|truncatewords_html:50 }}
172 {% endautoescape %}
176 {% endautoescape %}
173 </div>
177 </div>
174 {% if post.is_referenced %}
178 {% if post.is_referenced %}
175 <div class="refmap">
179 <div class="refmap">
176 {% trans "Replies" %}:
180 {% trans "Replies" %}:
177 {% for ref_post in post.get_sorted_referenced_posts %}
181 {% for ref_post in post.get_sorted_referenced_posts %}
178 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
182 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
179 >{% if not forloop.last %},{% endif %}
183 >{% if not forloop.last %},{% endif %}
180 {% endfor %}
184 {% endfor %}
181 </div>
185 </div>
182 {% endif %}
186 {% endif %}
183 </div>
187 </div>
184 {% endfor %}
188 {% endfor %}
185 </div>
189 </div>
186 {% endif %}
190 {% endif %}
187 </div>
191 </div>
188 {% endcache %}
192 {% endcache %}
189 {% endfor %}
193 {% endfor %}
190
194
191 {% if next_page %}
195 {% if next_page %}
192 <div class="page_link">
196 <div class="page_link">
193 <a href="
197 <a href="
194 {% if tag %}
198 {% if tag %}
195 {% url "tag" tag_name=tag page=next_page %}
199 {% url "tag" tag_name=tag page=next_page %}
196 {% else %}
200 {% else %}
197 {% url "index" page=next_page %}
201 {% url "index" page=next_page %}
198 {% endif %}
202 {% endif %}
199 ">{% trans "Next page" %}</a>
203 ">{% trans "Next page" %}</a>
200 </div>
204 </div>
201 {% endif %}
205 {% endif %}
202 {% else %}
206 {% else %}
203 <div class="post">
207 <div class="post">
204 {% trans 'No threads exist. Create the first one!' %}</div>
208 {% trans 'No threads exist. Create the first one!' %}</div>
205 {% endif %}
209 {% endif %}
206
210
207 <div class="post-form-w">
211 <div class="post-form-w">
208 <script src="{% static 'js/panel.js' %}"></script>
212 <script src="{% static 'js/panel.js' %}"></script>
209 <div class="post-form">
213 <div class="post-form">
210 <div class="form-title">{% trans "Create new thread" %}</div>
214 <div class="form-title">{% trans "Create new thread" %}</div>
211 <form enctype="multipart/form-data" method="post">{% csrf_token %}
215 <form enctype="multipart/form-data" method="post">{% csrf_token %}
212 {{ form.as_div }}
216 {{ form.as_div }}
213 <div class="form-submit">
217 <div class="form-submit">
214 <input type="submit" value="{% trans "Post" %}"/>
218 <input type="submit" value="{% trans "Post" %}"/>
215 </div>
219 </div>
216 </form>
220 </form>
217 <div>
221 <div>
218 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
222 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
219 </div>
223 </div>
220 <div><a href="{% url "staticpage" name="help" %}">
224 <div><a href="{% url "staticpage" name="help" %}">
221 {% trans 'Text syntax' %}</a></div>
225 {% trans 'Text syntax' %}</a></div>
222 </div>
226 </div>
223 </div>
227 </div>
224
228
225 {% endblock %}
229 {% endblock %}
226
230
227 {% block metapanel %}
231 {% block metapanel %}
228
232
229 <span class="metapanel">
233 <span class="metapanel">
230 <b><a href="{% url "authors" %}">Neboard</a> 1.4.1</b>
234 <b><a href="{% url "authors" %}">Neboard</a> 1.5 Aker</b>
231 {% trans "Pages:" %}
235 {% trans "Pages:" %}[
232 {% for page in pages %}
236 {% for page in pages %}
233 [<a href="
237 <a
238 {% ifequal page current_page %}
239 class="current_page"
240 {% endifequal %}
241 href="
234 {% if tag %}
242 {% if tag %}
235 {% url "tag" tag_name=tag page=page %}
243 {% url "tag" tag_name=tag page=page %}
236 {% else %}
244 {% else %}
237 {% url "index" page=page %}
245 {% url "index" page=page %}
238 {% endif %}
246 {% endif %}
239 ">{{ page }}</a>]
247 ">{{ page }}</a>
248 {% if not forloop.last %},{% endif %}
240 {% endfor %}
249 {% endfor %}
250 ]
241 [<a href="rss/">RSS</a>]
251 [<a href="rss/">RSS</a>]
242 </span>
252 </span>
243
253
244 {% endblock %}
254 {% endblock %}
@@ -1,128 +1,136 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load static from staticfiles %}
5 {% load static from staticfiles %}
6 {% load board %}
6 {% load board %}
7
7
8 {% block head %}
8 {% block head %}
9 <title>Neboard - {{ thread.get_replies.0.get_title }}</title>
9 <title>Neboard - {{ thread.get_opening_post.get_title }}</title>
10 {% endblock %}
10 {% endblock %}
11
11
12 {% block content %}
12 {% block content %}
13 {% spaceless %}
13 {% spaceless %}
14 {% get_current_language as LANGUAGE_CODE %}
14 {% get_current_language as LANGUAGE_CODE %}
15
15
16 <script src="{% static 'js/thread_update.js' %}"></script>
16 <script src="{% static 'js/thread_update.js' %}"></script>
17 <script src="{% static 'js/thread.js' %}"></script>
17 <script src="{% static 'js/thread.js' %}"></script>
18
18
19 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
19 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
20
21 <div class="image-mode-tab">
22 <a class="current_mode" href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
23 <a href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
24 </div>
25
20 {% if bumpable %}
26 {% if bumpable %}
21 <div class="bar-bg">
27 <div class="bar-bg">
22 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
28 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
23 </div>
29 </div>
24 <div class="bar-text">
30 <div class="bar-text">
25 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
31 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
26 </div>
32 </div>
27 </div>
33 </div>
28 {% endif %}
34 {% endif %}
29 <div class="thread">
35 <div class="thread">
30 {% for post in thread.get_replies %}
36 {% for post in thread.get_replies %}
31 {% if bumpable %}
37 {% if bumpable %}
32 <div class="post" id="{{ post.id }}">
38 <div class="post" id="{{ post.id }}">
33 {% else %}
39 {% else %}
34 <div class="post dead_post" id="{{ post.id }}">
40 <div class="post dead_post" id="{{ post.id }}">
35 {% endif %}
41 {% endif %}
36 {% if post.image %}
42 {% if post.image %}
37 <div class="image">
43 <div class="image">
38 <a
44 <a
39 class="thumb"
45 class="thumb"
40 href="{{ post.image.url }}"><img
46 href="{{ post.image.url }}"><img
41 src="{{ post.image.url_200x150 }}"
47 src="{{ post.image.url_200x150 }}"
42 alt="{{ post.id }}"
48 alt="{{ post.id }}"
49 width="{{ post.image_pre_width }}"
50 height="{{ post.image_pre_height }}"
43 data-width="{{ post.image_width }}"
51 data-width="{{ post.image_width }}"
44 data-height="{{ post.image_height }}"/>
52 data-height="{{ post.image_height }}"/>
45 </a>
53 </a>
46 </div>
54 </div>
47 {% endif %}
55 {% endif %}
48 <div class="message">
56 <div class="message">
49 <div class="post-info">
57 <div class="post-info">
50 <span class="title">{{ post.title }}</span>
58 <span class="title">{{ post.title }}</span>
51 <a class="post_id" href="#{{ post.id }}">
59 <a class="post_id" href="#{{ post.id }}">
52 ({{ post.id }})</a>
60 ({{ post.id }})</a>
53 [{{ post.pub_time }}]
61 [{{ post.pub_time }}]
54 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
62 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
55 ; return false;">&gt;&gt;</a>]
63 ; return false;">&gt;&gt;</a>]
56
64
57 {% if moderator %}
65 {% if moderator %}
58 <span class="moderator_info">
66 <span class="moderator_info">
59 [<a href="{% url 'delete' post_id=post.id %}"
67 [<a href="{% url 'delete' post_id=post.id %}"
60 >{% trans 'Delete' %}</a>]
68 >{% trans 'Delete' %}</a>]
61 ({{ post.poster_ip }})
69 ({{ post.poster_ip }})
62 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
70 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
63 >{% trans 'Ban IP' %}</a>]
71 >{% trans 'Ban IP' %}</a>]
64 </span>
72 </span>
65 {% endif %}
73 {% endif %}
66 </div>
74 </div>
67 {% autoescape off %}
75 {% autoescape off %}
68 {{ post.text.rendered }}
76 {{ post.text.rendered }}
69 {% endautoescape %}
77 {% endautoescape %}
70 {% if post.is_referenced %}
78 {% if post.is_referenced %}
71 <div class="refmap">
79 <div class="refmap">
72 {% trans "Replies" %}:
80 {% trans "Replies" %}:
73 {% for ref_post in post.get_sorted_referenced_posts %}
81 {% for ref_post in post.get_sorted_referenced_posts %}
74 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
82 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
75 >{% if not forloop.last %},{% endif %}
83 >{% if not forloop.last %},{% endif %}
76 {% endfor %}
84 {% endfor %}
77 </div>
85 </div>
78 {% endif %}
86 {% endif %}
79 </div>
87 </div>
80 {% if forloop.first %}
88 {% if forloop.first %}
81 <div class="metadata">
89 <div class="metadata">
82 <span class="tags">
90 <span class="tags">
83 {% for tag in thread.get_tags %}
91 {% for tag in thread.get_tags %}
84 <a class="tag" href="{% url 'tag' tag.name %}">
92 <a class="tag" href="{% url 'tag' tag.name %}">
85 #{{ tag.name }}</a
93 #{{ tag.name }}</a
86 >{% if not forloop.last %},{% endif %}
94 >{% if not forloop.last %},{% endif %}
87 {% endfor %}
95 {% endfor %}
88 </span>
96 </span>
89 </div>
97 </div>
90 {% endif %}
98 {% endif %}
91 </div>
99 </div>
92 {% endfor %}
100 {% endfor %}
93 </div>
101 </div>
94 {% endcache %}
102 {% endcache %}
95
103
96 <div class="post-form-w">
104 <div class="post-form-w">
97 <script src="{% static 'js/panel.js' %}"></script>
105 <script src="{% static 'js/panel.js' %}"></script>
98 <div class="form-title">{% trans "Reply to thread" %} #{{ thread.get_opening_post.id }}</div>
106 <div class="form-title">{% trans "Reply to thread" %} #{{ thread.get_opening_post.id }}</div>
99 <div class="post-form">
107 <div class="post-form">
100 <form id="form" enctype="multipart/form-data" method="post"
108 <form id="form" enctype="multipart/form-data" method="post"
101 >{% csrf_token %}
109 >{% csrf_token %}
102 {{ form.as_div }}
110 {{ form.as_div }}
103 <div class="form-submit">
111 <div class="form-submit">
104 <input type="submit" value="{% trans "Post" %}"/>
112 <input type="submit" value="{% trans "Post" %}"/>
105 </div>
113 </div>
106 </form>
114 </form>
107 <div><a href="{% url "staticpage" name="help" %}">
115 <div><a href="{% url "staticpage" name="help" %}">
108 {% trans 'Text syntax' %}</a></div>
116 {% trans 'Text syntax' %}</a></div>
109 </div>
117 </div>
110 </div>
118 </div>
111
119
112 {% endspaceless %}
120 {% endspaceless %}
113 {% endblock %}
121 {% endblock %}
114
122
115 {% block metapanel %}
123 {% block metapanel %}
116
124
117 {% get_current_language as LANGUAGE_CODE %}
125 {% get_current_language as LANGUAGE_CODE %}
118
126
119 <span class="metapanel" data-last-update="{{ last_update }}">
127 <span class="metapanel" data-last-update="{{ last_update }}">
120 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
128 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
121 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
129 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
122 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
130 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
123 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
131 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
124 [<a href="rss/">RSS</a>]
132 [<a href="rss/">RSS</a>]
125 {% endcache %}
133 {% endcache %}
126 </span>
134 </span>
127
135
128 {% endblock %}
136 {% endblock %}
@@ -1,23 +1,49 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 from django.shortcuts import get_object_or_404
2 from django.shortcuts import get_object_or_404
3 from boards.models import Post
3 from boards.models import Post
4 from boards.views import thread
4 from boards.views import thread
5 from django import template
5 from django import template
6
6
7 register = template.Library()
7 register = template.Library()
8
8
9 actions = [
10 {
11 'name': 'google',
12 'link': 'http://google.com/searchbyimage?image_url=%s',
13 },
14 {
15 'name': 'iqdb',
16 'link': 'http://iqdb.org/?url=%s',
17 },
18 ]
19
9
20
10 @register.simple_tag(name='post_url')
21 @register.simple_tag(name='post_url')
11 def post_url(*args, **kwargs):
22 def post_url(*args, **kwargs):
12 post_id = args[0]
23 post_id = args[0]
13
24
14 post = get_object_or_404(Post, id=post_id)
25 post = get_object_or_404(Post, id=post_id)
15
26
16 if not post.is_opening():
27 if not post.is_opening():
17 link = reverse(thread, kwargs={
28 link = reverse(thread, kwargs={
18 'post_id': post.thread_new.get_opening_post().id}) + '#' + str(
29 'post_id': post.thread_new.get_opening_post().id}) + '#' + str(
19 post_id)
30 post_id)
20 else:
31 else:
21 link = reverse(thread, kwargs={'post_id': post_id})
32 link = reverse(thread, kwargs={'post_id': post_id})
22
33
23 return link
34 return link
35
36
37 @register.simple_tag(name='image_actions')
38 def image_actions(*args, **kwargs):
39 image_link = args[0]
40 if len(args) > 1:
41 image_link = 'http://' + args[1] + image_link # TODO https?
42
43 result = ''
44
45 for action in actions:
46 result += '[<a href="' + action['link'] % image_link + '">' + \
47 action['name'] + '</a>]'
48
49 return result
@@ -1,173 +1,216 b''
1 # -*- encoding: utf-8 -*-
1 # -*- encoding: utf-8 -*-
2 """
2 """
3 django-thumbs by Antonio MelΓ©
3 django-thumbs by Antonio MelΓ©
4 http://django.es
4 http://django.es
5 """
5 """
6 from django.core.files.images import ImageFile
6 from django.db.models import ImageField
7 from django.db.models import ImageField
7 from django.db.models.fields.files import ImageFieldFile
8 from django.db.models.fields.files import ImageFieldFile
8 from PIL import Image
9 from PIL import Image
9 from django.core.files.base import ContentFile
10 from django.core.files.base import ContentFile
10 import cStringIO
11 import cStringIO
11
12
12
13
13 def generate_thumb(img, thumb_size, format):
14 def generate_thumb(img, thumb_size, format):
14 """
15 """
15 Generates a thumbnail image and returns a ContentFile object with the thumbnail
16 Generates a thumbnail image and returns a ContentFile object with the thumbnail
16
17
17 Parameters:
18 Parameters:
18 ===========
19 ===========
19 img File object
20 img File object
20
21
21 thumb_size desired thumbnail size, ie: (200,120)
22 thumb_size desired thumbnail size, ie: (200,120)
22
23
23 format format of the original image ('jpeg','gif','png',...)
24 format format of the original image ('jpeg','gif','png',...)
24 (this format will be used for the generated thumbnail, too)
25 (this format will be used for the generated thumbnail, too)
25 """
26 """
26
27
27 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
28 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
28 image = Image.open(img)
29 image = Image.open(img)
29
30
30 # get size
31 # get size
31 thumb_w, thumb_h = thumb_size
32 thumb_w, thumb_h = thumb_size
32 # If you want to generate a square thumbnail
33 # If you want to generate a square thumbnail
33 if thumb_w == thumb_h:
34 if thumb_w == thumb_h:
34 # quad
35 # quad
35 xsize, ysize = image.size
36 xsize, ysize = image.size
36 # get minimum size
37 # get minimum size
37 minsize = min(xsize, ysize)
38 minsize = min(xsize, ysize)
38 # largest square possible in the image
39 # largest square possible in the image
39 xnewsize = (xsize - minsize) / 2
40 xnewsize = (xsize - minsize) / 2
40 ynewsize = (ysize - minsize) / 2
41 ynewsize = (ysize - minsize) / 2
41 # crop it
42 # crop it
42 image2 = image.crop(
43 image2 = image.crop(
43 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
44 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
44 # load is necessary after crop
45 # load is necessary after crop
45 image2.load()
46 image2.load()
46 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
47 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
47 image2.thumbnail(thumb_size, Image.ANTIALIAS)
48 image2.thumbnail(thumb_size, Image.ANTIALIAS)
48 else:
49 else:
49 # not quad
50 # not quad
50 image2 = image
51 image2 = image
51 image2.thumbnail(thumb_size, Image.ANTIALIAS)
52 image2.thumbnail(thumb_size, Image.ANTIALIAS)
52
53
53 io = cStringIO.StringIO()
54 io = cStringIO.StringIO()
54 # PNG and GIF are the same, JPG is JPEG
55 # PNG and GIF are the same, JPG is JPEG
55 if format.upper() == 'JPG':
56 if format.upper() == 'JPG':
56 format = 'JPEG'
57 format = 'JPEG'
57
58
58 image2.save(io, format)
59 image2.save(io, format)
59 return ContentFile(io.getvalue())
60 return ContentFile(io.getvalue())
60
61
61
62
62 class ImageWithThumbsFieldFile(ImageFieldFile):
63 class ImageWithThumbsFieldFile(ImageFieldFile):
63 """
64 """
64 See ImageWithThumbsField for usage example
65 See ImageWithThumbsField for usage example
65 """
66 """
66
67
67 def __init__(self, *args, **kwargs):
68 def __init__(self, *args, **kwargs):
68 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
69 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
69 self.sizes = self.field.sizes
70 self.sizes = self.field.sizes
70
71
71 if self.sizes:
72 if self.sizes:
72 def get_size(self, size):
73 def get_size(self, size):
73 if not self:
74 if not self:
74 return ''
75 return ''
75 else:
76 else:
76 split = self.url.rsplit('.', 1)
77 split = self.url.rsplit('.', 1)
77 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
78 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
78 return thumb_url
79 return thumb_url
79
80
80 for size in self.sizes:
81 for size in self.sizes:
81 (w, h) = size
82 (w, h) = size
82 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
83 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
83
84
84 def save(self, name, content, save=True):
85 def save(self, name, content, save=True):
85 super(ImageWithThumbsFieldFile, self).save(name, content, save)
86 super(ImageWithThumbsFieldFile, self).save(name, content, save)
86
87
87 if self.sizes:
88 if self.sizes:
88 for size in self.sizes:
89 for size in self.sizes:
89 (w, h) = size
90 (w, h) = size
90 split = self.name.rsplit('.', 1)
91 split = self.name.rsplit('.', 1)
91 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
92 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
92
93
93 # you can use another thumbnailing function if you like
94 # you can use another thumbnailing function if you like
94 thumb_content = generate_thumb(content, size, split[1])
95 thumb_content = generate_thumb(content, size, split[1])
95
96
96 thumb_name_ = self.storage.save(thumb_name, thumb_content)
97 thumb_name_ = self.storage.save(thumb_name, thumb_content)
97
98
98 if not thumb_name == thumb_name_:
99 if not thumb_name == thumb_name_:
99 raise ValueError(
100 raise ValueError(
100 'There is already a file named %s' % thumb_name)
101 'There is already a file named %s' % thumb_name)
101
102
102 def delete(self, save=True):
103 def delete(self, save=True):
103 name = self.name
104 name = self.name
104 super(ImageWithThumbsFieldFile, self).delete(save)
105 super(ImageWithThumbsFieldFile, self).delete(save)
105 if self.sizes:
106 if self.sizes:
106 for size in self.sizes:
107 for size in self.sizes:
107 (w, h) = size
108 (w, h) = size
108 split = name.rsplit('.', 1)
109 split = name.rsplit('.', 1)
109 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
110 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
110 try:
111 try:
111 self.storage.delete(thumb_name)
112 self.storage.delete(thumb_name)
112 except:
113 except:
113 pass
114 pass
114
115
115
116
116 class ImageWithThumbsField(ImageField):
117 class ImageWithThumbsField(ImageField):
117 attr_class = ImageWithThumbsFieldFile
118 attr_class = ImageWithThumbsFieldFile
118 """
119 """
119 Usage example:
120 Usage example:
120 ==============
121 ==============
121 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
122 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
122
123
123 To retrieve image URL, exactly the same way as with ImageField:
124 To retrieve image URL, exactly the same way as with ImageField:
124 my_object.photo.url
125 my_object.photo.url
125 To retrieve thumbnails URL's just add the size to it:
126 To retrieve thumbnails URL's just add the size to it:
126 my_object.photo.url_125x125
127 my_object.photo.url_125x125
127 my_object.photo.url_300x200
128 my_object.photo.url_300x200
128
129
129 Note: The 'sizes' attribute is not required. If you don't provide it,
130 Note: The 'sizes' attribute is not required. If you don't provide it,
130 ImageWithThumbsField will act as a normal ImageField
131 ImageWithThumbsField will act as a normal ImageField
131
132
132 How it works:
133 How it works:
133 =============
134 =============
134 For each size in the 'sizes' atribute of the field it generates a
135 For each size in the 'sizes' atribute of the field it generates a
135 thumbnail with that size and stores it following this format:
136 thumbnail with that size and stores it following this format:
136
137
137 available_filename.[width]x[height].extension
138 available_filename.[width]x[height].extension
138
139
139 Where 'available_filename' is the available filename returned by the storage
140 Where 'available_filename' is the available filename returned by the storage
140 backend for saving the original file.
141 backend for saving the original file.
141
142
142 Following the usage example above: For storing a file called "photo.jpg" it saves:
143 Following the usage example above: For storing a file called "photo.jpg" it saves:
143 photo.jpg (original file)
144 photo.jpg (original file)
144 photo.125x125.jpg (first thumbnail)
145 photo.125x125.jpg (first thumbnail)
145 photo.300x200.jpg (second thumbnail)
146 photo.300x200.jpg (second thumbnail)
146
147
147 With the default storage backend if photo.jpg already exists it will use these filenames:
148 With the default storage backend if photo.jpg already exists it will use these filenames:
148 photo_.jpg
149 photo_.jpg
149 photo_.125x125.jpg
150 photo_.125x125.jpg
150 photo_.300x200.jpg
151 photo_.300x200.jpg
151
152
152 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
153 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
153 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
154 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
154
155
155 To do:
156 To do:
156 ======
157 ======
157 Add method to regenerate thubmnails
158 Add method to regenerate thubmnails
158
159
159
160
160 """
161 """
161
162
162 def __init__(self, verbose_name=None, name=None, width_field=None,
163 def __init__(self, verbose_name=None, name=None, width_field=None,
163 height_field=None, sizes=None, **kwargs):
164 height_field=None, sizes=None,
165 preview_width_field=None, preview_height_field=None,
166 **kwargs):
164 self.verbose_name = verbose_name
167 self.verbose_name = verbose_name
165 self.name = name
168 self.name = name
166 self.width_field = width_field
169 self.width_field = width_field
167 self.height_field = height_field
170 self.height_field = height_field
168 self.sizes = sizes
171 self.sizes = sizes
169 super(ImageField, self).__init__(**kwargs)
172 super(ImageField, self).__init__(**kwargs)
170
173
174 if sizes is not None and len(sizes) == 1:
175 self.preview_width_field = preview_width_field
176 self.preview_height_field = preview_height_field
177
178 def update_dimension_fields(self, instance, force=False, *args, **kwargs):
179 """
180 Update original image dimension fields and thumb dimension fields
181 (only if 1 thumb size is defined)
182 """
183
184 super(ImageWithThumbsField, self).update_dimension_fields(instance,
185 force, *args,
186 **kwargs)
187 thumb_width_field = self.preview_width_field
188 thumb_height_field = self.preview_height_field
189
190 if thumb_width_field is None or thumb_height_field is None \
191 or len(self.sizes) != 1:
192 return
193
194 original_width = getattr(instance, self.width_field)
195 original_height = getattr(instance, self.height_field)
196
197 if original_width > 0 and original_height > 0:
198 thumb_width, thumb_height = self.sizes[0]
199
200 w_scale = float(thumb_width) / original_width
201 h_scale = float(thumb_height) / original_height
202 scale_ratio = min(w_scale, h_scale)
203
204 if scale_ratio >= 1:
205 thumb_width_ratio = original_width
206 thumb_height_ratio = original_height
207 else:
208 thumb_width_ratio = int(original_width * scale_ratio)
209 thumb_height_ratio = int(original_height * scale_ratio)
210
211 setattr(instance, thumb_width_field, thumb_width_ratio)
212 setattr(instance, thumb_height_field, thumb_height_ratio)
213
171
214
172 from south.modelsinspector import add_introspection_rules
215 from south.modelsinspector import add_introspection_rules
173 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
216 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
@@ -1,59 +1,60 b''
1 from django.conf.urls import patterns, url, include
1 from django.conf.urls import patterns, url, include
2 from boards import views
2 from boards import views
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 from boards.views.api import api_get_threaddiff
4 from boards.views.api import api_get_threaddiff
5
5
6 js_info_dict = {
6 js_info_dict = {
7 'packages': ('boards',),
7 'packages': ('boards',),
8 }
8 }
9
9
10 urlpatterns = patterns('',
10 urlpatterns = patterns('',
11
11
12 # /boards/
12 # /boards/
13 url(r'^$', views.index, name='index'),
13 url(r'^$', views.index, name='index'),
14 # /boards/page/
14 # /boards/page/
15 url(r'^page/(?P<page>\w+)/$', views.index, name='index'),
15 url(r'^page/(?P<page>\w+)/$', views.index, name='index'),
16
16
17 # login page
17 # login page
18 url(r'^login/$', views.login, name='login'),
18 url(r'^login/$', views.login, name='login'),
19
19
20 # /boards/tag/tag_name/
20 # /boards/tag/tag_name/
21 url(r'^tag/(?P<tag_name>\w+)/$', views.tag, name='tag'),
21 url(r'^tag/(?P<tag_name>\w+)/$', views.tag, name='tag'),
22 # /boards/tag/tag_id/page/
22 # /boards/tag/tag_id/page/
23 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', views.tag, name='tag'),
23 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', views.tag, name='tag'),
24
24
25 # /boards/tag/tag_name/unsubscribe/
25 # /boards/tag/tag_name/unsubscribe/
26 url(r'^tag/(?P<tag_name>\w+)/subscribe/$', views.tag_subscribe,
26 url(r'^tag/(?P<tag_name>\w+)/subscribe/$', views.tag_subscribe,
27 name='tag_subscribe'),
27 name='tag_subscribe'),
28 # /boards/tag/tag_name/unsubscribe/
28 # /boards/tag/tag_name/unsubscribe/
29 url(r'^tag/(?P<tag_name>\w+)/unsubscribe/$', views.tag_unsubscribe,
29 url(r'^tag/(?P<tag_name>\w+)/unsubscribe/$', views.tag_unsubscribe,
30 name='tag_unsubscribe'),
30 name='tag_unsubscribe'),
31
31
32 # /boards/thread/
32 # /boards/thread/
33 url(r'^thread/(?P<post_id>\w+)/$', views.thread, name='thread'),
33 url(r'^thread/(?P<post_id>\w+)/$', views.thread, name='thread'),
34 url(r'^thread/(?P<post_id>\w+)/(?P<mode>\w+)/$', views.thread, name='thread_mode'),
34 url(r'^settings/$', views.settings, name='settings'),
35 url(r'^settings/$', views.settings, name='settings'),
35 url(r'^tags/$', views.all_tags, name='tags'),
36 url(r'^tags/$', views.all_tags, name='tags'),
36 url(r'^captcha/', include('captcha.urls')),
37 url(r'^captcha/', include('captcha.urls')),
37 url(r'^jump/(?P<post_id>\w+)/$', views.jump_to_post, name='jumper'),
38 url(r'^jump/(?P<post_id>\w+)/$', views.jump_to_post, name='jumper'),
38 url(r'^authors/$', views.authors, name='authors'),
39 url(r'^authors/$', views.authors, name='authors'),
39 url(r'^delete/(?P<post_id>\w+)/$', views.delete, name='delete'),
40 url(r'^delete/(?P<post_id>\w+)/$', views.delete, name='delete'),
40 url(r'^ban/(?P<post_id>\w+)/$', views.ban, name='ban'),
41 url(r'^ban/(?P<post_id>\w+)/$', views.ban, name='ban'),
41
42
42 url(r'^banned/$', views.you_are_banned, name='banned'),
43 url(r'^banned/$', views.you_are_banned, name='banned'),
43 url(r'^staticpage/(?P<name>\w+)/$', views.static_page, name='staticpage'),
44 url(r'^staticpage/(?P<name>\w+)/$', views.static_page, name='staticpage'),
44
45
45 # RSS feeds
46 # RSS feeds
46 url(r'^rss/$', AllThreadsFeed()),
47 url(r'^rss/$', AllThreadsFeed()),
47 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
48 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
48 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
49 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
49 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
50 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
50 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
51 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
51
52
52 # i18n
53 # i18n
53 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, name='js_info_dict'),
54 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, name='js_info_dict'),
54
55
55 # API
56 # API
56 url(r'^api/post/(?P<post_id>\w+)/$', views.get_post, name="get_post"),
57 url(r'^api/post/(?P<post_id>\w+)/$', views.get_post, name="get_post"),
57 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
58 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
58 api_get_threaddiff, name="get_thread_diff"),
59 api_get_threaddiff, name="get_thread_diff"),
59 )
60 )
@@ -1,548 +1,559 b''
1 __author__ = 'neko259'
1 __author__ = 'neko259'
2
2
3 import hashlib
3 import hashlib
4 import string
4 import string
5 import time
5 import time
6 import re
6 import re
7
7
8 from django.core import serializers
8 from django.core import serializers
9 from django.core.urlresolvers import reverse
9 from django.core.urlresolvers import reverse
10 from django.http import HttpResponseRedirect
10 from django.http import HttpResponseRedirect, Http404
11 from django.http.response import HttpResponse
11 from django.http.response import HttpResponse
12 from django.template import RequestContext
12 from django.template import RequestContext
13 from django.shortcuts import render, redirect, get_object_or_404
13 from django.shortcuts import render, redirect, get_object_or_404
14 from django.utils import timezone
14 from django.utils import timezone
15 from django.db import transaction
15 from django.db import transaction
16 from django.views.decorators.cache import cache_page
16 from django.views.decorators.cache import cache_page
17 from django.views.i18n import javascript_catalog
17 from django.views.i18n import javascript_catalog
18
18
19 from boards import forms
19 from boards import forms
20 import boards
20 import boards
21 from boards import utils
21 from boards import utils
22 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
22 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
23 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
23 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
24 from boards.models import Post, Tag, Ban, User
24 from boards.models import Post, Tag, Ban, User
25 from boards.models.post import SETTING_MODERATE, REGEX_REPLY
25 from boards.models.post import SETTING_MODERATE, REGEX_REPLY
26 from boards.models.user import RANK_USER
26 from boards.models.user import RANK_USER
27 from boards import authors
27 from boards import authors
28 from boards.utils import get_client_ip
28 from boards.utils import get_client_ip
29 import neboard
29 import neboard
30
30
31
31
32 BAN_REASON_SPAM = 'Autoban: spam bot'
32 BAN_REASON_SPAM = 'Autoban: spam bot'
33 MODE_GALLERY = 'gallery'
34 MODE_NORMAL = 'normal'
33
35
34
36
35 def index(request, page=0):
37 def index(request, page=0):
36 context = _init_default_context(request)
38 context = _init_default_context(request)
37
39
38 if utils.need_include_captcha(request):
40 if utils.need_include_captcha(request):
39 threadFormClass = ThreadCaptchaForm
41 threadFormClass = ThreadCaptchaForm
40 kwargs = {'request': request}
42 kwargs = {'request': request}
41 else:
43 else:
42 threadFormClass = ThreadForm
44 threadFormClass = ThreadForm
43 kwargs = {}
45 kwargs = {}
44
46
45 if request.method == 'POST':
47 if request.method == 'POST':
46 form = threadFormClass(request.POST, request.FILES,
48 form = threadFormClass(request.POST, request.FILES,
47 error_class=PlainErrorList, **kwargs)
49 error_class=PlainErrorList, **kwargs)
48 form.session = request.session
50 form.session = request.session
49
51
50 if form.is_valid():
52 if form.is_valid():
51 return _new_post(request, form)
53 return _new_post(request, form)
52 if form.need_to_ban:
54 if form.need_to_ban:
53 # Ban user because he is suspected to be a bot
55 # Ban user because he is suspected to be a bot
54 _ban_current_user(request)
56 _ban_current_user(request)
55 else:
57 else:
56 form = threadFormClass(error_class=PlainErrorList, **kwargs)
58 form = threadFormClass(error_class=PlainErrorList, **kwargs)
57
59
58 threads = []
60 threads = []
59 for thread_to_show in Post.objects.get_threads(page=int(page)):
61 for thread_to_show in Post.objects.get_threads(page=int(page)):
60 threads.append(_get_template_thread(thread_to_show))
62 threads.append(_get_template_thread(thread_to_show))
61
63
62 # TODO Make this generic for tag and threads list pages
64 # TODO Make this generic for tag and threads list pages
63 context['threads'] = None if len(threads) == 0 else threads
65 context['threads'] = None if len(threads) == 0 else threads
64 context['form'] = form
66 context['form'] = form
67 context['current_page'] = int(page)
65
68
66 page_count = Post.objects.get_thread_page_count()
69 page_count = Post.objects.get_thread_page_count()
67 context['pages'] = range(page_count)
70 context['pages'] = range(page_count)
68 page = int(page)
71 page = int(page)
69 if page < page_count - 1:
72 if page < page_count - 1:
70 context['next_page'] = str(page + 1)
73 context['next_page'] = str(page + 1)
71 if page > 0:
74 if page > 0:
72 context['prev_page'] = str(page - 1)
75 context['prev_page'] = str(page - 1)
73
76
74 return render(request, 'boards/posting_general.html',
77 return render(request, 'boards/posting_general.html',
75 context)
78 context)
76
79
77
80
78 @transaction.atomic
81 @transaction.atomic
79 def _new_post(request, form, opening_post=None):
82 def _new_post(request, form, opening_post=None):
80 """Add a new post (in thread or as a reply)."""
83 """Add a new post (in thread or as a reply)."""
81
84
82 ip = get_client_ip(request)
85 ip = get_client_ip(request)
83 is_banned = Ban.objects.filter(ip=ip).exists()
86 is_banned = Ban.objects.filter(ip=ip).exists()
84
87
85 if is_banned:
88 if is_banned:
86 return redirect(you_are_banned)
89 return redirect(you_are_banned)
87
90
88 data = form.cleaned_data
91 data = form.cleaned_data
89
92
90 title = data['title']
93 title = data['title']
91 text = data['text']
94 text = data['text']
92
95
93 text = _remove_invalid_links(text)
96 text = _remove_invalid_links(text)
94
97
95 if 'image' in data.keys():
98 if 'image' in data.keys():
96 image = data['image']
99 image = data['image']
97 else:
100 else:
98 image = None
101 image = None
99
102
100 tags = []
103 tags = []
101
104
102 if not opening_post:
105 if not opening_post:
103 tag_strings = data['tags']
106 tag_strings = data['tags']
104
107
105 if tag_strings:
108 if tag_strings:
106 tag_strings = tag_strings.split(' ')
109 tag_strings = tag_strings.split(' ')
107 for tag_name in tag_strings:
110 for tag_name in tag_strings:
108 tag_name = string.lower(tag_name.strip())
111 tag_name = string.lower(tag_name.strip())
109 if len(tag_name) > 0:
112 if len(tag_name) > 0:
110 tag, created = Tag.objects.get_or_create(name=tag_name)
113 tag, created = Tag.objects.get_or_create(name=tag_name)
111 tags.append(tag)
114 tags.append(tag)
112 post_thread = None
115 post_thread = None
113 else:
116 else:
114 post_thread = opening_post.thread_new
117 post_thread = opening_post.thread_new
115
118
116 post = Post.objects.create_post(title=title, text=text, ip=ip,
119 post = Post.objects.create_post(title=title, text=text, ip=ip,
117 thread=post_thread, image=image,
120 thread=post_thread, image=image,
118 tags=tags, user=_get_user(request))
121 tags=tags, user=_get_user(request))
119
122
120 thread_to_show = (opening_post.id if opening_post else post.id)
123 thread_to_show = (opening_post.id if opening_post else post.id)
121
124
122 if opening_post:
125 if opening_post:
123 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
126 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
124 '#' + str(post.id))
127 '#' + str(post.id))
125 else:
128 else:
126 return redirect(thread, post_id=thread_to_show)
129 return redirect(thread, post_id=thread_to_show)
127
130
128
131
129 def tag(request, tag_name, page=0):
132 def tag(request, tag_name, page=0):
130 """
133 """
131 Get all tag threads. Threads are split in pages, so some page is
134 Get all tag threads. Threads are split in pages, so some page is
132 requested. Default page is 0.
135 requested. Default page is 0.
133 """
136 """
134
137
135 tag = get_object_or_404(Tag, name=tag_name)
138 tag = get_object_or_404(Tag, name=tag_name)
136 threads = []
139 threads = []
137 for thread_to_show in Post.objects.get_threads(page=int(page), tag=tag):
140 for thread_to_show in Post.objects.get_threads(page=int(page), tag=tag):
138 threads.append(_get_template_thread(thread_to_show))
141 threads.append(_get_template_thread(thread_to_show))
139
142
140 if request.method == 'POST':
143 if request.method == 'POST':
141 form = ThreadForm(request.POST, request.FILES,
144 form = ThreadForm(request.POST, request.FILES,
142 error_class=PlainErrorList)
145 error_class=PlainErrorList)
143 form.session = request.session
146 form.session = request.session
144
147
145 if form.is_valid():
148 if form.is_valid():
146 return _new_post(request, form)
149 return _new_post(request, form)
147 if form.need_to_ban:
150 if form.need_to_ban:
148 # Ban user because he is suspected to be a bot
151 # Ban user because he is suspected to be a bot
149 _ban_current_user(request)
152 _ban_current_user(request)
150 else:
153 else:
151 form = forms.ThreadForm(initial={'tags': tag_name},
154 form = forms.ThreadForm(initial={'tags': tag_name},
152 error_class=PlainErrorList)
155 error_class=PlainErrorList)
153
156
154 context = _init_default_context(request)
157 context = _init_default_context(request)
155 context['threads'] = None if len(threads) == 0 else threads
158 context['threads'] = None if len(threads) == 0 else threads
156 context['tag'] = tag
159 context['tag'] = tag
160 context['current_page'] = int(page)
157
161
158 page_count = Post.objects.get_thread_page_count(tag=tag)
162 page_count = Post.objects.get_thread_page_count(tag=tag)
159 context['pages'] = range(page_count)
163 context['pages'] = range(page_count)
160 page = int(page)
164 page = int(page)
161 if page < page_count - 1:
165 if page < page_count - 1:
162 context['next_page'] = str(page + 1)
166 context['next_page'] = str(page + 1)
163 if page > 0:
167 if page > 0:
164 context['prev_page'] = str(page - 1)
168 context['prev_page'] = str(page - 1)
165
169
166 context['form'] = form
170 context['form'] = form
167
171
168 return render(request, 'boards/posting_general.html',
172 return render(request, 'boards/posting_general.html',
169 context)
173 context)
170
174
171
175
172 def thread(request, post_id):
176 def thread(request, post_id, mode=MODE_NORMAL):
173 """Get all thread posts"""
177 """Get all thread posts"""
174
178
175 if utils.need_include_captcha(request):
179 if utils.need_include_captcha(request):
176 postFormClass = PostCaptchaForm
180 postFormClass = PostCaptchaForm
177 kwargs = {'request': request}
181 kwargs = {'request': request}
178 else:
182 else:
179 postFormClass = PostForm
183 postFormClass = PostForm
180 kwargs = {}
184 kwargs = {}
181
185
182 if request.method == 'POST':
186 if request.method == 'POST':
183 form = postFormClass(request.POST, request.FILES,
187 form = postFormClass(request.POST, request.FILES,
184 error_class=PlainErrorList, **kwargs)
188 error_class=PlainErrorList, **kwargs)
185 form.session = request.session
189 form.session = request.session
186
190
187 opening_post = get_object_or_404(Post, id=post_id)
191 opening_post = get_object_or_404(Post, id=post_id)
188 if form.is_valid():
192 if form.is_valid():
189 return _new_post(request, form, opening_post)
193 return _new_post(request, form, opening_post)
190 if form.need_to_ban:
194 if form.need_to_ban:
191 # Ban user because he is suspected to be a bot
195 # Ban user because he is suspected to be a bot
192 _ban_current_user(request)
196 _ban_current_user(request)
193 else:
197 else:
194 form = postFormClass(error_class=PlainErrorList, **kwargs)
198 form = postFormClass(error_class=PlainErrorList, **kwargs)
195
199
196 thread_to_show = get_object_or_404(Post, id=post_id).thread_new
200 thread_to_show = get_object_or_404(Post, id=post_id).thread_new
197
201
198 context = _init_default_context(request)
202 context = _init_default_context(request)
199
203
200 posts = thread_to_show.get_replies()
204 posts = thread_to_show.get_replies()
201 context['form'] = form
205 context['form'] = form
202 context['bumpable'] = thread_to_show.can_bump()
206 context['bumpable'] = thread_to_show.can_bump()
203 if context['bumpable']:
207 if context['bumpable']:
204 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts \
208 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts \
205 .count()
209 .count()
206 context['bumplimit_progress'] = str(
210 context['bumplimit_progress'] = str(
207 float(context['posts_left']) /
211 float(context['posts_left']) /
208 neboard.settings.MAX_POSTS_PER_THREAD * 100)
212 neboard.settings.MAX_POSTS_PER_THREAD * 100)
209 context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time)
213 context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time)
210 context["thread"] = thread_to_show
214 context["thread"] = thread_to_show
211
215
212 return render(request, 'boards/thread.html', context)
216 if MODE_NORMAL == mode:
217 document = 'boards/thread.html'
218 elif MODE_GALLERY == mode:
219 document = 'boards/thread_gallery.html'
220 else:
221 raise Http404
222
223 return render(request, document, context)
213
224
214
225
215 def login(request):
226 def login(request):
216 """Log in with user id"""
227 """Log in with user id"""
217
228
218 context = _init_default_context(request)
229 context = _init_default_context(request)
219
230
220 if request.method == 'POST':
231 if request.method == 'POST':
221 form = LoginForm(request.POST, request.FILES,
232 form = LoginForm(request.POST, request.FILES,
222 error_class=PlainErrorList)
233 error_class=PlainErrorList)
223 form.session = request.session
234 form.session = request.session
224
235
225 if form.is_valid():
236 if form.is_valid():
226 user = User.objects.get(user_id=form.cleaned_data['user_id'])
237 user = User.objects.get(user_id=form.cleaned_data['user_id'])
227 request.session['user_id'] = user.id
238 request.session['user_id'] = user.id
228 return redirect(index)
239 return redirect(index)
229
240
230 else:
241 else:
231 form = LoginForm()
242 form = LoginForm()
232
243
233 context['form'] = form
244 context['form'] = form
234
245
235 return render(request, 'boards/login.html', context)
246 return render(request, 'boards/login.html', context)
236
247
237
248
238 def settings(request):
249 def settings(request):
239 """User's settings"""
250 """User's settings"""
240
251
241 context = _init_default_context(request)
252 context = _init_default_context(request)
242 user = _get_user(request)
253 user = _get_user(request)
243 is_moderator = user.is_moderator()
254 is_moderator = user.is_moderator()
244
255
245 if request.method == 'POST':
256 if request.method == 'POST':
246 with transaction.atomic():
257 with transaction.atomic():
247 if is_moderator:
258 if is_moderator:
248 form = ModeratorSettingsForm(request.POST,
259 form = ModeratorSettingsForm(request.POST,
249 error_class=PlainErrorList)
260 error_class=PlainErrorList)
250 else:
261 else:
251 form = SettingsForm(request.POST, error_class=PlainErrorList)
262 form = SettingsForm(request.POST, error_class=PlainErrorList)
252
263
253 if form.is_valid():
264 if form.is_valid():
254 selected_theme = form.cleaned_data['theme']
265 selected_theme = form.cleaned_data['theme']
255
266
256 user.save_setting('theme', selected_theme)
267 user.save_setting('theme', selected_theme)
257
268
258 if is_moderator:
269 if is_moderator:
259 moderate = form.cleaned_data['moderate']
270 moderate = form.cleaned_data['moderate']
260 user.save_setting(SETTING_MODERATE, moderate)
271 user.save_setting(SETTING_MODERATE, moderate)
261
272
262 return redirect(settings)
273 return redirect(settings)
263 else:
274 else:
264 selected_theme = _get_theme(request)
275 selected_theme = _get_theme(request)
265
276
266 if is_moderator:
277 if is_moderator:
267 form = ModeratorSettingsForm(initial={'theme': selected_theme,
278 form = ModeratorSettingsForm(initial={'theme': selected_theme,
268 'moderate': context['moderator']},
279 'moderate': context['moderator']},
269 error_class=PlainErrorList)
280 error_class=PlainErrorList)
270 else:
281 else:
271 form = SettingsForm(initial={'theme': selected_theme},
282 form = SettingsForm(initial={'theme': selected_theme},
272 error_class=PlainErrorList)
283 error_class=PlainErrorList)
273
284
274 context['form'] = form
285 context['form'] = form
275
286
276 return render(request, 'boards/settings.html', context)
287 return render(request, 'boards/settings.html', context)
277
288
278
289
279 def all_tags(request):
290 def all_tags(request):
280 """All tags list"""
291 """All tags list"""
281
292
282 context = _init_default_context(request)
293 context = _init_default_context(request)
283 context['all_tags'] = Tag.objects.get_not_empty_tags()
294 context['all_tags'] = Tag.objects.get_not_empty_tags()
284
295
285 return render(request, 'boards/tags.html', context)
296 return render(request, 'boards/tags.html', context)
286
297
287
298
288 def jump_to_post(request, post_id):
299 def jump_to_post(request, post_id):
289 """Determine thread in which the requested post is and open it's page"""
300 """Determine thread in which the requested post is and open it's page"""
290
301
291 post = get_object_or_404(Post, id=post_id)
302 post = get_object_or_404(Post, id=post_id)
292
303
293 if not post.thread:
304 if not post.thread:
294 return redirect(thread, post_id=post.id)
305 return redirect(thread, post_id=post.id)
295 else:
306 else:
296 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
307 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
297 + '#' + str(post.id))
308 + '#' + str(post.id))
298
309
299
310
300 def authors(request):
311 def authors(request):
301 """Show authors list"""
312 """Show authors list"""
302
313
303 context = _init_default_context(request)
314 context = _init_default_context(request)
304 context['authors'] = boards.authors.authors
315 context['authors'] = boards.authors.authors
305
316
306 return render(request, 'boards/authors.html', context)
317 return render(request, 'boards/authors.html', context)
307
318
308
319
309 @transaction.atomic
320 @transaction.atomic
310 def delete(request, post_id):
321 def delete(request, post_id):
311 """Delete post"""
322 """Delete post"""
312
323
313 user = _get_user(request)
324 user = _get_user(request)
314 post = get_object_or_404(Post, id=post_id)
325 post = get_object_or_404(Post, id=post_id)
315
326
316 if user.is_moderator():
327 if user.is_moderator():
317 # TODO Show confirmation page before deletion
328 # TODO Show confirmation page before deletion
318 Post.objects.delete_post(post)
329 Post.objects.delete_post(post)
319
330
320 if not post.thread:
331 if not post.thread:
321 return _redirect_to_next(request)
332 return _redirect_to_next(request)
322 else:
333 else:
323 return redirect(thread, post_id=post.thread.id)
334 return redirect(thread, post_id=post.thread.id)
324
335
325
336
326 @transaction.atomic
337 @transaction.atomic
327 def ban(request, post_id):
338 def ban(request, post_id):
328 """Ban user"""
339 """Ban user"""
329
340
330 user = _get_user(request)
341 user = _get_user(request)
331 post = get_object_or_404(Post, id=post_id)
342 post = get_object_or_404(Post, id=post_id)
332
343
333 if user.is_moderator():
344 if user.is_moderator():
334 # TODO Show confirmation page before ban
345 # TODO Show confirmation page before ban
335 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
346 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
336 if created:
347 if created:
337 ban.reason = 'Banned for post ' + str(post_id)
348 ban.reason = 'Banned for post ' + str(post_id)
338 ban.save()
349 ban.save()
339
350
340 return _redirect_to_next(request)
351 return _redirect_to_next(request)
341
352
342
353
343 def you_are_banned(request):
354 def you_are_banned(request):
344 """Show the page that notifies that user is banned"""
355 """Show the page that notifies that user is banned"""
345
356
346 context = _init_default_context(request)
357 context = _init_default_context(request)
347
358
348 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
359 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
349 context['ban_reason'] = ban.reason
360 context['ban_reason'] = ban.reason
350 return render(request, 'boards/staticpages/banned.html', context)
361 return render(request, 'boards/staticpages/banned.html', context)
351
362
352
363
353 def page_404(request):
364 def page_404(request):
354 """Show page 404 (not found error)"""
365 """Show page 404 (not found error)"""
355
366
356 context = _init_default_context(request)
367 context = _init_default_context(request)
357 return render(request, 'boards/404.html', context)
368 return render(request, 'boards/404.html', context)
358
369
359
370
360 @transaction.atomic
371 @transaction.atomic
361 def tag_subscribe(request, tag_name):
372 def tag_subscribe(request, tag_name):
362 """Add tag to favorites"""
373 """Add tag to favorites"""
363
374
364 user = _get_user(request)
375 user = _get_user(request)
365 tag = get_object_or_404(Tag, name=tag_name)
376 tag = get_object_or_404(Tag, name=tag_name)
366
377
367 if not tag in user.fav_tags.all():
378 if not tag in user.fav_tags.all():
368 user.add_tag(tag)
379 user.add_tag(tag)
369
380
370 return _redirect_to_next(request)
381 return _redirect_to_next(request)
371
382
372
383
373 @transaction.atomic
384 @transaction.atomic
374 def tag_unsubscribe(request, tag_name):
385 def tag_unsubscribe(request, tag_name):
375 """Remove tag from favorites"""
386 """Remove tag from favorites"""
376
387
377 user = _get_user(request)
388 user = _get_user(request)
378 tag = get_object_or_404(Tag, name=tag_name)
389 tag = get_object_or_404(Tag, name=tag_name)
379
390
380 if tag in user.fav_tags.all():
391 if tag in user.fav_tags.all():
381 user.remove_tag(tag)
392 user.remove_tag(tag)
382
393
383 return _redirect_to_next(request)
394 return _redirect_to_next(request)
384
395
385
396
386 def static_page(request, name):
397 def static_page(request, name):
387 """Show a static page that needs only tags list and a CSS"""
398 """Show a static page that needs only tags list and a CSS"""
388
399
389 context = _init_default_context(request)
400 context = _init_default_context(request)
390 return render(request, 'boards/staticpages/' + name + '.html', context)
401 return render(request, 'boards/staticpages/' + name + '.html', context)
391
402
392
403
393 def api_get_post(request, post_id):
404 def api_get_post(request, post_id):
394 """
405 """
395 Get the JSON of a post. This can be
406 Get the JSON of a post. This can be
396 used as and API for external clients.
407 used as and API for external clients.
397 """
408 """
398
409
399 post = get_object_or_404(Post, id=post_id)
410 post = get_object_or_404(Post, id=post_id)
400
411
401 json = serializers.serialize("json", [post], fields=(
412 json = serializers.serialize("json", [post], fields=(
402 "pub_time", "_text_rendered", "title", "text", "image",
413 "pub_time", "_text_rendered", "title", "text", "image",
403 "image_width", "image_height", "replies", "tags"
414 "image_width", "image_height", "replies", "tags"
404 ))
415 ))
405
416
406 return HttpResponse(content=json)
417 return HttpResponse(content=json)
407
418
408
419
409 def get_post(request, post_id):
420 def get_post(request, post_id):
410 """Get the html of a post. Used for popups."""
421 """Get the html of a post. Used for popups."""
411
422
412 post = get_object_or_404(Post, id=post_id)
423 post = get_object_or_404(Post, id=post_id)
413 thread = post.thread_new
424 thread = post.thread_new
414
425
415 context = RequestContext(request)
426 context = RequestContext(request)
416 context["post"] = post
427 context["post"] = post
417 context["can_bump"] = thread.can_bump()
428 context["can_bump"] = thread.can_bump()
418 if "truncated" in request.GET:
429 if "truncated" in request.GET:
419 context["truncated"] = True
430 context["truncated"] = True
420
431
421 return render(request, 'boards/post.html', context)
432 return render(request, 'boards/post.html', context)
422
433
423 @cache_page(86400)
434 @cache_page(86400)
424 def cached_js_catalog(request, domain='djangojs', packages=None):
435 def cached_js_catalog(request, domain='djangojs', packages=None):
425 return javascript_catalog(request, domain, packages)
436 return javascript_catalog(request, domain, packages)
426
437
427
438
428 def _get_theme(request, user=None):
439 def _get_theme(request, user=None):
429 """Get user's CSS theme"""
440 """Get user's CSS theme"""
430
441
431 if not user:
442 if not user:
432 user = _get_user(request)
443 user = _get_user(request)
433 theme = user.get_setting('theme')
444 theme = user.get_setting('theme')
434 if not theme:
445 if not theme:
435 theme = neboard.settings.DEFAULT_THEME
446 theme = neboard.settings.DEFAULT_THEME
436
447
437 return theme
448 return theme
438
449
439
450
440 def _init_default_context(request):
451 def _init_default_context(request):
441 """Create context with default values that are used in most views"""
452 """Create context with default values that are used in most views"""
442
453
443 context = RequestContext(request)
454 context = RequestContext(request)
444
455
445 user = _get_user(request)
456 user = _get_user(request)
446 context['user'] = user
457 context['user'] = user
447 context['tags'] = user.get_sorted_fav_tags()
458 context['tags'] = user.get_sorted_fav_tags()
448 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
459 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
449
460
450 theme = _get_theme(request, user)
461 theme = _get_theme(request, user)
451 context['theme'] = theme
462 context['theme'] = theme
452 context['theme_css'] = 'css/' + theme + '/base_page.css'
463 context['theme_css'] = 'css/' + theme + '/base_page.css'
453
464
454 # This shows the moderator panel
465 # This shows the moderator panel
455 moderate = user.get_setting(SETTING_MODERATE)
466 moderate = user.get_setting(SETTING_MODERATE)
456 if moderate == 'True':
467 if moderate == 'True':
457 context['moderator'] = user.is_moderator()
468 context['moderator'] = user.is_moderator()
458 else:
469 else:
459 context['moderator'] = False
470 context['moderator'] = False
460
471
461 return context
472 return context
462
473
463
474
464 def _get_user(request):
475 def _get_user(request):
465 """
476 """
466 Get current user from the session. If the user does not exist, create
477 Get current user from the session. If the user does not exist, create
467 a new one.
478 a new one.
468 """
479 """
469
480
470 session = request.session
481 session = request.session
471 if not 'user_id' in session:
482 if not 'user_id' in session:
472 request.session.save()
483 request.session.save()
473
484
474 md5 = hashlib.md5()
485 md5 = hashlib.md5()
475 md5.update(session.session_key)
486 md5.update(session.session_key)
476 new_id = md5.hexdigest()
487 new_id = md5.hexdigest()
477
488
478 time_now = timezone.now()
489 time_now = timezone.now()
479 user = User.objects.create(user_id=new_id, rank=RANK_USER,
490 user = User.objects.create(user_id=new_id, rank=RANK_USER,
480 registration_time=time_now)
491 registration_time=time_now)
481
492
482 session['user_id'] = user.id
493 session['user_id'] = user.id
483 else:
494 else:
484 user = User.objects.get(id=session['user_id'])
495 user = User.objects.get(id=session['user_id'])
485
496
486 return user
497 return user
487
498
488
499
489 def _redirect_to_next(request):
500 def _redirect_to_next(request):
490 """
501 """
491 If a 'next' parameter was specified, redirect to the next page. This is
502 If a 'next' parameter was specified, redirect to the next page. This is
492 used when the user is required to return to some page after the current
503 used when the user is required to return to some page after the current
493 view has finished its work.
504 view has finished its work.
494 """
505 """
495
506
496 if 'next' in request.GET:
507 if 'next' in request.GET:
497 next_page = request.GET['next']
508 next_page = request.GET['next']
498 return HttpResponseRedirect(next_page)
509 return HttpResponseRedirect(next_page)
499 else:
510 else:
500 return redirect(index)
511 return redirect(index)
501
512
502
513
503 @transaction.atomic
514 @transaction.atomic
504 def _ban_current_user(request):
515 def _ban_current_user(request):
505 """Add current user to the IP ban list"""
516 """Add current user to the IP ban list"""
506
517
507 ip = utils.get_client_ip(request)
518 ip = utils.get_client_ip(request)
508 ban, created = Ban.objects.get_or_create(ip=ip)
519 ban, created = Ban.objects.get_or_create(ip=ip)
509 if created:
520 if created:
510 ban.can_read = False
521 ban.can_read = False
511 ban.reason = BAN_REASON_SPAM
522 ban.reason = BAN_REASON_SPAM
512 ban.save()
523 ban.save()
513
524
514
525
515 def _remove_invalid_links(text):
526 def _remove_invalid_links(text):
516 """
527 """
517 Replace invalid links in posts so that they won't be parsed.
528 Replace invalid links in posts so that they won't be parsed.
518 Invalid links are links to non-existent posts
529 Invalid links are links to non-existent posts
519 """
530 """
520
531
521 for reply_number in re.finditer(REGEX_REPLY, text):
532 for reply_number in re.finditer(REGEX_REPLY, text):
522 post_id = reply_number.group(1)
533 post_id = reply_number.group(1)
523 post = Post.objects.filter(id=post_id)
534 post = Post.objects.filter(id=post_id)
524 if not post.exists():
535 if not post.exists():
525 text = string.replace(text, '>>' + post_id, post_id)
536 text = string.replace(text, '>>' + post_id, post_id)
526
537
527 return text
538 return text
528
539
529
540
530 def _datetime_to_epoch(datetime):
541 def _datetime_to_epoch(datetime):
531 return int(time.mktime(timezone.localtime(
542 return int(time.mktime(timezone.localtime(
532 datetime,timezone.get_current_timezone()).timetuple())
543 datetime,timezone.get_current_timezone()).timetuple())
533 * 1000000 + datetime.microsecond)
544 * 1000000 + datetime.microsecond)
534
545
535
546
536 def _get_template_thread(thread_to_show):
547 def _get_template_thread(thread_to_show):
537 """Get template values for thread"""
548 """Get template values for thread"""
538
549
539 last_replies = thread_to_show.get_last_replies()
550 last_replies = thread_to_show.get_last_replies()
540 skipped_replies_count = thread_to_show.get_replies().count() \
551 skipped_replies_count = thread_to_show.get_replies().count() \
541 - len(last_replies) - 1
552 - len(last_replies) - 1
542 return {
553 return {
543 'thread': thread_to_show,
554 'thread': thread_to_show,
544 'op': thread_to_show.get_replies()[0],
555 'op': thread_to_show.get_replies()[0],
545 'bumpable': thread_to_show.can_bump(),
556 'bumpable': thread_to_show.can_bump(),
546 'last_replies': last_replies,
557 'last_replies': last_replies,
547 'skipped_replies': skipped_replies_count,
558 'skipped_replies': skipped_replies_count,
548 }
559 }
@@ -1,46 +1,55 b''
1 # INTRO #
1 # INTRO #
2
2
3 This project aims to create centralized forum-like discussion platform with
3 This project aims to create centralized forum-like discussion platform with
4 anonymity in mind.
4 anonymity in mind.
5
5
6 Main repository: https://bitbucket.org/neko259/neboard/
6 Main repository: https://bitbucket.org/neko259/neboard/
7
7
8 Site: http://neboard.me/
8 Site: http://neboard.me/
9
9
10 # DEPENDENCIES #
10 # DEPENDENCIES #
11
11
12 ## REQUIRED ##
12 ## REQUIRED ##
13
13
14 * pillow
14 * pillow
15 * django >= 1.6
15 * django >= 1.6
16 * django_cleanup
16 * django_cleanup
17 * django-markupfield
17 * django-markupfield
18 * markdown
18 * markdown
19 * python-markdown
19 * python-markdown
20 * django-simple-captcha
20 * django-simple-captcha
21 * line-profiler
21 * line-profiler
22
22
23 ## OPTIONAL ##
23 ## OPTIONAL ##
24
24
25 * django-debug-toolbar
25 * django-debug-toolbar
26
26
27 # INSTALLATION #
27 # INSTALLATION #
28
28
29 1. Install all dependencies over pip or system-wide
29 1. Install all dependencies over pip or system-wide
30 2. Setup a database in neboard/settings.py
30 2. Setup a database in `neboard/settings.py`
31 3. Run `./manage.py syncdb` and ensure the database was created
31 3. Run `./manage.py syncdb` and ensure the database was created
32 4. Run `./manage.py migrate boards` to apply all south migrations
32 4. Run `./manage.py migrate boards` to apply all south migrations
33
33
34 # RUNNING #
34 # RUNNING #
35
35
36 You can run the server using django default embedded webserver by running
36 You can run the server using django default embedded webserver by running
37
37
38 ./manage.py runserver <address>:<port>
38 ./manage.py runserver <address>:<port>
39
39
40 See django-admin command help for details
40 See django-admin command help for details
41
41
42 Also consider using wsgi or fcgi interfaces on production servers.
42 Also consider using wsgi or fcgi interfaces on production servers.
43
43
44 # UPGRADE #
45
46 1. Backup your project data.
47 2. Save the settings in `neboard/settings.py` and `boards/settings.py`
48 3. Copy the project contents over the old project directory
49 4. Run migrations by `./manage.py migrate boards`
50
51 You can also just clone the mercurial project and pull it to update
52
44 # CONCLUSION #
53 # CONCLUSION #
45
54
46 Enjoy our software and thank you!
55 Enjoy our software and thank you!
@@ -1,50 +1,50 b''
1 = Features =
1 = Features =
2 [DONE] Connecting tags to each other
2 [DONE] Connecting tags to each other
3 [DONE] Connect posts to the replies (in messages), get rid of the JS reply map
3 [DONE] Connect posts to the replies (in messages), get rid of the JS reply map
4 [DONE] Better django admin pages to simplify admin operations
4 [DONE] Better django admin pages to simplify admin operations
5 [DONE] Regen script to update all posts
5 [DONE] Regen script to update all posts
6 [DONE] Remove jump links from refmaps
6 [DONE] Remove jump links from refmaps
7 [DONE] Ban reasons. Split bans into 2 types "read-only" and "read
7 [DONE] Ban reasons. Split bans into 2 types "read-only" and "read
8 denied". Use second only for autoban for spam
8 denied". Use second only for autoban for spam
9 [DONE] Clean up tests and make them run ALWAYS
9 [DONE] Clean up tests and make them run ALWAYS
10 [DONE] Use transactions in tests
10 [DONE] Use transactions in tests
11 [DONE] Thread autoupdate (JS + API)
11 [DONE] Thread autoupdate (JS + API)
12 [DONE] Split up post model into post and thread,
12 [DONE] Split up post model into post and thread,
13 and move everything that is used only in 1st post to thread model.
13 and move everything that is used only in 1st post to thread model.
14 [DONE] Show board speed in the lower panel (posts per day)
14 [DONE] Show board speed in the lower panel (posts per day)
15 [DONE] Save image thumbnails size to the separate field
15
16
16 [NOT STARTED] Tree view (JS)
17 [NOT STARTED] Tree view (JS)
17 [NOT STARTED] Adding tags to images filename
18 [NOT STARTED] Adding tags to images filename
18 [NOT STARTED] Federative network for s2s communication
19 [NOT STARTED] Federative network for s2s communication
19 [NOT STARTED] XMPP gate
20 [NOT STARTED] XMPP gate
20 [NOT STARTED] Bitmessage gate
21 [NOT STARTED] Bitmessage gate
21 [NOT STARTED] Notification engine
22 [NOT STARTED] Notification engine
22 [NOT STARTED] Javascript disabling engine
23 [NOT STARTED] Javascript disabling engine
23 [NOT STARTED] Group tags by first letter in all tags list
24 [NOT STARTED] Group tags by first letter in all tags list
24 [NOT STARTED] Character counter in the post field
25 [NOT STARTED] Character counter in the post field
25 [NOT STARTED] Save image thumbnails size to the separate field
26 [NOT STARTED] Whitelist functionality. Permin autoban of an address
26 [NOT STARTED] Whitelist functionality. Permin autoban of an address
27 [NOT STARTED] Statistics module. Count views (optional, may result in bad
27 [NOT STARTED] Statistics module. Count views (optional, may result in bad
28 performance), posts per day/week/month, users (or IPs)
28 performance), posts per day/week/month, users (or IPs)
29 [NOT STARTED] Quote button next to "reply" for posts in thread to include full
29 [NOT STARTED] Quote button next to "reply" for posts in thread to include full
30 post or its part (delimited by N characters) into quote of the new post.
30 post or its part (delimited by N characters) into quote of the new post.
31 [NOT STARTED] Ban confirmation page with reason
31 [NOT STARTED] Ban confirmation page with reason
32 [NOT STARTED] Post deletion confirmation page
32 [NOT STARTED] Post deletion confirmation page
33 [NOT STARTED] Moderating page. Tags editing and adding
33 [NOT STARTED] Moderating page. Tags editing and adding
34 [NOT STARTED] Get thread graph image using pygraphviz
34 [NOT STARTED] Get thread graph image using pygraphviz
35 [NOT STARTED] Creating post via AJAX without reloading page
35 [NOT STARTED] Creating post via AJAX without reloading page
36 [NOT STARTED] Subscribing to tag via AJAX
36 [NOT STARTED] Subscribing to tag via AJAX
37 [NOT STARTED] Count posts by user not by current active posts, but by adding 1
37 [NOT STARTED] Count posts by user not by current active posts, but by adding 1
38 on evety posting
38 on evety posting
39
39
40 = Bugs =
40 = Bugs =
41 [DONE] Fix bug with creating threads from tag view
41 [DONE] Fix bug with creating threads from tag view
42 [DONE] Quote characters within quote causes quote parsing to fail
42 [DONE] Quote characters within quote causes quote parsing to fail
43
43
44 [IN PROGRESS] Replies, images, last update time in bottom panel doesn't change when
44 [IN PROGRESS] Replies, images, last update time in bottom panel doesn't change when
45 thread updates (last update changing left)
45 thread updates (last update changing left)
46
46
47 = Testing =
47 = Testing =
48 [NOT STARTED] Make tests for every view
48 [NOT STARTED] Make tests for every view
49 [NOT STARTED] Make tests for every model
49 [NOT STARTED] Make tests for every model
50 [NOT STARTED] Make tests for every form
50 [NOT STARTED] Make tests for every form
General Comments 0
You need to be logged in to leave comments. Login now