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