##// END OF EJS Templates
Merged in the 1.6 version
neko259 -
r513:4f928387 merge 1.6 default
parent child Browse files
Show More
@@ -0,0 +1,85 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 'Thread.archived'
12 db.add_column(u'boards_thread', 'archived',
13 self.gf('django.db.models.fields.BooleanField')(default=0),
14 keep_default=False)
15
16
17 def backwards(self, orm):
18 # Deleting field 'Thread.archived'
19 db.delete_column(u'boards_thread', 'archived')
20
21
22 models = {
23 'boards.ban': {
24 'Meta': {'object_name': 'Ban'},
25 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
26 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
27 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
28 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
29 },
30 'boards.post': {
31 'Meta': {'object_name': 'Post'},
32 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
33 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
34 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
35 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
36 'image_pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
37 'image_pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
38 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
39 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
40 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
41 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
42 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
43 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rfp+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
44 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
45 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
46 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Post']", 'null': 'True'}),
47 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
48 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
49 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
50 },
51 'boards.setting': {
52 'Meta': {'object_name': 'Setting'},
53 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
54 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
55 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
56 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
57 },
58 'boards.tag': {
59 'Meta': {'object_name': 'Tag'},
60 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
61 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
62 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
63 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
64 },
65 'boards.thread': {
66 'Meta': {'object_name': 'Thread'},
67 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
68 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
69 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
70 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
71 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
72 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
73 },
74 'boards.user': {
75 'Meta': {'object_name': 'User'},
76 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
77 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
78 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
79 'rank': ('django.db.models.fields.IntegerField', [], {}),
80 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
81 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
82 }
83 }
84
85 complete_apps = ['boards'] No newline at end of file
@@ -0,0 +1,25 b''
1 # INTRO #
2
3 The API is provided to query the data from a neaboard server by any client
4 application.
5
6 Tha data is returned in the json format and got by an http query.
7
8 # METHODS #
9
10 /api/threads/N/?offset=M&tag=O
11
12 Get a thread list. You will get N threads (required parameter) starting from
13 Mth one (optional parameter, default is 0) with the tag O (optional parameter,
14 threads with any tags are shown by default).
15
16 /api/tags/
17
18 Get all active tag list. Active tag is a tag that has at least 1 active thread
19 associated with it.
20
21 /api/thread/N/
22
23 Get all Nth thread post. N is an opening post ID for the thread.
24
25 In case of incorrect request you can get http error 404.
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,383 +1,392 b''
1 # SOME DESCRIPTIVE TITLE.
1 # SOME DESCRIPTIVE TITLE.
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 # This file is distributed under the same license as the PACKAGE package.
3 # This file is distributed under the same license as the PACKAGE package.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 #
5 #
6 msgid ""
6 msgid ""
7 msgstr ""
7 msgstr ""
8 "Project-Id-Version: PACKAGE VERSION\n"
8 "Project-Id-Version: PACKAGE VERSION\n"
9 "Report-Msgid-Bugs-To: \n"
9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2013-12-24 20:39+0200\n"
10 "POT-Creation-Date: 2014-01-06 23:43+0200\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 "Language: ru\n"
14 "Language: ru\n"
15 "MIME-Version: 1.0\n"
15 "MIME-Version: 1.0\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
16 "Content-Type: text/plain; charset=UTF-8\n"
17 "Content-Transfer-Encoding: 8bit\n"
17 "Content-Transfer-Encoding: 8bit\n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20
20
21 #: authors.py:5
21 #: authors.py:5
22 msgid "author"
22 msgid "author"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
24
24
25 #: authors.py:6
25 #: authors.py:6
26 msgid "developer"
26 msgid "developer"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
28
28
29 #: authors.py:7
29 #: authors.py:7
30 msgid "javascript developer"
30 msgid "javascript developer"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
32
32
33 #: authors.py:8
33 #: authors.py:8
34 msgid "designer"
34 msgid "designer"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
36
36
37 #: forms.py:72
37 #: forms.py:72
38 msgid "Title"
38 msgid "Title"
39 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
39 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
40
40
41 #: forms.py:74
41 #: forms.py:74
42 msgid "Text"
42 msgid "Text"
43 msgstr "ВСкст"
43 msgstr "ВСкст"
44
44
45 #: forms.py:75
45 #: forms.py:75
46 msgid "Image"
46 msgid "Image"
47 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
47 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
48
48
49 #: forms.py:78
49 #: forms.py:78
50 msgid "e-mail"
50 msgid "e-mail"
51 msgstr ""
51 msgstr ""
52
52
53 #: forms.py:89
53 #: forms.py:89
54 #, python-format
54 #, python-format
55 msgid "Title must have less than %s characters"
55 msgid "Title must have less than %s characters"
56 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
56 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
57
57
58 #: forms.py:98
58 #: forms.py:98
59 #, python-format
59 #, python-format
60 msgid "Text must have less than %s characters"
60 msgid "Text must have less than %s characters"
61 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
61 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
62
62
63 #: forms.py:109
63 #: forms.py:109
64 #, python-format
64 #, python-format
65 msgid "Image must be less than %s bytes"
65 msgid "Image must be less than %s bytes"
66 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
66 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
67
67
68 #: forms.py:136
68 #: forms.py:136
69 msgid "Either text or image must be entered."
69 msgid "Either text or image must be entered."
70 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
70 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
71
71
72 #: forms.py:149
72 #: forms.py:149
73 #, python-format
73 #, python-format
74 msgid "Wait %s seconds after last posting"
74 msgid "Wait %s seconds after last posting"
75 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
75 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
76
76
77 #: forms.py:163 templates/boards/post.html:61 templates/boards/tags.html:6
77 #: forms.py:163 templates/boards/tags.html:6 templates/boards/rss/post.html:10
78 #: templates/boards/rss/post.html:10
79 msgid "Tags"
78 msgid "Tags"
80 msgstr "Π’Π΅Π³ΠΈ"
79 msgstr "Π’Π΅Π³ΠΈ"
81
80
82 #: forms.py:171
81 #: forms.py:171
83 msgid "Inappropriate characters in tags."
82 msgid "Inappropriate characters in tags."
84 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
83 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
85
84
86 #: forms.py:199 forms.py:220
85 #: forms.py:199 forms.py:220
87 msgid "Captcha validation failed"
86 msgid "Captcha validation failed"
88 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
87 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
89
88
90 #: forms.py:226
89 #: forms.py:226
91 msgid "Theme"
90 msgid "Theme"
92 msgstr "Π’Π΅ΠΌΠ°"
91 msgstr "Π’Π΅ΠΌΠ°"
93
92
94 #: forms.py:231
93 #: forms.py:231
95 msgid "Enable moderation panel"
94 msgid "Enable moderation panel"
96 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
95 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
97
96
98 #: forms.py:246
97 #: forms.py:246
99 msgid "No such user found"
98 msgid "No such user found"
100 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
99 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
101
100
102 #: forms.py:260
101 #: forms.py:260
103 #, python-format
102 #, python-format
104 msgid "Wait %s minutes after last login"
103 msgid "Wait %s minutes after last login"
105 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
104 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
106
105
107 #: templates/boards/404.html:6
106 #: templates/boards/404.html:6
108 msgid "Not found"
107 msgid "Not found"
109 msgstr "НС найдСно"
108 msgstr "НС найдСно"
110
109
111 #: templates/boards/404.html:12
110 #: templates/boards/404.html:12
112 msgid "This page does not exist"
111 msgid "This page does not exist"
113 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
112 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
114
113
114 #: templates/boards/archive.html:45 templates/boards/posting_general.html:64
115 msgid "Previous page"
116 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
117
118 #: templates/boards/archive.html:75
119 msgid "Open"
120 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
121
122 #: templates/boards/archive.html:81 templates/boards/post.html:37
123 #: templates/boards/posting_general.html:103 templates/boards/thread.html:69
124 msgid "Delete"
125 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
126
127 #: templates/boards/archive.html:85 templates/boards/post.html:40
128 #: templates/boards/posting_general.html:107 templates/boards/thread.html:72
129 msgid "Ban IP"
130 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
131
132 #: templates/boards/archive.html:94 templates/boards/post.html:53
133 #: templates/boards/posting_general.html:116
134 #: templates/boards/posting_general.html:180 templates/boards/thread.html:81
135 msgid "Replies"
136 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
137
138 #: templates/boards/archive.html:103 templates/boards/posting_general.html:125
139 #: templates/boards/thread.html:138 templates/boards/thread_gallery.html:58
140 msgid "images"
141 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
142
143 #: templates/boards/archive.html:104 templates/boards/thread.html:137
144 #: templates/boards/thread_gallery.html:57
145 msgid "replies"
146 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
147
148 #: templates/boards/archive.html:129 templates/boards/posting_general.html:203
149 msgid "Next page"
150 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
151
152 #: templates/boards/archive.html:134 templates/boards/posting_general.html:208
153 msgid "No threads exist. Create the first one!"
154 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
155
156 #: templates/boards/archive.html:143 templates/boards/posting_general.html:235
157 msgid "Pages:"
158 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
159
115 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
160 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
116 msgid "Authors"
161 msgid "Authors"
117 msgstr "Авторы"
162 msgstr "Авторы"
118
163
119 #: templates/boards/authors.html:25
164 #: templates/boards/authors.html:25
120 msgid "Distributed under the"
165 msgid "Distributed under the"
121 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
166 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
122
167
123 #: templates/boards/authors.html:27
168 #: templates/boards/authors.html:27
124 msgid "license"
169 msgid "license"
125 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
170 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
126
171
127 #: templates/boards/authors.html:29
172 #: templates/boards/authors.html:29
128 msgid "Repository"
173 msgid "Repository"
129 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
174 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
130
175
131 #: templates/boards/base.html:14
176 #: templates/boards/base.html:14
132 msgid "Feed"
177 msgid "Feed"
133 msgstr "Π›Π΅Π½Ρ‚Π°"
178 msgstr "Π›Π΅Π½Ρ‚Π°"
134
179
135 #: templates/boards/base.html:31
180 #: templates/boards/base.html:31
136 msgid "All threads"
181 msgid "All threads"
137 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
182 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
138
183
139 #: templates/boards/base.html:36
184 #: templates/boards/base.html:36
140 msgid "Tag management"
185 msgid "Tag management"
141 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
186 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
142
187
143 #: templates/boards/base.html:38
188 #: templates/boards/base.html:38
144 msgid "Settings"
189 msgid "Settings"
145 msgstr "Настройки"
190 msgstr "Настройки"
146
191
147 #: templates/boards/base.html:50 templates/boards/login.html:6
192 #: templates/boards/base.html:50 templates/boards/login.html:6
148 #: templates/boards/login.html.py:21
193 #: templates/boards/login.html.py:21
149 msgid "Login"
194 msgid "Login"
150 msgstr "Π’Ρ…ΠΎΠ΄"
195 msgstr "Π’Ρ…ΠΎΠ΄"
151
196
152 #: templates/boards/base.html:52
197 #: templates/boards/base.html:51
198 msgid "Archive"
199 msgstr "Архив"
200
201 #: templates/boards/base.html:53
153 #, python-format
202 #, python-format
154 msgid "Speed: %(ppd)s posts per day"
203 msgid "Speed: %(ppd)s posts per day"
155 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
204 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
156
205
157 #: templates/boards/base.html:54
206 #: templates/boards/base.html:55
158 msgid "Up"
207 msgid "Up"
159 msgstr "Π’Π²Π΅Ρ€Ρ…"
208 msgstr "Π’Π²Π΅Ρ€Ρ…"
160
209
161 #: templates/boards/login.html:15
210 #: templates/boards/login.html:15
162 msgid "User ID"
211 msgid "User ID"
163 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
212 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
164
213
165 #: templates/boards/login.html:24
214 #: templates/boards/login.html:24
166 msgid "Insert your user id above"
215 msgid "Insert your user id above"
167 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
216 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
168
217
169 #: templates/boards/post.html:35 templates/boards/posting_general.html:103
170 #: templates/boards/thread.html:68
171 msgid "Delete"
172 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
173
174 #: templates/boards/post.html:38 templates/boards/posting_general.html:107
175 #: templates/boards/thread.html:71
176 msgid "Ban IP"
177 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
178
179 #: templates/boards/post.html:51 templates/boards/posting_general.html:116
180 #: templates/boards/posting_general.html:180 templates/boards/thread.html:80
181 msgid "Replies"
182 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
183
184 #: templates/boards/posting_general.html:64
185 msgid "Previous page"
186 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
187
188 #: templates/boards/posting_general.html:97
218 #: templates/boards/posting_general.html:97
189 msgid "Reply"
219 msgid "Reply"
190 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
220 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
191
221
192 #: templates/boards/posting_general.html:125 templates/boards/thread.html:130
193 #: templates/boards/thread_gallery.html:52
194 msgid "images"
195 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
196
197 #: templates/boards/posting_general.html:142
222 #: templates/boards/posting_general.html:142
198 #, python-format
223 #, python-format
199 msgid "Skipped %(count)s replies. Open thread to see all replies."
224 msgid "Skipped %(count)s replies. Open thread to see all replies."
200 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
225 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
201
226
202 #: templates/boards/posting_general.html:203
203 msgid "Next page"
204 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
205
206 #: templates/boards/posting_general.html:208
207 msgid "No threads exist. Create the first one!"
208 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
209
210 #: templates/boards/posting_general.html:214
227 #: templates/boards/posting_general.html:214
211 msgid "Create new thread"
228 msgid "Create new thread"
212 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
229 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
213
230
214 #: templates/boards/posting_general.html:218 templates/boards/thread.html:112
231 #: templates/boards/posting_general.html:218 templates/boards/thread.html:115
215 msgid "Post"
232 msgid "Post"
216 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
233 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
217
234
218 #: templates/boards/posting_general.html:222
235 #: templates/boards/posting_general.html:222
219 msgid "Tags must be delimited by spaces. Text or image is required."
236 msgid "Tags must be delimited by spaces. Text or image is required."
220 msgstr ""
237 msgstr ""
221 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
238 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
222
239
223 #: templates/boards/posting_general.html:225 templates/boards/thread.html:116
240 #: templates/boards/posting_general.html:225 templates/boards/thread.html:119
224 msgid "Text syntax"
241 msgid "Text syntax"
225 msgstr "Бинтаксис тСкста"
242 msgstr "Бинтаксис тСкста"
226
243
227 #: templates/boards/posting_general.html:235
228 msgid "Pages:"
229 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
230
231 #: templates/boards/settings.html:14
244 #: templates/boards/settings.html:14
232 msgid "User:"
245 msgid "User:"
233 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
246 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
234
247
235 #: templates/boards/settings.html:16
248 #: templates/boards/settings.html:16
236 msgid "You are moderator."
249 msgid "You are moderator."
237 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
250 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
238
251
239 #: templates/boards/settings.html:19
252 #: templates/boards/settings.html:19
240 msgid "Posts:"
253 msgid "Posts:"
241 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
254 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
242
255
243 #: templates/boards/settings.html:20
256 #: templates/boards/settings.html:20
244 msgid "First access:"
257 msgid "First access:"
245 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
258 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
246
259
247 #: templates/boards/settings.html:22
260 #: templates/boards/settings.html:22
248 msgid "Last access:"
261 msgid "Last access:"
249 msgstr "ПослСдний доступ: "
262 msgstr "ПослСдний доступ: "
250
263
251 #: templates/boards/settings.html:31
264 #: templates/boards/settings.html:31
252 msgid "Save"
265 msgid "Save"
253 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
266 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
254
267
255 #: templates/boards/tags.html:24
268 #: templates/boards/tags.html:24
256 msgid "threads"
269 msgid "threads"
257 msgstr "Ρ‚Π΅ΠΌ"
270 msgstr "Ρ‚Π΅ΠΌ"
258
271
259 #: templates/boards/tags.html:37
272 #: templates/boards/tags.html:37
260 msgid "No tags found."
273 msgid "No tags found."
261 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
274 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
262
275
263 #: templates/boards/thread.html:22 templates/boards/thread_gallery.html:20
276 #: templates/boards/thread.html:19 templates/boards/thread_gallery.html:20
264 msgid "Normal mode"
277 msgid "Normal mode"
265 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
278 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
266
279
267 #: templates/boards/thread.html:23 templates/boards/thread_gallery.html:21
280 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21
268 msgid "Gallery mode"
281 msgid "Gallery mode"
269 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
282 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
270
283
271 #: templates/boards/thread.html:31
284 #: templates/boards/thread.html:28
272 msgid "posts to bumplimit"
285 msgid "posts to bumplimit"
273 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
286 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
274
287
275 #: templates/boards/thread.html:106
288 #: templates/boards/thread.html:109
276 msgid "Reply to thread"
289 msgid "Reply to thread"
277 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
290 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
278
291
279 #: templates/boards/thread.html:129 templates/boards/thread_gallery.html:51
292 #: templates/boards/thread.html:139 templates/boards/thread_gallery.html:59
280 msgid "replies"
281 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
282
283 #: templates/boards/thread.html:131 templates/boards/thread_gallery.html:53
284 msgid "Last update: "
293 msgid "Last update: "
285 msgstr "ПослСднСС обновлСниС: "
294 msgstr "ПослСднСС обновлСниС: "
286
295
287 #: templates/boards/rss/post.html:5
296 #: templates/boards/rss/post.html:5
288 msgid "Post image"
297 msgid "Post image"
289 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
298 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
290
299
291 #: templates/boards/staticpages/banned.html:6
300 #: templates/boards/staticpages/banned.html:6
292 msgid "Banned"
301 msgid "Banned"
293 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
302 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
294
303
295 #: templates/boards/staticpages/banned.html:11
304 #: templates/boards/staticpages/banned.html:11
296 msgid "Your IP address has been banned. Contact the administrator"
305 msgid "Your IP address has been banned. Contact the administrator"
297 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
306 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
298
307
299 #: templates/boards/staticpages/help.html:6
308 #: templates/boards/staticpages/help.html:6
300 #: templates/boards/staticpages/help.html:10
309 #: templates/boards/staticpages/help.html:10
301 msgid "Syntax"
310 msgid "Syntax"
302 msgstr "Бинтаксис"
311 msgstr "Бинтаксис"
303
312
304 #: templates/boards/staticpages/help.html:11
313 #: templates/boards/staticpages/help.html:11
305 msgid "2 line breaks for a new line."
314 msgid "2 line breaks for a new line."
306 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
315 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
307
316
308 #: templates/boards/staticpages/help.html:12
317 #: templates/boards/staticpages/help.html:12
309 msgid "Italic text"
318 msgid "Italic text"
310 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
319 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
311
320
312 #: templates/boards/staticpages/help.html:13
321 #: templates/boards/staticpages/help.html:13
313 msgid "Bold text"
322 msgid "Bold text"
314 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
323 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
315
324
316 #: templates/boards/staticpages/help.html:14
325 #: templates/boards/staticpages/help.html:14
317 msgid "Spoiler"
326 msgid "Spoiler"
318 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
327 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
319
328
320 #: templates/boards/staticpages/help.html:17
329 #: templates/boards/staticpages/help.html:17
321 msgid "Link to a post"
330 msgid "Link to a post"
322 msgstr "Бсылка Π½Π° сообщСниС"
331 msgstr "Бсылка Π½Π° сообщСниС"
323
332
324 #: templates/boards/staticpages/help.html:18
333 #: templates/boards/staticpages/help.html:18
325 msgid "Strikethrough text"
334 msgid "Strikethrough text"
326 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
335 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
327
336
328 #: templates/boards/staticpages/help.html:11
337 #: templates/boards/staticpages/help.html:11
329 msgid "You need to new line before:"
338 msgid "You need to new line before:"
330 msgstr "ΠŸΠ΅Ρ€Π΅Π΄ этими Ρ‚Π΅Π³Π°ΠΌΠΈ Π½ΡƒΠΆΠ½Π° новая строка:"
339 msgstr "ΠŸΠ΅Ρ€Π΅Π΄ этими Ρ‚Π΅Π³Π°ΠΌΠΈ Π½ΡƒΠΆΠ½Π° новая строка:"
331
340
332 #: templates/boards/staticpages/help.html:15
341 #: templates/boards/staticpages/help.html:15
333 msgid "Comment"
342 msgid "Comment"
334 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
343 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
335
344
336 #: templates/boards/staticpages/help.html:16
345 #: templates/boards/staticpages/help.html:16
337 msgid "Quote"
346 msgid "Quote"
338 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
347 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
339
348
340 #~ msgid "Formatting"
349 #~ msgid "Formatting"
341 #~ msgstr "Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅"
350 #~ msgstr "Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅"
342
351
343 #~ msgid "quote"
352 #~ msgid "quote"
344 #~ msgstr "Ρ†ΠΈΡ‚Π°Ρ‚Π°"
353 #~ msgstr "Ρ†ΠΈΡ‚Π°Ρ‚Π°"
345
354
346 #~ msgid "italic"
355 #~ msgid "italic"
347 #~ msgstr "курсив"
356 #~ msgstr "курсив"
348
357
349 #~ msgid "bold"
358 #~ msgid "bold"
350 #~ msgstr "ΠΏΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ"
359 #~ msgstr "ΠΏΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ"
351
360
352 #~ msgid "spoiler"
361 #~ msgid "spoiler"
353 #~ msgstr "спойлСр"
362 #~ msgstr "спойлСр"
354
363
355 #~ msgid "comment"
364 #~ msgid "comment"
356 #~ msgstr "ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
365 #~ msgstr "ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
357
366
358 #~ msgid "Tag: "
367 #~ msgid "Tag: "
359 #~ msgstr "Π’Π΅Π³: "
368 #~ msgstr "Π’Π΅Π³: "
360
369
361 #~ msgid "Remove"
370 #~ msgid "Remove"
362 #~ msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
371 #~ msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
363
372
364 #~ msgid "Add"
373 #~ msgid "Add"
365 #~ msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ"
374 #~ msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ"
366
375
367 #~ msgid "Basic markdown syntax."
376 #~ msgid "Basic markdown syntax."
368 #~ msgstr "Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ синтаксис markdown."
377 #~ msgstr "Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ синтаксис markdown."
369
378
370 #~ msgid "Example: "
379 #~ msgid "Example: "
371 #~ msgstr "ΠŸΡ€ΠΈΠΌΠ΅Ρ€: "
380 #~ msgstr "ΠŸΡ€ΠΈΠΌΠ΅Ρ€: "
372
381
373 #~ msgid "tags"
382 #~ msgid "tags"
374 #~ msgstr "Ρ‚Π΅Π³ΠΎΠ²"
383 #~ msgstr "Ρ‚Π΅Π³ΠΎΠ²"
375
384
376 #~ msgid "Get!"
385 #~ msgid "Get!"
377 #~ msgstr "Π“Π΅Ρ‚!"
386 #~ msgstr "Π“Π΅Ρ‚!"
378
387
379 #~ msgid "View"
388 #~ msgid "View"
380 #~ msgstr "ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
389 #~ msgstr "ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
381
390
382 #~ msgid "gets"
391 #~ msgid "gets"
383 #~ msgstr "Π³Π΅Ρ‚ΠΎΠ²"
392 #~ msgstr "Π³Π΅Ρ‚ΠΎΠ²"
@@ -1,198 +1,198 b''
1 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
2 import markdown
2 import markdown
3 from markdown.inlinepatterns import Pattern
3 from markdown.inlinepatterns import Pattern
4 from markdown.util import etree
4 from markdown.util import etree
5 import boards
5 import boards
6
6
7 __author__ = 'neko259'
7 __author__ = 'neko259'
8
8
9
9
10 AUTOLINK_PATTERN = r'(https?://\S+)'
10 AUTOLINK_PATTERN = r'(https?://\S+)'
11 QUOTE_PATTERN = r'^(?<!>)(>[^>].+)$'
11 QUOTE_PATTERN = r'^(?<!>)(>[^>].+)$'
12 REFLINK_PATTERN = r'((>>)(\d+))'
12 REFLINK_PATTERN = r'((>>)(\d+))'
13 SPOILER_PATTERN = r'%%(.+)%%'
13 SPOILER_PATTERN = r'%%([^(%%)]+)%%'
14 COMMENT_PATTERN = r'^(//(.+))'
14 COMMENT_PATTERN = r'^(//(.+))'
15 STRIKETHROUGH_PATTERN = r'~(.+)~'
15 STRIKETHROUGH_PATTERN = r'~(.+)~'
16
16
17
17
18 class TextFormatter():
18 class TextFormatter():
19 """
19 """
20 An interface for formatter that can be used in the text format panel
20 An interface for formatter that can be used in the text format panel
21 """
21 """
22
22
23 name = ''
23 name = ''
24
24
25 # Left and right tags for the button preview
25 # Left and right tags for the button preview
26 preview_left = ''
26 preview_left = ''
27 preview_right = ''
27 preview_right = ''
28
28
29 # Left and right characters for the textarea input
29 # Left and right characters for the textarea input
30 format_left = ''
30 format_left = ''
31 format_right = ''
31 format_right = ''
32
32
33
33
34 class AutolinkPattern(Pattern):
34 class AutolinkPattern(Pattern):
35 def handleMatch(self, m):
35 def handleMatch(self, m):
36 link_element = etree.Element('a')
36 link_element = etree.Element('a')
37 href = m.group(2)
37 href = m.group(2)
38 link_element.set('href', href)
38 link_element.set('href', href)
39 link_element.text = href
39 link_element.text = href
40
40
41 return link_element
41 return link_element
42
42
43
43
44 class QuotePattern(Pattern, TextFormatter):
44 class QuotePattern(Pattern, TextFormatter):
45 name = ''
45 name = ''
46 preview_left = '<span class="quote">&gt; '
46 preview_left = '<span class="quote">&gt; '
47 preview_right = '</span>'
47 preview_right = '</span>'
48
48
49 format_left = '&gt;'
49 format_left = '&gt;'
50
50
51 def handleMatch(self, m):
51 def handleMatch(self, m):
52 quote_element = etree.Element('span')
52 quote_element = etree.Element('span')
53 quote_element.set('class', 'quote')
53 quote_element.set('class', 'quote')
54 quote_element.text = m.group(2)
54 quote_element.text = m.group(2)
55
55
56 return quote_element
56 return quote_element
57
57
58
58
59 class ReflinkPattern(Pattern):
59 class ReflinkPattern(Pattern):
60 def handleMatch(self, m):
60 def handleMatch(self, m):
61 post_id = m.group(4)
61 post_id = m.group(4)
62
62
63 posts = boards.models.Post.objects.filter(id=post_id)
63 posts = boards.models.Post.objects.filter(id=post_id)
64 if posts.count() > 0:
64 if posts.count() > 0:
65 ref_element = etree.Element('a')
65 ref_element = etree.Element('a')
66
66
67 post = posts[0]
67 post = posts[0]
68 if not post.is_opening():
68 if not post.is_opening():
69 link = reverse(boards.views.thread, kwargs={
69 link = reverse(boards.views.thread, kwargs={
70 'post_id': post.thread_new.get_opening_post().id})\
70 'post_id': post.thread_new.get_opening_post().id})\
71 + '#' + post_id
71 + '#' + post_id
72 else:
72 else:
73 link = reverse(boards.views.thread, kwargs={'post_id': post_id})
73 link = reverse(boards.views.thread, kwargs={'post_id': post_id})
74
74
75 ref_element.set('href', link)
75 ref_element.set('href', link)
76 ref_element.text = m.group(2)
76 ref_element.text = m.group(2)
77
77
78 return ref_element
78 return ref_element
79
79
80
80
81 class SpoilerPattern(Pattern, TextFormatter):
81 class SpoilerPattern(Pattern, TextFormatter):
82 name = 's'
82 name = 's'
83 preview_left = '<span class="spoiler">'
83 preview_left = '<span class="spoiler">'
84 preview_right = '</span>'
84 preview_right = '</span>'
85
85
86 format_left = '%%'
86 format_left = '%%'
87 format_right = '%%'
87 format_right = '%%'
88
88
89 def handleMatch(self, m):
89 def handleMatch(self, m):
90 quote_element = etree.Element('span')
90 quote_element = etree.Element('span')
91 quote_element.set('class', 'spoiler')
91 quote_element.set('class', 'spoiler')
92 quote_element.text = m.group(2)
92 quote_element.text = m.group(2)
93
93
94 return quote_element
94 return quote_element
95
95
96
96
97 class CommentPattern(Pattern, TextFormatter):
97 class CommentPattern(Pattern, TextFormatter):
98 name = ''
98 name = ''
99 preview_left = '<span class="comment">// '
99 preview_left = '<span class="comment">// '
100 preview_right = '</span>'
100 preview_right = '</span>'
101
101
102 format_left = '//'
102 format_left = '//'
103
103
104 def handleMatch(self, m):
104 def handleMatch(self, m):
105 quote_element = etree.Element('span')
105 quote_element = etree.Element('span')
106 quote_element.set('class', 'comment')
106 quote_element.set('class', 'comment')
107 quote_element.text = '//' + m.group(3)
107 quote_element.text = '//' + m.group(3)
108
108
109 return quote_element
109 return quote_element
110
110
111
111
112 class StrikeThroughPattern(Pattern, TextFormatter):
112 class StrikeThroughPattern(Pattern, TextFormatter):
113 name = 's'
113 name = 's'
114 preview_left = '<span class="strikethrough">'
114 preview_left = '<span class="strikethrough">'
115 preview_right = '</span>'
115 preview_right = '</span>'
116
116
117 format_left = '~'
117 format_left = '~'
118 format_right = '~'
118 format_right = '~'
119
119
120 def handleMatch(self, m):
120 def handleMatch(self, m):
121 quote_element = etree.Element('span')
121 quote_element = etree.Element('span')
122 quote_element.set('class', 'strikethrough')
122 quote_element.set('class', 'strikethrough')
123 quote_element.text = m.group(2)
123 quote_element.text = m.group(2)
124
124
125 return quote_element
125 return quote_element
126
126
127
127
128 class ItalicPattern(TextFormatter):
128 class ItalicPattern(TextFormatter):
129 name = 'i'
129 name = 'i'
130 preview_left = '<i>'
130 preview_left = '<i>'
131 preview_right = '</i>'
131 preview_right = '</i>'
132
132
133 format_left = '_'
133 format_left = '_'
134 format_right = '_'
134 format_right = '_'
135
135
136
136
137 class BoldPattern(TextFormatter):
137 class BoldPattern(TextFormatter):
138 name = 'b'
138 name = 'b'
139 preview_left = '<b>'
139 preview_left = '<b>'
140 preview_right = '</b>'
140 preview_right = '</b>'
141
141
142 format_left = '__'
142 format_left = '__'
143 format_right = '__'
143 format_right = '__'
144
144
145
145
146 class CodePattern(TextFormatter):
146 class CodePattern(TextFormatter):
147 name = 'code'
147 name = 'code'
148 preview_left = '<code>'
148 preview_left = '<code>'
149 preview_right = '</code>'
149 preview_right = '</code>'
150
150
151 format_left = ' '
151 format_left = ' '
152
152
153
153
154 class NeboardMarkdown(markdown.Extension):
154 class NeboardMarkdown(markdown.Extension):
155 def extendMarkdown(self, md, md_globals):
155 def extendMarkdown(self, md, md_globals):
156 self._add_neboard_patterns(md)
156 self._add_neboard_patterns(md)
157 self._delete_patterns(md)
157 self._delete_patterns(md)
158
158
159 def _delete_patterns(self, md):
159 def _delete_patterns(self, md):
160 del md.parser.blockprocessors['quote']
160 del md.parser.blockprocessors['quote']
161
161
162 del md.inlinePatterns['image_link']
162 del md.inlinePatterns['image_link']
163 del md.inlinePatterns['image_reference']
163 del md.inlinePatterns['image_reference']
164
164
165 def _add_neboard_patterns(self, md):
165 def _add_neboard_patterns(self, md):
166 autolink = AutolinkPattern(AUTOLINK_PATTERN, md)
166 autolink = AutolinkPattern(AUTOLINK_PATTERN, md)
167 quote = QuotePattern(QUOTE_PATTERN, md)
167 quote = QuotePattern(QUOTE_PATTERN, md)
168 reflink = ReflinkPattern(REFLINK_PATTERN, md)
168 reflink = ReflinkPattern(REFLINK_PATTERN, md)
169 spoiler = SpoilerPattern(SPOILER_PATTERN, md)
169 spoiler = SpoilerPattern(SPOILER_PATTERN, md)
170 comment = CommentPattern(COMMENT_PATTERN, md)
170 comment = CommentPattern(COMMENT_PATTERN, md)
171 strikethrough = StrikeThroughPattern(STRIKETHROUGH_PATTERN, md)
171 strikethrough = StrikeThroughPattern(STRIKETHROUGH_PATTERN, md)
172
172
173 md.inlinePatterns[u'autolink_ext'] = autolink
173 md.inlinePatterns[u'autolink_ext'] = autolink
174 md.inlinePatterns[u'spoiler'] = spoiler
174 md.inlinePatterns[u'spoiler'] = spoiler
175 md.inlinePatterns[u'strikethrough'] = strikethrough
175 md.inlinePatterns[u'strikethrough'] = strikethrough
176 md.inlinePatterns[u'comment'] = comment
176 md.inlinePatterns[u'comment'] = comment
177 md.inlinePatterns[u'reflink'] = reflink
177 md.inlinePatterns[u'reflink'] = reflink
178 md.inlinePatterns[u'quote'] = quote
178 md.inlinePatterns[u'quote'] = quote
179
179
180
180
181 def make_extension(configs=None):
181 def make_extension(configs=None):
182 return NeboardMarkdown(configs=configs)
182 return NeboardMarkdown(configs=configs)
183
183
184 neboard_extension = make_extension()
184 neboard_extension = make_extension()
185
185
186
186
187 def markdown_extended(markup):
187 def markdown_extended(markup):
188 return markdown.markdown(markup, [neboard_extension], safe_mode=True)
188 return markdown.markdown(markup, [neboard_extension], safe_mode=True)
189
189
190 formatters = [
190 formatters = [
191 QuotePattern,
191 QuotePattern,
192 SpoilerPattern,
192 SpoilerPattern,
193 ItalicPattern,
193 ItalicPattern,
194 BoldPattern,
194 BoldPattern,
195 CommentPattern,
195 CommentPattern,
196 StrikeThroughPattern,
196 StrikeThroughPattern,
197 CodePattern,
197 CodePattern,
198 ]
198 ]
@@ -1,385 +1,371 b''
1 from datetime import datetime, timedelta
1 from datetime import datetime, timedelta
2 from datetime import time as dtime
2 from datetime import time as dtime
3 import os
3 import os
4 from random import random
4 from random import random
5 import time
5 import time
6 import math
6 import math
7 import re
7 import re
8 from django.core.cache import cache
8 from django.core.cache import cache
9 from django.core.paginator import Paginator
9
10
10 from django.db import models
11 from django.db import models
11 from django.http import Http404
12 from django.http import Http404
12 from django.utils import timezone
13 from django.utils import timezone
13 from markupfield.fields import MarkupField
14 from markupfield.fields import MarkupField
14
15
15 from neboard import settings
16 from neboard import settings
16 from boards import thumbs
17 from boards import thumbs
17
18
19 MAX_TITLE_LENGTH = 50
20
18 APP_LABEL_BOARDS = 'boards'
21 APP_LABEL_BOARDS = 'boards'
19
22
20 CACHE_KEY_PPD = 'ppd'
23 CACHE_KEY_PPD = 'ppd'
21
24
22 POSTS_PER_DAY_RANGE = range(7)
25 POSTS_PER_DAY_RANGE = range(7)
23
26
24 BAN_REASON_AUTO = 'Auto'
27 BAN_REASON_AUTO = 'Auto'
25
28
26 IMAGE_THUMB_SIZE = (200, 150)
29 IMAGE_THUMB_SIZE = (200, 150)
27
30
28 TITLE_MAX_LENGTH = 50
31 TITLE_MAX_LENGTH = 50
29
32
30 DEFAULT_MARKUP_TYPE = 'markdown'
33 DEFAULT_MARKUP_TYPE = 'markdown'
31
34
32 NO_PARENT = -1
35 NO_PARENT = -1
33 NO_IP = '0.0.0.0'
36 NO_IP = '0.0.0.0'
34 UNKNOWN_UA = ''
37 UNKNOWN_UA = ''
35 ALL_PAGES = -1
38 ALL_PAGES = -1
36 IMAGES_DIRECTORY = 'images/'
39 IMAGES_DIRECTORY = 'images/'
37 FILE_EXTENSION_DELIMITER = '.'
40 FILE_EXTENSION_DELIMITER = '.'
38
41
39 SETTING_MODERATE = "moderate"
42 SETTING_MODERATE = "moderate"
40
43
41 REGEX_REPLY = re.compile('>>(\d+)')
44 REGEX_REPLY = re.compile('>>(\d+)')
42
45
43
46
44 class PostManager(models.Manager):
47 class PostManager(models.Manager):
45
48
46 def create_post(self, title, text, image=None, thread=None,
49 def create_post(self, title, text, image=None, thread=None,
47 ip=NO_IP, tags=None, user=None):
50 ip=NO_IP, tags=None, user=None):
48 """
51 """
49 Create new post
52 Create new post
50 """
53 """
51
54
52 posting_time = timezone.now()
55 posting_time = timezone.now()
53 if not thread:
56 if not thread:
54 thread = Thread.objects.create(bump_time=posting_time,
57 thread = Thread.objects.create(bump_time=posting_time,
55 last_edit_time=posting_time)
58 last_edit_time=posting_time)
56 else:
59 else:
57 thread.bump()
60 thread.bump()
58 thread.last_edit_time = posting_time
61 thread.last_edit_time = posting_time
59 thread.save()
62 thread.save()
60
63
61 post = self.create(title=title,
64 post = self.create(title=title,
62 text=text,
65 text=text,
63 pub_time=posting_time,
66 pub_time=posting_time,
64 thread_new=thread,
67 thread_new=thread,
65 image=image,
68 image=image,
66 poster_ip=ip,
69 poster_ip=ip,
67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at last!
70 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
71 # last!
68 last_edit_time=posting_time,
72 last_edit_time=posting_time,
69 user=user)
73 user=user)
70
74
71 thread.replies.add(post)
75 thread.replies.add(post)
72 if tags:
76 if tags:
73 linked_tags = []
77 linked_tags = []
74 for tag in tags:
78 for tag in tags:
75 tag_linked_tags = tag.get_linked_tags()
79 tag_linked_tags = tag.get_linked_tags()
76 if len(tag_linked_tags) > 0:
80 if len(tag_linked_tags) > 0:
77 linked_tags.extend(tag_linked_tags)
81 linked_tags.extend(tag_linked_tags)
78
82
79 tags.extend(linked_tags)
83 tags.extend(linked_tags)
80 map(thread.add_tag, tags)
84 map(thread.add_tag, tags)
81
85
82 self._delete_old_threads()
86 self._delete_old_threads()
83 self.connect_replies(post)
87 self.connect_replies(post)
84
88
85 return post
89 return post
86
90
87 def delete_post(self, post):
91 def delete_post(self, post):
88 """
92 """
89 Delete post and update or delete its thread
93 Delete post and update or delete its thread
90 """
94 """
91
95
92 thread = post.thread_new
96 thread = post.thread_new
93
97
94 if thread.get_opening_post() == self:
98 if thread.get_opening_post() == self:
95 thread.replies.delete()
99 thread.replies.delete()
96
100
97 thread.delete()
101 thread.delete()
98 else:
102 else:
99 thread.last_edit_time = timezone.now()
103 thread.last_edit_time = timezone.now()
100 thread.save()
104 thread.save()
101
105
102 post.delete()
106 post.delete()
103
107
104 def delete_posts_by_ip(self, ip):
108 def delete_posts_by_ip(self, ip):
105 """
109 """
106 Delete all posts of the author with same IP
110 Delete all posts of the author with same IP
107 """
111 """
108
112
109 posts = self.filter(poster_ip=ip)
113 posts = self.filter(poster_ip=ip)
110 map(self.delete_post, posts)
114 map(self.delete_post, posts)
111
115
112 # TODO Move this method to thread manager
116 # TODO Move this method to thread manager
113 def get_threads(self, tag=None, page=ALL_PAGES,
117 def get_threads(self, tag=None, page=ALL_PAGES,
114 order_by='-bump_time'):
118 order_by='-bump_time', archived=False):
115 if tag:
119 if tag:
116 threads = tag.threads
120 threads = tag.threads
117
121
118 if not threads.exists():
122 if not threads.exists():
119 raise Http404
123 raise Http404
120 else:
124 else:
121 threads = Thread.objects.all()
125 threads = Thread.objects.all()
122
126
123 threads = threads.order_by(order_by)
127 threads = threads.filter(archived=archived).order_by(order_by)
124
128
125 if page != ALL_PAGES:
129 if page != ALL_PAGES:
126 thread_count = threads.count()
130 threads = Paginator(threads, settings.THREADS_PER_PAGE).page(
127
131 page).object_list
128 if page < self._get_page_count(thread_count):
129 start_thread = page * settings.THREADS_PER_PAGE
130 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
131 thread_count)
132 threads = threads[start_thread:end_thread]
133
132
134 return threads
133 return threads
135
134
136 # TODO Move this method to thread manager
135 # TODO Move this method to thread manager
137 def get_thread_page_count(self, tag=None):
138 if tag:
139 threads = Thread.objects.filter(tags=tag)
140 else:
141 threads = Thread.objects.all()
142
143 return self._get_page_count(threads.count())
144
145 # TODO Move this method to thread manager
146 def _delete_old_threads(self):
136 def _delete_old_threads(self):
147 """
137 """
148 Preserves maximum thread count. If there are too many threads,
138 Preserves maximum thread count. If there are too many threads,
149 delete the old ones.
139 archive the old ones.
150 """
140 """
151
141
152 # TODO Move old threads to the archive instead of deleting them.
153 # Maybe make some 'old' field in the model to indicate the thread
154 # must not be shown and be able for replying.
155
156 threads = self.get_threads()
142 threads = self.get_threads()
157 thread_count = threads.count()
143 thread_count = threads.count()
158
144
159 if thread_count > settings.MAX_THREAD_COUNT:
145 if thread_count > settings.MAX_THREAD_COUNT:
160 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
146 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
161 old_threads = threads[thread_count - num_threads_to_delete:]
147 old_threads = threads[thread_count - num_threads_to_delete:]
162
148
163 map(Thread.delete_with_posts, old_threads)
149 for thread in old_threads:
150 thread.archived = True
151 thread.last_edit_time = timezone.now()
152 thread.save()
164
153
165 def connect_replies(self, post):
154 def connect_replies(self, post):
166 """
155 """
167 Connect replies to a post to show them as a reflink map
156 Connect replies to a post to show them as a reflink map
168 """
157 """
169
158
170 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
159 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
171 post_id = reply_number.group(1)
160 post_id = reply_number.group(1)
172 ref_post = self.filter(id=post_id)
161 ref_post = self.filter(id=post_id)
173 if ref_post.count() > 0:
162 if ref_post.count() > 0:
174 referenced_post = ref_post[0]
163 referenced_post = ref_post[0]
175 referenced_post.referenced_posts.add(post)
164 referenced_post.referenced_posts.add(post)
176 referenced_post.last_edit_time = post.pub_time
165 referenced_post.last_edit_time = post.pub_time
177 referenced_post.save()
166 referenced_post.save()
178
167
179 def _get_page_count(self, thread_count):
180 """
181 Get number of pages that will be needed for all threads
182 """
183
184 return int(math.ceil(thread_count / float(settings.THREADS_PER_PAGE)))
185
186 def get_posts_per_day(self):
168 def get_posts_per_day(self):
187 """
169 """
188 Get average count of posts per day for the last 7 days
170 Get average count of posts per day for the last 7 days
189 """
171 """
190
172
191 today = datetime.now().date()
173 today = datetime.now().date()
192 ppd = cache.get(CACHE_KEY_PPD + str(today))
174 ppd = cache.get(CACHE_KEY_PPD + str(today))
193 if ppd:
175 if ppd:
194 return ppd
176 return ppd
195
177
196 posts_per_days = []
178 posts_per_days = []
197 for i in POSTS_PER_DAY_RANGE:
179 for i in POSTS_PER_DAY_RANGE:
198 day_end = today - timedelta(i + 1)
180 day_end = today - timedelta(i + 1)
199 day_start = today - timedelta(i + 2)
181 day_start = today - timedelta(i + 2)
200
182
201 day_time_start = timezone.make_aware(datetime.combine(day_start,
183 day_time_start = timezone.make_aware(datetime.combine(day_start,
202 dtime()), timezone.get_current_timezone())
184 dtime()), timezone.get_current_timezone())
203 day_time_end = timezone.make_aware(datetime.combine(day_end,
185 day_time_end = timezone.make_aware(datetime.combine(day_end,
204 dtime()), timezone.get_current_timezone())
186 dtime()), timezone.get_current_timezone())
205
187
206 posts_per_days.append(float(self.filter(
188 posts_per_days.append(float(self.filter(
207 pub_time__lte=day_time_end,
189 pub_time__lte=day_time_end,
208 pub_time__gte=day_time_start).count()))
190 pub_time__gte=day_time_start).count()))
209
191
210 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
192 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
211 len(posts_per_days))
193 len(posts_per_days))
212 cache.set(CACHE_KEY_PPD, ppd)
194 cache.set(CACHE_KEY_PPD, ppd)
213 return ppd
195 return ppd
214
196
215
197
216 class Post(models.Model):
198 class Post(models.Model):
217 """A post is a message."""
199 """A post is a message."""
218
200
219 objects = PostManager()
201 objects = PostManager()
220
202
221 class Meta:
203 class Meta:
222 app_label = APP_LABEL_BOARDS
204 app_label = APP_LABEL_BOARDS
223
205
224 # TODO Save original file name to some field
206 # TODO Save original file name to some field
225 def _update_image_filename(self, filename):
207 def _update_image_filename(self, filename):
226 """Get unique image filename"""
208 """Get unique image filename"""
227
209
228 path = IMAGES_DIRECTORY
210 path = IMAGES_DIRECTORY
229 new_name = str(int(time.mktime(time.gmtime())))
211 new_name = str(int(time.mktime(time.gmtime())))
230 new_name += str(int(random() * 1000))
212 new_name += str(int(random() * 1000))
231 new_name += FILE_EXTENSION_DELIMITER
213 new_name += FILE_EXTENSION_DELIMITER
232 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
214 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
233
215
234 return os.path.join(path, new_name)
216 return os.path.join(path, new_name)
235
217
236 title = models.CharField(max_length=TITLE_MAX_LENGTH)
218 title = models.CharField(max_length=TITLE_MAX_LENGTH)
237 pub_time = models.DateTimeField()
219 pub_time = models.DateTimeField()
238 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
220 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
239 escape_html=False)
221 escape_html=False)
240
222
241 image_width = models.IntegerField(default=0)
223 image_width = models.IntegerField(default=0)
242 image_height = models.IntegerField(default=0)
224 image_height = models.IntegerField(default=0)
243
225
244 image_pre_width = models.IntegerField(default=0)
226 image_pre_width = models.IntegerField(default=0)
245 image_pre_height = models.IntegerField(default=0)
227 image_pre_height = models.IntegerField(default=0)
246
228
247 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
229 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
248 blank=True, sizes=(IMAGE_THUMB_SIZE,),
230 blank=True, sizes=(IMAGE_THUMB_SIZE,),
249 width_field='image_width',
231 width_field='image_width',
250 height_field='image_height',
232 height_field='image_height',
251 preview_width_field='image_pre_width',
233 preview_width_field='image_pre_width',
252 preview_height_field='image_pre_height')
234 preview_height_field='image_pre_height')
253
235
254 poster_ip = models.GenericIPAddressField()
236 poster_ip = models.GenericIPAddressField()
255 poster_user_agent = models.TextField()
237 poster_user_agent = models.TextField()
256
238
257 thread = models.ForeignKey('Post', null=True, default=None)
239 thread = models.ForeignKey('Post', null=True, default=None)
258 thread_new = models.ForeignKey('Thread', null=True, default=None)
240 thread_new = models.ForeignKey('Thread', null=True, default=None)
259 last_edit_time = models.DateTimeField()
241 last_edit_time = models.DateTimeField()
260 user = models.ForeignKey('User', null=True, default=None)
242 user = models.ForeignKey('User', null=True, default=None)
261
243
262 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
244 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
263 null=True,
245 null=True,
264 blank=True, related_name='rfp+')
246 blank=True, related_name='rfp+')
265
247
266 def __unicode__(self):
248 def __unicode__(self):
267 return '#' + str(self.id) + ' ' + self.title + ' (' + \
249 return '#' + str(self.id) + ' ' + self.title + ' (' + \
268 self.text.raw[:50] + ')'
250 self.text.raw[:50] + ')'
269
251
270 def get_title(self):
252 def get_title(self):
271 title = self.title
253 title = self.title
272 if len(title) == 0:
254 if len(title) == 0:
273 title = self.text.raw[:20]
255 title = self.text.rendered[:MAX_TITLE_LENGTH]
274
256
275 return title
257 return title
276
258
277 def get_sorted_referenced_posts(self):
259 def get_sorted_referenced_posts(self):
278 return self.referenced_posts.order_by('id')
260 return self.referenced_posts.order_by('id')
279
261
280 def is_referenced(self):
262 def is_referenced(self):
281 return self.referenced_posts.all().exists()
263 return self.referenced_posts.all().exists()
282
264
283 def is_opening(self):
265 def is_opening(self):
284 return self.thread_new.get_replies()[0] == self
266 return self.thread_new.get_replies()[0] == self
285
267
286
268
287 class Thread(models.Model):
269 class Thread(models.Model):
288
270
289 class Meta:
271 class Meta:
290 app_label = APP_LABEL_BOARDS
272 app_label = APP_LABEL_BOARDS
291
273
292 tags = models.ManyToManyField('Tag')
274 tags = models.ManyToManyField('Tag')
293 bump_time = models.DateTimeField()
275 bump_time = models.DateTimeField()
294 last_edit_time = models.DateTimeField()
276 last_edit_time = models.DateTimeField()
295 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
277 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
296 blank=True, related_name='tre+')
278 blank=True, related_name='tre+')
279 archived = models.BooleanField(default=False)
297
280
298 def get_tags(self):
281 def get_tags(self):
299 """
282 """
300 Get a sorted tag list
283 Get a sorted tag list
301 """
284 """
302
285
303 return self.tags.order_by('name')
286 return self.tags.order_by('name')
304
287
305 def bump(self):
288 def bump(self):
306 """
289 """
307 Bump (move to up) thread
290 Bump (move to up) thread
308 """
291 """
309
292
310 if self.can_bump():
293 if self.can_bump():
311 self.bump_time = timezone.now()
294 self.bump_time = timezone.now()
312
295
313 def get_reply_count(self):
296 def get_reply_count(self):
314 return self.replies.count()
297 return self.replies.count()
315
298
316 def get_images_count(self):
299 def get_images_count(self):
317 return self.replies.filter(image_width__gt=0).count()
300 return self.replies.filter(image_width__gt=0).count()
318
301
319 def can_bump(self):
302 def can_bump(self):
320 """
303 """
321 Check if the thread can be bumped by replying
304 Check if the thread can be bumped by replying
322 """
305 """
323
306
307 if self.archived:
308 return False
309
324 post_count = self.get_reply_count()
310 post_count = self.get_reply_count()
325
311
326 return post_count < settings.MAX_POSTS_PER_THREAD
312 return post_count < settings.MAX_POSTS_PER_THREAD
327
313
328 def delete_with_posts(self):
314 def delete_with_posts(self):
329 """
315 """
330 Completely delete thread and all its posts
316 Completely delete thread and all its posts
331 """
317 """
332
318
333 if self.replies.count() > 0:
319 if self.replies.count() > 0:
334 self.replies.all().delete()
320 self.replies.all().delete()
335
321
336 self.delete()
322 self.delete()
337
323
338 def get_last_replies(self):
324 def get_last_replies(self):
339 """
325 """
340 Get last replies, not including opening post
326 Get last replies, not including opening post
341 """
327 """
342
328
343 if settings.LAST_REPLIES_COUNT > 0:
329 if settings.LAST_REPLIES_COUNT > 0:
344 reply_count = self.get_reply_count()
330 reply_count = self.get_reply_count()
345
331
346 if reply_count > 0:
332 if reply_count > 0:
347 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
333 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
348 reply_count - 1)
334 reply_count - 1)
349 last_replies = self.replies.all().order_by('pub_time')[
335 last_replies = self.replies.all().order_by('pub_time')[
350 reply_count - reply_count_to_show:]
336 reply_count - reply_count_to_show:]
351
337
352 return last_replies
338 return last_replies
353
339
354 def get_replies(self):
340 def get_replies(self):
355 """
341 """
356 Get sorted thread posts
342 Get sorted thread posts
357 """
343 """
358
344
359 return self.replies.all().order_by('pub_time')
345 return self.replies.all().order_by('pub_time')
360
346
361 def add_tag(self, tag):
347 def add_tag(self, tag):
362 """
348 """
363 Connect thread to a tag and tag to a thread
349 Connect thread to a tag and tag to a thread
364 """
350 """
365
351
366 self.tags.add(tag)
352 self.tags.add(tag)
367 tag.threads.add(self)
353 tag.threads.add(self)
368
354
369 def get_opening_post(self):
355 def get_opening_post(self):
370 """
356 """
371 Get first post of the thread
357 Get first post of the thread
372 """
358 """
373
359
374 return self.get_replies()[0]
360 return self.get_replies()[0]
375
361
376 def __unicode__(self):
362 def __unicode__(self):
377 return str(self.get_replies()[0].id)
363 return str(self.get_replies()[0].id)
378
364
379 def get_pub_time(self):
365 def get_pub_time(self):
380 """
366 """
381 Thread does not have its own pub time, so we need to get it from
367 Thread does not have its own pub time, so we need to get it from
382 the opening post
368 the opening post
383 """
369 """
384
370
385 return self.get_opening_post().pub_time
371 return self.get_opening_post().pub_time
@@ -1,84 +1,91 b''
1 from boards.models import Thread
1 from boards.models import Thread
2 from django.db import models
2 from django.db import models
3 from django.db.models import Count
3 from django.db.models import Count
4
4
5 __author__ = 'neko259'
5 __author__ = 'neko259'
6
6
7 TAG_FONT_MULTIPLIER = 0.1
7 TAG_FONT_MULTIPLIER = 0.1
8 MAX_TAG_FONT = 10
8 MAX_TAG_FONT = 10
9 OPENING_POST_POPULARITY_WEIGHT = 2
9
10 OPENING_POST_POPULARITY = 0.5
11 ARCHIVE_POPULARITY = 0.01
12 REPLY_POPULARITY = 0.1
10
13
11
14
12 class TagManager(models.Manager):
15 class TagManager(models.Manager):
13
16
14 def get_not_empty_tags(self):
17 def get_not_empty_tags(self):
15 tags = self.annotate(Count('threads')) \
18 tags = self.annotate(Count('threads')) \
16 .filter(threads__count__gt=0).order_by('name')
19 .filter(threads__count__gt=0).order_by('name')
17
20
18 return tags
21 return tags
19
22
20
23
21 class Tag(models.Model):
24 class Tag(models.Model):
22 """
25 """
23 A tag is a text node assigned to the thread. The tag serves as a board
26 A tag is a text node assigned to the thread. The tag serves as a board
24 section. There can be multiple tags for each thread
27 section. There can be multiple tags for each thread
25 """
28 """
26
29
27 objects = TagManager()
30 objects = TagManager()
28
31
29 class Meta:
32 class Meta:
30 app_label = 'boards'
33 app_label = 'boards'
31
34
32 name = models.CharField(max_length=100)
35 name = models.CharField(max_length=100)
33 threads = models.ManyToManyField(Thread, null=True,
36 threads = models.ManyToManyField(Thread, null=True,
34 blank=True, related_name='tag+')
37 blank=True, related_name='tag+')
35 linked = models.ForeignKey('Tag', null=True, blank=True)
38 linked = models.ForeignKey('Tag', null=True, blank=True)
36
39
37 def __unicode__(self):
40 def __unicode__(self):
38 return self.name
41 return self.name
39
42
40 def is_empty(self):
43 def is_empty(self):
41 return self.get_post_count() == 0
44 return self.get_post_count() == 0
42
45
43 def get_post_count(self):
46 def get_post_count(self):
44 return self.threads.count()
47 return self.threads.count()
45
48
46 # TODO Reenable this method after migration
49 def get_popularity(self):
47 # def get_popularity(self):
50 popularity = 0.0
48 # posts_with_tag = Thread.objects.get_threads(tag=self)
51
49 # reply_count = 0
52 for thread in self.threads.all():
50 # for post in posts_with_tag:
53 reply_count = thread.get_reply_count()
51 # reply_count += post.get_reply_count()
54
52 # reply_count += OPENING_POST_POPULARITY_WEIGHT
55 if thread.archived:
53 #
56 popularity += ARCHIVE_POPULARITY * reply_count
54 # return reply_count
57 else:
58 popularity += REPLY_POPULARITY * reply_count
59 popularity += OPENING_POST_POPULARITY
60
61 return popularity
55
62
56 def get_linked_tags(self):
63 def get_linked_tags(self):
57 tag_list = []
64 tag_list = []
58 self.get_linked_tags_list(tag_list)
65 self.get_linked_tags_list(tag_list)
59
66
60 return tag_list
67 return tag_list
61
68
62 def get_linked_tags_list(self, tag_list=[]):
69 def get_linked_tags_list(self, tag_list=[]):
63 """
70 """
64 Returns the list of tags linked to current. The list can be got
71 Returns the list of tags linked to current. The list can be got
65 through returned value or tag_list parameter
72 through returned value or tag_list parameter
66 """
73 """
67
74
68 linked_tag = self.linked
75 linked_tag = self.linked
69
76
70 if linked_tag and not (linked_tag in tag_list):
77 if linked_tag and not (linked_tag in tag_list):
71 tag_list.append(linked_tag)
78 tag_list.append(linked_tag)
72
79
73 linked_tag.get_linked_tags_list(tag_list)
80 linked_tag.get_linked_tags_list(tag_list)
74
81
75 def get_font_value(self):
82 def get_font_value(self):
76 """Get tag font value to differ most popular tags in the list"""
83 """Get tag font value to differ most popular tags in the list"""
77
84
78 post_count = self.get_post_count()
85 popularity = self.get_popularity()
79 if post_count > MAX_TAG_FONT:
86 if popularity > MAX_TAG_FONT:
80 post_count = MAX_TAG_FONT
87 popularity = MAX_TAG_FONT
81
88
82 font_value = str(1 + (post_count - 1) * TAG_FONT_MULTIPLIER)
89 font_value = str(1 + (popularity - 1) * TAG_FONT_MULTIPLIER)
83
90
84 return font_value No newline at end of file
91 return font_value
@@ -1,383 +1,400 b''
1 html {
1 html {
2 background: #555;
2 background: #555;
3 color: #ffffff;
3 color: #ffffff;
4 }
4 }
5
5
6 #admin_panel {
6 #admin_panel {
7 background: #FF0000;
7 background: #FF0000;
8 color: #00FF00
8 color: #00FF00
9 }
9 }
10
10
11 .input_field {
11 .input_field {
12
12
13 }
13 }
14
14
15 .input_field_name {
15 .input_field_name {
16
16
17 }
17 }
18
18
19 .input_field_error {
19 .input_field_error {
20 color: #FF0000;
20 color: #FF0000;
21 }
21 }
22
22
23 .title {
23 .title {
24 font-weight: bold;
24 font-weight: bold;
25 color: #ffcc00;
25 color: #ffcc00;
26 font-size: 2ex;
26 font-size: 2ex;
27 }
27 }
28
28
29 .link, a {
29 .link, a {
30 color: #afdcec;
30 color: #afdcec;
31 }
31 }
32
32
33 .block {
33 .block {
34 display: inline-block;
34 display: inline-block;
35 vertical-align: top;
35 vertical-align: top;
36 }
36 }
37
37
38 .tag {
38 .tag {
39 color: #b4cfec;
39 color: #b4cfec;
40 }
40 }
41
41
42 .post_id {
42 .post_id {
43 color: #fff380;
43 color: #fff380;
44 }
44 }
45
45
46 .post, .dead_post, #posts-table {
46 .post, .dead_post, .archive_post, #posts-table {
47 background: #333;
47 background: #333;
48 margin: 5px;
48 margin: 5px;
49 padding: 10px;
49 padding: 10px;
50 border: solid 1px #888;
50 border: solid 1px #888;
51 clear: left;
51 clear: left;
52 word-wrap: break-word;
52 word-wrap: break-word;
53 }
53 }
54
54
55 .metadata {
55 .metadata {
56 padding-top: 5px;
56 padding-top: 5px;
57 margin-top: 10px;
57 margin-top: 10px;
58 border-top: solid 1px #666;
58 border-top: solid 1px #666;
59 color: #ddd;
59 color: #ddd;
60 }
60 }
61
61
62 .navigation_panel, .tag_info {
62 .navigation_panel, .tag_info {
63 background: #444;
63 background: #444;
64 margin: 5px;
64 margin: 5px;
65 padding: 10px;
65 padding: 10px;
66 border: solid 1px #888;
66 border: solid 1px #888;
67 color: #eee;
67 color: #eee;
68 }
68 }
69
69
70 .navigation_panel .link {
70 .navigation_panel .link {
71 border-right: 1px solid #fff;
71 border-right: 1px solid #fff;
72 font-weight: bold;
72 font-weight: bold;
73 margin-right: 1ex;
73 margin-right: 1ex;
74 padding-right: 1ex;
74 padding-right: 1ex;
75 }
75 }
76 .navigation_panel .link:last-child {
76 .navigation_panel .link:last-child {
77 border-left: 1px solid #fff;
77 border-left: 1px solid #fff;
78 border-right: none;
78 border-right: none;
79 float: right;
79 float: right;
80 margin-left: 1ex;
80 margin-left: 1ex;
81 margin-right: 0;
81 margin-right: 0;
82 padding-left: 1ex;
82 padding-left: 1ex;
83 padding-right: 0;
83 padding-right: 0;
84 }
84 }
85
85
86 .navigation_panel::after, .post::after {
86 .navigation_panel::after, .post::after {
87 clear: both;
87 clear: both;
88 content: ".";
88 content: ".";
89 display: block;
89 display: block;
90 height: 0;
90 height: 0;
91 line-height: 0;
91 line-height: 0;
92 visibility: hidden;
92 visibility: hidden;
93 }
93 }
94
94
95 p {
95 p {
96 margin-top: .5em;
96 margin-top: .5em;
97 margin-bottom: .5em;
97 margin-bottom: .5em;
98 }
98 }
99
99
100 .post-form-w {
100 .post-form-w {
101 display: table;
101 display: table;
102 background: #333344;
102 background: #333344;
103 border: solid 1px #888;
103 border: solid 1px #888;
104 color: #fff;
104 color: #fff;
105 padding: 10px;
105 padding: 10px;
106 margin: 5px;
106 margin: 5px;
107 }
107 }
108
108
109 .form-row {
109 .form-row {
110 display: table-row;
110 display: table-row;
111 }
111 }
112
112
113 .form-label, .form-input, .form-errors {
113 .form-label, .form-input, .form-errors {
114 display: table-cell;
114 display: table-cell;
115 }
115 }
116
116
117 .form-label {
117 .form-label {
118 padding: .25em 1ex .25em 0;
118 padding: .25em 1ex .25em 0;
119 vertical-align: top;
119 vertical-align: top;
120 }
120 }
121
121
122 .form-input {
122 .form-input {
123 padding: .25em 0;
123 padding: .25em 0;
124 }
124 }
125
125
126 .form-errors {
126 .form-errors {
127 font-weight: bolder;
127 font-weight: bolder;
128 vertical-align: middle;
128 vertical-align: middle;
129 }
129 }
130
130
131 .post-form input, .post-form textarea {
131 .post-form input, .post-form textarea {
132 background: #333;
132 background: #333;
133 color: #fff;
133 color: #fff;
134 border: solid 1px;
134 border: solid 1px;
135 padding: 0;
135 padding: 0;
136 width: 100%;
136 width: 100%;
137 font: medium sans;
137 font: medium sans;
138 }
138 }
139
139
140 .form-submit {
140 .form-submit {
141 display: table;
141 display: table;
142 margin-bottom: 1ex;
142 margin-bottom: 1ex;
143 }
143 }
144
144
145 .form-title {
145 .form-title {
146 font-weight: bold;
146 font-weight: bold;
147 font-size: 2.5ex;
147 font-size: 2.5ex;
148 text-decoration: underline;
148 text-decoration: underline;
149 }
149 }
150
150
151 input[type="submit"] {
151 input[type="submit"] {
152 background: #222;
152 background: #222;
153 border: solid 2px #fff;
153 border: solid 2px #fff;
154 color: #fff;
154 color: #fff;
155 padding: 0.5ex;
155 padding: 0.5ex;
156 }
156 }
157
157
158 input[type="submit"]:hover {
158 input[type="submit"]:hover {
159 background: #060;
159 background: #060;
160 }
160 }
161
161
162 blockquote {
162 blockquote {
163 border-left: solid 2px;
163 border-left: solid 2px;
164 padding-left: 5px;
164 padding-left: 5px;
165 color: #B1FB17;
165 color: #B1FB17;
166 margin: 0;
166 margin: 0;
167 }
167 }
168
168
169 .post > .image {
169 .post > .image {
170 float: left;
170 float: left;
171 margin: 0 1ex .5ex 0;
171 margin: 0 1ex .5ex 0;
172 min-width: 1px;
172 min-width: 1px;
173 text-align: center;
173 text-align: center;
174 display: table-row;
174 display: table-row;
175 }
175 }
176
176
177 .post > .metadata {
177 .post > .metadata {
178 clear: left;
178 clear: left;
179 }
179 }
180
180
181 .get {
181 .get {
182 font-weight: bold;
182 font-weight: bold;
183 color: #d55;
183 color: #d55;
184 }
184 }
185
185
186 * {
186 * {
187 text-decoration: none;
187 text-decoration: none;
188 }
188 }
189
189
190 .dead_post {
190 .dead_post {
191 background-color: #442222;
191 background-color: #442222;
192 }
192 }
193
193
194 .archive_post {
195 background-color: #000;
196 }
197
194 .mark_btn {
198 .mark_btn {
195 border: 1px solid;
199 border: 1px solid;
196 min-width: 2ex;
200 min-width: 2ex;
197 padding: 2px 2ex;
201 padding: 2px 2ex;
198 }
202 }
199
203
200 .mark_btn:hover {
204 .mark_btn:hover {
201 background: #555;
205 background: #555;
202 }
206 }
203
207
204 .quote {
208 .quote {
205 color: #92cf38;
209 color: #92cf38;
206 font-style: italic;
210 font-style: italic;
207 }
211 }
208
212
209 .spoiler {
213 .spoiler {
210 background: white;
214 background: white;
211 color: white;
215 color: white;
212 }
216 }
213
217
214 .spoiler:hover {
218 .spoiler:hover {
215 color: black;
219 color: black;
216 }
220 }
217
221
218 .comment {
222 .comment {
219 color: #eb2;
223 color: #eb2;
220 font-style: italic;
224 font-style: italic;
221 }
225 }
222
226
223 a:hover {
227 a:hover {
224 text-decoration: underline;
228 text-decoration: underline;
225 }
229 }
226
230
227 .last-replies {
231 .last-replies {
228 margin-left: 3ex;
232 margin-left: 3ex;
229 }
233 }
230
234
231 .thread {
235 .thread {
232 margin-bottom: 3ex;
236 margin-bottom: 3ex;
233 }
237 }
234
238
235 .post:target {
239 .post:target {
236 border: solid 2px white;
240 border: solid 2px white;
237 }
241 }
238
242
239 pre{
243 pre{
240 white-space:pre-wrap
244 white-space:pre-wrap
241 }
245 }
242
246
243 li {
247 li {
244 list-style-position: inside;
248 list-style-position: inside;
245 }
249 }
246
250
247 .fancybox-skin {
251 .fancybox-skin {
248 position: relative;
252 position: relative;
249 background-color: #fff;
253 background-color: #fff;
250 color: #ddd;
254 color: #ddd;
251 text-shadow: none;
255 text-shadow: none;
252 }
256 }
253
257
254 .fancybox-image {
258 .fancybox-image {
255 border: 1px solid black;
259 border: 1px solid black;
256 }
260 }
257
261
258 .image-mode-tab {
262 .image-mode-tab {
259 background: #444;
263 background: #444;
260 color: #eee;
264 color: #eee;
261 display: table;
265 display: table;
262 margin: 5px;
266 margin: 5px;
263 padding: 5px;
267 padding: 5px;
264 border: 1px solid #888;
268 border: 1px solid #888;
265 }
269 }
266
270
267 .image-mode-tab > label {
271 .image-mode-tab > label {
268 margin: 0 1ex;
272 margin: 0 1ex;
269 }
273 }
270
274
271 .image-mode-tab > label > input {
275 .image-mode-tab > label > input {
272 margin-right: .5ex;
276 margin-right: .5ex;
273 }
277 }
274
278
275 #posts-table {
279 #posts-table {
276 margin: 5px;
280 margin: 5px;
277 }
281 }
278
282
279 .tag_info {
283 .tag_info {
280 display: table;
284 display: table;
281 }
285 }
282
286
283 .tag_info > h2 {
287 .tag_info > h2 {
284 margin: 0;
288 margin: 0;
285 }
289 }
286
290
287 .post-info {
291 .post-info {
288 color: #ddd;
292 color: #ddd;
289 }
293 }
290
294
291 .moderator_info {
295 .moderator_info {
292 color: #e99d41;
296 color: #e99d41;
293 border: dashed 1px;
297 border: dashed 1px;
294 padding: 3px;
298 padding: 3px;
295 }
299 }
296
300
297 .refmap {
301 .refmap {
298 font-size: 0.9em;
302 font-size: 0.9em;
299 color: #ccc;
303 color: #ccc;
300 margin-top: 1em;
304 margin-top: 1em;
301 }
305 }
302
306
303 .fav {
307 .fav {
304 color: yellow;
308 color: yellow;
305 }
309 }
306
310
307 .not_fav {
311 .not_fav {
308 color: #ccc;
312 color: #ccc;
309 }
313 }
310
314
311 .role {
315 .role {
312 text-decoration: underline;
316 text-decoration: underline;
313 }
317 }
314
318
315 .form-email {
319 .form-email {
316 display: none;
320 display: none;
317 }
321 }
318
322
319 .footer {
323 .footer {
320 margin: 5px;
324 margin: 5px;
321 }
325 }
322
326
323 .bar-value {
327 .bar-value {
324 background: rgba(50, 55, 164, 0.45);
328 background: rgba(50, 55, 164, 0.45);
325 font-size: 0.9em;
329 font-size: 0.9em;
326 height: 1.5em;
330 height: 1.5em;
327 }
331 }
328
332
329 .bar-bg {
333 .bar-bg {
330 position: relative;
334 position: relative;
331 border: solid 1px #888;
335 border: solid 1px #888;
332 margin: 5px;
336 margin: 5px;
333 overflow: hidden;
337 overflow: hidden;
334 }
338 }
335
339
336 .bar-text {
340 .bar-text {
337 padding: 2px;
341 padding: 2px;
338 position: absolute;
342 position: absolute;
339 left: 0;
343 left: 0;
340 top: 0;
344 top: 0;
341 }
345 }
342
346
343 .page_link {
347 .page_link {
344 display: table;
348 display: table;
345 background: #444;
349 background: #444;
346 margin: 5px;
350 margin: 5px;
347 border: solid 1px #888;
351 border: solid 1px #888;
348 padding: 5px;
352 padding: 5px;
349 font-weight: bolder;
353 font-weight: bolder;
350 color: #eee;
354 color: #eee;
351 }
355 }
352
356
353 .skipped_replies {
357 .skipped_replies {
354 margin: 5px;
358 margin: 5px;
355 }
359 }
356
360
357 .current_page {
361 .current_page {
358 border: solid 1px #afdcec;
362 border: solid 1px #afdcec;
359 padding: 2px;
363 padding: 2px;
360 }
364 }
361
365
362 .current_mode {
366 .current_mode {
363 font-weight: bold;
367 font-weight: bold;
364 }
368 }
365
369
366 .gallery_image {
370 .gallery_image {
367 border: solid 1px;
371 border: solid 1px;
368 padding: 0.5ex;
372 padding: 0.5ex;
369 margin: 0.5ex;
373 margin: 0.5ex;
370 text-align: center;
374 text-align: center;
371 }
375 }
372
376
373 code {
377 code {
374 border: dashed 1px #ccc;
378 border: dashed 1px #ccc;
375 background: #111;
379 background: #111;
376 padding: 2px;
380 padding: 2px;
377 font-size: 1.2em;
381 font-size: 1.2em;
378 display: inline-block;
382 display: inline-block;
379 }
383 }
380
384
381 pre {
385 pre {
382 overflow: auto;
386 overflow: auto;
383 }
387 }
388
389 .img-full {
390 background: #222;
391 border: solid 1px white;
392 }
393
394 .tag_item {
395 display: inline-block;
396 background: #555;
397 border: 1px solid #ccc;
398 margin: 0.3ex;
399 padding: 0.2ex;
400 }
@@ -1,347 +1,354 b''
1 html {
1 html {
2 background: rgb(238, 238, 238);
2 background: rgb(238, 238, 238);
3 color: rgb(51, 51, 51);
3 color: rgb(51, 51, 51);
4 }
4 }
5
5
6 #admin_panel {
6 #admin_panel {
7 background: #FF0000;
7 background: #FF0000;
8 color: #00FF00
8 color: #00FF00
9 }
9 }
10
10
11 .input_field {
11 .input_field {
12
12
13 }
13 }
14
14
15 .input_field_name {
15 .input_field_name {
16
16
17 }
17 }
18
18
19 .input_field_error {
19 .input_field_error {
20 color: #FF0000;
20 color: #FF0000;
21 }
21 }
22
22
23
23
24 .title {
24 .title {
25 font-weight: bold;
25 font-weight: bold;
26 color: #333;
26 color: #333;
27 font-size: 2ex;
27 font-size: 2ex;
28 }
28 }
29
29
30 .link, a {
30 .link, a {
31 color: rgb(255, 102, 0);
31 color: rgb(255, 102, 0);
32 }
32 }
33
33
34 .block {
34 .block {
35 display: inline-block;
35 display: inline-block;
36 vertical-align: top;
36 vertical-align: top;
37 }
37 }
38
38
39 .tag {
39 .tag {
40 color: #222;
40 color: #222;
41 }
41 }
42
42
43 .post_id:hover {
43 .post_id:hover {
44 color: #11f;
44 color: #11f;
45 }
45 }
46
46
47 .post_id {
47 .post_id {
48 color: #444;
48 color: #444;
49 }
49 }
50
50
51 .post, .dead_post, #posts-table {
51 .post, .dead_post, #posts-table {
52 margin: 5px;
52 margin: 5px;
53 padding: 10px;
53 padding: 10px;
54 background: rgb(221, 221, 221);
54 background: rgb(221, 221, 221);
55 border: 1px solid rgb(204, 204, 204);
55 border: 1px solid rgb(204, 204, 204);
56 border-radius: 5px 5px 5px 5px;
56 border-radius: 5px 5px 5px 5px;
57 clear: left;
57 clear: left;
58 word-wrap: break-word;
58 word-wrap: break-word;
59 display: table;
59 display: table;
60 }
60 }
61
61
62 .metadata {
62 .metadata {
63 padding: 5px;
63 padding: 5px;
64 margin-top: 10px;
64 margin-top: 10px;
65 border: solid 1px #666;
65 border: solid 1px #666;
66 font-size: 0.9em;
66 font-size: 0.9em;
67 display: table;
67 display: table;
68 }
68 }
69
69
70 .navigation_panel, .tag_info, .page_link {
70 .navigation_panel, .tag_info, .page_link {
71 margin: 5px;
71 margin: 5px;
72 padding: 10px;
72 padding: 10px;
73 border: 1px solid rgb(204, 204, 204);
73 border: 1px solid rgb(204, 204, 204);
74 border-radius: 5px 5px 5px 5px;
74 border-radius: 5px 5px 5px 5px;
75 }
75 }
76
76
77 .navigation_panel .link {
77 .navigation_panel .link {
78 border-right: 1px solid #000;
78 border-right: 1px solid #000;
79 font-weight: bold;
79 font-weight: bold;
80 margin-right: 1ex;
80 margin-right: 1ex;
81 padding-right: 1ex;
81 padding-right: 1ex;
82 }
82 }
83 .navigation_panel .link:last-child {
83 .navigation_panel .link:last-child {
84 border-left: 1px solid #000;
84 border-left: 1px solid #000;
85 border-right: none;
85 border-right: none;
86 float: right;
86 float: right;
87 margin-left: 1ex;
87 margin-left: 1ex;
88 margin-right: 0;
88 margin-right: 0;
89 padding-left: 1ex;
89 padding-left: 1ex;
90 padding-right: 0;
90 padding-right: 0;
91 }
91 }
92
92
93 .navigation_panel::after, .post::after {
93 .navigation_panel::after, .post::after {
94 clear: both;
94 clear: both;
95 content: ".";
95 content: ".";
96 display: block;
96 display: block;
97 height: 0;
97 height: 0;
98 line-height: 0;
98 line-height: 0;
99 visibility: hidden;
99 visibility: hidden;
100 }
100 }
101
101
102 p {
102 p {
103 margin-top: .5em;
103 margin-top: .5em;
104 margin-bottom: .5em;
104 margin-bottom: .5em;
105 }
105 }
106
106
107 .post-form-w {
107 .post-form-w {
108 display: table;
108 display: table;
109 padding: 10px;
109 padding: 10px;
110 margin: 5px
110 margin: 5px
111 }
111 }
112
112
113 .form-row {
113 .form-row {
114 display: table-row;
114 display: table-row;
115 }
115 }
116
116
117 .form-label, .form-input, .form-errors {
117 .form-label, .form-input, .form-errors {
118 display: table-cell;
118 display: table-cell;
119 }
119 }
120
120
121 .form-label {
121 .form-label {
122 padding: .25em 1ex .25em 0;
122 padding: .25em 1ex .25em 0;
123 vertical-align: top;
123 vertical-align: top;
124 }
124 }
125
125
126 .form-input {
126 .form-input {
127 padding: .25em 0;
127 padding: .25em 0;
128 }
128 }
129
129
130 .form-errors {
130 .form-errors {
131 padding-left: 1ex;
131 padding-left: 1ex;
132 font-weight: bold;
132 font-weight: bold;
133 vertical-align: middle;
133 vertical-align: middle;
134 }
134 }
135
135
136 .post-form input, .post-form textarea {
136 .post-form input, .post-form textarea {
137 background: #fff;
137 background: #fff;
138 color: #000;
138 color: #000;
139 border: solid 1px;
139 border: solid 1px;
140 padding: 0;
140 padding: 0;
141 width: 100%;
141 width: 100%;
142 font: medium sans;
142 font: medium sans;
143 }
143 }
144
144
145 .form-submit {
145 .form-submit {
146 border-bottom: 2px solid #ddd;
146 border-bottom: 2px solid #ddd;
147 margin-bottom: .5em;
147 margin-bottom: .5em;
148 padding-bottom: .5em;
148 padding-bottom: .5em;
149 }
149 }
150
150
151 .form-title {
151 .form-title {
152 font-weight: bold;
152 font-weight: bold;
153 }
153 }
154
154
155 input[type="submit"] {
155 input[type="submit"] {
156 background: #fff;
156 background: #fff;
157 border: solid 1px #000;
157 border: solid 1px #000;
158 color: #000;
158 color: #000;
159 }
159 }
160
160
161 blockquote {
161 blockquote {
162 border-left: solid 2px;
162 border-left: solid 2px;
163 padding-left: 5px;
163 padding-left: 5px;
164 color: #B1FB17;
164 color: #B1FB17;
165 margin: 0;
165 margin: 0;
166 }
166 }
167
167
168 .post > .image {
168 .post > .image {
169 float: left;
169 float: left;
170 margin: 0 1ex .5ex 0;
170 margin: 0 1ex .5ex 0;
171 min-width: 1px;
171 min-width: 1px;
172 text-align: center;
172 text-align: center;
173 display: table-row;
173 display: table-row;
174 }
174 }
175
175
176 .post > .metadata {
176 .post > .metadata {
177 clear: left;
177 clear: left;
178 }
178 }
179
179
180 .get {
180 .get {
181 font-weight: bold;
181 font-weight: bold;
182 color: #d55;
182 color: #d55;
183 }
183 }
184
184
185 * {
185 * {
186 text-decoration: none;
186 text-decoration: none;
187 }
187 }
188
188
189 .dead_post {
189 .dead_post {
190 background-color: #ecc;
190 background-color: #ecc;
191 }
191 }
192
192
193 .quote {
193 .quote {
194 color: #080;
194 color: #080;
195 font-style: italic;
195 font-style: italic;
196 }
196 }
197
197
198 .spoiler {
198 .spoiler {
199 background: white;
199 background: white;
200 color: white;
200 color: white;
201 }
201 }
202
202
203 .spoiler:hover {
203 .spoiler:hover {
204 color: black;
204 color: black;
205 }
205 }
206
206
207 .comment {
207 .comment {
208 color: #8B6914;
208 color: #8B6914;
209 font-style: italic;
209 font-style: italic;
210 }
210 }
211
211
212 a:hover {
212 a:hover {
213 text-decoration: underline;
213 text-decoration: underline;
214 }
214 }
215
215
216 .last-replies {
216 .last-replies {
217 margin-left: 3ex;
217 margin-left: 3ex;
218 }
218 }
219
219
220 .thread {
220 .thread {
221 margin-bottom: 3ex;
221 margin-bottom: 3ex;
222 }
222 }
223
223
224 .post:target {
224 .post:target {
225 border: solid 2px black;
225 border: solid 2px black;
226 }
226 }
227
227
228 pre{
228 pre{
229 white-space:pre-wrap
229 white-space:pre-wrap
230 }
230 }
231
231
232 li {
232 li {
233 list-style-position: inside;
233 list-style-position: inside;
234 }
234 }
235
235
236 .fancybox-skin {
236 .fancybox-skin {
237 position: relative;
237 position: relative;
238 background-color: #fff;
238 background-color: #fff;
239 color: #ddd;
239 color: #ddd;
240 text-shadow: none;
240 text-shadow: none;
241 }
241 }
242
242
243 .fancybox-image {
243 .fancybox-image {
244 border: 1px solid black;
244 border: 1px solid black;
245 }
245 }
246
246
247 .image-mode-tab {
247 .image-mode-tab {
248 display: table;
248 display: table;
249 margin: 5px;
249 margin: 5px;
250 padding: 5px;
250 padding: 5px;
251 background: rgb(221, 221, 221);
251 background: rgb(221, 221, 221);
252 border: 1px solid rgb(204, 204, 204);
252 border: 1px solid rgb(204, 204, 204);
253 border-radius: 5px 5px 5px 5px;
253 border-radius: 5px 5px 5px 5px;
254 }
254 }
255
255
256 .image-mode-tab > label {
256 .image-mode-tab > label {
257 margin: 0 1ex;
257 margin: 0 1ex;
258 }
258 }
259
259
260 .image-mode-tab > label > input {
260 .image-mode-tab > label > input {
261 margin-right: .5ex;
261 margin-right: .5ex;
262 }
262 }
263
263
264 #posts-table {
264 #posts-table {
265 margin: 5px;
265 margin: 5px;
266 }
266 }
267
267
268 .tag_info, .page_link {
268 .tag_info, .page_link {
269 display: table;
269 display: table;
270 }
270 }
271
271
272 .tag_info > h2 {
272 .tag_info > h2 {
273 margin: 0;
273 margin: 0;
274 }
274 }
275
275
276 .moderator_info {
276 .moderator_info {
277 color: #e99d41;
277 color: #e99d41;
278 border: dashed 1px;
278 border: dashed 1px;
279 padding: 3px;
279 padding: 3px;
280 }
280 }
281
281
282 .refmap {
282 .refmap {
283 font-size: 0.9em;
283 font-size: 0.9em;
284 color: #444;
284 color: #444;
285 margin-top: 1em;
285 margin-top: 1em;
286 }
286 }
287
287
288 input[type="submit"]:hover {
288 input[type="submit"]:hover {
289 background: #ccc;
289 background: #ccc;
290 }
290 }
291
291
292
292
293 .fav {
293 .fav {
294 color: rgb(255, 102, 0);
294 color: rgb(255, 102, 0);
295 }
295 }
296
296
297 .not_fav {
297 .not_fav {
298 color: #555;
298 color: #555;
299 }
299 }
300
300
301 .role {
301 .role {
302 text-decoration: underline;
302 text-decoration: underline;
303 }
303 }
304
304
305 .form-email {
305 .form-email {
306 display: none;
306 display: none;
307 }
307 }
308
308
309 .mark_btn {
309 .mark_btn {
310 padding: 2px 2ex;
310 padding: 2px 2ex;
311 border: 1px solid;
311 border: 1px solid;
312 }
312 }
313
313
314 .mark_btn:hover {
314 .mark_btn:hover {
315 background: #ccc;
315 background: #ccc;
316 }
316 }
317
317
318 .bar-value {
318 .bar-value {
319 background: rgba(251, 199, 16, 0.61);
319 background: rgba(251, 199, 16, 0.61);
320 padding: 2px;
320 padding: 2px;
321 font-size: 0.9em;
321 font-size: 0.9em;
322 height: 1.5em;
322 height: 1.5em;
323 }
323 }
324
324
325 .bar-bg {
325 .bar-bg {
326 position: relative;
326 position: relative;
327 border: 1px solid rgb(204, 204, 204);
327 border: 1px solid rgb(204, 204, 204);
328 border-radius: 5px 5px 5px 5px;
328 border-radius: 5px 5px 5px 5px;
329 margin: 5px;
329 margin: 5px;
330 overflow: hidden;
330 overflow: hidden;
331 }
331 }
332
332
333 .bar-text {
333 .bar-text {
334 padding: 2px;
334 padding: 2px;
335 position: absolute;
335 position: absolute;
336 left: 0;
336 left: 0;
337 top: 0;
337 top: 0;
338 }
338 }
339
339
340 .skipped_replies {
340 .skipped_replies {
341 margin: 5px;
341 margin: 5px;
342 }
342 }
343
343
344 .current_page, .current_mode {
344 .current_page, .current_mode {
345 border: solid 1px #000;
345 border: solid 1px #000;
346 padding: 2px;
346 padding: 2px;
347 } No newline at end of file
347 }
348
349 .tag_item {
350 display: inline-block;
351 border: 1px solid #ccc;
352 margin: 0.3ex;
353 padding: 0.2ex;
354 }
@@ -1,254 +1,145 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load board %}
5 {% load board %}
6 {% load static %}
6 {% load static %}
7
7
8 {% block head %}
8 {% block head %}
9 {% if tag %}
9 <title>Neboard - {% trans 'Archive' %}</title>
10 <title>Neboard - {{ tag.name }}</title>
11 {% else %}
12 <title>Neboard</title>
13 {% endif %}
14
10
15 {% if prev_page %}
11 {% if current_page.has_previous %}
16 <link rel="next" href="
12 <link rel="prev" href="
17 {% if tag %}
13 {% if tag %}
18 {% url "tag" tag_name=tag page=prev_page %}
14 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
19 {% else %}
15 {% else %}
20 {% url "index" page=prev_page %}
16 {% url "index" page=current_page.previous_page_number %}
21 {% endif %}
17 {% endif %}
22 " />
18 " />
23 {% endif %}
19 {% endif %}
24 {% if next_page %}
20 {% if current_page.has_next %}
25 <link rel="next" href="
21 <link rel="next" href="
26 {% if tag %}
22 {% if tag %}
27 {% url "tag" tag_name=tag page=next_page %}
23 {% url "tag" tag_name=tag page=current_page.next_page_number %}
28 {% else %}
24 {% else %}
29 {% url "index" page=next_page %}
25 {% url "index" page=current_page.next_page_number %}
30 {% endif %}
26 {% endif %}
31 " />
27 " />
32 {% endif %}
28 {% endif %}
33
29
34 {% endblock %}
30 {% endblock %}
35
31
36 {% block content %}
32 {% block content %}
37
33
38 {% get_current_language as LANGUAGE_CODE %}
34 {% get_current_language as LANGUAGE_CODE %}
39
35
40 {% if tag %}
41 <div class="tag_info">
42 <h2>
43 {% if tag in user.fav_tags.all %}
44 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
45 class="fav">β˜…</a>
46 {% else %}
47 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
48 class="not_fav">β˜…</a>
49 {% endif %}
50 #{{ tag.name }}
51 </h2>
52 </div>
53 {% endif %}
54
55 {% if threads %}
36 {% if threads %}
56 {% if prev_page %}
37 {% if current_page.has_previous %}
57 <div class="page_link">
38 <div class="page_link">
58 <a href="
39 <a href="{% url "archive" page=current_page.previous_page_number %}">{% trans "Previous page" %}</a>
59 {% if tag %}
60 {% url "tag" tag_name=tag page=prev_page %}
61 {% else %}
62 {% url "index" page=prev_page %}
63 {% endif %}
64 ">{% trans "Previous page" %}</a>
65 </div>
40 </div>
66 {% endif %}
41 {% endif %}
67
42
68 {% for thread in threads %}
43 {% for thread in threads %}
69 {% cache 600 thread_short thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
44 {% cache 600 thread_short thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
70 <div class="thread">
45 <div class="thread">
71 {% if thread.bumpable %}
46 <div class="post archive_post" id="{{ thread.op.id }}">
72 <div class="post" id="{{ thread.op.id }}">
73 {% else %}
74 <div class="post dead_post" id="{{ thread.op.id }}">
75 {% endif %}
76 {% if thread.op.image %}
47 {% if thread.op.image %}
77 <div class="image">
48 <div class="image">
78 <a class="thumb"
49 <a class="thumb"
79 href="{{ thread.op.image.url }}"><img
50 href="{{ thread.op.image.url }}"><img
80 src="{{ thread.op.image.url_200x150 }}"
51 src="{{ thread.op.image.url_200x150 }}"
81 alt="{{ thread.op.id }}"
52 alt="{{ thread.op.id }}"
82 width="{{ thread.op.image_pre_width }}"
53 width="{{ thread.op.image_pre_width }}"
83 height="{{ thread.op.image_pre_height }}"
54 height="{{ thread.op.image_pre_height }}"
84 data-width="{{ thread.op.image_width }}"
55 data-width="{{ thread.op.image_width }}"
85 data-height="{{ thread.op.image_height }}"/>
56 data-height="{{ thread.op.image_height }}"/>
86 </a>
57 </a>
87 </div>
58 </div>
88 {% endif %}
59 {% endif %}
89 <div class="message">
60 <div class="message">
90 <div class="post-info">
61 <div class="post-info">
91 <span class="title">{{ thread.op.title }}</span>
62 <span class="title">{{ thread.op.title }}</span>
92 <a class="post_id" href="{% url 'thread' thread.op.id %}"
63 <a class="post_id" href="{% url 'thread' thread.op.id %}"
93 > ({{ thread.op.id }})</a>
64 > ({{ thread.op.id }})</a>
94 [{{ thread.op.pub_time }}]
65 [{{ thread.op.pub_time }}] β€” [{{ thread.thread.last_edit_time }}]
66
95 [<a class="link" href="
67 [<a class="link" href="
96 {% url 'thread' thread.op.id %}#form"
68 {% url 'thread' thread.op.id %}">{% trans "Open" %}</a>]
97 >{% trans "Reply" %}</a>]
98
69
99 {% if moderator %}
70 {% if moderator %}
100 <span class="moderator_info">
71 <span class="moderator_info">
101 [<a href="
72 [<a href="
102 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
73 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
103 >{% trans 'Delete' %}</a>]
74 >{% trans 'Delete' %}</a>]
104 ({{ thread.op.poster_ip }})
75 ({{ thread.op.poster_ip }})
105 [<a href="
76 [<a href="
106 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
77 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
107 >{% trans 'Ban IP' %}</a>]
78 >{% trans 'Ban IP' %}</a>]
108 </span>
79 </span>
109 {% endif %}
80 {% endif %}
110 </div>
81 </div>
111 {% autoescape off %}
82 {% autoescape off %}
112 {{ thread.op.text.rendered|truncatewords_html:50 }}
83 {{ thread.op.text.rendered|truncatewords_html:50 }}
113 {% endautoescape %}
84 {% endautoescape %}
114 {% if thread.op.is_referenced %}
85 {% if thread.op.is_referenced %}
115 <div class="refmap">
86 <div class="refmap">
116 {% trans "Replies" %}:
87 {% trans "Replies" %}:
117 {% for ref_post in thread.op.get_sorted_referenced_posts %}
88 {% for ref_post in thread.op.get_sorted_referenced_posts %}
118 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
89 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
119 >{% if not forloop.last %},{% endif %}
90 >{% if not forloop.last %},{% endif %}
120 {% endfor %}
91 {% endfor %}
121 </div>
92 </div>
122 {% endif %}
93 {% endif %}
123 </div>
94 </div>
124 <div class="metadata">
95 <div class="metadata">
125 {{ thread.thread.get_images_count }} {% trans 'images' %}.
96 {{ thread.thread.get_images_count }} {% trans 'images' %},
97 {{ thread.thread.get_reply_count }} {% trans 'replies' %}.
126 {% if thread.thread.tags %}
98 {% if thread.thread.tags %}
127 <span class="tags">
99 <span class="tags">
128 {% for tag in thread.thread.get_tags %}
100 {% for tag in thread.thread.get_tags %}
129 <a class="tag" href="
101 <a class="tag" href="
130 {% url 'tag' tag_name=tag.name %}">
102 {% url 'tag' tag_name=tag.name %}">
131 #{{ tag.name }}</a
103 #{{ tag.name }}</a
132 >{% if not forloop.last %},{% endif %}
104 >{% if not forloop.last %},{% endif %}
133 {% endfor %}
105 {% endfor %}
134 </span>
106 </span>
135 {% endif %}
107 {% endif %}
136 </div>
108 </div>
137 </div>
109 </div>
138 {% if thread.last_replies.exists %}
139 {% if thread.skipped_replies %}
140 <div class="skipped_replies">
141 <a href="{% url 'thread' thread.op.id %}">
142 {% blocktrans with count=thread.skipped_replies %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
143 </a>
144 </div>
145 {% endif %}
146 <div class="last-replies">
147 {% for post in thread.last_replies %}
148 {% if thread.bumpable %}
149 <div class="post" id="{{ post.id }}">
150 {% else %}
151 <div class="post dead_post" id="{{ post.id }}">
152 {% endif %}
153 {% if post.image %}
154 <div class="image">
155 <a class="thumb"
156 href="{{ post.image.url }}"><img
157 src=" {{ post.image.url_200x150 }}"
158 alt="{{ post.id }}"
159 width="{{ post.image_pre_width }}"
160 height="{{ post.image_pre_height }}"
161 data-width="{{ post.image_width }}"
162 data-height="{{ post.image_height }}"/>
163 </a>
164 </div>
165 {% endif %}
166 <div class="message">
167 <div class="post-info">
168 <span class="title">{{ post.title }}</span>
169 <a class="post_id" href="
170 {% url 'thread' thread.op.id %}#{{ post.id }}">
171 ({{ post.id }})</a>
172 [{{ post.pub_time }}]
173 </div>
174 {% autoescape off %}
175 {{ post.text.rendered|truncatewords_html:50 }}
176 {% endautoescape %}
177 </div>
178 {% if post.is_referenced %}
179 <div class="refmap">
180 {% trans "Replies" %}:
181 {% for ref_post in post.get_sorted_referenced_posts %}
182 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
183 >{% if not forloop.last %},{% endif %}
184 {% endfor %}
185 </div>
186 {% endif %}
187 </div>
188 {% endfor %}
189 </div>
190 {% endif %}
191 </div>
110 </div>
192 {% endcache %}
111 {% endcache %}
193 {% endfor %}
112 {% endfor %}
194
113
195 {% if next_page %}
114 {% if current_page.has_next %}
196 <div class="page_link">
115 <div class="page_link">
197 <a href="
116 <a href="{% url "archive" page=current_page.next_page_number %}">{% trans "Next page" %}</a>
198 {% if tag %}
199 {% url "tag" tag_name=tag page=next_page %}
200 {% else %}
201 {% url "index" page=next_page %}
202 {% endif %}
203 ">{% trans "Next page" %}</a>
204 </div>
117 </div>
205 {% endif %}
118 {% endif %}
206 {% else %}
119 {% else %}
207 <div class="post">
120 <div class="post">
208 {% trans 'No threads exist. Create the first one!' %}</div>
121 {% trans 'No threads exist. Create the first one!' %}</div>
209 {% endif %}
122 {% endif %}
210
123
211 <div class="post-form-w">
212 <script src="{% static 'js/panel.js' %}"></script>
213 <div class="post-form">
214 <div class="form-title">{% trans "Create new thread" %}</div>
215 <form enctype="multipart/form-data" method="post">{% csrf_token %}
216 {{ form.as_div }}
217 <div class="form-submit">
218 <input type="submit" value="{% trans "Post" %}"/>
219 </div>
220 </form>
221 <div>
222 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
223 </div>
224 <div><a href="{% url "staticpage" name="help" %}">
225 {% trans 'Text syntax' %}</a></div>
226 </div>
227 </div>
228
229 {% endblock %}
124 {% endblock %}
230
125
231 {% block metapanel %}
126 {% block metapanel %}
232
127
233 <span class="metapanel">
128 <span class="metapanel">
234 <b><a href="{% url "authors" %}">Neboard</a> 1.5 Aker</b>
129 <b><a href="{% url "authors" %}">Neboard</a> 1.6 Amon</b>
235 {% trans "Pages:" %}[
130 {% trans "Pages:" %}[
236 {% for page in pages %}
131 {% for page in paginator.page_range %}
237 <a
132 <a
238 {% ifequal page current_page %}
133 {% ifequal page current_page.number %}
239 class="current_page"
134 class="current_page"
240 {% endifequal %}
135 {% endifequal %}
241 href="
136 href="
242 {% if tag %}
137 {% url "archive" page=page %}
243 {% url "tag" tag_name=tag page=page %}
244 {% else %}
245 {% url "index" page=page %}
246 {% endif %}
247 ">{{ page }}</a>
138 ">{{ page }}</a>
248 {% if not forloop.last %},{% endif %}
139 {% if not forloop.last %},{% endif %}
249 {% endfor %}
140 {% endfor %}
250 ]
141 ]
251 [<a href="rss/">RSS</a>]
142 [<a href="rss/">RSS</a>]
252 </span>
143 </span>
253
144
254 {% endblock %}
145 {% endblock %}
@@ -1,62 +1,63 b''
1 {% load staticfiles %}
1 {% load staticfiles %}
2 {% load i18n %}
2 {% load i18n %}
3 {% load l10n %}
3 {% load l10n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5
5
6 <!DOCTYPE html>
6 <!DOCTYPE html>
7 <html>
7 <html>
8 <head>
8 <head>
9 <link rel="stylesheet" type="text/css"
9 <link rel="stylesheet" type="text/css"
10 href="{% static 'css/base.css' %}" media="all"/>
10 href="{% static 'css/base.css' %}" media="all"/>
11 <link rel="stylesheet" type="text/css"
11 <link rel="stylesheet" type="text/css"
12 href="{% static theme_css %}" media="all"/>
12 href="{% static theme_css %}" media="all"/>
13 <link rel="alternate" type="application/rss+xml" href="rss/" title=
13 <link rel="alternate" type="application/rss+xml" href="rss/" title=
14 "{% trans 'Feed' %}"/>
14 "{% trans 'Feed' %}"/>
15
15
16 <link rel="icon" type="image/png"
16 <link rel="icon" type="image/png"
17 href="{% static 'favicon.png' %}">
17 href="{% static 'favicon.png' %}">
18
18
19 <meta name="viewport" content="width=device-width, initial-scale=1"/>
19 <meta name="viewport" content="width=device-width, initial-scale=1"/>
20 <meta charset="utf-8"/>
20 <meta charset="utf-8"/>
21
21
22 {% block head %}{% endblock %}
22 {% block head %}{% endblock %}
23 </head>
23 </head>
24 <body>
24 <body>
25 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
25 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
26 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
26 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
27 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
27 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
28 <script src="{% url 'js_info_dict' %}"></script>
28 <script src="{% url 'js_info_dict' %}"></script>
29
29
30 <div class="navigation_panel">
30 <div class="navigation_panel">
31 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
31 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
32 {% for tag in tags %}
32 {% for tag in tags %}
33 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
33 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
34 >#{{ tag.name }}</a>,
34 >#{{ tag.name }}</a>,
35 {% endfor %}
35 {% endfor %}
36 <a class="tag" href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
36 <a class="tag" href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
37 >[...]</a>
37 >[...]</a>
38 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
38 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
39 </div>
39 </div>
40
40
41 {% block content %}{% endblock %}
41 {% block content %}{% endblock %}
42
42
43 <script src="{% static 'js/popup.js' %}"></script>
43 <script src="{% static 'js/popup.js' %}"></script>
44 <script src="{% static 'js/image.js' %}"></script>
44 <script src="{% static 'js/image.js' %}"></script>
45 <script src="{% static 'js/refpopup.js' %}"></script>
45 <script src="{% static 'js/refpopup.js' %}"></script>
46 <script src="{% static 'js/main.js' %}"></script>
46 <script src="{% static 'js/main.js' %}"></script>
47
47
48 <div class="navigation_panel">
48 <div class="navigation_panel">
49 {% block metapanel %}{% endblock %}
49 {% block metapanel %}{% endblock %}
50 [<a href="{% url "login" %}">{% trans 'Login' %}</a>]
50 [<a href="{% url "login" %}">{% trans 'Login' %}</a>]
51 [<a href="{% url "archive" %}">{% trans 'Archive' %}</a>]
51 {% with ppd=posts_per_day|floatformat:2 %}
52 {% with ppd=posts_per_day|floatformat:2 %}
52 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
53 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
53 {% endwith %}
54 {% endwith %}
54 <a class="link" href="#top">{% trans 'Up' %}</a>
55 <a class="link" href="#top">{% trans 'Up' %}</a>
55 </div>
56 </div>
56
57
57 <div class="footer">
58 <div class="footer">
58 <!-- Put your banners here -->
59 <!-- Put your banners here -->
59 </div>
60 </div>
60
61
61 </body>
62 </body>
62 </html>
63 </html>
@@ -1,254 +1,254 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load board %}
5 {% load board %}
6 {% load static %}
6 {% load static %}
7
7
8 {% block head %}
8 {% block head %}
9 {% if tag %}
9 {% if tag %}
10 <title>Neboard - {{ tag.name }}</title>
10 <title>Neboard - {{ tag.name }}</title>
11 {% else %}
11 {% else %}
12 <title>Neboard</title>
12 <title>Neboard</title>
13 {% endif %}
13 {% endif %}
14
14
15 {% if prev_page %}
15 {% if current_page.has_previous %}
16 <link rel="next" href="
16 <link rel="prev" href="
17 {% if tag %}
17 {% if tag %}
18 {% url "tag" tag_name=tag page=prev_page %}
18 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
19 {% else %}
19 {% else %}
20 {% url "index" page=prev_page %}
20 {% url "index" page=current_page.previous_page_number %}
21 {% endif %}
21 {% endif %}
22 " />
22 " />
23 {% endif %}
23 {% endif %}
24 {% if next_page %}
24 {% if current_page.has_next %}
25 <link rel="next" href="
25 <link rel="next" href="
26 {% if tag %}
26 {% if tag %}
27 {% url "tag" tag_name=tag page=next_page %}
27 {% url "tag" tag_name=tag page=current_page.next_page_number %}
28 {% else %}
28 {% else %}
29 {% url "index" page=next_page %}
29 {% url "index" page=current_page.next_page_number %}
30 {% endif %}
30 {% endif %}
31 " />
31 " />
32 {% endif %}
32 {% endif %}
33
33
34 {% endblock %}
34 {% endblock %}
35
35
36 {% block content %}
36 {% block content %}
37
37
38 {% get_current_language as LANGUAGE_CODE %}
38 {% get_current_language as LANGUAGE_CODE %}
39
39
40 {% if tag %}
40 {% if tag %}
41 <div class="tag_info">
41 <div class="tag_info">
42 <h2>
42 <h2>
43 {% if tag in user.fav_tags.all %}
43 {% if tag in user.fav_tags.all %}
44 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
44 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
45 class="fav">β˜…</a>
45 class="fav">β˜…</a>
46 {% else %}
46 {% else %}
47 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
47 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
48 class="not_fav">β˜…</a>
48 class="not_fav">β˜…</a>
49 {% endif %}
49 {% endif %}
50 #{{ tag.name }}
50 #{{ tag.name }}
51 </h2>
51 </h2>
52 </div>
52 </div>
53 {% endif %}
53 {% endif %}
54
54
55 {% if threads %}
55 {% if threads %}
56 {% if prev_page %}
56 {% if current_page.has_previous %}
57 <div class="page_link">
57 <div class="page_link">
58 <a href="
58 <a href="
59 {% if tag %}
59 {% if tag %}
60 {% url "tag" tag_name=tag page=prev_page %}
60 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
61 {% else %}
61 {% else %}
62 {% url "index" page=prev_page %}
62 {% url "index" page=current_page.previous_page_number %}
63 {% endif %}
63 {% endif %}
64 ">{% trans "Previous page" %}</a>
64 ">{% trans "Previous page" %}</a>
65 </div>
65 </div>
66 {% endif %}
66 {% endif %}
67
67
68 {% for thread in threads %}
68 {% for thread in threads %}
69 {% cache 600 thread_short thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
69 {% cache 600 thread_short thread.id thread.thread.last_edit_time moderator LANGUAGE_CODE %}
70 <div class="thread">
70 <div class="thread">
71 {% if thread.bumpable %}
71 {% if thread.bumpable %}
72 <div class="post" id="{{ thread.op.id }}">
72 <div class="post" id="{{ thread.op.id }}">
73 {% else %}
73 {% else %}
74 <div class="post dead_post" id="{{ thread.op.id }}">
74 <div class="post dead_post" id="{{ thread.op.id }}">
75 {% endif %}
75 {% endif %}
76 {% if thread.op.image %}
76 {% if thread.op.image %}
77 <div class="image">
77 <div class="image">
78 <a class="thumb"
78 <a class="thumb"
79 href="{{ thread.op.image.url }}"><img
79 href="{{ thread.op.image.url }}"><img
80 src="{{ thread.op.image.url_200x150 }}"
80 src="{{ thread.op.image.url_200x150 }}"
81 alt="{{ thread.op.id }}"
81 alt="{{ thread.op.id }}"
82 width="{{ thread.op.image_pre_width }}"
82 width="{{ thread.op.image_pre_width }}"
83 height="{{ thread.op.image_pre_height }}"
83 height="{{ thread.op.image_pre_height }}"
84 data-width="{{ thread.op.image_width }}"
84 data-width="{{ thread.op.image_width }}"
85 data-height="{{ thread.op.image_height }}"/>
85 data-height="{{ thread.op.image_height }}"/>
86 </a>
86 </a>
87 </div>
87 </div>
88 {% endif %}
88 {% endif %}
89 <div class="message">
89 <div class="message">
90 <div class="post-info">
90 <div class="post-info">
91 <span class="title">{{ thread.op.title }}</span>
91 <span class="title">{{ thread.op.title }}</span>
92 <a class="post_id" href="{% url 'thread' thread.op.id %}"
92 <a class="post_id" href="{% url 'thread' thread.op.id %}"
93 > ({{ thread.op.id }})</a>
93 > ({{ thread.op.id }})</a>
94 [{{ thread.op.pub_time }}]
94 [{{ thread.op.pub_time }}]
95 [<a class="link" href="
95 [<a class="link" href="
96 {% url 'thread' thread.op.id %}#form"
96 {% url 'thread' thread.op.id %}#form"
97 >{% trans "Reply" %}</a>]
97 >{% trans "Reply" %}</a>]
98
98
99 {% if moderator %}
99 {% if moderator %}
100 <span class="moderator_info">
100 <span class="moderator_info">
101 [<a href="
101 [<a href="
102 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
102 {% url 'delete' post_id=thread.op.id %}?next={{ request.path }}"
103 >{% trans 'Delete' %}</a>]
103 >{% trans 'Delete' %}</a>]
104 ({{ thread.op.poster_ip }})
104 ({{ thread.op.poster_ip }})
105 [<a href="
105 [<a href="
106 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
106 {% url 'ban' post_id=thread.op.id %}?next={{ request.path }}"
107 >{% trans 'Ban IP' %}</a>]
107 >{% trans 'Ban IP' %}</a>]
108 </span>
108 </span>
109 {% endif %}
109 {% endif %}
110 </div>
110 </div>
111 {% autoescape off %}
111 {% autoescape off %}
112 {{ thread.op.text.rendered|truncatewords_html:50 }}
112 {{ thread.op.text.rendered|truncatewords_html:50 }}
113 {% endautoescape %}
113 {% endautoescape %}
114 {% if thread.op.is_referenced %}
114 {% if thread.op.is_referenced %}
115 <div class="refmap">
115 <div class="refmap">
116 {% trans "Replies" %}:
116 {% trans "Replies" %}:
117 {% for ref_post in thread.op.get_sorted_referenced_posts %}
117 {% for ref_post in thread.op.get_sorted_referenced_posts %}
118 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
118 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
119 >{% if not forloop.last %},{% endif %}
119 >{% if not forloop.last %},{% endif %}
120 {% endfor %}
120 {% endfor %}
121 </div>
121 </div>
122 {% endif %}
122 {% endif %}
123 </div>
123 </div>
124 <div class="metadata">
124 <div class="metadata">
125 {{ thread.thread.get_images_count }} {% trans 'images' %}.
125 {{ thread.thread.get_images_count }} {% trans 'images' %}.
126 {% if thread.thread.tags %}
126 {% if thread.thread.tags %}
127 <span class="tags">
127 <span class="tags">
128 {% for tag in thread.thread.get_tags %}
128 {% for tag in thread.thread.get_tags %}
129 <a class="tag" href="
129 <a class="tag" href="
130 {% url 'tag' tag_name=tag.name %}">
130 {% url 'tag' tag_name=tag.name %}">
131 #{{ tag.name }}</a
131 #{{ tag.name }}</a
132 >{% if not forloop.last %},{% endif %}
132 >{% if not forloop.last %},{% endif %}
133 {% endfor %}
133 {% endfor %}
134 </span>
134 </span>
135 {% endif %}
135 {% endif %}
136 </div>
136 </div>
137 </div>
137 </div>
138 {% if thread.last_replies.exists %}
138 {% if thread.last_replies.exists %}
139 {% if thread.skipped_replies %}
139 {% if thread.skipped_replies %}
140 <div class="skipped_replies">
140 <div class="skipped_replies">
141 <a href="{% url 'thread' thread.op.id %}">
141 <a href="{% url 'thread' thread.op.id %}">
142 {% blocktrans with count=thread.skipped_replies %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
142 {% blocktrans with count=thread.skipped_replies %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
143 </a>
143 </a>
144 </div>
144 </div>
145 {% endif %}
145 {% endif %}
146 <div class="last-replies">
146 <div class="last-replies">
147 {% for post in thread.last_replies %}
147 {% for post in thread.last_replies %}
148 {% if thread.bumpable %}
148 {% if thread.bumpable %}
149 <div class="post" id="{{ post.id }}">
149 <div class="post" id="{{ post.id }}">
150 {% else %}
150 {% else %}
151 <div class="post dead_post" id="{{ post.id }}">
151 <div class="post dead_post" id="{{ post.id }}">
152 {% endif %}
152 {% endif %}
153 {% if post.image %}
153 {% if post.image %}
154 <div class="image">
154 <div class="image">
155 <a class="thumb"
155 <a class="thumb"
156 href="{{ post.image.url }}"><img
156 href="{{ post.image.url }}"><img
157 src=" {{ post.image.url_200x150 }}"
157 src=" {{ post.image.url_200x150 }}"
158 alt="{{ post.id }}"
158 alt="{{ post.id }}"
159 width="{{ post.image_pre_width }}"
159 width="{{ post.image_pre_width }}"
160 height="{{ post.image_pre_height }}"
160 height="{{ post.image_pre_height }}"
161 data-width="{{ post.image_width }}"
161 data-width="{{ post.image_width }}"
162 data-height="{{ post.image_height }}"/>
162 data-height="{{ post.image_height }}"/>
163 </a>
163 </a>
164 </div>
164 </div>
165 {% endif %}
165 {% endif %}
166 <div class="message">
166 <div class="message">
167 <div class="post-info">
167 <div class="post-info">
168 <span class="title">{{ post.title }}</span>
168 <span class="title">{{ post.title }}</span>
169 <a class="post_id" href="
169 <a class="post_id" href="
170 {% url 'thread' thread.op.id %}#{{ post.id }}">
170 {% url 'thread' thread.op.id %}#{{ post.id }}">
171 ({{ post.id }})</a>
171 ({{ post.id }})</a>
172 [{{ post.pub_time }}]
172 [{{ post.pub_time }}]
173 </div>
173 </div>
174 {% autoescape off %}
174 {% autoescape off %}
175 {{ post.text.rendered|truncatewords_html:50 }}
175 {{ post.text.rendered|truncatewords_html:50 }}
176 {% endautoescape %}
176 {% endautoescape %}
177 </div>
177 </div>
178 {% if post.is_referenced %}
178 {% if post.is_referenced %}
179 <div class="refmap">
179 <div class="refmap">
180 {% trans "Replies" %}:
180 {% trans "Replies" %}:
181 {% for ref_post in post.get_sorted_referenced_posts %}
181 {% for ref_post in post.get_sorted_referenced_posts %}
182 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
182 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
183 >{% if not forloop.last %},{% endif %}
183 >{% if not forloop.last %},{% endif %}
184 {% endfor %}
184 {% endfor %}
185 </div>
185 </div>
186 {% endif %}
186 {% endif %}
187 </div>
187 </div>
188 {% endfor %}
188 {% endfor %}
189 </div>
189 </div>
190 {% endif %}
190 {% endif %}
191 </div>
191 </div>
192 {% endcache %}
192 {% endcache %}
193 {% endfor %}
193 {% endfor %}
194
194
195 {% if next_page %}
195 {% if current_page.has_next %}
196 <div class="page_link">
196 <div class="page_link">
197 <a href="
197 <a href="
198 {% if tag %}
198 {% if tag %}
199 {% url "tag" tag_name=tag page=next_page %}
199 {% url "tag" tag_name=tag page=current_page.next_page_number %}
200 {% else %}
200 {% else %}
201 {% url "index" page=next_page %}
201 {% url "index" page=current_page.next_page_number %}
202 {% endif %}
202 {% endif %}
203 ">{% trans "Next page" %}</a>
203 ">{% trans "Next page" %}</a>
204 </div>
204 </div>
205 {% endif %}
205 {% endif %}
206 {% else %}
206 {% else %}
207 <div class="post">
207 <div class="post">
208 {% trans 'No threads exist. Create the first one!' %}</div>
208 {% trans 'No threads exist. Create the first one!' %}</div>
209 {% endif %}
209 {% endif %}
210
210
211 <div class="post-form-w">
211 <div class="post-form-w">
212 <script src="{% static 'js/panel.js' %}"></script>
212 <script src="{% static 'js/panel.js' %}"></script>
213 <div class="post-form">
213 <div class="post-form">
214 <div class="form-title">{% trans "Create new thread" %}</div>
214 <div class="form-title">{% trans "Create new thread" %}</div>
215 <form enctype="multipart/form-data" method="post">{% csrf_token %}
215 <form enctype="multipart/form-data" method="post">{% csrf_token %}
216 {{ form.as_div }}
216 {{ form.as_div }}
217 <div class="form-submit">
217 <div class="form-submit">
218 <input type="submit" value="{% trans "Post" %}"/>
218 <input type="submit" value="{% trans "Post" %}"/>
219 </div>
219 </div>
220 </form>
220 </form>
221 <div>
221 <div>
222 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
222 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
223 </div>
223 </div>
224 <div><a href="{% url "staticpage" name="help" %}">
224 <div><a href="{% url "staticpage" name="help" %}">
225 {% trans 'Text syntax' %}</a></div>
225 {% trans 'Text syntax' %}</a></div>
226 </div>
226 </div>
227 </div>
227 </div>
228
228
229 {% endblock %}
229 {% endblock %}
230
230
231 {% block metapanel %}
231 {% block metapanel %}
232
232
233 <span class="metapanel">
233 <span class="metapanel">
234 <b><a href="{% url "authors" %}">Neboard</a> 1.5 Aker</b>
234 <b><a href="{% url "authors" %}">Neboard</a> 1.6 Amon</b>
235 {% trans "Pages:" %}[
235 {% trans "Pages:" %}[
236 {% for page in pages %}
236 {% for page in paginator.page_range %}
237 <a
237 <a
238 {% ifequal page current_page %}
238 {% ifequal page current_page.number %}
239 class="current_page"
239 class="current_page"
240 {% endifequal %}
240 {% endifequal %}
241 href="
241 href="
242 {% if tag %}
242 {% if tag %}
243 {% url "tag" tag_name=tag page=page %}
243 {% url "tag" tag_name=tag page=page %}
244 {% else %}
244 {% else %}
245 {% url "index" page=page %}
245 {% url "index" page=page %}
246 {% endif %}
246 {% endif %}
247 ">{{ page }}</a>
247 ">{{ page }}</a>
248 {% if not forloop.last %},{% endif %}
248 {% if not forloop.last %},{% endif %}
249 {% endfor %}
249 {% endfor %}
250 ]
250 ]
251 [<a href="rss/">RSS</a>]
251 [<a href="rss/">RSS</a>]
252 </span>
252 </span>
253
253
254 {% endblock %}
254 {% endblock %}
@@ -1,41 +1,42 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4
4
5 {% block head %}
5 {% block head %}
6 <title>Neboard - {% trans "Tags" %}</title>
6 <title>Neboard - {% trans "Tags" %}</title>
7 {% endblock %}
7 {% endblock %}
8
8
9 {% block content %}
9 {% block content %}
10
10
11 <div class="post">
11 <div class="post">
12 {% if all_tags %}
12 {% if all_tags %}
13 {% for tag in all_tags %}
13 {% for tag in all_tags %}
14 {% if tag in user.fav_tags.all %}
14 <div class="tag_item">
15 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
15 {% if tag in user.fav_tags.all %}
16 class="fav">β˜…</a>
16 <a href="{% url 'tag_unsubscribe' tag.name %}?next={{ request.path }}"
17 {% else %}
17 class="fav">β˜…</a>
18 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
18 {% else %}
19 class="not_fav">β˜…</a>
19 <a href="{% url 'tag_subscribe' tag.name %}?next={{ request.path }}"
20 {% endif %}
20 class="not_fav">β˜…</a>
21 <a class="tag" href="{% url 'tag' tag.name %}"
21 {% endif %}
22 style="font-size: {{ tag.get_font_value }}em">
22 <a class="tag" href="{% url 'tag' tag.name %}"
23 #{{ tag.name }}</a>
23 style="font-size: {{ tag.get_font_value }}em">
24 ({{ tag.get_post_count }} {% trans 'threads' %})
24 #{{ tag.name }}</a>
25 {% if tag.linked %}
25 ({{ tag.get_post_count }} {% trans 'threads' %})
26 ( +
26 {% if tag.linked %}
27 {% for linked_tag in tag.get_linked_tags %}
27 ( +
28 <a class="tag" href="{% url 'tag' linked_tag.name %}">
28 {% for linked_tag in tag.get_linked_tags %}
29 #{{ linked_tag.name }}
29 <a class="tag" href="{% url 'tag' linked_tag.name %}">
30 </a>
30 #{{ linked_tag.name }}
31 {% endfor %}
31 </a>
32 )
32 {% endfor %}
33 {% endif %}
33 )
34 <br />
34 {% endif %}
35 {% endfor %}
35 </div>
36 {% else %}
36 {% endfor %}
37 {% trans 'No tags found.' %}
37 {% else %}
38 {% endif %}
38 {% trans 'No tags found.' %}
39 {% endif %}
39 </div>
40 </div>
40
41
41 {% endblock %}
42 {% endblock %}
@@ -1,136 +1,144 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load static from staticfiles %}
5 {% load static from staticfiles %}
6 {% load board %}
6 {% load board %}
7
7
8 {% block head %}
8 {% block head %}
9 <title>Neboard - {{ thread.get_opening_post.get_title }}</title>
9 <title>Neboard - {{ thread.get_opening_post.get_title|striptags }}</title>
10 {% endblock %}
10 {% endblock %}
11
11
12 {% block content %}
12 {% block content %}
13 {% spaceless %}
13 {% spaceless %}
14 {% get_current_language as LANGUAGE_CODE %}
14 {% get_current_language as LANGUAGE_CODE %}
15
15
16 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
16 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
17
17
18 <div class="image-mode-tab">
18 <div class="image-mode-tab">
19 <a class="current_mode" href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
19 <a class="current_mode" href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
20 <a href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
20 <a href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
21 </div>
21 </div>
22
22
23 {% if bumpable %}
23 {% if bumpable %}
24 <div class="bar-bg">
24 <div class="bar-bg">
25 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
25 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
26 </div>
26 </div>
27 <div class="bar-text">
27 <div class="bar-text">
28 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
28 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
29 </div>
29 </div>
30 </div>
30 </div>
31 {% endif %}
31 {% endif %}
32 <div class="thread">
32 <div class="thread">
33 {% for post in posts %}
33 {% for post in posts %}
34 {% if bumpable %}
34 {% if bumpable %}
35 <div class="post" id="{{ post.id }}">
35 <div class="post" id="{{ post.id }}">
36 {% elif thread.archived %}
37 <div class="post archive_post" id="{{ post.id }}">
36 {% else %}
38 {% else %}
37 <div class="post dead_post" id="{{ post.id }}">
39 <div class="post dead_post" id="{{ post.id }}">
38 {% endif %}
40 {% endif %}
39 {% if post.image %}
41 {% if post.image %}
40 <div class="image">
42 <div class="image">
41 <a
43 <a
42 class="thumb"
44 class="thumb"
43 href="{{ post.image.url }}"><img
45 href="{{ post.image.url }}"><img
44 src="{{ post.image.url_200x150 }}"
46 src="{{ post.image.url_200x150 }}"
45 alt="{{ post.id }}"
47 alt="{{ post.id }}"
46 width="{{ post.image_pre_width }}"
48 width="{{ post.image_pre_width }}"
47 height="{{ post.image_pre_height }}"
49 height="{{ post.image_pre_height }}"
48 data-width="{{ post.image_width }}"
50 data-width="{{ post.image_width }}"
49 data-height="{{ post.image_height }}"/>
51 data-height="{{ post.image_height }}"/>
50 </a>
52 </a>
51 </div>
53 </div>
52 {% endif %}
54 {% endif %}
53 <div class="message">
55 <div class="message">
54 <div class="post-info">
56 <div class="post-info">
55 <span class="title">{{ post.title }}</span>
57 <span class="title">{{ post.title }}</span>
56 <a class="post_id" href="#{{ post.id }}">
58 <a class="post_id" href="#{{ post.id }}">
57 ({{ post.id }})</a>
59 ({{ post.id }})</a>
58 [{{ post.pub_time }}]
60 [{{ post.pub_time }}]
61 {% if not thread.archived %}
59 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
62 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
60 ; return false;">&gt;&gt;</a>]
63 ; return false;">&gt;&gt;</a>]
64 {% endif %}
61
65
62 {% if moderator %}
66 {% if moderator %}
63 <span class="moderator_info">
67 <span class="moderator_info">
64 [<a href="{% url 'delete' post_id=post.id %}"
68 [<a href="{% url 'delete' post_id=post.id %}"
65 >{% trans 'Delete' %}</a>]
69 >{% trans 'Delete' %}</a>]
66 ({{ post.poster_ip }})
70 ({{ post.poster_ip }})
67 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
71 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
68 >{% trans 'Ban IP' %}</a>]
72 >{% trans 'Ban IP' %}</a>]
69 </span>
73 </span>
70 {% endif %}
74 {% endif %}
71 </div>
75 </div>
72 {% autoescape off %}
76 {% autoescape off %}
73 {{ post.text.rendered }}
77 {{ post.text.rendered }}
74 {% endautoescape %}
78 {% endautoescape %}
75 {% if post.is_referenced %}
79 {% if post.is_referenced %}
76 <div class="refmap">
80 <div class="refmap">
77 {% trans "Replies" %}:
81 {% trans "Replies" %}:
78 {% for ref_post in post.get_sorted_referenced_posts %}
82 {% for ref_post in post.get_sorted_referenced_posts %}
79 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
83 <a href="{% post_url ref_post.id %}">&gt;&gt;{{ ref_post.id }}</a
80 >{% if not forloop.last %},{% endif %}
84 >{% if not forloop.last %},{% endif %}
81 {% endfor %}
85 {% endfor %}
82 </div>
86 </div>
83 {% endif %}
87 {% endif %}
84 </div>
88 </div>
85 {% if forloop.first %}
89 {% if forloop.first %}
86 <div class="metadata">
90 <div class="metadata">
87 <span class="tags">
91 <span class="tags">
88 {% for tag in thread.get_tags %}
92 {% for tag in thread.get_tags %}
89 <a class="tag" href="{% url 'tag' tag.name %}">
93 <a class="tag" href="{% url 'tag' tag.name %}">
90 #{{ tag.name }}</a
94 #{{ tag.name }}</a
91 >{% if not forloop.last %},{% endif %}
95 >{% if not forloop.last %},{% endif %}
92 {% endfor %}
96 {% endfor %}
93 </span>
97 </span>
94 </div>
98 </div>
95 {% endif %}
99 {% endif %}
96 </div>
100 </div>
97 {% endfor %}
101 {% endfor %}
98 </div>
102 </div>
99 {% endcache %}
103 {% endcache %}
100
104
105 {% if not thread.archived %}
106
101 <div class="post-form-w">
107 <div class="post-form-w">
102 <script src="{% static 'js/panel.js' %}"></script>
108 <script src="{% static 'js/panel.js' %}"></script>
103 <div class="form-title">{% trans "Reply to thread" %} #{{ thread.get_opening_post.id }}</div>
109 <div class="form-title">{% trans "Reply to thread" %} #{{ thread.get_opening_post.id }}</div>
104 <div class="post-form">
110 <div class="post-form">
105 <form id="form" enctype="multipart/form-data" method="post"
111 <form id="form" enctype="multipart/form-data" method="post"
106 >{% csrf_token %}
112 >{% csrf_token %}
107 {{ form.as_div }}
113 {{ form.as_div }}
108 <div class="form-submit">
114 <div class="form-submit">
109 <input type="submit" value="{% trans "Post" %}"/>
115 <input type="submit" value="{% trans "Post" %}"/>
110 </div>
116 </div>
111 </form>
117 </form>
112 <div><a href="{% url "staticpage" name="help" %}">
118 <div><a href="{% url "staticpage" name="help" %}">
113 {% trans 'Text syntax' %}</a></div>
119 {% trans 'Text syntax' %}</a></div>
114 </div>
120 </div>
115 </div>
121 </div>
116
122
117 <script src="{% static 'js/thread_update.js' %}"></script>
123 <script src="{% static 'js/thread_update.js' %}"></script>
124 {% endif %}
125
118 <script src="{% static 'js/thread.js' %}"></script>
126 <script src="{% static 'js/thread.js' %}"></script>
119
127
120 {% endspaceless %}
128 {% endspaceless %}
121 {% endblock %}
129 {% endblock %}
122
130
123 {% block metapanel %}
131 {% block metapanel %}
124
132
125 {% get_current_language as LANGUAGE_CODE %}
133 {% get_current_language as LANGUAGE_CODE %}
126
134
127 <span class="metapanel" data-last-update="{{ last_update }}">
135 <span class="metapanel" data-last-update="{{ last_update }}">
128 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
136 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
129 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
137 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
130 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
138 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
131 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
139 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
132 [<a href="rss/">RSS</a>]
140 [<a href="rss/">RSS</a>]
133 {% endcache %}
141 {% endcache %}
134 </span>
142 </span>
135
143
136 {% endblock %}
144 {% endblock %}
@@ -1,64 +1,64 b''
1 {% extends "boards/base.html" %}
1 {% extends "boards/base.html" %}
2
2
3 {% load i18n %}
3 {% load i18n %}
4 {% load cache %}
4 {% load cache %}
5 {% load static from staticfiles %}
5 {% load static from staticfiles %}
6 {% load board %}
6 {% load board %}
7
7
8 {% block head %}
8 {% block head %}
9 <title>Neboard - {{ thread.get_opening_post.get_title }}</title>
9 <title>Neboard - {{ thread.get_opening_post.get_title|striptags }}</title>
10 {% endblock %}
10 {% endblock %}
11
11
12 {% block content %}
12 {% block content %}
13 {% spaceless %}
13 {% spaceless %}
14 {% get_current_language as LANGUAGE_CODE %}
14 {% get_current_language as LANGUAGE_CODE %}
15
15
16 <script src="{% static 'js/thread.js' %}"></script>
16 <script src="{% static 'js/thread.js' %}"></script>
17
17
18 {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE request.get_host %}
18 {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE request.get_host %}
19 <div class="image-mode-tab">
19 <div class="image-mode-tab">
20 <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
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>
21 <a class="current_mode" href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
22 </div>
22 </div>
23
23
24 <div id="posts-table">
24 <div id="posts-table">
25 {% for post in posts %}
25 {% for post in posts %}
26 <div class="gallery_image">
26 <div class="gallery_image">
27 <div>
27 <div>
28 <a
28 <a
29 class="thumb"
29 class="thumb"
30 href="{{ post.image.url }}"><img
30 href="{{ post.image.url }}"><img
31 src="{{ post.image.url_200x150 }}"
31 src="{{ post.image.url_200x150 }}"
32 alt="{{ post.id }}"
32 alt="{{ post.id }}"
33 width="{{ post.image_pre_width }}"
33 width="{{ post.image_pre_width }}"
34 height="{{ post.image_pre_height }}"
34 height="{{ post.image_pre_height }}"
35 data-width="{{ post.image_width }}"
35 data-width="{{ post.image_width }}"
36 data-height="{{ post.image_height }}"/>
36 data-height="{{ post.image_height }}"/>
37 </a>
37 </a>
38 </div>
38 </div>
39 <div class="gallery_image_metadata">
39 <div class="gallery_image_metadata">
40 {{ post.image_width }}x{{ post.image_height }}
40 {{ post.image_width }}x{{ post.image_height }}
41 {% image_actions post.image.url request.get_host %}
41 {% image_actions post.image.url request.get_host %}
42 </div>
42 </div>
43 </div>
43 </div>
44 {% endfor %}
44 {% endfor %}
45 </div>
45 </div>
46 {% endcache %}
46 {% endcache %}
47
47
48 {% endspaceless %}
48 {% endspaceless %}
49 {% endblock %}
49 {% endblock %}
50
50
51 {% block metapanel %}
51 {% block metapanel %}
52
52
53 {% get_current_language as LANGUAGE_CODE %}
53 {% get_current_language as LANGUAGE_CODE %}
54
54
55 <span class="metapanel" data-last-update="{{ last_update }}">
55 <span class="metapanel" data-last-update="{{ last_update }}">
56 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
56 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
57 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
57 <span id="reply-count">{{ thread.get_reply_count }}</span> {% trans 'replies' %},
58 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
58 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
59 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
59 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
60 [<a href="rss/">RSS</a>]
60 [<a href="rss/">RSS</a>]
61 {% endcache %}
61 {% endcache %}
62 </span>
62 </span>
63
63
64 {% endblock %}
64 {% endblock %}
@@ -1,233 +1,233 b''
1 # coding=utf-8
1 # coding=utf-8
2 from django.test import TestCase
2 from django.test import TestCase
3 from django.test.client import Client
3 from django.test.client import Client
4 import time
4 import time
5
5
6 from boards.models import Post, Tag
6 from boards.models import Post, Tag
7 from neboard import settings
7 from neboard import settings
8
8
9 PAGE_404 = 'boards/404.html'
9 PAGE_404 = 'boards/404.html'
10
10
11 TEST_TEXT = 'test text'
11 TEST_TEXT = 'test text'
12
12
13 NEW_THREAD_PAGE = '/'
13 NEW_THREAD_PAGE = '/'
14 THREAD_PAGE_ONE = '/thread/1/'
14 THREAD_PAGE_ONE = '/thread/1/'
15 THREAD_PAGE = '/thread/'
15 THREAD_PAGE = '/thread/'
16 TAG_PAGE = '/tag/'
16 TAG_PAGE = '/tag/'
17 HTTP_CODE_REDIRECT = 302
17 HTTP_CODE_REDIRECT = 302
18 HTTP_CODE_OK = 200
18 HTTP_CODE_OK = 200
19 HTTP_CODE_NOT_FOUND = 404
19 HTTP_CODE_NOT_FOUND = 404
20
20
21
21
22 class PostTests(TestCase):
22 class PostTests(TestCase):
23
23
24 def _create_post(self):
24 def _create_post(self):
25 return Post.objects.create_post(title='title',
25 return Post.objects.create_post(title='title',
26 text='text')
26 text='text')
27
27
28 def test_post_add(self):
28 def test_post_add(self):
29 """Test adding post"""
29 """Test adding post"""
30
30
31 post = self._create_post()
31 post = self._create_post()
32
32
33 self.assertIsNotNone(post, 'No post was created')
33 self.assertIsNotNone(post, 'No post was created')
34
34
35 def test_delete_post(self):
35 def test_delete_post(self):
36 """Test post deletion"""
36 """Test post deletion"""
37
37
38 post = self._create_post()
38 post = self._create_post()
39 post_id = post.id
39 post_id = post.id
40
40
41 Post.objects.delete_post(post)
41 Post.objects.delete_post(post)
42
42
43 self.assertFalse(Post.objects.filter(id=post_id).exists())
43 self.assertFalse(Post.objects.filter(id=post_id).exists())
44
44
45 def test_delete_thread(self):
45 def test_delete_thread(self):
46 """Test thread deletion"""
46 """Test thread deletion"""
47
47
48 opening_post = self._create_post()
48 opening_post = self._create_post()
49 thread = opening_post.thread_new
49 thread = opening_post.thread_new
50 reply = Post.objects.create_post("", "", thread=thread)
50 reply = Post.objects.create_post("", "", thread=thread)
51
51
52 thread.delete_with_posts()
52 thread.delete_with_posts()
53
53
54 self.assertFalse(Post.objects.filter(id=reply.id).exists())
54 self.assertFalse(Post.objects.filter(id=reply.id).exists())
55
55
56 def test_post_to_thread(self):
56 def test_post_to_thread(self):
57 """Test adding post to a thread"""
57 """Test adding post to a thread"""
58
58
59 op = self._create_post()
59 op = self._create_post()
60 post = Post.objects.create_post("", "", thread=op.thread_new)
60 post = Post.objects.create_post("", "", thread=op.thread_new)
61
61
62 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
62 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
63 self.assertEqual(op.thread_new.last_edit_time, post.pub_time,
63 self.assertEqual(op.thread_new.last_edit_time, post.pub_time,
64 'Post\'s create time doesn\'t match thread last edit'
64 'Post\'s create time doesn\'t match thread last edit'
65 ' time')
65 ' time')
66
66
67 def test_delete_posts_by_ip(self):
67 def test_delete_posts_by_ip(self):
68 """Test deleting posts with the given ip"""
68 """Test deleting posts with the given ip"""
69
69
70 post = self._create_post()
70 post = self._create_post()
71 post_id = post.id
71 post_id = post.id
72
72
73 Post.objects.delete_posts_by_ip('0.0.0.0')
73 Post.objects.delete_posts_by_ip('0.0.0.0')
74
74
75 self.assertFalse(Post.objects.filter(id=post_id).exists())
75 self.assertFalse(Post.objects.filter(id=post_id).exists())
76
76
77 def test_get_thread(self):
77 def test_get_thread(self):
78 """Test getting all posts of a thread"""
78 """Test getting all posts of a thread"""
79
79
80 opening_post = self._create_post()
80 opening_post = self._create_post()
81
81
82 for i in range(0, 2):
82 for i in range(0, 2):
83 Post.objects.create_post('title', 'text',
83 Post.objects.create_post('title', 'text',
84 thread=opening_post.thread_new)
84 thread=opening_post.thread_new)
85
85
86 thread = opening_post.thread_new
86 thread = opening_post.thread_new
87
87
88 self.assertEqual(3, thread.replies.count())
88 self.assertEqual(3, thread.replies.count())
89
89
90 def test_create_post_with_tag(self):
90 def test_create_post_with_tag(self):
91 """Test adding tag to post"""
91 """Test adding tag to post"""
92
92
93 tag = Tag.objects.create(name='test_tag')
93 tag = Tag.objects.create(name='test_tag')
94 post = Post.objects.create_post(title='title', text='text', tags=[tag])
94 post = Post.objects.create_post(title='title', text='text', tags=[tag])
95
95
96 thread = post.thread_new
96 thread = post.thread_new
97 self.assertIsNotNone(post, 'Post not created')
97 self.assertIsNotNone(post, 'Post not created')
98 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
98 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
99 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
99 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
100
100
101 def test_thread_max_count(self):
101 def test_thread_max_count(self):
102 """Test deletion of old posts when the max thread count is reached"""
102 """Test deletion of old posts when the max thread count is reached"""
103
103
104 for i in range(settings.MAX_THREAD_COUNT + 1):
104 for i in range(settings.MAX_THREAD_COUNT + 1):
105 self._create_post()
105 self._create_post()
106
106
107 self.assertEqual(settings.MAX_THREAD_COUNT,
107 self.assertEqual(settings.MAX_THREAD_COUNT,
108 len(Post.objects.get_threads()))
108 len(Post.objects.get_threads()))
109
109
110 def test_pages(self):
110 def test_pages(self):
111 """Test that the thread list is properly split into pages"""
111 """Test that the thread list is properly split into pages"""
112
112
113 for i in range(settings.MAX_THREAD_COUNT):
113 for i in range(settings.MAX_THREAD_COUNT):
114 self._create_post()
114 self._create_post()
115
115
116 all_threads = Post.objects.get_threads()
116 all_threads = Post.objects.get_threads()
117
117
118 posts_in_second_page = Post.objects.get_threads(page=1)
118 posts_in_second_page = Post.objects.get_threads(page=2)
119 first_post = posts_in_second_page[0]
119 first_post = posts_in_second_page[0]
120
120
121 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
121 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
122 first_post.id)
122 first_post.id)
123
123
124 def test_linked_tag(self):
124 def test_linked_tag(self):
125 """Test adding a linked tag"""
125 """Test adding a linked tag"""
126
126
127 linked_tag = Tag.objects.create(name=u'tag1')
127 linked_tag = Tag.objects.create(name=u'tag1')
128 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
128 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
129
129
130 post = Post.objects.create_post("", "", tags=[tag])
130 post = Post.objects.create_post("", "", tags=[tag])
131
131
132 self.assertTrue(linked_tag in post.thread_new.tags.all(),
132 self.assertTrue(linked_tag in post.thread_new.tags.all(),
133 'Linked tag was not added')
133 'Linked tag was not added')
134
134
135
135
136 class PagesTest(TestCase):
136 class PagesTest(TestCase):
137
137
138 def test_404(self):
138 def test_404(self):
139 """Test receiving error 404 when opening a non-existent page"""
139 """Test receiving error 404 when opening a non-existent page"""
140
140
141 tag_name = u'test_tag'
141 tag_name = u'test_tag'
142 tag = Tag.objects.create(name=tag_name)
142 tag = Tag.objects.create(name=tag_name)
143 client = Client()
143 client = Client()
144
144
145 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
145 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
146
146
147 existing_post_id = Post.objects.all()[0].id
147 existing_post_id = Post.objects.all()[0].id
148 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
148 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
149 '/')
149 '/')
150 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
150 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
151 u'Cannot open existing thread')
151 u'Cannot open existing thread')
152
152
153 response_not_existing = client.get(THREAD_PAGE + str(
153 response_not_existing = client.get(THREAD_PAGE + str(
154 existing_post_id + 1) + '/')
154 existing_post_id + 1) + '/')
155 self.assertEqual(PAGE_404,
155 self.assertEqual(PAGE_404,
156 response_not_existing.templates[0].name,
156 response_not_existing.templates[0].name,
157 u'Not existing thread is opened')
157 u'Not existing thread is opened')
158
158
159 response_existing = client.get(TAG_PAGE + tag_name + '/')
159 response_existing = client.get(TAG_PAGE + tag_name + '/')
160 self.assertEqual(HTTP_CODE_OK,
160 self.assertEqual(HTTP_CODE_OK,
161 response_existing.status_code,
161 response_existing.status_code,
162 u'Cannot open existing tag')
162 u'Cannot open existing tag')
163
163
164 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
164 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
165 self.assertEqual(PAGE_404,
165 self.assertEqual(PAGE_404,
166 response_not_existing.templates[0].name,
166 response_not_existing.templates[0].name,
167 u'Not existing tag is opened')
167 u'Not existing tag is opened')
168
168
169 reply_id = Post.objects.create_post('', TEST_TEXT,
169 reply_id = Post.objects.create_post('', TEST_TEXT,
170 thread=Post.objects.all()[0]
170 thread=Post.objects.all()[0]
171 .thread)
171 .thread)
172 response_not_existing = client.get(THREAD_PAGE + str(
172 response_not_existing = client.get(THREAD_PAGE + str(
173 reply_id) + '/')
173 reply_id) + '/')
174 self.assertEqual(PAGE_404,
174 self.assertEqual(PAGE_404,
175 response_not_existing.templates[0].name,
175 response_not_existing.templates[0].name,
176 u'Reply is opened as a thread')
176 u'Reply is opened as a thread')
177
177
178
178
179 class FormTest(TestCase):
179 class FormTest(TestCase):
180 def test_post_validation(self):
180 def test_post_validation(self):
181 # Disable captcha for the test
181 # Disable captcha for the test
182 captcha_enabled = settings.ENABLE_CAPTCHA
182 captcha_enabled = settings.ENABLE_CAPTCHA
183 settings.ENABLE_CAPTCHA = False
183 settings.ENABLE_CAPTCHA = False
184
184
185 client = Client()
185 client = Client()
186
186
187 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
187 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
188 invalid_tags = u'$%_356 ---'
188 invalid_tags = u'$%_356 ---'
189
189
190 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
190 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
191 'text': TEST_TEXT,
191 'text': TEST_TEXT,
192 'tags': valid_tags})
192 'tags': valid_tags})
193 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
193 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
194 msg='Posting new message failed: got code ' +
194 msg='Posting new message failed: got code ' +
195 str(response.status_code))
195 str(response.status_code))
196
196
197 self.assertEqual(1, Post.objects.count(),
197 self.assertEqual(1, Post.objects.count(),
198 msg='No posts were created')
198 msg='No posts were created')
199
199
200 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
200 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
201 'tags': invalid_tags})
201 'tags': invalid_tags})
202 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
202 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
203 'where it should fail')
203 'where it should fail')
204
204
205 # Change posting delay so we don't have to wait for 30 seconds or more
205 # Change posting delay so we don't have to wait for 30 seconds or more
206 old_posting_delay = settings.POSTING_DELAY
206 old_posting_delay = settings.POSTING_DELAY
207 # Wait fot the posting delay or we won't be able to post
207 # Wait fot the posting delay or we won't be able to post
208 settings.POSTING_DELAY = 1
208 settings.POSTING_DELAY = 1
209 time.sleep(settings.POSTING_DELAY + 1)
209 time.sleep(settings.POSTING_DELAY + 1)
210 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
210 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
211 'tags': valid_tags})
211 'tags': valid_tags})
212 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
212 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
213 msg=u'Posting new message failed: got code ' +
213 msg=u'Posting new message failed: got code ' +
214 str(response.status_code))
214 str(response.status_code))
215 # Restore posting delay
215 # Restore posting delay
216 settings.POSTING_DELAY = old_posting_delay
216 settings.POSTING_DELAY = old_posting_delay
217
217
218 self.assertEqual(2, Post.objects.count(),
218 self.assertEqual(2, Post.objects.count(),
219 msg=u'No posts were created')
219 msg=u'No posts were created')
220
220
221 # Restore captcha setting
221 # Restore captcha setting
222 settings.ENABLE_CAPTCHA = captcha_enabled
222 settings.ENABLE_CAPTCHA = captcha_enabled
223
223
224
224
225 class ViewTest(TestCase):
225 class ViewTest(TestCase):
226 def test_index(self):
226 def test_index(self):
227 client = Client()
227 client = Client()
228
228
229 response = client.get('/')
229 response = client.get('/')
230 self.assertEqual(HTTP_CODE_OK, response.status_code, 'Index page not '
230 self.assertEqual(HTTP_CODE_OK, response.status_code, 'Index page not '
231 'opened')
231 'opened')
232 self.assertEqual('boards/posting_general.html', response.templates[0]
232 self.assertEqual('boards/posting_general.html', response.templates[0]
233 .name, 'Index page should open posting_general template') No newline at end of file
233 .name, 'Index page should open posting_general template')
@@ -1,60 +1,69 b''
1 from django.conf.urls import patterns, url, include
1 from django.conf.urls import patterns, url, include
2 from boards import views
2 from boards import views
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 from boards.views.api import api_get_threaddiff
4 from boards.views import api
5
5
6 js_info_dict = {
6 js_info_dict = {
7 'packages': ('boards',),
7 'packages': ('boards',),
8 }
8 }
9
9
10 urlpatterns = patterns('',
10 urlpatterns = patterns('',
11
11
12 # /boards/
12 # /boards/
13 url(r'^$', views.index, name='index'),
13 url(r'^$', views.index, name='index'),
14 # /boards/page/
14 # /boards/page/
15 url(r'^page/(?P<page>\w+)/$', views.index, name='index'),
15 url(r'^page/(?P<page>\w+)/$', views.index, name='index'),
16
16
17 url(r'^archive/$', views.archive, name='archive'),
18 url(r'^archive/page/(?P<page>\w+)/$', views.archive, name='archive'),
19
17 # login page
20 # login page
18 url(r'^login/$', views.login, name='login'),
21 url(r'^login/$', views.login, name='login'),
19
22
20 # /boards/tag/tag_name/
23 # /boards/tag/tag_name/
21 url(r'^tag/(?P<tag_name>\w+)/$', views.tag, name='tag'),
24 url(r'^tag/(?P<tag_name>\w+)/$', views.tag, name='tag'),
22 # /boards/tag/tag_id/page/
25 # /boards/tag/tag_id/page/
23 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', views.tag, name='tag'),
26 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', views.tag, name='tag'),
24
27
25 # /boards/tag/tag_name/unsubscribe/
28 # /boards/tag/tag_name/unsubscribe/
26 url(r'^tag/(?P<tag_name>\w+)/subscribe/$', views.tag_subscribe,
29 url(r'^tag/(?P<tag_name>\w+)/subscribe/$', views.tag_subscribe,
27 name='tag_subscribe'),
30 name='tag_subscribe'),
28 # /boards/tag/tag_name/unsubscribe/
31 # /boards/tag/tag_name/unsubscribe/
29 url(r'^tag/(?P<tag_name>\w+)/unsubscribe/$', views.tag_unsubscribe,
32 url(r'^tag/(?P<tag_name>\w+)/unsubscribe/$', views.tag_unsubscribe,
30 name='tag_unsubscribe'),
33 name='tag_unsubscribe'),
31
34
32 # /boards/thread/
35 # /boards/thread/
33 url(r'^thread/(?P<post_id>\w+)/$', views.thread, name='thread'),
36 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'),
37 url(r'^thread/(?P<post_id>\w+)/(?P<mode>\w+)/$', views.thread, name='thread_mode'),
35 url(r'^settings/$', views.settings, name='settings'),
38 url(r'^settings/$', views.settings, name='settings'),
36 url(r'^tags/$', views.all_tags, name='tags'),
39 url(r'^tags/$', views.all_tags, name='tags'),
37 url(r'^captcha/', include('captcha.urls')),
40 url(r'^captcha/', include('captcha.urls')),
38 url(r'^jump/(?P<post_id>\w+)/$', views.jump_to_post, name='jumper'),
41 url(r'^jump/(?P<post_id>\w+)/$', views.jump_to_post, name='jumper'),
39 url(r'^authors/$', views.authors, name='authors'),
42 url(r'^authors/$', views.authors, name='authors'),
40 url(r'^delete/(?P<post_id>\w+)/$', views.delete, name='delete'),
43 url(r'^delete/(?P<post_id>\w+)/$', views.delete, name='delete'),
41 url(r'^ban/(?P<post_id>\w+)/$', views.ban, name='ban'),
44 url(r'^ban/(?P<post_id>\w+)/$', views.ban, name='ban'),
42
45
43 url(r'^banned/$', views.you_are_banned, name='banned'),
46 url(r'^banned/$', views.you_are_banned, name='banned'),
44 url(r'^staticpage/(?P<name>\w+)/$', views.static_page, name='staticpage'),
47 url(r'^staticpage/(?P<name>\w+)/$', views.static_page, name='staticpage'),
45
48
46 # RSS feeds
49 # RSS feeds
47 url(r'^rss/$', AllThreadsFeed()),
50 url(r'^rss/$', AllThreadsFeed()),
48 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
51 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
49 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
52 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
50 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
53 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
51 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
54 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
52
55
53 # i18n
56 # i18n
54 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, name='js_info_dict'),
57 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict, name='js_info_dict'),
55
58
56 # API
59 # API
57 url(r'^api/post/(?P<post_id>\w+)/$', views.get_post, name="get_post"),
60 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
58 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
61 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
59 api_get_threaddiff, name="get_thread_diff"),
62 api.api_get_threaddiff, name="get_thread_diff"),
63 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
64 name='get_threads'),
65 url(r'api/tags/$', api.api_get_tags, name='get_tags'),
66 url(r'api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
67 name='get_thread'),
68
60 )
69 )
@@ -1,564 +1,605 b''
1 from datetime import datetime, timedelta
2
3 from django.db.models import Count
4
5
6 OLD_USER_AGE_DAYS = 90
7
1 __author__ = 'neko259'
8 __author__ = 'neko259'
2
9
3 import hashlib
10 import hashlib
4 import string
11 import string
5 import time
12 import time
6 import re
13 import re
7
14
8 from django.core import serializers
15 from django.core import serializers
9 from django.core.urlresolvers import reverse
16 from django.core.urlresolvers import reverse
10 from django.http import HttpResponseRedirect, Http404
17 from django.http import HttpResponseRedirect, Http404
11 from django.http.response import HttpResponse
18 from django.http.response import HttpResponse
12 from django.template import RequestContext
19 from django.template import RequestContext
13 from django.shortcuts import render, redirect, get_object_or_404
20 from django.shortcuts import render, redirect, get_object_or_404
14 from django.utils import timezone
21 from django.utils import timezone
15 from django.db import transaction
22 from django.db import transaction
16 from django.views.decorators.cache import cache_page
23 from django.views.decorators.cache import cache_page
17 from django.views.i18n import javascript_catalog
24 from django.views.i18n import javascript_catalog
25 from django.core.paginator import Paginator
18
26
19 from boards import forms
27 from boards import forms
20 import boards
28 import boards
21 from boards import utils
29 from boards import utils
22 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
30 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
23 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
31 ThreadCaptchaForm, PostCaptchaForm, LoginForm, ModeratorSettingsForm
24 from boards.models import Post, Tag, Ban, User
32 from boards.models import Post, Tag, Ban, User, Thread
25 from boards.models.post import SETTING_MODERATE, REGEX_REPLY
33 from boards.models.post import SETTING_MODERATE, REGEX_REPLY
26 from boards.models.user import RANK_USER
34 from boards.models.user import RANK_USER
27 from boards import authors
35 from boards import authors
28 from boards.utils import get_client_ip
36 from boards.utils import get_client_ip
29 import neboard
37 import neboard
30
38
31
39
32 BAN_REASON_SPAM = 'Autoban: spam bot'
40 BAN_REASON_SPAM = 'Autoban: spam bot'
33 MODE_GALLERY = 'gallery'
41 MODE_GALLERY = 'gallery'
34 MODE_NORMAL = 'normal'
42 MODE_NORMAL = 'normal'
35
43
44 DEFAULT_PAGE = 1
36
45
37 def index(request, page=0):
46
47 def index(request, page=DEFAULT_PAGE):
38 context = _init_default_context(request)
48 context = _init_default_context(request)
39
49
40 if utils.need_include_captcha(request):
50 if utils.need_include_captcha(request):
41 threadFormClass = ThreadCaptchaForm
51 threadFormClass = ThreadCaptchaForm
42 kwargs = {'request': request}
52 kwargs = {'request': request}
43 else:
53 else:
44 threadFormClass = ThreadForm
54 threadFormClass = ThreadForm
45 kwargs = {}
55 kwargs = {}
46
56
47 if request.method == 'POST':
57 if request.method == 'POST':
48 form = threadFormClass(request.POST, request.FILES,
58 form = threadFormClass(request.POST, request.FILES,
49 error_class=PlainErrorList, **kwargs)
59 error_class=PlainErrorList, **kwargs)
50 form.session = request.session
60 form.session = request.session
51
61
52 if form.is_valid():
62 if form.is_valid():
53 return _new_post(request, form)
63 return _new_post(request, form)
54 if form.need_to_ban:
64 if form.need_to_ban:
55 # Ban user because he is suspected to be a bot
65 # Ban user because he is suspected to be a bot
56 _ban_current_user(request)
66 _ban_current_user(request)
57 else:
67 else:
58 form = threadFormClass(error_class=PlainErrorList, **kwargs)
68 form = threadFormClass(error_class=PlainErrorList, **kwargs)
59
69
60 threads = []
70 threads = []
61 for thread_to_show in Post.objects.get_threads(page=int(page)):
71 for thread_to_show in Post.objects.get_threads(page=int(page)):
62 threads.append(_get_template_thread(thread_to_show))
72 threads.append(_get_template_thread(thread_to_show))
63
73
64 # TODO Make this generic for tag and threads list pages
74 # TODO Make this generic for tag and threads list pages
65 context['threads'] = None if len(threads) == 0 else threads
75 context['threads'] = None if len(threads) == 0 else threads
66 context['form'] = form
76 context['form'] = form
67 context['current_page'] = int(page)
68
77
69 page_count = Post.objects.get_thread_page_count()
78 paginator = Paginator(Thread.objects.filter(archived=False),
70 context['pages'] = range(page_count)
79 neboard.settings.THREADS_PER_PAGE)
71 page = int(page)
80 _get_page_context(paginator, context, page)
72 if page < page_count - 1:
73 context['next_page'] = str(page + 1)
74 if page > 0:
75 context['prev_page'] = str(page - 1)
76
81
77 return render(request, 'boards/posting_general.html',
82 return render(request, 'boards/posting_general.html',
78 context)
83 context)
79
84
80
85
86 def archive(request, page=DEFAULT_PAGE):
87 """
88 Get archived posts
89 """
90
91 context = _init_default_context(request)
92
93 threads = []
94 for thread_to_show in Post.objects.get_threads(page=int(page),
95 archived=True):
96 threads.append(_get_template_thread(thread_to_show))
97
98 context['threads'] = threads
99
100 paginator = Paginator(Thread.objects.filter(archived=True),
101 neboard.settings.THREADS_PER_PAGE)
102 _get_page_context(paginator, context, page)
103
104 return render(request, 'boards/archive.html', context)
105
106
81 @transaction.atomic
107 @transaction.atomic
82 def _new_post(request, form, opening_post=None):
108 def _new_post(request, form, opening_post=None):
83 """Add a new post (in thread or as a reply)."""
109 """Add a new post (in thread or as a reply)."""
84
110
85 ip = get_client_ip(request)
111 ip = get_client_ip(request)
86 is_banned = Ban.objects.filter(ip=ip).exists()
112 is_banned = Ban.objects.filter(ip=ip).exists()
87
113
88 if is_banned:
114 if is_banned:
89 return redirect(you_are_banned)
115 return redirect(you_are_banned)
90
116
91 data = form.cleaned_data
117 data = form.cleaned_data
92
118
93 title = data['title']
119 title = data['title']
94 text = data['text']
120 text = data['text']
95
121
96 text = _remove_invalid_links(text)
122 text = _remove_invalid_links(text)
97
123
98 if 'image' in data.keys():
124 if 'image' in data.keys():
99 image = data['image']
125 image = data['image']
100 else:
126 else:
101 image = None
127 image = None
102
128
103 tags = []
129 tags = []
104
130
105 if not opening_post:
131 if not opening_post:
106 tag_strings = data['tags']
132 tag_strings = data['tags']
107
133
108 if tag_strings:
134 if tag_strings:
109 tag_strings = tag_strings.split(' ')
135 tag_strings = tag_strings.split(' ')
110 for tag_name in tag_strings:
136 for tag_name in tag_strings:
111 tag_name = string.lower(tag_name.strip())
137 tag_name = string.lower(tag_name.strip())
112 if len(tag_name) > 0:
138 if len(tag_name) > 0:
113 tag, created = Tag.objects.get_or_create(name=tag_name)
139 tag, created = Tag.objects.get_or_create(name=tag_name)
114 tags.append(tag)
140 tags.append(tag)
115 post_thread = None
141 post_thread = None
116 else:
142 else:
117 post_thread = opening_post.thread_new
143 post_thread = opening_post.thread_new
118
144
119 post = Post.objects.create_post(title=title, text=text, ip=ip,
145 post = Post.objects.create_post(title=title, text=text, ip=ip,
120 thread=post_thread, image=image,
146 thread=post_thread, image=image,
121 tags=tags, user=_get_user(request))
147 tags=tags, user=_get_user(request))
122
148
123 thread_to_show = (opening_post.id if opening_post else post.id)
149 thread_to_show = (opening_post.id if opening_post else post.id)
124
150
125 if opening_post:
151 if opening_post:
126 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
152 return redirect(reverse(thread, kwargs={'post_id': thread_to_show}) +
127 '#' + str(post.id))
153 '#' + str(post.id))
128 else:
154 else:
129 return redirect(thread, post_id=thread_to_show)
155 return redirect(thread, post_id=thread_to_show)
130
156
131
157
132 def tag(request, tag_name, page=0):
158 def tag(request, tag_name, page=DEFAULT_PAGE):
133 """
159 """
134 Get all tag threads. Threads are split in pages, so some page is
160 Get all tag threads. Threads are split in pages, so some page is
135 requested. Default page is 0.
161 requested.
136 """
162 """
137
163
138 tag = get_object_or_404(Tag, name=tag_name)
164 tag = get_object_or_404(Tag, name=tag_name)
139 threads = []
165 threads = []
140 for thread_to_show in Post.objects.get_threads(page=int(page), tag=tag):
166 for thread_to_show in Post.objects.get_threads(page=int(page), tag=tag):
141 threads.append(_get_template_thread(thread_to_show))
167 threads.append(_get_template_thread(thread_to_show))
142
168
143 if request.method == 'POST':
169 if request.method == 'POST':
144 form = ThreadForm(request.POST, request.FILES,
170 form = ThreadForm(request.POST, request.FILES,
145 error_class=PlainErrorList)
171 error_class=PlainErrorList)
146 form.session = request.session
172 form.session = request.session
147
173
148 if form.is_valid():
174 if form.is_valid():
149 return _new_post(request, form)
175 return _new_post(request, form)
150 if form.need_to_ban:
176 if form.need_to_ban:
151 # Ban user because he is suspected to be a bot
177 # Ban user because he is suspected to be a bot
152 _ban_current_user(request)
178 _ban_current_user(request)
153 else:
179 else:
154 form = forms.ThreadForm(initial={'tags': tag_name},
180 form = forms.ThreadForm(initial={'tags': tag_name},
155 error_class=PlainErrorList)
181 error_class=PlainErrorList)
156
182
157 context = _init_default_context(request)
183 context = _init_default_context(request)
158 context['threads'] = None if len(threads) == 0 else threads
184 context['threads'] = None if len(threads) == 0 else threads
159 context['tag'] = tag
185 context['tag'] = tag
160 context['current_page'] = int(page)
161
186
162 page_count = Post.objects.get_thread_page_count(tag=tag)
187 paginator = Paginator(Post.objects.get_threads(tag=tag),
163 context['pages'] = range(page_count)
188 neboard.settings.THREADS_PER_PAGE)
164 page = int(page)
189 _get_page_context(paginator, context, page)
165 if page < page_count - 1:
166 context['next_page'] = str(page + 1)
167 if page > 0:
168 context['prev_page'] = str(page - 1)
169
190
170 context['form'] = form
191 context['form'] = form
171
192
172 return render(request, 'boards/posting_general.html',
193 return render(request, 'boards/posting_general.html',
173 context)
194 context)
174
195
175
196
176 def thread(request, post_id, mode=MODE_NORMAL):
197 def thread(request, post_id, mode=MODE_NORMAL):
177 """Get all thread posts"""
198 """Get all thread posts"""
178
199
179 if utils.need_include_captcha(request):
200 if utils.need_include_captcha(request):
180 postFormClass = PostCaptchaForm
201 postFormClass = PostCaptchaForm
181 kwargs = {'request': request}
202 kwargs = {'request': request}
182 else:
203 else:
183 postFormClass = PostForm
204 postFormClass = PostForm
184 kwargs = {}
205 kwargs = {}
185
206
186 opening_post = get_object_or_404(Post, id=post_id)
207 opening_post = get_object_or_404(Post, id=post_id)
187 if request.method == 'POST':
208
209 # If this is not OP, don't show it as it is
210 if not opening_post.is_opening():
211 raise Http404
212
213 if request.method == 'POST' and not opening_post.thread_new.archived:
188 form = postFormClass(request.POST, request.FILES,
214 form = postFormClass(request.POST, request.FILES,
189 error_class=PlainErrorList, **kwargs)
215 error_class=PlainErrorList, **kwargs)
190 form.session = request.session
216 form.session = request.session
191
217
192 if form.is_valid():
218 if form.is_valid():
193 return _new_post(request, form, opening_post)
219 return _new_post(request, form, opening_post)
194 if form.need_to_ban:
220 if form.need_to_ban:
195 # Ban user because he is suspected to be a bot
221 # Ban user because he is suspected to be a bot
196 _ban_current_user(request)
222 _ban_current_user(request)
197 else:
223 else:
198 form = postFormClass(error_class=PlainErrorList, **kwargs)
224 form = postFormClass(error_class=PlainErrorList, **kwargs)
199
225
200 thread_to_show = opening_post.thread_new
226 thread_to_show = opening_post.thread_new
201
227
202 context = _init_default_context(request)
228 context = _init_default_context(request)
203
229
204 posts = thread_to_show.get_replies()
230 posts = thread_to_show.get_replies()
205 context['form'] = form
231 context['form'] = form
206 context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time)
232 context["last_update"] = _datetime_to_epoch(thread_to_show.last_edit_time)
207 context["thread"] = thread_to_show
233 context["thread"] = thread_to_show
208
234
209 if MODE_NORMAL == mode:
235 if MODE_NORMAL == mode:
210 context['bumpable'] = thread_to_show.can_bump()
236 context['bumpable'] = thread_to_show.can_bump()
211 if context['bumpable']:
237 if context['bumpable']:
212 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts \
238 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD - posts \
213 .count()
239 .count()
214 context['bumplimit_progress'] = str(
240 context['bumplimit_progress'] = str(
215 float(context['posts_left']) /
241 float(context['posts_left']) /
216 neboard.settings.MAX_POSTS_PER_THREAD * 100)
242 neboard.settings.MAX_POSTS_PER_THREAD * 100)
217
243
218 context['posts'] = posts
244 context['posts'] = posts
219
245
220 document = 'boards/thread.html'
246 document = 'boards/thread.html'
221 elif MODE_GALLERY == mode:
247 elif MODE_GALLERY == mode:
222 context['posts'] = posts.filter(image_width__gt=0)
248 context['posts'] = posts.filter(image_width__gt=0)
223
249
224 document = 'boards/thread_gallery.html'
250 document = 'boards/thread_gallery.html'
225 else:
251 else:
226 raise Http404
252 raise Http404
227
253
228 return render(request, document, context)
254 return render(request, document, context)
229
255
230
256
231 def login(request):
257 def login(request):
232 """Log in with user id"""
258 """Log in with user id"""
233
259
234 context = _init_default_context(request)
260 context = _init_default_context(request)
235
261
236 if request.method == 'POST':
262 if request.method == 'POST':
237 form = LoginForm(request.POST, request.FILES,
263 form = LoginForm(request.POST, request.FILES,
238 error_class=PlainErrorList)
264 error_class=PlainErrorList)
239 form.session = request.session
265 form.session = request.session
240
266
241 if form.is_valid():
267 if form.is_valid():
242 user = User.objects.get(user_id=form.cleaned_data['user_id'])
268 user = User.objects.get(user_id=form.cleaned_data['user_id'])
243 request.session['user_id'] = user.id
269 request.session['user_id'] = user.id
244 return redirect(index)
270 return redirect(index)
245
271
246 else:
272 else:
247 form = LoginForm()
273 form = LoginForm()
248
274
249 context['form'] = form
275 context['form'] = form
250
276
251 return render(request, 'boards/login.html', context)
277 return render(request, 'boards/login.html', context)
252
278
253
279
254 def settings(request):
280 def settings(request):
255 """User's settings"""
281 """User's settings"""
256
282
257 context = _init_default_context(request)
283 context = _init_default_context(request)
258 user = _get_user(request)
284 user = _get_user(request)
259 is_moderator = user.is_moderator()
285 is_moderator = user.is_moderator()
260
286
261 if request.method == 'POST':
287 if request.method == 'POST':
262 with transaction.atomic():
288 with transaction.atomic():
263 if is_moderator:
289 if is_moderator:
264 form = ModeratorSettingsForm(request.POST,
290 form = ModeratorSettingsForm(request.POST,
265 error_class=PlainErrorList)
291 error_class=PlainErrorList)
266 else:
292 else:
267 form = SettingsForm(request.POST, error_class=PlainErrorList)
293 form = SettingsForm(request.POST, error_class=PlainErrorList)
268
294
269 if form.is_valid():
295 if form.is_valid():
270 selected_theme = form.cleaned_data['theme']
296 selected_theme = form.cleaned_data['theme']
271
297
272 user.save_setting('theme', selected_theme)
298 user.save_setting('theme', selected_theme)
273
299
274 if is_moderator:
300 if is_moderator:
275 moderate = form.cleaned_data['moderate']
301 moderate = form.cleaned_data['moderate']
276 user.save_setting(SETTING_MODERATE, moderate)
302 user.save_setting(SETTING_MODERATE, moderate)
277
303
278 return redirect(settings)
304 return redirect(settings)
279 else:
305 else:
280 selected_theme = _get_theme(request)
306 selected_theme = _get_theme(request)
281
307
282 if is_moderator:
308 if is_moderator:
283 form = ModeratorSettingsForm(initial={'theme': selected_theme,
309 form = ModeratorSettingsForm(initial={'theme': selected_theme,
284 'moderate': context['moderator']},
310 'moderate': context['moderator']},
285 error_class=PlainErrorList)
311 error_class=PlainErrorList)
286 else:
312 else:
287 form = SettingsForm(initial={'theme': selected_theme},
313 form = SettingsForm(initial={'theme': selected_theme},
288 error_class=PlainErrorList)
314 error_class=PlainErrorList)
289
315
290 context['form'] = form
316 context['form'] = form
291
317
292 return render(request, 'boards/settings.html', context)
318 return render(request, 'boards/settings.html', context)
293
319
294
320
295 def all_tags(request):
321 def all_tags(request):
296 """All tags list"""
322 """All tags list"""
297
323
298 context = _init_default_context(request)
324 context = _init_default_context(request)
299 context['all_tags'] = Tag.objects.get_not_empty_tags()
325 context['all_tags'] = Tag.objects.get_not_empty_tags()
300
326
301 return render(request, 'boards/tags.html', context)
327 return render(request, 'boards/tags.html', context)
302
328
303
329
304 def jump_to_post(request, post_id):
330 def jump_to_post(request, post_id):
305 """Determine thread in which the requested post is and open it's page"""
331 """Determine thread in which the requested post is and open it's page"""
306
332
307 post = get_object_or_404(Post, id=post_id)
333 post = get_object_or_404(Post, id=post_id)
308
334
309 if not post.thread:
335 if not post.thread:
310 return redirect(thread, post_id=post.id)
336 return redirect(thread, post_id=post.id)
311 else:
337 else:
312 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
338 return redirect(reverse(thread, kwargs={'post_id': post.thread.id})
313 + '#' + str(post.id))
339 + '#' + str(post.id))
314
340
315
341
316 def authors(request):
342 def authors(request):
317 """Show authors list"""
343 """Show authors list"""
318
344
319 context = _init_default_context(request)
345 context = _init_default_context(request)
320 context['authors'] = boards.authors.authors
346 context['authors'] = boards.authors.authors
321
347
322 return render(request, 'boards/authors.html', context)
348 return render(request, 'boards/authors.html', context)
323
349
324
350
325 @transaction.atomic
351 @transaction.atomic
326 def delete(request, post_id):
352 def delete(request, post_id):
327 """Delete post"""
353 """Delete post"""
328
354
329 user = _get_user(request)
355 user = _get_user(request)
330 post = get_object_or_404(Post, id=post_id)
356 post = get_object_or_404(Post, id=post_id)
331
357
332 if user.is_moderator():
358 if user.is_moderator():
333 # TODO Show confirmation page before deletion
359 # TODO Show confirmation page before deletion
334 Post.objects.delete_post(post)
360 Post.objects.delete_post(post)
335
361
336 if not post.thread:
362 if not post.thread:
337 return _redirect_to_next(request)
363 return _redirect_to_next(request)
338 else:
364 else:
339 return redirect(thread, post_id=post.thread.id)
365 return redirect(thread, post_id=post.thread.id)
340
366
341
367
342 @transaction.atomic
368 @transaction.atomic
343 def ban(request, post_id):
369 def ban(request, post_id):
344 """Ban user"""
370 """Ban user"""
345
371
346 user = _get_user(request)
372 user = _get_user(request)
347 post = get_object_or_404(Post, id=post_id)
373 post = get_object_or_404(Post, id=post_id)
348
374
349 if user.is_moderator():
375 if user.is_moderator():
350 # TODO Show confirmation page before ban
376 # TODO Show confirmation page before ban
351 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
377 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
352 if created:
378 if created:
353 ban.reason = 'Banned for post ' + str(post_id)
379 ban.reason = 'Banned for post ' + str(post_id)
354 ban.save()
380 ban.save()
355
381
356 return _redirect_to_next(request)
382 return _redirect_to_next(request)
357
383
358
384
359 def you_are_banned(request):
385 def you_are_banned(request):
360 """Show the page that notifies that user is banned"""
386 """Show the page that notifies that user is banned"""
361
387
362 context = _init_default_context(request)
388 context = _init_default_context(request)
363
389
364 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
390 ban = get_object_or_404(Ban, ip=utils.get_client_ip(request))
365 context['ban_reason'] = ban.reason
391 context['ban_reason'] = ban.reason
366 return render(request, 'boards/staticpages/banned.html', context)
392 return render(request, 'boards/staticpages/banned.html', context)
367
393
368
394
369 def page_404(request):
395 def page_404(request):
370 """Show page 404 (not found error)"""
396 """Show page 404 (not found error)"""
371
397
372 context = _init_default_context(request)
398 context = _init_default_context(request)
373 return render(request, 'boards/404.html', context)
399 return render(request, 'boards/404.html', context)
374
400
375
401
376 @transaction.atomic
402 @transaction.atomic
377 def tag_subscribe(request, tag_name):
403 def tag_subscribe(request, tag_name):
378 """Add tag to favorites"""
404 """Add tag to favorites"""
379
405
380 user = _get_user(request)
406 user = _get_user(request)
381 tag = get_object_or_404(Tag, name=tag_name)
407 tag = get_object_or_404(Tag, name=tag_name)
382
408
383 if not tag in user.fav_tags.all():
409 if not tag in user.fav_tags.all():
384 user.add_tag(tag)
410 user.add_tag(tag)
385
411
386 return _redirect_to_next(request)
412 return _redirect_to_next(request)
387
413
388
414
389 @transaction.atomic
415 @transaction.atomic
390 def tag_unsubscribe(request, tag_name):
416 def tag_unsubscribe(request, tag_name):
391 """Remove tag from favorites"""
417 """Remove tag from favorites"""
392
418
393 user = _get_user(request)
419 user = _get_user(request)
394 tag = get_object_or_404(Tag, name=tag_name)
420 tag = get_object_or_404(Tag, name=tag_name)
395
421
396 if tag in user.fav_tags.all():
422 if tag in user.fav_tags.all():
397 user.remove_tag(tag)
423 user.remove_tag(tag)
398
424
399 return _redirect_to_next(request)
425 return _redirect_to_next(request)
400
426
401
427
402 def static_page(request, name):
428 def static_page(request, name):
403 """Show a static page that needs only tags list and a CSS"""
429 """Show a static page that needs only tags list and a CSS"""
404
430
405 context = _init_default_context(request)
431 context = _init_default_context(request)
406 return render(request, 'boards/staticpages/' + name + '.html', context)
432 return render(request, 'boards/staticpages/' + name + '.html', context)
407
433
408
434
409 def api_get_post(request, post_id):
435 def api_get_post(request, post_id):
410 """
436 """
411 Get the JSON of a post. This can be
437 Get the JSON of a post. This can be
412 used as and API for external clients.
438 used as and API for external clients.
413 """
439 """
414
440
415 post = get_object_or_404(Post, id=post_id)
441 post = get_object_or_404(Post, id=post_id)
416
442
417 json = serializers.serialize("json", [post], fields=(
443 json = serializers.serialize("json", [post], fields=(
418 "pub_time", "_text_rendered", "title", "text", "image",
444 "pub_time", "_text_rendered", "title", "text", "image",
419 "image_width", "image_height", "replies", "tags"
445 "image_width", "image_height", "replies", "tags"
420 ))
446 ))
421
447
422 return HttpResponse(content=json)
448 return HttpResponse(content=json)
423
449
424
450
425 def get_post(request, post_id):
426 """Get the html of a post. Used for popups."""
427
428 post = get_object_or_404(Post, id=post_id)
429 thread = post.thread_new
430
431 context = RequestContext(request)
432 context["post"] = post
433 context["can_bump"] = thread.can_bump()
434 if "truncated" in request.GET:
435 context["truncated"] = True
436
437 return render(request, 'boards/post.html', context)
438
439 @cache_page(86400)
451 @cache_page(86400)
440 def cached_js_catalog(request, domain='djangojs', packages=None):
452 def cached_js_catalog(request, domain='djangojs', packages=None):
441 return javascript_catalog(request, domain, packages)
453 return javascript_catalog(request, domain, packages)
442
454
443
455
444 def _get_theme(request, user=None):
456 def _get_theme(request, user=None):
445 """Get user's CSS theme"""
457 """Get user's CSS theme"""
446
458
447 if not user:
459 if not user:
448 user = _get_user(request)
460 user = _get_user(request)
449 theme = user.get_setting('theme')
461 theme = user.get_setting('theme')
450 if not theme:
462 if not theme:
451 theme = neboard.settings.DEFAULT_THEME
463 theme = neboard.settings.DEFAULT_THEME
452
464
453 return theme
465 return theme
454
466
455
467
456 def _init_default_context(request):
468 def _init_default_context(request):
457 """Create context with default values that are used in most views"""
469 """Create context with default values that are used in most views"""
458
470
459 context = RequestContext(request)
471 context = RequestContext(request)
460
472
461 user = _get_user(request)
473 user = _get_user(request)
462 context['user'] = user
474 context['user'] = user
463 context['tags'] = user.get_sorted_fav_tags()
475 context['tags'] = user.get_sorted_fav_tags()
464 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
476 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
465
477
466 theme = _get_theme(request, user)
478 theme = _get_theme(request, user)
467 context['theme'] = theme
479 context['theme'] = theme
468 context['theme_css'] = 'css/' + theme + '/base_page.css'
480 context['theme_css'] = 'css/' + theme + '/base_page.css'
469
481
470 # This shows the moderator panel
482 # This shows the moderator panel
471 moderate = user.get_setting(SETTING_MODERATE)
483 moderate = user.get_setting(SETTING_MODERATE)
472 if moderate == 'True':
484 if moderate == 'True':
473 context['moderator'] = user.is_moderator()
485 context['moderator'] = user.is_moderator()
474 else:
486 else:
475 context['moderator'] = False
487 context['moderator'] = False
476
488
477 return context
489 return context
478
490
479
491
480 def _get_user(request):
492 def _get_user(request):
481 """
493 """
482 Get current user from the session. If the user does not exist, create
494 Get current user from the session. If the user does not exist, create
483 a new one.
495 a new one.
484 """
496 """
485
497
486 session = request.session
498 session = request.session
487 if not 'user_id' in session:
499 if not 'user_id' in session:
488 request.session.save()
500 request.session.save()
489
501
490 md5 = hashlib.md5()
502 md5 = hashlib.md5()
491 md5.update(session.session_key)
503 md5.update(session.session_key)
492 new_id = md5.hexdigest()
504 new_id = md5.hexdigest()
493
505
506 while User.objects.filter(user_id=new_id).exists():
507 md5.update(str(timezone.now()))
508 new_id = md5.hexdigest()
509
494 time_now = timezone.now()
510 time_now = timezone.now()
495 user = User.objects.create(user_id=new_id, rank=RANK_USER,
511 user = User.objects.create(user_id=new_id, rank=RANK_USER,
496 registration_time=time_now)
512 registration_time=time_now)
497
513
514 _delete_old_users()
515
498 session['user_id'] = user.id
516 session['user_id'] = user.id
499 else:
517 else:
500 user = User.objects.get(id=session['user_id'])
518 user = User.objects.get(id=session['user_id'])
501
519
502 return user
520 return user
503
521
504
522
505 def _redirect_to_next(request):
523 def _redirect_to_next(request):
506 """
524 """
507 If a 'next' parameter was specified, redirect to the next page. This is
525 If a 'next' parameter was specified, redirect to the next page. This is
508 used when the user is required to return to some page after the current
526 used when the user is required to return to some page after the current
509 view has finished its work.
527 view has finished its work.
510 """
528 """
511
529
512 if 'next' in request.GET:
530 if 'next' in request.GET:
513 next_page = request.GET['next']
531 next_page = request.GET['next']
514 return HttpResponseRedirect(next_page)
532 return HttpResponseRedirect(next_page)
515 else:
533 else:
516 return redirect(index)
534 return redirect(index)
517
535
518
536
519 @transaction.atomic
537 @transaction.atomic
520 def _ban_current_user(request):
538 def _ban_current_user(request):
521 """Add current user to the IP ban list"""
539 """Add current user to the IP ban list"""
522
540
523 ip = utils.get_client_ip(request)
541 ip = utils.get_client_ip(request)
524 ban, created = Ban.objects.get_or_create(ip=ip)
542 ban, created = Ban.objects.get_or_create(ip=ip)
525 if created:
543 if created:
526 ban.can_read = False
544 ban.can_read = False
527 ban.reason = BAN_REASON_SPAM
545 ban.reason = BAN_REASON_SPAM
528 ban.save()
546 ban.save()
529
547
530
548
531 def _remove_invalid_links(text):
549 def _remove_invalid_links(text):
532 """
550 """
533 Replace invalid links in posts so that they won't be parsed.
551 Replace invalid links in posts so that they won't be parsed.
534 Invalid links are links to non-existent posts
552 Invalid links are links to non-existent posts
535 """
553 """
536
554
537 for reply_number in re.finditer(REGEX_REPLY, text):
555 for reply_number in re.finditer(REGEX_REPLY, text):
538 post_id = reply_number.group(1)
556 post_id = reply_number.group(1)
539 post = Post.objects.filter(id=post_id)
557 post = Post.objects.filter(id=post_id)
540 if not post.exists():
558 if not post.exists():
541 text = string.replace(text, '>>' + post_id, post_id)
559 text = string.replace(text, '>>' + post_id, post_id)
542
560
543 return text
561 return text
544
562
545
563
546 def _datetime_to_epoch(datetime):
564 def _datetime_to_epoch(datetime):
547 return int(time.mktime(timezone.localtime(
565 return int(time.mktime(timezone.localtime(
548 datetime,timezone.get_current_timezone()).timetuple())
566 datetime,timezone.get_current_timezone()).timetuple())
549 * 1000000 + datetime.microsecond)
567 * 1000000 + datetime.microsecond)
550
568
551
569
552 def _get_template_thread(thread_to_show):
570 def _get_template_thread(thread_to_show):
553 """Get template values for thread"""
571 """Get template values for thread"""
554
572
555 last_replies = thread_to_show.get_last_replies()
573 last_replies = thread_to_show.get_last_replies()
556 skipped_replies_count = thread_to_show.get_replies().count() \
574 skipped_replies_count = thread_to_show.get_replies().count() \
557 - len(last_replies) - 1
575 - len(last_replies) - 1
558 return {
576 return {
559 'thread': thread_to_show,
577 'thread': thread_to_show,
560 'op': thread_to_show.get_replies()[0],
578 'op': thread_to_show.get_replies()[0],
561 'bumpable': thread_to_show.can_bump(),
579 'bumpable': thread_to_show.can_bump(),
562 'last_replies': last_replies,
580 'last_replies': last_replies,
563 'skipped_replies': skipped_replies_count,
581 'skipped_replies': skipped_replies_count,
564 }
582 }
583
584
585 def _delete_old_users():
586 """
587 Delete users with no favorite tags and posted messages. These can be spam
588 bots or just old user accounts
589 """
590
591 old_registration_date = datetime.now().date() - timedelta(OLD_USER_AGE_DAYS)
592
593 for user in User.objects.annotate(tags_count=Count('fav_tags')).filter(
594 tags_count=0).filter(registration_time__lt=old_registration_date):
595 if not Post.objects.filter(user=user).exists():
596 user.delete()
597
598
599 def _get_page_context(paginator, context, page):
600 """
601 Get pagination context variables
602 """
603
604 context['paginator'] = paginator
605 context['current_page'] = paginator.page(int(page))
@@ -1,75 +1,185 b''
1 from datetime import datetime
1 from datetime import datetime
2 import json
2 import json
3 from django.db import transaction
3 from django.db import transaction
4 from django.http import HttpResponse
4 from django.http import HttpResponse
5 from django.shortcuts import get_object_or_404
5 from django.shortcuts import get_object_or_404, render
6 from django.template import RequestContext
6 from django.utils import timezone
7 from django.utils import timezone
7 from boards.forms import ThreadForm, PlainErrorList
8 from boards.forms import ThreadForm, PlainErrorList
8 from boards.models import Post
9 from boards.models import Post, Thread, Tag
9 from boards.views import get_post, _datetime_to_epoch, _new_post, \
10 from boards.views import _datetime_to_epoch, _new_post, \
10 _ban_current_user
11 _ban_current_user
11
12
12 __author__ = 'neko259'
13 __author__ = 'neko259'
13
14
15 PARAMETER_TRUNCATED = 'truncated'
16 PARAMETER_TAG = 'tag'
17 PARAMETER_OFFSET = 'offset'
18
14
19
15 @transaction.atomic
20 @transaction.atomic
16 def api_get_threaddiff(request, thread_id, last_update_time):
21 def api_get_threaddiff(request, thread_id, last_update_time):
17 """Get posts that were changed or added since time"""
22 """Get posts that were changed or added since time"""
18
23
19 thread = get_object_or_404(Post, id=thread_id).thread_new
24 thread = get_object_or_404(Post, id=thread_id).thread_new
20
25
21 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
26 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
22 timezone.get_current_timezone())
27 timezone.get_current_timezone())
23
28
24 json_data = {
29 json_data = {
25 'added': [],
30 'added': [],
26 'updated': [],
31 'updated': [],
27 'last_update': None,
32 'last_update': None,
28 }
33 }
29 added_posts = Post.objects.filter(thread_new=thread,
34 added_posts = Post.objects.filter(thread_new=thread,
30 pub_time__gt=filter_time) \
35 pub_time__gt=filter_time) \
31 .order_by('pub_time')
36 .order_by('pub_time')
32 updated_posts = Post.objects.filter(thread_new=thread,
37 updated_posts = Post.objects.filter(thread_new=thread,
33 pub_time__lte=filter_time,
38 pub_time__lte=filter_time,
34 last_edit_time__gt=filter_time)
39 last_edit_time__gt=filter_time)
35 for post in added_posts:
40 for post in added_posts:
36 json_data['added'].append(get_post(request, post.id).content.strip())
41 json_data['added'].append(get_post(request, post.id).content.strip())
37 for post in updated_posts:
42 for post in updated_posts:
38 json_data['updated'].append(get_post(request, post.id).content.strip())
43 json_data['updated'].append(get_post(request, post.id).content.strip())
39 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
44 json_data['last_update'] = _datetime_to_epoch(thread.last_edit_time)
40
45
41 return HttpResponse(content=json.dumps(json_data))
46 return HttpResponse(content=json.dumps(json_data))
42
47
43
48
44 # TODO This method needs to be implemented properly
49 # TODO This method needs to be implemented properly
45 # def api_add_post(request, form):
50 # def api_add_post(request, form):
46 # """
51 # """
47 # Add a post and return the JSON response for it
52 # Add a post and return the JSON response for it
48 # """
53 # """
49 #
54 #
50 # status = 'ok'
55 # status = 'ok'
51 # errors = []
56 # errors = []
52 #
57 #
53 # if request.method == 'POST':
58 # if request.method == 'POST':
54 # form = ThreadForm(request.POST, request.FILES,
59 # form = ThreadForm(request.POST, request.FILES,
55 # error_class=PlainErrorList)
60 # error_class=PlainErrorList)
56 # form.session = request.session
61 # form.session = request.session
57 #
62 #
58 # if form.is_valid():
63 # if form.is_valid():
59 # # TODO Don't form a response here cause we'll not need it
64 # # TODO Don't form a response here cause we'll not need it
60 # _new_post(request, form)
65 # _new_post(request, form)
61 # if form.need_to_ban:
66 # if form.need_to_ban:
62 # # Ban user because he is suspected to be a bot
67 # # Ban user because he is suspected to be a bot
63 # _ban_current_user(request)
68 # _ban_current_user(request)
64 # else:
69 # else:
65 # status = 'error'
70 # status = 'error'
66 # for field in form.fields:
71 # for field in form.fields:
67 # if field.errors:
72 # if field.errors:
68 # errors.append(field.errors)
73 # errors.append(field.errors)
69 #
74 #
70 # response = {
75 # response = {
71 # 'status': status,
76 # 'status': status,
72 # 'errors': errors,
77 # 'errors': errors,
73 # }
78 # }
74 #
79 #
75 # return HttpResponse(content=json.dumps(response))
80 # return HttpResponse(content=json.dumps(response))
81
82
83 def get_post(request, post_id):
84 """
85 Get the html of a post. Used for popups. Post can be truncated if used
86 in threads list with 'truncated' get parameter.
87 """
88
89 post = get_object_or_404(Post, id=post_id)
90 thread = post.thread_new
91
92 context = RequestContext(request)
93 context['post'] = post
94 context['can_bump'] = thread.can_bump()
95 if PARAMETER_TRUNCATED in request.GET:
96 context[PARAMETER_TRUNCATED] = True
97
98 return render(request, 'boards/post.html', context)
99
100
101 # TODO Test this
102 def api_get_threads(request, count):
103 """
104 Get the JSON thread opening posts list.
105 Parameters that can be used for filtering:
106 tag, offset (from which thread to get results)
107 """
108
109 if PARAMETER_TAG in request.GET:
110 tag_name = request.GET[PARAMETER_TAG]
111 if tag_name is not None:
112 tag = get_object_or_404(Tag, name=tag_name)
113 threads = tag.threads.filter(archived=False)
114 else:
115 threads = Thread.objects.filter(archived=False)
116
117 if PARAMETER_OFFSET in request.GET:
118 offset = request.GET[PARAMETER_OFFSET]
119 offset = int(offset) if offset is not None else 0
120 else:
121 offset = 0
122
123 threads = threads.order_by('-bump_time')
124 threads = threads[offset:offset + int(count)]
125
126 opening_posts = []
127 for thread in threads:
128 opening_post = thread.get_opening_post()
129
130 # TODO Add pub time, tags, replies and images count
131 post_json = {
132 'id': opening_post.id,
133 'title': opening_post.title,
134 'text': opening_post.text.rendered,
135 }
136 if opening_post.image:
137 post_json['image'] = opening_post.image.url
138 post_json['image_preview'] = opening_post.image.url_200x150
139 opening_posts.append(post_json)
140
141 return HttpResponse(content=json.dumps(opening_posts))
142
143
144 # TODO Test this
145 def api_get_tags(request):
146 """
147 Get all tags or user tags.
148 """
149
150 # TODO Get favorite tags for the given user ID
151
152 tags = Tag.objects.get_not_empty_tags()
153 tag_names = []
154 for tag in tags:
155 tag_names.append(tag.name)
156
157 return HttpResponse(content=json.dumps(tag_names))
158
159
160 # TODO The result can be cached by the thread last update time
161 # TODO Test this
162 def api_get_thread_posts(request, opening_post_id):
163 """
164 Get the JSON array of thread posts
165 """
166
167 opening_post = get_object_or_404(Post, id=opening_post_id)
168 thread = opening_post.thread_new
169 posts = thread.get_replies()
170
171 json_post_list = []
172
173 for post in posts:
174 # TODO Add pub time and replies
175 post_json = {
176 'id': post.id,
177 'title': post.title,
178 'text': post.text.rendered,
179 }
180 if post.image:
181 post_json['image'] = post.image.url
182 post_json['image_preview'] = post.image.url_200x150
183 json_post_list.append(post_json)
184
185 return HttpResponse(content=json.dumps(json_post_list))
@@ -1,7 +1,16 b''
1 # 1.5 Aker #
1 # 1.5 Aker #
2 * Saving image previews size. No space will be shown below images in some
2 * Saving image previews size. No space will be shown below images in some
3 styles.
3 styles.
4 * Showing notification in page title when new posts are loaded into the open
4 * Showing notification in page title when new posts are loaded into the open
5 thread.
5 thread.
6 * Thread moderation fixes
6 * Thread moderation fixes
7 * Added new gallery with search links and image metadata No newline at end of file
7 * Added new gallery with search links and image metadata
8
9 # 1.6 Amon #
10 * Deleted threads are moved to archive instead of permanent delete
11 * User management fixes and optimizations
12 * Markdown fixes
13 * Pagination changes. Pages counter now starts from 1 instead of 0
14 * Added API for viewing threads and posts
15 * New tag popularity algorithm
16 * Tags list page changes. Now tags list is more like a tag cloud
@@ -1,50 +1,48 b''
1 = Features =
1 = Features =
2 [DONE] Connecting tags to each other
2 [DONE] Connecting tags to each other
3 [DONE] Connect posts to the replies (in messages), get rid of the JS reply map
3 [DONE] Connect posts to the replies (in messages), get rid of the JS reply map
4 [DONE] Better django admin pages to simplify admin operations
4 [DONE] Better django admin pages to simplify admin operations
5 [DONE] Regen script to update all posts
5 [DONE] Regen script to update all posts
6 [DONE] Remove jump links from refmaps
6 [DONE] Remove jump links from refmaps
7 [DONE] Ban reasons. Split bans into 2 types "read-only" and "read
7 [DONE] Ban reasons. Split bans into 2 types "read-only" and "read
8 denied". Use second only for autoban for spam
8 denied". Use second only for autoban for spam
9 [DONE] Clean up tests and make them run ALWAYS
9 [DONE] Clean up tests and make them run ALWAYS
10 [DONE] Use transactions in tests
10 [DONE] Use transactions in tests
11 [DONE] Thread autoupdate (JS + API)
11 [DONE] Thread autoupdate (JS + API)
12 [DONE] Split up post model into post and thread,
12 [DONE] Split up post model into post and thread,
13 and move everything that is used only in 1st post to thread model.
13 and move everything that is used only in 1st post to thread model.
14 [DONE] Show board speed in the lower panel (posts per day)
14 [DONE] Show board speed in the lower panel (posts per day)
15 [DONE] Save image thumbnails size to the separate field
15 [DONE] Save image thumbnails size to the separate field
16
16
17 [NOT STARTED] Tree view (JS)
17 [NOT STARTED] Tree view (JS)
18 [NOT STARTED] Adding tags to images filename
18 [NOT STARTED] Adding tags to images filename
19 [NOT STARTED] Federative network for s2s communication
19 [NOT STARTED] Federative network for s2s communication
20 [NOT STARTED] XMPP gate
20 [NOT STARTED] XMPP gate
21 [NOT STARTED] Bitmessage gate
21 [NOT STARTED] Bitmessage gate
22 [NOT STARTED] Notification engine
22 [NOT STARTED] Notification engine
23 [NOT STARTED] Javascript disabling engine
23 [NOT STARTED] Javascript disabling engine
24 [NOT STARTED] Group tags by first letter in all tags list
24 [NOT STARTED] Group tags by first letter in all tags list
25 [NOT STARTED] Character counter in the post field
25 [NOT STARTED] Character counter in the post field
26 [NOT STARTED] Whitelist functionality. Permin autoban of an address
26 [NOT STARTED] Whitelist functionality. Permin autoban of an address
27 [NOT STARTED] Statistics module. Count views (optional, may result in bad
27 [NOT STARTED] Statistics module. Count views (optional, may result in bad
28 performance), posts per day/week/month, users (or IPs)
28 performance), posts per day/week/month, users (or IPs)
29 [NOT STARTED] Quote button next to "reply" for posts in thread to include full
29 [NOT STARTED] Quote button next to "reply" for posts in thread to include full
30 post or its part (delimited by N characters) into quote of the new post.
30 post or its part (delimited by N characters) into quote of the new post.
31 [NOT STARTED] Ban confirmation page with reason
31 [NOT STARTED] Ban confirmation page with reason
32 [NOT STARTED] Post deletion confirmation page
32 [NOT STARTED] Post deletion confirmation page
33 [NOT STARTED] Moderating page. Tags editing and adding
33 [NOT STARTED] Moderating page. Tags editing and adding
34 [NOT STARTED] Get thread graph image using pygraphviz
34 [NOT STARTED] Get thread graph image using pygraphviz
35 [NOT STARTED] Creating post via AJAX without reloading page
35 [NOT STARTED] Creating post via AJAX without reloading page
36 [NOT STARTED] Subscribing to tag via AJAX
36 [NOT STARTED] Subscribing to tag via AJAX
37 [NOT STARTED] Count posts by user not by current active posts, but by adding 1
38 on evety posting
39
37
40 = Bugs =
38 = Bugs =
41 [DONE] Fix bug with creating threads from tag view
39 [DONE] Fix bug with creating threads from tag view
42 [DONE] Quote characters within quote causes quote parsing to fail
40 [DONE] Quote characters within quote causes quote parsing to fail
43
41
44 [IN PROGRESS] Replies, images, last update time in bottom panel doesn't change when
42 [IN PROGRESS] Replies, images, last update time in bottom panel doesn't change when
45 thread updates (last update changing left)
43 thread updates (last update changing left)
46
44
47 = Testing =
45 = Testing =
48 [NOT STARTED] Make tests for every view
46 [NOT STARTED] Make tests for every view
49 [NOT STARTED] Make tests for every model
47 [NOT STARTED] Make tests for every model
50 [NOT STARTED] Make tests for every form
48 [NOT STARTED] Make tests for every form
General Comments 0
You need to be logged in to leave comments. Login now